From 0287b186135431c67ddb01834b12e65bd599aaf3 Mon Sep 17 00:00:00 2001 From: Jeremy Guarini Date: Fri, 29 Oct 2021 10:20:14 -0700 Subject: [PATCH 001/181] UI updates to GCP-projects plugin Adds the following to the project list page: * pagination * filtering * sorting * rows per page * show/hide columns Makes breadcrumb a link back to project list for the project details and new project views. In project list page, updates New project button to use RouterLink instead of href to avoid login prompt. In project details view, links to project details and logs now work, clicking on these will open the project or logs in GCP in new tab. Signed-off-by: Jeremy Guarini --- plugins/gcp-projects/package.json | 1 + .../NewProjectPage/NewProjectPage.tsx | 43 +++-- .../ProjectDetailsPage/ProjectDetailsPage.tsx | 51 ++++-- .../ProjectListPage/ProjectListPage.tsx | 161 +++++++++++------- yarn.lock | 30 +++- 5 files changed, 186 insertions(+), 100 deletions(-) diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 65458ea88b..71b212a698 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -35,6 +35,7 @@ "@backstage/core-plugin-api": "^0.1.12", "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", + "@material-ui/data-grid": "^4.0.0-alpha.37", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "react": "^16.13.1", diff --git a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx index 584cffc50c..5ecc7f15d4 100644 --- a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx +++ b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx @@ -28,6 +28,10 @@ import { StructuredMetadataTable, SupportButton, } from '@backstage/core-components'; +import { Link as RouterLink } from 'react-router-dom'; + +import { useRouteRef } from '@backstage/core-plugin-api'; +import { rootRouteRef } from '../../routes'; export const Project = () => { const [projectName, setProjectName] = useState(''); @@ -79,18 +83,20 @@ export const Project = () => { All your software catalog entities diff --git a/yarn.lock b/yarn.lock index 517d66ba0c..fc66a02031 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4416,6 +4416,16 @@ react-is "^16.8.0 || ^17.0.0" react-transition-group "^4.4.0" +"@material-ui/data-grid@^4.0.0-alpha.37": + version "4.0.0-alpha.37" + resolved "https://registry.npmjs.org/@material-ui/data-grid/-/data-grid-4.0.0-alpha.37.tgz#89d907c4e94e6a0db4e89e4f59160f7811546ca2" + integrity sha512-3T2AG31aad/lWLMLwn1XUP4mUf3H9YZES17dGuYByzkRLCXbBZHBTPEnCctWukajzwm+v0KGg3QpwitGoiDAjA== + dependencies: + "@material-ui/utils" "^5.0.0-alpha.14" + clsx "^1.0.4" + prop-types "^15.7.2" + reselect "^4.0.0" + "@material-ui/icons@^4.11.2", "@material-ui/icons@^4.9.1": version "4.11.2" resolved "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz#b3a7353266519cd743b6461ae9fdfcb1b25eb4c5" @@ -4503,6 +4513,17 @@ prop-types "^15.7.2" react-is "^16.8.0 || ^17.0.0" +"@material-ui/utils@^5.0.0-alpha.14": + version "5.0.0-beta.5" + resolved "https://registry.npmjs.org/@material-ui/utils/-/utils-5.0.0-beta.5.tgz#de492037e1f1f0910fda32e6f11b66dfcde2a1c2" + integrity sha512-wtJ3ovXWZdTAz5eLBqvMpYH/IBJb3qMQbGCyL1i00+sf7AUlAuv4QLx+QtX/siA6L7IpxUQVfqpoCpQH1eYRpQ== + dependencies: + "@babel/runtime" "^7.14.8" + "@types/prop-types" "^15.7.4" + "@types/react-is" "^16.7.1 || ^17.0.0" + prop-types "^15.7.2" + react-is "^17.0.2" + "@mattiasbuelens/web-streams-polyfill@^0.2.0": version "0.2.1" resolved "https://registry.npmjs.org/@mattiasbuelens/web-streams-polyfill/-/web-streams-polyfill-0.2.1.tgz#d7c4aa94f98084ec0787be084d47167d62ea5f67" @@ -7434,7 +7455,7 @@ resolved "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz#72a26101dc567b0d68fd956cf42314556e42d601" integrity sha512-VjID5MJb1eGKthz2qUerWT8+R4b9N+CHvGCzg9fn4kWZgaF9AhdYikQio3R7wV8YY1NsQKPaCwKz1Yff+aHNUQ== -"@types/prop-types@*", "@types/prop-types@^15.7.3": +"@types/prop-types@*", "@types/prop-types@^15.7.3", "@types/prop-types@^15.7.4": version "15.7.4" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== @@ -7475,6 +7496,13 @@ dependencies: "@types/react" "*" +"@types/react-is@^16.7.1 || ^17.0.0": + version "17.0.3" + resolved "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a" + integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw== + dependencies: + "@types/react" "*" + "@types/react-lazylog@^4.5.0": version "4.5.1" resolved "https://registry.npmjs.org/@types/react-lazylog/-/react-lazylog-4.5.1.tgz#babb5d814f7035b5434518769975e12f299356a8" From 741bcb168e875e537d7851f32195d9d30102d806 Mon Sep 17 00:00:00 2001 From: Jeremy Guarini Date: Fri, 29 Oct 2021 16:55:03 -0700 Subject: [PATCH 002/181] add changeset Signed-off-by: Jeremy Guarini --- .changeset/eight-months-agree.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .changeset/eight-months-agree.md diff --git a/.changeset/eight-months-agree.md b/.changeset/eight-months-agree.md new file mode 100644 index 0000000000..372c0b624c --- /dev/null +++ b/.changeset/eight-months-agree.md @@ -0,0 +1,19 @@ +--- +'@backstage/plugin-gcp-projects': patch +--- + +UI updates to GCP-projects plugin + +Adds the following to the project list page: + +- pagination +- filtering +- sorting +- rows per page +- show/hide columns + +Makes breadcrumb a link back to project list for the project details and new project views. + +In project list page, updates New project button to use RouterLink instead of href to avoid login prompt. + +In project details view, links to project details and logs now work, clicking on these will open the project or logs in GCP in new tab. From 044c38e73982d0793686b203d9dd356397c9a544 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 1 Nov 2021 13:10:54 +0100 Subject: [PATCH 003/181] Lazy load all API definition widgets Signed-off-by: Oliver Sand --- .changeset/swift-turtles-provide.md | 7 + plugins/api-docs/api-report.md | 18 ++- ...t.test.tsx => AsyncApiDefinition.test.tsx} | 8 +- .../AsyncApiDefinition.tsx | 145 ++++++++++++++++++ .../AsyncApiDefinitionWidget.tsx | 137 ++--------------- ...et.test.tsx => GraphQlDefinition.test.tsx} | 6 +- .../GraphQlDefinition.tsx | 63 ++++++++ .../GraphQlDefinitionWidget.tsx | 55 ++----- ...et.test.tsx => OpenApiDefinition.test.tsx} | 8 +- .../OpenApiDefinition.tsx | 92 +++++++++++ .../OpenApiDefinitionWidget.tsx | 86 ++--------- 11 files changed, 375 insertions(+), 250 deletions(-) create mode 100644 .changeset/swift-turtles-provide.md rename plugins/api-docs/src/components/AsyncApiDefinitionWidget/{AsyncApiDefinitionWidget.test.tsx => AsyncApiDefinition.test.tsx} (87%) create mode 100644 plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx rename plugins/api-docs/src/components/GraphQlDefinitionWidget/{GraphQlDefinitionWidget.test.tsx => GraphQlDefinition.test.tsx} (89%) create mode 100644 plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinition.tsx rename plugins/api-docs/src/components/OpenApiDefinitionWidget/{OpenApiDefinitionWidget.test.tsx => OpenApiDefinition.test.tsx} (87%) create mode 100644 plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx diff --git a/.changeset/swift-turtles-provide.md b/.changeset/swift-turtles-provide.md new file mode 100644 index 0000000000..52cf9a7c8f --- /dev/null +++ b/.changeset/swift-turtles-provide.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Lazy load all API definition widgets. The widgets use libraries like +`swagger-ui`, `graphiql`, and `@asyncapi/react-component` which are quite heavy +weight. To improve initial load times, the widgets are only loaded once used. diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md index de36857945..8ec6175a6d 100644 --- a/plugins/api-docs/api-report.md +++ b/plugins/api-docs/api-report.md @@ -84,11 +84,13 @@ export const ApiTypeTitle: ({ apiEntity: ApiEntity; }) => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "AsyncApiDefinitionWidgetProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "AsyncApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const AsyncApiDefinitionWidget: ({ definition }: Props_5) => JSX.Element; +export const AsyncApiDefinitionWidget: ({ + definition, +}: AsyncApiDefinitionWidgetProps) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ConsumedApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -100,7 +102,7 @@ export const ConsumedApisCard: ({ variant }: Props_2) => JSX.Element; // Warning: (ae-missing-release-tag) "ConsumingComponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ConsumingComponentsCard: ({ variant }: Props_6) => JSX.Element; +export const ConsumingComponentsCard: ({ variant }: Props_5) => JSX.Element; // Warning: (ae-missing-release-tag) "defaultDefinitionWidgets" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -169,11 +171,13 @@ export const EntityProvidingComponentsCard: ({ // @public (undocumented) export const HasApisCard: ({ variant }: Props_3) => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "OpenApiDefinitionWidgetProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "OpenApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const OpenApiDefinitionWidget: ({ definition }: Props_8) => JSX.Element; +export const OpenApiDefinitionWidget: ({ + definition, +}: OpenApiDefinitionWidgetProps) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PlainApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -182,7 +186,7 @@ export const OpenApiDefinitionWidget: ({ definition }: Props_8) => JSX.Element; export const PlainApiDefinitionWidget: ({ definition, language, -}: Props_9) => JSX.Element; +}: Props_7) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ProvidedApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -194,5 +198,5 @@ export const ProvidedApisCard: ({ variant }: Props_4) => JSX.Element; // Warning: (ae-missing-release-tag) "ProvidingComponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ProvidingComponentsCard: ({ variant }: Props_7) => JSX.Element; +export const ProvidingComponentsCard: ({ variant }: Props_6) => JSX.Element; ``` diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.test.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.test.tsx similarity index 87% rename from plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.test.tsx rename to plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.test.tsx index 0170ada180..cb89476c03 100644 --- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.test.tsx +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.test.tsx @@ -16,9 +16,9 @@ import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; -import { AsyncApiDefinitionWidget } from './AsyncApiDefinitionWidget'; +import { AsyncApiDefinition } from './AsyncApiDefinition'; -describe('', () => { +describe('', () => { it('renders asyncapi spec', async () => { const definition = ` asyncapi: 2.0.0 @@ -40,7 +40,7 @@ components: type: string `; const { getByText, getAllByText } = await renderInTestApp( - , + , ); expect(getByText(/Account Service/i)).toBeInTheDocument(); @@ -51,7 +51,7 @@ components: it('renders error if definition is missing', async () => { const { getByText } = await renderInTestApp( - , + , ); expect(getByText(/Error/i)).toBeInTheDocument(); expect(getByText(/Document can't be null or falsey/i)).toBeInTheDocument(); diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx new file mode 100644 index 0000000000..1c993f67d7 --- /dev/null +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import AsyncApi from '@asyncapi/react-component'; +import '@asyncapi/react-component/lib/styles/fiori.css'; +import { alpha, makeStyles } from '@material-ui/core/styles'; +import React from 'react'; + +const useStyles = makeStyles(theme => ({ + root: { + '& .asyncapi': { + 'font-family': 'inherit', + background: 'none', + }, + '& h2': { + ...theme.typography.h6, + }, + '& .text-teal': { + color: theme.palette.primary.main, + }, + '& button': { + ...theme.typography.button, + background: 'none', + boxSizing: 'border-box', + minWidth: 64, + borderRadius: theme.shape.borderRadius, + transition: theme.transitions.create( + ['background-color', 'box-shadow', 'border'], + { + duration: theme.transitions.duration.short, + }, + ), + padding: '5px 15px', + color: theme.palette.primary.main, + border: `1px solid ${alpha(theme.palette.primary.main, 0.5)}`, + '&:hover': { + textDecoration: 'none', + '&.Mui-disabled': { + backgroundColor: 'transparent', + }, + border: `1px solid ${theme.palette.primary.main}`, + backgroundColor: alpha( + theme.palette.primary.main, + theme.palette.action.hoverOpacity, + ), + // Reset on touch devices, it doesn't add specificity + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + }, + '&.Mui-disabled': { + color: theme.palette.action.disabled, + }, + }, + '& .asyncapi__collapse-button:hover': { + color: theme.palette.primary.main, + }, + '& button.asyncapi__toggle-button': { + 'min-width': 'inherit', + }, + '& .asyncapi__info-list li': { + 'border-color': theme.palette.primary.main, + '&:hover': { + color: theme.palette.text.primary, + 'border-color': theme.palette.primary.main, + 'background-color': theme.palette.primary.main, + }, + }, + '& .asyncapi__info-list li a': { + color: theme.palette.primary.main, + '&:hover': { + color: theme.palette.getContrastText(theme.palette.primary.main), + }, + }, + '& .asyncapi__enum': { + color: theme.palette.secondary.main, + }, + '& .asyncapi__info, .asyncapi__channel, .asyncapi__channels > div, .asyncapi__schema, .asyncapi__channel-operations-list .asyncapi__messages-list-item .asyncapi__message, .asyncapi__message, .asyncapi__server, .asyncapi__servers > div, .asyncapi__messages > div, .asyncapi__schemas > div': + { + 'background-color': 'inherit', + }, + '& .asyncapi__channel-parameters-header, .asyncapi__channel-operations-header, .asyncapi__channel-operation-oneOf-subscribe-header, .asyncapi__channel-operation-oneOf-publish-header, .asyncapi__channel-operation-message-header, .asyncapi__message-header, .asyncapi__message-header-title, .asyncapi__message-header-title > h3, .asyncapi__bindings, .asyncapi__bindings-header, .asyncapi__bindings-header > h4': + { + 'background-color': 'inherit', + color: theme.palette.text.primary, + }, + '& .asyncapi__additional-properties-notice': { + color: theme.palette.text.hint, + }, + '& .asyncapi__code, .asyncapi__code-pre': { + background: theme.palette.background.default, + }, + '& .asyncapi__schema-example-header-title': { + color: theme.palette.text.secondary, + }, + '& .asyncapi__message-headers-header, .asyncapi__message-payload-header, .asyncapi__server-variables-header, .asyncapi__server-security-header': + { + 'background-color': 'inherit', + color: theme.palette.text.secondary, + }, + '& .asyncapi__table-header': { + background: theme.palette.background.default, + }, + '& .asyncapi__table-body': { + color: theme.palette.text.primary, + }, + '& .asyncapi__server-security-flow': { + background: theme.palette.background.default, + border: 'none', + }, + '& .asyncapi__server-security-flows-list a': { + color: theme.palette.primary.main, + }, + '& .asyncapi__table-row--nested': { + color: theme.palette.text.secondary, + }, + }, +})); + +type Props = { + definition: string; +}; + +export const AsyncApiDefinition = ({ definition }: Props) => { + const classes = useStyles(); + + return ( +
+ +
+ ); +}; diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx index f32b5d01b5..a3d9bc1f64 100644 --- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx @@ -14,132 +14,27 @@ * limitations under the License. */ -import AsyncApi from '@asyncapi/react-component'; -import '@asyncapi/react-component/lib/styles/fiori.css'; -import { fade, makeStyles } from '@material-ui/core/styles'; -import React from 'react'; +import { Progress } from '@backstage/core-components'; +import React, { Suspense } from 'react'; -const useStyles = makeStyles(theme => ({ - root: { - '& .asyncapi': { - 'font-family': 'inherit', - background: 'none', - }, - '& h2': { - ...theme.typography.h6, - }, - '& .text-teal': { - color: theme.palette.primary.main, - }, - '& button': { - ...theme.typography.button, - background: 'none', - boxSizing: 'border-box', - minWidth: 64, - borderRadius: theme.shape.borderRadius, - transition: theme.transitions.create( - ['background-color', 'box-shadow', 'border'], - { - duration: theme.transitions.duration.short, - }, - ), - padding: '5px 15px', - color: theme.palette.primary.main, - border: `1px solid ${fade(theme.palette.primary.main, 0.5)}`, - '&:hover': { - textDecoration: 'none', - '&.Mui-disabled': { - backgroundColor: 'transparent', - }, - border: `1px solid ${theme.palette.primary.main}`, - backgroundColor: fade( - theme.palette.primary.main, - theme.palette.action.hoverOpacity, - ), - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - backgroundColor: 'transparent', - }, - }, - '&.Mui-disabled': { - color: theme.palette.action.disabled, - }, - }, - '& .asyncapi__collapse-button:hover': { - color: theme.palette.primary.main, - }, - '& button.asyncapi__toggle-button': { - 'min-width': 'inherit', - }, - '& .asyncapi__info-list li': { - 'border-color': theme.palette.primary.main, - '&:hover': { - color: theme.palette.text.primary, - 'border-color': theme.palette.primary.main, - 'background-color': theme.palette.primary.main, - }, - }, - '& .asyncapi__info-list li a': { - color: theme.palette.primary.main, - '&:hover': { - color: theme.palette.getContrastText(theme.palette.primary.main), - }, - }, - '& .asyncapi__enum': { - color: theme.palette.secondary.main, - }, - '& .asyncapi__info, .asyncapi__channel, .asyncapi__channels > div, .asyncapi__schema, .asyncapi__channel-operations-list .asyncapi__messages-list-item .asyncapi__message, .asyncapi__message, .asyncapi__server, .asyncapi__servers > div, .asyncapi__messages > div, .asyncapi__schemas > div': - { - 'background-color': 'inherit', - }, - '& .asyncapi__channel-parameters-header, .asyncapi__channel-operations-header, .asyncapi__channel-operation-oneOf-subscribe-header, .asyncapi__channel-operation-oneOf-publish-header, .asyncapi__channel-operation-message-header, .asyncapi__message-header, .asyncapi__message-header-title, .asyncapi__message-header-title > h3, .asyncapi__bindings, .asyncapi__bindings-header, .asyncapi__bindings-header > h4': - { - 'background-color': 'inherit', - color: theme.palette.text.primary, - }, - '& .asyncapi__additional-properties-notice': { - color: theme.palette.text.hint, - }, - '& .asyncapi__code, .asyncapi__code-pre': { - background: theme.palette.background.default, - }, - '& .asyncapi__schema-example-header-title': { - color: theme.palette.text.secondary, - }, - '& .asyncapi__message-headers-header, .asyncapi__message-payload-header, .asyncapi__server-variables-header, .asyncapi__server-security-header': - { - 'background-color': 'inherit', - color: theme.palette.text.secondary, - }, - '& .asyncapi__table-header': { - background: theme.palette.background.default, - }, - '& .asyncapi__table-body': { - color: theme.palette.text.primary, - }, - '& .asyncapi__server-security-flow': { - background: theme.palette.background.default, - border: 'none', - }, - '& .asyncapi__server-security-flows-list a': { - color: theme.palette.primary.main, - }, - '& .asyncapi__table-row--nested': { - color: theme.palette.text.secondary, - }, - }, -})); +// The asyncapi component and related CSS has a significant size, only load it +// if the element is actually used. +const LazyAsyncApiDefinition = React.lazy(() => + import('./AsyncApiDefinition').then(m => ({ + default: m.AsyncApiDefinition, + })), +); -type Props = { +export type AsyncApiDefinitionWidgetProps = { definition: string; }; -export const AsyncApiDefinitionWidget = ({ definition }: Props) => { - const classes = useStyles(); - +export const AsyncApiDefinitionWidget = ({ + definition, +}: AsyncApiDefinitionWidgetProps) => { return ( -
- -
+ }> + + ); }; diff --git a/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.test.tsx b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinition.test.tsx similarity index 89% rename from plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.test.tsx rename to plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinition.test.tsx index 63e5a73692..fd78fa9204 100644 --- a/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.test.tsx +++ b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinition.test.tsx @@ -16,9 +16,9 @@ import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; -import { GraphQlDefinitionWidget } from './GraphQlDefinitionWidget'; +import { GraphQlDefinition } from './GraphQlDefinition'; -describe('', () => { +describe('', () => { it('renders graphql schema', async () => { const definition = ` """Hello World!""" @@ -53,7 +53,7 @@ type Film { }; const { getByText } = await renderInTestApp( - , + , ); expect(getByText(/Film/i)).toBeInTheDocument(); diff --git a/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinition.tsx b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinition.tsx new file mode 100644 index 0000000000..d3fddf70cf --- /dev/null +++ b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinition.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BackstageTheme } from '@backstage/theme'; +import { makeStyles } from '@material-ui/core/styles'; +import GraphiQL from 'graphiql'; +import 'graphiql/graphiql.css'; +import { buildSchema } from 'graphql'; +import React from 'react'; + +const useStyles = makeStyles(() => ({ + root: { + height: '100%', + display: 'flex', + flexFlow: 'column nowrap', + }, + graphiQlWrapper: { + flex: 1, + '@global': { + '.graphiql-container': { + boxSizing: 'initial', + height: '100%', + minHeight: '600px', + flex: '1 1 auto', + }, + }, + }, +})); + +type Props = { + definition: string; +}; + +export const GraphQlDefinition = ({ definition }: Props) => { + const classes = useStyles(); + const schema = buildSchema(definition); + + return ( +
+
+ Promise.resolve(null) as any} + schema={schema} + docExplorerOpen + defaultSecondaryEditorOpen={false} + /> +
+
+ ); +}; diff --git a/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx index 8d15ba08a2..e91387bffa 100644 --- a/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx @@ -14,54 +14,27 @@ * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; -import { makeStyles } from '@material-ui/core/styles'; -import 'graphiql/graphiql.css'; -import { buildSchema } from 'graphql'; -import React, { Suspense } from 'react'; import { Progress } from '@backstage/core-components'; +import React, { Suspense } from 'react'; -const GraphiQL = React.lazy(() => import('graphiql')); +// The graphql component, graphql and related CSS has a significant size, only +// load it if the element is actually used. +const LazyGraphQlDefinition = React.lazy(() => + import('./GraphQlDefinition').then(m => ({ + default: m.GraphQlDefinition, + })), +); -const useStyles = makeStyles(() => ({ - root: { - height: '100%', - display: 'flex', - flexFlow: 'column nowrap', - }, - graphiQlWrapper: { - flex: 1, - '@global': { - '.graphiql-container': { - boxSizing: 'initial', - height: '100%', - minHeight: '600px', - flex: '1 1 auto', - }, - }, - }, -})); - -type Props = { - definition: any; +export type GraphQlDefinitionWidgetProps = { + definition: string; }; -export const GraphQlDefinitionWidget = ({ definition }: Props) => { - const classes = useStyles(); - const schema = buildSchema(definition); - +export const GraphQlDefinitionWidget = ({ + definition, +}: GraphQlDefinitionWidgetProps) => { return ( }> -
-
- Promise.resolve(null) as any} - schema={schema} - docExplorerOpen - defaultSecondaryEditorOpen={false} - /> -
-
+
); }; diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.test.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.test.tsx similarity index 87% rename from plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.test.tsx rename to plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.test.tsx index 080aa11a3a..735fba86f7 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.test.tsx +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.test.tsx @@ -17,9 +17,9 @@ import { renderInTestApp } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; -import { OpenApiDefinitionWidget } from './OpenApiDefinitionWidget'; +import { OpenApiDefinition } from './OpenApiDefinition'; -describe('', () => { +describe('', () => { it('renders openapi spec', async () => { const definition = ` openapi: "3.0.0" @@ -39,7 +39,7 @@ paths: description: Success `; const { getByText } = await renderInTestApp( - , + , ); // swagger-ui loads the documentation asynchronously @@ -51,7 +51,7 @@ paths: it('renders error if definition is missing', async () => { const { getByText } = await renderInTestApp( - , + , ); expect(getByText(/No API definition provided/i)).toBeInTheDocument(); }); diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx new file mode 100644 index 0000000000..cc3df40ae5 --- /dev/null +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx @@ -0,0 +1,92 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeStyles } from '@material-ui/core/styles'; +import React, { useEffect, useState } from 'react'; +import SwaggerUI from 'swagger-ui-react'; +import 'swagger-ui-react/swagger-ui.css'; + +const useStyles = makeStyles(theme => ({ + root: { + '& .swagger-ui, .info h1, .info h2, .info h3, .info h4, .info h': { + 'font-family': 'inherit', + color: theme.palette.text.primary, + }, + '& .scheme-container': { + 'background-color': theme.palette.background.default, + }, + '& .opblock-tag, .opblock-tag small, table thead tr td, table thead tr th': + { + color: theme.palette.text.primary, + 'border-color': theme.palette.divider, + }, + '& section.models, section.models.is-open h4': { + 'border-color': theme.palette.divider, + }, + '& .opblock .opblock-summary-description, .parameter__type, table.headers td, .model-title, .model .property.primitive, section h3': + { + color: theme.palette.text.secondary, + }, + '& .opblock .opblock-summary-operation-id, .opblock .opblock-summary-path, .opblock .opblock-summary-path__deprecated, .opblock .opblock-section-header h4, .parameter__name, .response-col_status, .response-col_links, .responses-inner h4, .swagger-ui .responses-inner h5, .opblock-section-header .btn, .tab li, .info li, .info p, .info table, section.models h4, .info .title, table.model tr.description, .property-row': + { + color: theme.palette.text.primary, + }, + '& .opblock .opblock-section-header, .model-box, section.models .model-container': + { + background: theme.palette.background.default, + }, + '& .prop-format, .parameter__in': { + color: theme.palette.text.disabled, + }, + '& ': { + color: theme.palette.text.primary, + 'border-color': theme.palette.divider, + }, + '& .opblock-description-wrapper p, .opblock-external-docs-wrapper p, .opblock-title_normal p, .response-control-media-type__accept-message, .opblock .opblock-section-header>label, .scheme-container .schemes>label, .info .base-url, .model': + { + color: theme.palette.text.hint, + }, + '& .parameter__name.required:after': { + color: theme.palette.warning.dark, + }, + '& .prop-type': { + color: theme.palette.primary.main, + }, + }, +})); + +type Props = { + definition: string; +}; + +export const OpenApiDefinition = ({ definition }: Props) => { + const classes = useStyles(); + + // Due to a bug in the swagger-ui-react component, the component needs + // to be created without content first. + const [def, setDef] = useState(''); + + useEffect(() => { + const timer = setTimeout(() => setDef(definition), 0); + return () => clearTimeout(timer); + }, [definition, setDef]); + + return ( +
+ +
+ ); +}; diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx index 4c22d29bc2..2c223243d1 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx @@ -14,81 +14,27 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core/styles'; -import React, { useEffect, useState } from 'react'; -import SwaggerUI from 'swagger-ui-react'; -import 'swagger-ui-react/swagger-ui.css'; +import { Progress } from '@backstage/core-components'; +import React, { Suspense } from 'react'; -// TODO: Schemas +// The swagger-ui component and related CSS has a significant size, only load it +// if the element is actually used. +const LazyOpenApiDefinition = React.lazy(() => + import('./OpenApiDefinition').then(m => ({ + default: m.OpenApiDefinition, + })), +); -const useStyles = makeStyles(theme => ({ - root: { - '& .swagger-ui, .info h1, .info h2, .info h3, .info h4, .info h': { - 'font-family': 'inherit', - color: theme.palette.text.primary, - }, - '& .scheme-container': { - 'background-color': theme.palette.background.default, - }, - '& .opblock-tag, .opblock-tag small, table thead tr td, table thead tr th': - { - color: theme.palette.text.primary, - 'border-color': theme.palette.divider, - }, - '& section.models, section.models.is-open h4': { - 'border-color': theme.palette.divider, - }, - '& .opblock .opblock-summary-description, .parameter__type, table.headers td, .model-title, .model .property.primitive, section h3': - { - color: theme.palette.text.secondary, - }, - '& .opblock .opblock-summary-operation-id, .opblock .opblock-summary-path, .opblock .opblock-summary-path__deprecated, .opblock .opblock-section-header h4, .parameter__name, .response-col_status, .response-col_links, .responses-inner h4, .swagger-ui .responses-inner h5, .opblock-section-header .btn, .tab li, .info li, .info p, .info table, section.models h4, .info .title, table.model tr.description, .property-row': - { - color: theme.palette.text.primary, - }, - '& .opblock .opblock-section-header, .model-box, section.models .model-container': - { - background: theme.palette.background.default, - }, - '& .prop-format, .parameter__in': { - color: theme.palette.text.disabled, - }, - '& ': { - color: theme.palette.text.primary, - 'border-color': theme.palette.divider, - }, - '& .opblock-description-wrapper p, .opblock-external-docs-wrapper p, .opblock-title_normal p, .response-control-media-type__accept-message, .opblock .opblock-section-header>label, .scheme-container .schemes>label, .info .base-url, .model': - { - color: theme.palette.text.hint, - }, - '& .parameter__name.required:after': { - color: theme.palette.warning.dark, - }, - '& .prop-type': { - color: theme.palette.primary.main, - }, - }, -})); - -type Props = { +export type OpenApiDefinitionWidgetProps = { definition: string; }; -export const OpenApiDefinitionWidget = ({ definition }: Props) => { - const classes = useStyles(); - - // Due to a bug in the swagger-ui-react component, the component needs - // to be created without content first. - const [def, setDef] = useState(''); - - useEffect(() => { - const timer = setTimeout(() => setDef(definition), 0); - return () => clearTimeout(timer); - }, [definition, setDef]); - +export const OpenApiDefinitionWidget = ({ + definition, +}: OpenApiDefinitionWidgetProps) => { return ( -
- -
+ }> + + ); }; From 4891a8f2e6f8e3c0993b6826676ea97bf0592895 Mon Sep 17 00:00:00 2001 From: Dede Hamzah Date: Wed, 3 Nov 2021 16:08:27 +0700 Subject: [PATCH 004/181] Add Props Icon for Sidebar Item SidebarSearchField and Settings Signed-off-by: Dede Hamzah --- .../src/layout/Sidebar/Items.tsx | 4 +++- .../user-settings/src/components/Settings.tsx | 17 +++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index 883e0395cd..75317ed9ac 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -293,11 +293,13 @@ export const SidebarItem = forwardRef((props, ref) => { type SidebarSearchFieldProps = { onSearch: (input: string) => void; to?: string; + icon?: IconComponent; }; export function SidebarSearchField(props: SidebarSearchFieldProps) { const [input, setInput] = useState(''); const classes = useStyles(); + const Icon = props.icon ? props.icon : SearchIcon; const search = () => { props.onSearch(input); @@ -329,7 +331,7 @@ export function SidebarSearchField(props: SidebarSearchFieldProps) { return (
- + { - return ( - - ); +type SettingsProps = { + icon?: IconComponent; +}; + +export const Settings = (props: SettingsProps) => { + const Icon = props.icon ? props.icon : SettingsIcon; + + return ; }; From 274a4fc6335eff6091e995254636c98b7650d79e Mon Sep 17 00:00:00 2001 From: Dede Hamzah Date: Wed, 3 Nov 2021 16:11:23 +0700 Subject: [PATCH 005/181] add changeset Signed-off-by: Dede Hamzah --- .changeset/khaki-rice-kick.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/khaki-rice-kick.md diff --git a/.changeset/khaki-rice-kick.md b/.changeset/khaki-rice-kick.md new file mode 100644 index 0000000000..54819b855a --- /dev/null +++ b/.changeset/khaki-rice-kick.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-user-settings': patch +--- + +Add Props Icon for Sidebar Item SidebarSearchField and Settings From b90fc74d7099b815d8a22eab4f04bbf106a65b33 Mon Sep 17 00:00:00 2001 From: Bryce Larson Date: Thu, 4 Nov 2021 20:08:58 +1100 Subject: [PATCH 006/181] feat: Add getDefaultProcessors to CatalogBuilder Signed-off-by: Bryce Larson --- .changeset/violet-panthers-care.md | 5 +++ .../src/service/NextCatalogBuilder.ts | 38 +++++++++++++------ 2 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 .changeset/violet-panthers-care.md diff --git a/.changeset/violet-panthers-care.md b/.changeset/violet-panthers-care.md new file mode 100644 index 0000000000..7a43c87f37 --- /dev/null +++ b/.changeset/violet-panthers-care.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +adds getDefaultProcessor method to CatalogBuilder diff --git a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index 835c1dc700..5e0bcd9a75 100644 --- a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts @@ -265,7 +265,8 @@ export class NextCatalogBuilder { * Sets what entity processors to use. These are responsible for reading, * parsing, and processing entities before they are persisted in the catalog. * - * This function replaces the default set of processors; use with care. + * This function replaces the default set of processors, consider using with + * {@link NextCatalogBuilder#getDefaultProcessors}; use with care. * * @param processors One or more processors */ @@ -275,6 +276,30 @@ export class NextCatalogBuilder { return this; } + /** + * Returns the default list of entity processors. These are responsible for reading, + * parsing, and processing entities before they are persisted in the catalog. Changing + * the order of processing can give more control to custom processors. + * + * Consider using with with {@link NextCatalogBuilder#replaceProcessors} + * + */ + getDefaultProcessors(): CatalogProcessor[] { + const { config, logger, reader } = this.env; + const integrations = ScmIntegrations.fromConfig(config); + + return [ + new FileReaderProcessor(), + BitbucketDiscoveryProcessor.fromConfig(config, { logger }), + GithubDiscoveryProcessor.fromConfig(config, { logger }), + GithubOrgReaderProcessor.fromConfig(config, { logger }), + GitLabDiscoveryProcessor.fromConfig(config, { logger }), + new UrlReaderProcessor({ reader, logger }), + CodeOwnersProcessor.fromConfig(config, { logger, reader }), + new AnnotateLocationEntityProcessor({ integrations }), + ]; + } + /** * Sets up the catalog to use a custom parser for entity data. * @@ -416,16 +441,7 @@ export class NextCatalogBuilder { // These are only added unless the user replaced them all if (!this.processorsReplace) { - processors.push( - new FileReaderProcessor(), - BitbucketDiscoveryProcessor.fromConfig(config, { logger }), - GithubDiscoveryProcessor.fromConfig(config, { logger }), - GithubOrgReaderProcessor.fromConfig(config, { logger }), - GitLabDiscoveryProcessor.fromConfig(config, { logger }), - new UrlReaderProcessor({ reader, logger }), - CodeOwnersProcessor.fromConfig(config, { logger, reader }), - new AnnotateLocationEntityProcessor({ integrations }), - ); + processors.push(...this.getDefaultProcessors()); } // Add the ones (if any) that the user added From 23348dd5864a2427c40288b6cf30a103bb267556 Mon Sep 17 00:00:00 2001 From: Bryce Larson <85923137+bryce-od@users.noreply.github.com> Date: Fri, 5 Nov 2021 10:25:28 +1100 Subject: [PATCH 007/181] Update plugins/catalog-backend/src/service/NextCatalogBuilder.ts Co-authored-by: Jussi Hallila Signed-off-by: Bryce Larson --- plugins/catalog-backend/src/service/NextCatalogBuilder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index 5e0bcd9a75..bbda6f070c 100644 --- a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts @@ -281,7 +281,7 @@ export class NextCatalogBuilder { * parsing, and processing entities before they are persisted in the catalog. Changing * the order of processing can give more control to custom processors. * - * Consider using with with {@link NextCatalogBuilder#replaceProcessors} + * Consider using with {@link NextCatalogBuilder#replaceProcessors} * */ getDefaultProcessors(): CatalogProcessor[] { @@ -417,7 +417,7 @@ export class NextCatalogBuilder { } private buildProcessors(): CatalogProcessor[] { - const { config, logger, reader } = this.env; + const { config, reader } = this.env; const integrations = ScmIntegrations.fromConfig(config); this.checkDeprecatedReaderProcessors(); From 1b8efd52f0b1aab5c7860c0ef71675bdd49ce95e Mon Sep 17 00:00:00 2001 From: Jeremy Guarini Date: Mon, 8 Nov 2021 10:33:40 -0800 Subject: [PATCH 008/181] address vale issue with href Signed-off-by: Jeremy Guarini --- .changeset/eight-months-agree.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eight-months-agree.md b/.changeset/eight-months-agree.md index 372c0b624c..0f117d8a40 100644 --- a/.changeset/eight-months-agree.md +++ b/.changeset/eight-months-agree.md @@ -14,6 +14,6 @@ Adds the following to the project list page: Makes breadcrumb a link back to project list for the project details and new project views. -In project list page, updates New project button to use RouterLink instead of href to avoid login prompt. +In project list page, updates New project button to use RouterLink instead of `href` to avoid login prompt. In project details view, links to project details and logs now work, clicking on these will open the project or logs in GCP in new tab. From fdc35dbff7e2226b9230ec8a52552ddb36f2bb73 Mon Sep 17 00:00:00 2001 From: Jeremy Guarini Date: Tue, 9 Nov 2021 14:18:54 -0800 Subject: [PATCH 009/181] swap out data-grid for material-table Signed-off-by: Jeremy Guarini --- plugins/gcp-projects/package.json | 2 +- .../ProjectListPage/ProjectListPage.tsx | 156 +++++++++--------- 2 files changed, 80 insertions(+), 78 deletions(-) diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 71b212a698..48205e4fd8 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -35,9 +35,9 @@ "@backstage/core-plugin-api": "^0.1.12", "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", - "@material-ui/data-grid": "^4.0.0-alpha.37", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", + "material-table": "^1.69.3", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "^6.0.0-beta.0", diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx index e4137f96c7..2797d17d0d 100644 --- a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx @@ -16,8 +16,17 @@ // NEEDS WORK import { Button, LinearProgress, Tooltip, Typography } from '@material-ui/core'; -import { DataGrid, GridColDef, GridCellParams } from '@material-ui/data-grid'; -import React from 'react'; +import MaterialTable from 'material-table'; +import ArrowDownward from '@material-ui/icons/ArrowDownward'; +import ChevronLeft from '@material-ui/icons/ChevronLeft'; +import ChevronRight from '@material-ui/icons/ChevronRight'; +import Clear from '@material-ui/icons/Clear'; +import FilterList from '@material-ui/icons/FilterList'; +import FirstPage from '@material-ui/icons/FirstPage'; +import LastPage from '@material-ui/icons/LastPage'; +import Search from '@material-ui/icons/Search'; +import React, { forwardRef } from 'react'; + import { useAsync } from 'react-use'; import { gcpApiRef, Project } from '../../api'; @@ -59,18 +68,6 @@ const PageContents = () => { const { loading, error, value } = useAsync(() => api.listProjects()); - const [page, setPage] = React.useState(0); - const [rowsPerPage, setRowsPerPage] = React.useState(10); - - const handleChangePage = (newPage: number) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (pageSize: number) => { - setRowsPerPage(pageSize); - setPage(0); - }; - if (loading) { return ; } else if (error) { @@ -81,78 +78,83 @@ const PageContents = () => { ); } - function renderLink(params: GridCellParams) { + function renderLink(id: string) { return ( - + - + ); } - const columns: GridColDef[] = [ - { - align: 'left', - field: 'name', - flex: 1, - headerAlign: 'left', - headerName: 'Name', - }, - { - align: 'left', - field: 'projectNumber', - flex: 0.6, - headerAlign: 'left', - headerName: 'Project Number', - }, - { - align: 'left', - field: 'projectID', - flex: 1, - headerAlign: 'left', - headerName: 'Project ID', - renderCell: renderLink, - }, - { - align: 'left', - field: 'state', - flex: 0.6, - headerAlign: 'left', - headerName: 'State', - }, - { - align: 'left', - field: 'creationTime', - flex: 0.7, - headerAlign: 'left', - headerName: 'Creation Time', - }, - ]; - - const rows = - value?.map((project: Project) => ({ - id: project.projectId, - name: project.name, - projectNumber: project?.projectNumber || 'Error', - projectID: project.projectId, - state: project?.lifecycleState || 'Error', - creationTime: project?.createTime || 'Error', - })) || []; - return (
- ( + + )), + FirstPage: forwardRef((props, ref) => ( + + )), + LastPage: forwardRef((props, ref) => ( + + )), + NextPage: forwardRef((props, ref) => ( + + )), + PreviousPage: forwardRef((props, ref) => ( + + )), + ResetSearch: forwardRef((props, ref) => ( + + )), + Search: forwardRef((props, ref) => ), + SortArrow: forwardRef((props, ref) => ( + + )), + }} + columns={[ + { + field: 'name', + title: 'Name', + defaultSort: 'asc', + }, + { + field: 'projectNumber', + title: 'Project Number', + }, + { + field: 'projectID', + title: 'Project ID', + render: (rowData: { id: string }) => renderLink(rowData.id), + }, + { + field: 'state', + title: 'State', + }, + { + field: 'creationTime', + title: 'Creation Time', + }, + ]} + data={ + value?.map((project: Project) => ({ + id: project.projectId, + name: project.name, + projectNumber: project?.projectNumber || 'Error', + projectID: project.projectId, + state: project?.lifecycleState || 'Error', + creationTime: project?.createTime || 'Error', + })) || [] + } + options={{ + filtering: true, + pageSize: 5, + pageSizeOptions: [5, 10, 25, 50, 100], + showTitle: false, + }} />
); From 4a336fd292944b642f57e891cc8e9fbb5a48a5ee Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 10 Nov 2021 09:21:02 +0100 Subject: [PATCH 010/181] core-plugin-api: Deprecate use of extensions without name Signed-off-by: Johan Haals --- .changeset/chilly-emus-roll.md | 5 +++++ .changeset/mighty-gifts-visit.md | 5 +++++ .changeset/quiet-clocks-push.md | 5 +++++ .../software-catalog/catalog-customization.md | 1 + docs/plugins/composability.md | 2 ++ docs/plugins/plugin-development.md | 1 + docs/plugins/structure-of-a-plugin.md | 1 + packages/core-app-api/src/app/App.test.tsx | 1 + .../src/extensions/extensions.test.tsx | 2 ++ .../src/extensions/extensions.tsx | 19 +++++++++++++------ plugins/bazaar/src/plugin.ts | 1 + plugins/home/src/extensions.tsx | 3 +++ plugins/home/src/plugin.ts | 2 ++ 13 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 .changeset/chilly-emus-roll.md create mode 100644 .changeset/mighty-gifts-visit.md create mode 100644 .changeset/quiet-clocks-push.md diff --git a/.changeset/chilly-emus-roll.md b/.changeset/chilly-emus-roll.md new file mode 100644 index 0000000000..1569820980 --- /dev/null +++ b/.changeset/chilly-emus-roll.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Add name option to `createCardExtension` to remove deprecation warnings for extensions without name. Name will be required for extensions in a future release of `core-plugin-api` and therefore also in `createCardExtension`. diff --git a/.changeset/mighty-gifts-visit.md b/.changeset/mighty-gifts-visit.md new file mode 100644 index 0000000000..8ceab2906e --- /dev/null +++ b/.changeset/mighty-gifts-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-bazaar': patch +--- + +Name extension to remove deprecation warning diff --git a/.changeset/quiet-clocks-push.md b/.changeset/quiet-clocks-push.md new file mode 100644 index 0000000000..3289e1886b --- /dev/null +++ b/.changeset/quiet-clocks-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': patch +--- + +Deprecate use of extensions without name. Adds a warning to the developer console to opt prompt integrators to provide names for extensions. diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index 8f855776a0..017285719a 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -172,6 +172,7 @@ This page itself can be exported as a routable extension in the plugin: ```ts export const CustomCatalogIndexPage = myPlugin.provide( createRoutableExtension({ + name: 'CustomCatalogPage', component: () => import('./components/CustomCatalogPage').then(m => m.CustomCatalogPage), mountPoint: catalogRouteRef, diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index 8d22c05a79..33568ac561 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -136,6 +136,7 @@ a component: ```ts export const FooPage = plugin.provide( createRoutableExtension({ + name: 'FooPage', component: () => import('./components/FooPage').then(m => m.FooPage), mountPoint: fooPageRouteRef, }), @@ -417,6 +418,7 @@ export const myPlugin = createPlugin({ export const MyPage = myPlugin.provide( createRoutableExtension({ + name: 'MyPage', component: () => import('./components/MyPage').then(m => m.MyPage), mountPoint: rootRouteRef, }), diff --git a/docs/plugins/plugin-development.md b/docs/plugins/plugin-development.md index f61fae0968..362ecc5d5a 100644 --- a/docs/plugins/plugin-development.md +++ b/docs/plugins/plugin-development.md @@ -65,6 +65,7 @@ export const examplePlugin = createPlugin({ // Each extension should also be exported from your plugin package. export const ExamplePage = examplePlugin.provide( createRoutableExtension({ + name: 'ExamplePage', // The component needs to be lazy-loaded. It's what will actually be rendered in the end. component: () => import('./components/ExampleComponent').then(m => m.ExampleComponent), diff --git a/docs/plugins/structure-of-a-plugin.md b/docs/plugins/structure-of-a-plugin.md index 83fcfbddba..9e8b67710a 100644 --- a/docs/plugins/structure-of-a-plugin.md +++ b/docs/plugins/structure-of-a-plugin.md @@ -73,6 +73,7 @@ export const examplePlugin = createPlugin({ export const ExamplePage = examplePlugin.provide( createRoutableExtension({ + name: 'ExamplePage', component: () => import('./components/ExampleComponent').then(m => m.ExampleComponent), mountPoint: rootRouteRef, diff --git a/packages/core-app-api/src/app/App.test.tsx b/packages/core-app-api/src/app/App.test.tsx index ab287a3ece..6ed5d45b86 100644 --- a/packages/core-app-api/src/app/App.test.tsx +++ b/packages/core-app-api/src/app/App.test.tsx @@ -169,6 +169,7 @@ describe('Integration Test', () => { const NavigateComponent = plugin1.provide( createRoutableExtension({ + name: 'NavigateComponent', component: () => Promise.resolve((_: PropsWithChildren<{ path?: string }>) => { return ; diff --git a/packages/core-plugin-api/src/extensions/extensions.test.tsx b/packages/core-plugin-api/src/extensions/extensions.test.tsx index 29c696c71f..dcc92d9d47 100644 --- a/packages/core-plugin-api/src/extensions/extensions.test.tsx +++ b/packages/core-plugin-api/src/extensions/extensions.test.tsx @@ -41,6 +41,7 @@ describe('extensions', () => { const Component = () =>
; const extension = createReactExtension({ + name: 'Extension', component: { sync: Component, }, @@ -67,6 +68,7 @@ describe('extensions', () => { }); const extension2 = createRoutableExtension({ + name: 'Extension2', component: () => Promise.resolve(Component), mountPoint: routeRef, }); diff --git a/packages/core-plugin-api/src/extensions/extensions.tsx b/packages/core-plugin-api/src/extensions/extensions.tsx index f85570a35b..ca5ada8478 100644 --- a/packages/core-plugin-api/src/extensions/extensions.tsx +++ b/packages/core-plugin-api/src/extensions/extensions.tsx @@ -55,7 +55,7 @@ export function createRoutableExtension< mountPoint: RouteRef; name?: string; }): Extension { - const { component, mountPoint } = options; + const { component, mountPoint, name } = options; return createReactExtension({ component: { lazy: () => @@ -85,7 +85,7 @@ export function createRoutableExtension< }; const componentName = - options.name || + name || (InnerComponent as { displayName?: string }).displayName || InnerComponent.name || 'LazyComponent'; @@ -108,7 +108,7 @@ export function createRoutableExtension< data: { 'core.mountPoint': mountPoint, }, - name: options.name, + name, }); } @@ -152,7 +152,14 @@ export function createReactExtension< data?: Record; name?: string; }): Extension { - const { data = {} } = options; + const { data = {}, name } = options; + if (!name) { + // eslint-disable-next-line no-console + console.warn( + 'Declaring extensions without name is DEPRECATED. ' + + 'Make sure that all usages of createReactExtension, createComponentExtension and createRoutableExtension provide a name.', + ); + } let Component: T; if ('lazy' in options.component) { @@ -164,7 +171,7 @@ export function createReactExtension< Component = options.component.sync; } const componentName = - options.name || + name || (Component as { displayName?: string }).displayName || Component.name || 'Component'; @@ -186,7 +193,7 @@ export function createReactExtension< diff --git a/plugins/bazaar/src/plugin.ts b/plugins/bazaar/src/plugin.ts index f96eaf522d..5eb5e2aacd 100644 --- a/plugins/bazaar/src/plugin.ts +++ b/plugins/bazaar/src/plugin.ts @@ -44,6 +44,7 @@ export const bazaarPlugin = createPlugin({ export const BazaarPage = bazaarPlugin.provide( createRoutableExtension({ + name: 'BazaarPage', component: () => import('./components/HomePage').then(m => m.HomePage), mountPoint: rootRouteRef, }), diff --git a/plugins/home/src/extensions.tsx b/plugins/home/src/extensions.tsx index 93fe4ceb4f..82394cf1ea 100644 --- a/plugins/home/src/extensions.tsx +++ b/plugins/home/src/extensions.tsx @@ -37,11 +37,14 @@ type RendererProps = { title: string } & ComponentParts; export function createCardExtension({ title, components, + name, }: { title: string; components: () => Promise; + name?: string; }) { return createReactExtension({ + name, component: { lazy: () => components().then(({ Content, Actions, Settings, ContextProvider }) => { diff --git a/plugins/home/src/plugin.ts b/plugins/home/src/plugin.ts index d67ea846a5..b4b9f9b6ee 100644 --- a/plugins/home/src/plugin.ts +++ b/plugins/home/src/plugin.ts @@ -71,6 +71,7 @@ export const ComponentTab = homePlugin.provide( */ export const WelcomeTitle = homePlugin.provide( createComponentExtension({ + name: 'WelcomeTitle', component: { lazy: () => import('./homePageComponents/WelcomeTitle').then(m => m.WelcomeTitle), @@ -80,6 +81,7 @@ export const WelcomeTitle = homePlugin.provide( export const HomePageRandomJoke = homePlugin.provide( createCardExtension<{ defaultCategory?: 'any' | 'programming' }>({ + name: 'HomePageRandomJoke', title: 'Random Joke', components: () => import('./homePageComponents/RandomJoke'), }), From 3487920e5e7b705ae96365052ff2b6ff8144b4b5 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 10 Nov 2021 09:49:44 +0100 Subject: [PATCH 011/181] api report Signed-off-by: Johan Haals --- plugins/home/api-report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/home/api-report.md b/plugins/home/api-report.md index 57b0b899e2..99d88fa4b5 100644 --- a/plugins/home/api-report.md +++ b/plugins/home/api-report.md @@ -71,9 +71,11 @@ export const ComponentTabs: ({ export function createCardExtension({ title, components, + name, }: { title: string; components: () => Promise; + name?: string; }): Extension< ({ Renderer, From 598057f348028332f2d55dcd59a9f4ac42bdd557 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 10 Nov 2021 10:59:38 +0100 Subject: [PATCH 012/181] Update docs/features/software-catalog/catalog-customization.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johan Haals Co-authored-by: Fredrik Adelöw --- docs/features/software-catalog/catalog-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index 017285719a..e80e8a1842 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -172,7 +172,7 @@ This page itself can be exported as a routable extension in the plugin: ```ts export const CustomCatalogIndexPage = myPlugin.provide( createRoutableExtension({ - name: 'CustomCatalogPage', + name: 'CustomCatalogIndexPage', component: () => import('./components/CustomCatalogPage').then(m => m.CustomCatalogPage), mountPoint: catalogRouteRef, From f1403fac6437a2726d226d8ebe4eedcba93e0d14 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 10 Nov 2021 11:01:16 +0100 Subject: [PATCH 013/181] Update .changeset/quiet-clocks-push.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johan Haals Co-authored-by: Fredrik Adelöw --- .changeset/quiet-clocks-push.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/quiet-clocks-push.md b/.changeset/quiet-clocks-push.md index 3289e1886b..2b469a5fc5 100644 --- a/.changeset/quiet-clocks-push.md +++ b/.changeset/quiet-clocks-push.md @@ -2,4 +2,4 @@ '@backstage/core-plugin-api': patch --- -Deprecate use of extensions without name. Adds a warning to the developer console to opt prompt integrators to provide names for extensions. +Deprecate use of extensions without name. Adds a warning to the developer console to prompt integrators to provide names for extensions. From 1d0e5c55f828d25395ad44a14f0428af8ffe06ca Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Wed, 10 Nov 2021 12:36:50 +0000 Subject: [PATCH 014/181] Install dependeices before trying to use them in the Snyk github issue workflow Signed-off-by: Harry Hogg --- .github/workflows/snyk-github-issue-sync.yml | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/snyk-github-issue-sync.yml b/.github/workflows/snyk-github-issue-sync.yml index 5f49be4e1a..77604a126d 100644 --- a/.github/workflows/snyk-github-issue-sync.yml +++ b/.github/workflows/snyk-github-issue-sync.yml @@ -7,8 +7,42 @@ on: jobs: sync: runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x] + steps: - uses: actions/checkout@v2 + + # Beginning of yarn setup, keep in sync between all workflows, see ci.yml + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ # Needed for auth + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: yarn install + run: yarn install --frozen-lockfile + # End of yarn setup + - name: Create Snyk report uses: snyk/actions/node@master continue-on-error: true # Snyk CLI exits with error when vulnerabilities are found From 7e18ed7f29d1dfb4dbc8a290386ecb806619db40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 10 Nov 2021 14:29:35 +0100 Subject: [PATCH 015/181] Remove the unused UserFlags type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/sharp-meals-fetch.md | 5 +++++ packages/core-plugin-api/api-report.md | 3 --- .../src/apis/definitions/FeatureFlagsApi.ts | 7 ------- 3 files changed, 5 insertions(+), 10 deletions(-) create mode 100644 .changeset/sharp-meals-fetch.md diff --git a/.changeset/sharp-meals-fetch.md b/.changeset/sharp-meals-fetch.md new file mode 100644 index 0000000000..1d3df65ba4 --- /dev/null +++ b/.changeset/sharp-meals-fetch.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': minor +--- + +Removed the unused `UserFlags` type. diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index acfc8d4a45..f0cee2a42c 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -799,9 +799,6 @@ export function useElementFilter( dependencies?: any[], ): T; -// @public -export type UserFlags = {}; - // @public export function useRouteRef( routeRef: ExternalRouteRef, diff --git a/packages/core-plugin-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-plugin-api/src/apis/definitions/FeatureFlagsApi.ts index c669710895..ac43bbebe8 100644 --- a/packages/core-plugin-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/FeatureFlagsApi.ts @@ -61,13 +61,6 @@ export type FeatureFlagsSaveOptions = { merge?: boolean; }; -/** - * User flags alias. - * - * @public - */ -export type UserFlags = {}; - /** * The feature flags API is used to toggle functionality to users across plugins and Backstage. * From e0861b92ffd3435deb02288e1cf3d34ea309a121 Mon Sep 17 00:00:00 2001 From: James Turley Date: Thu, 4 Nov 2021 16:35:15 +0000 Subject: [PATCH 016/181] Add prop to override color selection in gauge components Signed-off-by: James Turley --- .changeset/grumpy-pugs-report.md | 23 +++++++++++++++++++ .../components/ProgressBars/Gauge.stories.tsx | 6 +++++ .../src/components/ProgressBars/Gauge.tsx | 20 ++++++++++++++-- .../ProgressBars/GaugeCard.stories.tsx | 21 +++++++++++++++++ .../src/components/ProgressBars/GaugeCard.tsx | 14 ++++++++--- .../ProgressBars/LinearGauge.stories.tsx | 6 +++++ .../components/ProgressBars/LinearGauge.tsx | 14 +++++++---- 7 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 .changeset/grumpy-pugs-report.md diff --git a/.changeset/grumpy-pugs-report.md b/.changeset/grumpy-pugs-report.md new file mode 100644 index 0000000000..6ee56f2bf7 --- /dev/null +++ b/.changeset/grumpy-pugs-report.md @@ -0,0 +1,23 @@ +--- +'@backstage/core-components': patch +--- + +Add new way to override color selection to progress bar/gauge components. + +`Gauge`, `LinearGauge` and `GaugeCard` all accept a `getColor` prop, +which is a function of the type: + +```ts +export type GetColor = (args: { + palette: Palette; + value: number; + inverse?: boolean; + max?: number; +}) => string | PaletteColor; +``` + +Either return a standard Material UI palette color object or a CSS color +string (e.g. "red", "#f02020"), and the gauge will be set to that color. + +If the prop is omitted, the default implementation is unchanged from previous +versions. diff --git a/packages/core-components/src/components/ProgressBars/Gauge.stories.tsx b/packages/core-components/src/components/ProgressBars/Gauge.stories.tsx index 3481c48cef..401f502697 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.stories.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.stories.tsx @@ -53,3 +53,9 @@ export const AbsoluteProgress = () => (
); + +export const StaticColor = () => ( +
+ '#f0f'} value={0.5} /> +
+); diff --git a/packages/core-components/src/components/ProgressBars/Gauge.tsx b/packages/core-components/src/components/ProgressBars/Gauge.tsx index 83e8d05b16..097fc9419d 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.tsx @@ -52,8 +52,16 @@ type Props = { inverse?: boolean; unit?: string; max?: number; + getColor?: GetColor; }; +export type GetColor = (args: { + palette: BackstageTheme['palette']; + value: number; + inverse?: boolean; + max?: number; +}) => string | BackstageTheme['palette']['error']; + const defaultProps = { fractional: true, inverse: false, @@ -83,10 +91,18 @@ export function getProgressColor( return palette.status.ok; } +export const defaultGetProgressColor: GetColor = ({ + palette, + value, + inverse, + max, +}) => getProgressColor(palette, value, inverse, max); + /** @public */ export function Gauge(props: Props) { + const { getColor = defaultGetProgressColor } = props; const classes = useStyles(props); - const theme = useTheme(); + const { palette } = useTheme(); const { value, fractional, inverse, unit, max } = { ...defaultProps, ...props, @@ -102,7 +118,7 @@ export function Gauge(props: Props) { percent={asPercentage} strokeWidth={12} trailWidth={12} - strokeColor={getProgressColor(theme.palette, asActual, inverse, max)} + strokeColor={getColor({ palette, value: asActual, inverse, max })} className={classes.circle} />
diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx index ad3323f8d3..f2aad14cd9 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx @@ -97,3 +97,24 @@ export const LinkInFooter = () => ( ); + +export const StaticColor = () => ( + + + '#f00'} title="Red" progress={0.5} /> + + + '#0f0'} title="Green" progress={0.5} /> + + + '#00f'} title="Blue" progress={0.5} /> + + + palette.error} + title="palette.error" + progress={0.5} + /> + + +); diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.tsx index fe17de30cd..f1fd281b35 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import { InfoCard, InfoCardVariants } from '../../layout/InfoCard'; import { BottomLinkProps } from '../../layout/BottomLink'; -import { Gauge } from './Gauge'; +import { Gauge, GetColor } from './Gauge'; type Props = { title: string; @@ -28,6 +28,7 @@ type Props = { progress: number; inverse?: boolean; deepLink?: BottomLinkProps; + getColor?: GetColor; }; /** @public */ @@ -46,7 +47,14 @@ const useStyles = makeStyles( /** @public */ export function GaugeCard(props: Props) { const classes = useStyles(props); - const { title, subheader, progress, inverse, deepLink, variant } = props; + const { title, subheader, progress, inverse, deepLink, variant, getColor } = + props; + + const gaugeProps = { + inverse, + getColor, + value: progress, + }; return (
@@ -56,7 +64,7 @@ export function GaugeCard(props: Props) { deepLink={deepLink} variant={variant} > - +
); diff --git a/packages/core-components/src/components/ProgressBars/LinearGauge.stories.tsx b/packages/core-components/src/components/ProgressBars/LinearGauge.stories.tsx index ee2ed2eae6..5d36ea9178 100644 --- a/packages/core-components/src/components/ProgressBars/LinearGauge.stories.tsx +++ b/packages/core-components/src/components/ProgressBars/LinearGauge.stories.tsx @@ -41,3 +41,9 @@ export const LowProgress = () => (
); + +export const StaticColor = () => ( +
+ '#f0f'} value={0.5} /> +
+); diff --git a/packages/core-components/src/components/ProgressBars/LinearGauge.tsx b/packages/core-components/src/components/ProgressBars/LinearGauge.tsx index afc93fd337..ac6097195d 100644 --- a/packages/core-components/src/components/ProgressBars/LinearGauge.tsx +++ b/packages/core-components/src/components/ProgressBars/LinearGauge.tsx @@ -20,18 +20,19 @@ import Tooltip from '@material-ui/core/Tooltip'; // @ts-ignore import { Line } from 'rc-progress'; import { BackstageTheme } from '@backstage/theme'; -import { getProgressColor } from './Gauge'; +import { defaultGetProgressColor, GetColor } from './Gauge'; type Props = { /** * Progress value between 0.0 - 1.0. */ value: number; + getColor?: GetColor; }; export function LinearGauge(props: Props) { - const { value } = props; - const theme = useTheme(); + const { value, getColor = defaultGetProgressColor } = props; + const { palette } = useTheme(); if (isNaN(value)) { return null; } @@ -39,7 +40,12 @@ export function LinearGauge(props: Props) { if (percent > 100) { percent = 100; } - const strokeColor = getProgressColor(theme.palette, percent, false, 100); + const strokeColor = getColor({ + palette, + value: percent, + inverse: false, + max: 100, + }); return ( From 6df5e58f7b86ce476a768794492f482fff5c72fe Mon Sep 17 00:00:00 2001 From: James Turley Date: Fri, 5 Nov 2021 12:46:49 +0000 Subject: [PATCH 017/181] Modify exported getProgressColor to accept arguments object Signed-off-by: James Turley --- .../components/ProgressBars/Gauge.test.tsx | 26 +++++++++++----- .../src/components/ProgressBars/Gauge.tsx | 31 +++++++++---------- .../components/ProgressBars/LinearGauge.tsx | 4 +-- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/packages/core-components/src/components/ProgressBars/Gauge.test.tsx b/packages/core-components/src/components/ProgressBars/Gauge.test.tsx index 066e1233a0..2d9e852e6c 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.test.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.test.tsx @@ -56,16 +56,26 @@ describe('', () => { }; it('colors the progress correctly', () => { - expect(getProgressColor(palette, 'Not a Number' as any)).toBe('#ddd'); - expect(getProgressColor(palette, 10)).toBe(error); - expect(getProgressColor(palette, 50)).toBe(warning); - expect(getProgressColor(palette, 90)).toBe(ok); + expect(getProgressColor({ palette, value: 'Not a Number' as any })).toBe( + '#ddd', + ); + expect(getProgressColor({ palette, value: 10 })).toBe(error); + expect(getProgressColor({ palette, value: 50 })).toBe(warning); + expect(getProgressColor({ palette, value: 90 })).toBe(ok); }); it('colors the inverse progress correctly', () => { - expect(getProgressColor(palette, 'Not a Number' as any)).toBe('#ddd'); - expect(getProgressColor(palette, 10, true)).toBe(ok); - expect(getProgressColor(palette, 50, true)).toBe(warning); - expect(getProgressColor(palette, 90, true)).toBe(error); + expect( + getProgressColor({ + palette, + value: 'Not a Number' as any, + inverse: true, + }), + ).toBe('#ddd'); + expect(getProgressColor({ palette, value: 10, inverse: true })).toBe(ok); + expect(getProgressColor({ palette, value: 50, inverse: true })).toBe( + warning, + ); + expect(getProgressColor({ palette, value: 90, inverse: true })).toBe(error); }); }); diff --git a/packages/core-components/src/components/ProgressBars/Gauge.tsx b/packages/core-components/src/components/ProgressBars/Gauge.tsx index 097fc9419d..2bcc0c3976 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.tsx @@ -55,12 +55,16 @@ type Props = { getColor?: GetColor; }; -export type GetColor = (args: { +type GetColorArgs = { palette: BackstageTheme['palette']; value: number; inverse?: boolean; max?: number; -}) => string | BackstageTheme['palette']['error']; +}; + +export type GetColor = ( + args: GetColorArgs, +) => string | BackstageTheme['palette']['error']; const defaultProps = { fractional: true, @@ -69,12 +73,12 @@ const defaultProps = { max: 100, }; -export function getProgressColor( - palette: BackstageTheme['palette'], - value: number, - inverse?: boolean, - max?: number, -) { +export const getProgressColor: GetColor = ({ + palette, + value, + inverse, + max, +}) => { if (isNaN(value)) { return '#ddd'; } @@ -89,18 +93,11 @@ export function getProgressColor( } return palette.status.ok; -} - -export const defaultGetProgressColor: GetColor = ({ - palette, - value, - inverse, - max, -}) => getProgressColor(palette, value, inverse, max); +}; /** @public */ export function Gauge(props: Props) { - const { getColor = defaultGetProgressColor } = props; + const { getColor = getProgressColor } = props; const classes = useStyles(props); const { palette } = useTheme(); const { value, fractional, inverse, unit, max } = { diff --git a/packages/core-components/src/components/ProgressBars/LinearGauge.tsx b/packages/core-components/src/components/ProgressBars/LinearGauge.tsx index ac6097195d..e331ed1fa5 100644 --- a/packages/core-components/src/components/ProgressBars/LinearGauge.tsx +++ b/packages/core-components/src/components/ProgressBars/LinearGauge.tsx @@ -20,7 +20,7 @@ import Tooltip from '@material-ui/core/Tooltip'; // @ts-ignore import { Line } from 'rc-progress'; import { BackstageTheme } from '@backstage/theme'; -import { defaultGetProgressColor, GetColor } from './Gauge'; +import { getProgressColor, GetColor } from './Gauge'; type Props = { /** @@ -31,7 +31,7 @@ type Props = { }; export function LinearGauge(props: Props) { - const { value, getColor = defaultGetProgressColor } = props; + const { value, getColor = getProgressColor } = props; const { palette } = useTheme(); if (isNaN(value)) { return null; From 60f5d96953210a320934c8b248b0f0f41a02a42e Mon Sep 17 00:00:00 2001 From: James Turley Date: Wed, 10 Nov 2021 14:43:24 +0000 Subject: [PATCH 018/181] Rename gauge props to GaugeProps and export Signed-off-by: James Turley --- packages/core-components/api-report.md | 24 +++++++++---------- .../src/components/ProgressBars/Gauge.tsx | 10 ++++---- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index cc884c9e8f..13929db7a3 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -100,7 +100,7 @@ export type BottomLinkProps = { // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function Breadcrumbs(props: Props_21): JSX.Element; +export function Breadcrumbs(props: Props_20): JSX.Element; // @public (undocumented) export type BreadcrumbsClickableTextClassKey = 'root'; @@ -157,7 +157,7 @@ export interface CodeSnippetProps { // Warning: (ae-missing-release-tag) "Content" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Content(props: PropsWithChildren): JSX.Element; +export function Content(props: PropsWithChildren): JSX.Element; // Warning: (ae-forgotten-export) The symbol "ContentHeaderProps" needs to be exported by the entry point index.d.ts // @@ -368,10 +368,10 @@ export function FeatureCalloutCircular( // @public (undocumented) export type FiltersContainerClassKey = 'root' | 'title'; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "GaugeProps" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function Gauge(props: Props_11): JSX.Element; +export function Gauge(props: GaugeProps): JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // @@ -393,7 +393,7 @@ export function GroupIcon(props: IconComponentProps): JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function Header(props: PropsWithChildren): JSX.Element; +export function Header(props: PropsWithChildren): JSX.Element; // @public (undocumented) export type HeaderClassKey = @@ -484,7 +484,7 @@ export type IconLinkVerticalProps = { // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function InfoCard(props: Props_16): JSX.Element; +export function InfoCard(props: Props_15): JSX.Element; // @public (undocumented) export type InfoCardClassKey = @@ -565,7 +565,7 @@ export type LifecycleClassKey = 'alpha' | 'beta'; // Warning: (ae-missing-release-tag) "LinearGauge" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function LinearGauge(props: Props_12): JSX.Element | null; +export function LinearGauge(props: Props_11): JSX.Element | null; // Warning: (ae-missing-release-tag) "LinkType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -666,7 +666,7 @@ export type OverflowTooltipClassKey = 'container'; // Warning: (ae-missing-release-tag) "Page" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Page(props: PropsWithChildren): JSX.Element; +export function Page(props: PropsWithChildren): JSX.Element; // Warning: (ae-missing-release-tag) "PageClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -771,7 +771,7 @@ export type SelectInputBaseClassKey = 'root' | 'input'; // Warning: (ae-missing-release-tag) "Sidebar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Sidebar(props: PropsWithChildren): JSX.Element; +export function Sidebar(props: PropsWithChildren): JSX.Element; // Warning: (ae-missing-release-tag) "SIDEBAR_INTRO_LOCAL_STORAGE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1961,7 +1961,7 @@ export const SidebarSpacer: React_2.ComponentType< // Warning: (ae-missing-release-tag) "SignInPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SignInPage(props: Props_19): JSX.Element; +export function SignInPage(props: Props_18): JSX.Element; // Warning: (ae-missing-release-tag) "SignInPageClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2050,7 +2050,7 @@ export function StatusWarning(props: PropsWithChildren<{}>): JSX.Element; // Warning: (ae-missing-release-tag) "StructuredMetadataTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StructuredMetadataTable(props: Props_13): JSX.Element; +export function StructuredMetadataTable(props: Props_12): JSX.Element; // Warning: (ae-missing-release-tag) "StructuredMetadataTableListClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2132,7 +2132,7 @@ export type TabBarClassKey = 'indicator' | 'flexContainer' | 'root'; // Warning: (ae-missing-release-tag) "TabbedCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function TabbedCard(props: PropsWithChildren): JSX.Element; +export function TabbedCard(props: PropsWithChildren): JSX.Element; // Warning: (ae-missing-release-tag) "TabbedCardClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/packages/core-components/src/components/ProgressBars/Gauge.tsx b/packages/core-components/src/components/ProgressBars/Gauge.tsx index 2bcc0c3976..98d5a8a000 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.tsx @@ -46,7 +46,7 @@ const useStyles = makeStyles( { name: 'BackstageGauge' }, ); -type Props = { +export type GaugeProps = { value: number; fractional?: boolean; inverse?: boolean; @@ -66,7 +66,7 @@ export type GetColor = ( args: GetColorArgs, ) => string | BackstageTheme['palette']['error']; -const defaultProps = { +const defaultGaugeProps = { fractional: true, inverse: false, unit: '%', @@ -83,7 +83,7 @@ export const getProgressColor: GetColor = ({ return '#ddd'; } - const actualMax = max ? max : defaultProps.max; + const actualMax = max ? max : defaultGaugeProps.max; const actualValue = inverse ? actualMax - value : value; if (actualValue < actualMax / 3) { @@ -96,12 +96,12 @@ export const getProgressColor: GetColor = ({ }; /** @public */ -export function Gauge(props: Props) { +export function Gauge(props: GaugeProps) { const { getColor = getProgressColor } = props; const classes = useStyles(props); const { palette } = useTheme(); const { value, fractional, inverse, unit, max } = { - ...defaultProps, + ...defaultGaugeProps, ...props, }; From 2e641b622b02b256240f5664a782a820e20cc617 Mon Sep 17 00:00:00 2001 From: James Turley Date: Wed, 10 Nov 2021 14:49:15 +0000 Subject: [PATCH 019/181] Only return string from color getter type Signed-off-by: James Turley --- .../core-components/src/components/ProgressBars/Gauge.tsx | 4 +--- .../src/components/ProgressBars/GaugeCard.stories.tsx | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/core-components/src/components/ProgressBars/Gauge.tsx b/packages/core-components/src/components/ProgressBars/Gauge.tsx index 98d5a8a000..ef39a81d4e 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.tsx @@ -62,9 +62,7 @@ type GetColorArgs = { max?: number; }; -export type GetColor = ( - args: GetColorArgs, -) => string | BackstageTheme['palette']['error']; +export type GetColor = (args: GetColorArgs) => string; const defaultGaugeProps = { fractional: true, diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx index f2aad14cd9..64a6cc1b16 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx @@ -111,8 +111,8 @@ export const StaticColor = () => ( palette.error} - title="palette.error" + getColor={({ palette }) => palette.status.error} + title="palette.status.error" progress={0.5} /> From 1e3388a0f47843fabbb1b80515514145f5879845 Mon Sep 17 00:00:00 2001 From: James Turley Date: Wed, 10 Nov 2021 15:13:03 +0000 Subject: [PATCH 020/181] Fixup and rename Gauge color getter function types Signed-off-by: James Turley --- .../src/components/ProgressBars/Gauge.tsx | 12 ++++++------ .../src/components/ProgressBars/GaugeCard.tsx | 4 ++-- .../src/components/ProgressBars/LinearGauge.tsx | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core-components/src/components/ProgressBars/Gauge.tsx b/packages/core-components/src/components/ProgressBars/Gauge.tsx index ef39a81d4e..ef8ec08378 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.tsx @@ -15,7 +15,7 @@ */ import { makeStyles, useTheme } from '@material-ui/core/styles'; -import { BackstageTheme } from '@backstage/theme'; +import { BackstagePalette, BackstageTheme } from '@backstage/theme'; import { Circle } from 'rc-progress'; import React from 'react'; @@ -52,17 +52,17 @@ export type GaugeProps = { inverse?: boolean; unit?: string; max?: number; - getColor?: GetColor; + getColor?: GaugePropsGetColor; }; -type GetColorArgs = { - palette: BackstageTheme['palette']; +export type GaugePropsGetColorOptions = { + palette: BackstagePalette; value: number; inverse?: boolean; max?: number; }; -export type GetColor = (args: GetColorArgs) => string; +export type GaugePropsGetColor = (args: GaugePropsGetColorOptions) => string; const defaultGaugeProps = { fractional: true, @@ -71,7 +71,7 @@ const defaultGaugeProps = { max: 100, }; -export const getProgressColor: GetColor = ({ +export const getProgressColor: GaugePropsGetColor = ({ palette, value, inverse, diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.tsx index f1fd281b35..0fa51b3919 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import { InfoCard, InfoCardVariants } from '../../layout/InfoCard'; import { BottomLinkProps } from '../../layout/BottomLink'; -import { Gauge, GetColor } from './Gauge'; +import { Gauge, GaugePropsGetColor } from './Gauge'; type Props = { title: string; @@ -28,7 +28,7 @@ type Props = { progress: number; inverse?: boolean; deepLink?: BottomLinkProps; - getColor?: GetColor; + getColor?: GaugePropsGetColor; }; /** @public */ diff --git a/packages/core-components/src/components/ProgressBars/LinearGauge.tsx b/packages/core-components/src/components/ProgressBars/LinearGauge.tsx index e331ed1fa5..f1e4fb3334 100644 --- a/packages/core-components/src/components/ProgressBars/LinearGauge.tsx +++ b/packages/core-components/src/components/ProgressBars/LinearGauge.tsx @@ -20,14 +20,14 @@ import Tooltip from '@material-ui/core/Tooltip'; // @ts-ignore import { Line } from 'rc-progress'; import { BackstageTheme } from '@backstage/theme'; -import { getProgressColor, GetColor } from './Gauge'; +import { getProgressColor, GaugePropsGetColor } from './Gauge'; type Props = { /** * Progress value between 0.0 - 1.0. */ value: number; - getColor?: GetColor; + getColor?: GaugePropsGetColor; }; export function LinearGauge(props: Props) { From f5e5c297ceb396cb87a3ad0fba734d461da225fb Mon Sep 17 00:00:00 2001 From: James Turley Date: Wed, 10 Nov 2021 15:19:58 +0000 Subject: [PATCH 021/181] Update changeset to reflect naming changes Signed-off-by: James Turley --- .changeset/grumpy-pugs-report.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/grumpy-pugs-report.md b/.changeset/grumpy-pugs-report.md index 6ee56f2bf7..db915b7a79 100644 --- a/.changeset/grumpy-pugs-report.md +++ b/.changeset/grumpy-pugs-report.md @@ -8,16 +8,16 @@ Add new way to override color selection to progress bar/gauge components. which is a function of the type: ```ts -export type GetColor = (args: { +export type GaugePropsGetColor = (args: { palette: Palette; value: number; inverse?: boolean; max?: number; -}) => string | PaletteColor; +}) => string; ``` -Either return a standard Material UI palette color object or a CSS color -string (e.g. "red", "#f02020"), and the gauge will be set to that color. +Return a standard CSS color string (e.g. "red", "#f02020"), and the gauge will +be set to that color. If the prop is omitted, the default implementation is unchanged from previous versions. From b393c4d4be0ddbf4ce8c343ea95b8a57479b07c6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 14:57:56 +0100 Subject: [PATCH 022/181] cli: fix for config:check only validating frontend config Signed-off-by: Patrik Oldsberg --- .changeset/chilled-pandas-leave.md | 5 +++++ docs/local-dev/cli-commands.md | 3 ++- packages/cli/src/commands/config/print.ts | 1 + packages/cli/src/commands/config/validate.ts | 1 + packages/cli/src/commands/index.ts | 1 + packages/cli/src/lib/config.ts | 5 ++++- 6 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 .changeset/chilled-pandas-leave.md diff --git a/.changeset/chilled-pandas-leave.md b/.changeset/chilled-pandas-leave.md new file mode 100644 index 0000000000..7ad5953f7f --- /dev/null +++ b/.changeset/chilled-pandas-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed the `config:check` command that was incorrectly only validating frontend configuration. Also added a `--frontend` flag to the command which maintains that behavior. diff --git a/docs/local-dev/cli-commands.md b/docs/local-dev/cli-commands.md index 0397ffa09c..0b14a2e136 100644 --- a/docs/local-dev/cli-commands.md +++ b/docs/local-dev/cli-commands.md @@ -512,7 +512,8 @@ Usage: backstage-cli config:check [options] Options: --package <name> Only load config schema that applies to the given package - --lax Do not require environment variables to be set + --lax Do not require environment variables to be set + --frontend Only validate the frontend configuration --config <path> Config files to load instead of app-config.yaml (default: []) -h, --help display help for command ``` diff --git a/packages/cli/src/commands/config/print.ts b/packages/cli/src/commands/config/print.ts index 6bd575f9bf..b640ee4f37 100644 --- a/packages/cli/src/commands/config/print.ts +++ b/packages/cli/src/commands/config/print.ts @@ -25,6 +25,7 @@ export default async (cmd: Command) => { args: cmd.config, fromPackage: cmd.package, mockEnv: cmd.lax, + fullVisibility: !cmd.frontend, }); const visibility = getVisibilityOption(cmd); const data = serializeConfigData(appConfigs, schema, visibility); diff --git a/packages/cli/src/commands/config/validate.ts b/packages/cli/src/commands/config/validate.ts index 9041272c5a..2d3ce60366 100644 --- a/packages/cli/src/commands/config/validate.ts +++ b/packages/cli/src/commands/config/validate.ts @@ -22,5 +22,6 @@ export default async (cmd: Command) => { args: cmd.config, fromPackage: cmd.package, mockEnv: cmd.lax, + fullVisibility: !cmd.frontend, }); }; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 302f76cc5d..4e01a8ca9f 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -172,6 +172,7 @@ export function registerCommands(program: CommanderStatic) { 'Only load config schema that applies to the given package', ) .option('--lax', 'Do not require environment variables to be set') + .option('--frontend', 'Only validate the frontend configuration') .option(...configOption) .description( 'Validate that the given configuration loads and matches schema', diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index 80b374039f..6f93318ead 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -28,6 +28,7 @@ type Options = { fromPackage?: string; mockEnv?: boolean; withFilteredKeys?: boolean; + fullVisibility?: boolean; }; export async function loadCliConfig(options: Options) { @@ -70,7 +71,9 @@ export async function loadCliConfig(options: Options) { try { const frontendAppConfigs = schema.process(appConfigs, { - visibility: ['frontend'], + visibility: options.fullVisibility + ? ['frontend', 'backend', 'secret'] + : ['frontend'], withFilteredKeys: options.withFilteredKeys, }); const frontendConfig = ConfigReader.fromConfigs(frontendAppConfigs); From 145913e6a0bfe4cf4df7f955b262d65b0aac5d7c Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Wed, 10 Nov 2021 08:25:58 -0700 Subject: [PATCH 023/181] Add missing link to Gitlab discovery Signed-off-by: Tim Hansen --- microsite/sidebars.json | 5 ++++- mkdocs.yml | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 23cc6135c0..91525d4938 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -151,7 +151,10 @@ { "type": "subcategory", "label": "GitLab", - "ids": ["integrations/gitlab/locations"] + "ids": [ + "integrations/gitlab/locations", + "integrations/gitlab/discovery" + ] }, { "type": "subcategory", diff --git a/mkdocs.yml b/mkdocs.yml index 2f4aa4203f..0ddae446e6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -100,6 +100,7 @@ nav: - Org Data: 'integrations/github/org.md' - GitLab: - Locations: 'integrations/gitlab/locations.md' + - Discovery: 'integrations/gitlab/discovery.md' - Google GCS: - Locations: 'integrations/google-cloud-storage/locations.md' - LDAP: From 36099bf941649d26b591007978337a7d15a7b7f3 Mon Sep 17 00:00:00 2001 From: James Turley Date: Wed, 10 Nov 2021 16:18:34 +0000 Subject: [PATCH 024/181] Update API report Signed-off-by: James Turley --- packages/core-components/api-report.md | 24 +++++++++++++++++-- .../src/components/ProgressBars/Gauge.tsx | 3 +++ .../src/components/ProgressBars/index.ts | 5 ++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 13929db7a3..0f1e3510ae 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -7,6 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; +import { BackstagePalette } from '@backstage/theme'; import { BackstageTheme } from '@backstage/theme'; import { ButtonProps as ButtonProps_2 } from '@material-ui/core/Button'; import { CardHeaderProps } from '@material-ui/core/CardHeader'; @@ -368,8 +369,6 @@ export function FeatureCalloutCircular( // @public (undocumented) export type FiltersContainerClassKey = 'root' | 'title'; -// Warning: (ae-forgotten-export) The symbol "GaugeProps" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export function Gauge(props: GaugeProps): JSX.Element; @@ -384,6 +383,27 @@ export type GaugeCardClassKey = 'root'; // @public (undocumented) export type GaugeClassKey = 'root' | 'overlay' | 'circle' | 'colorUnknown'; +// @public (undocumented) +export type GaugeProps = { + value: number; + fractional?: boolean; + inverse?: boolean; + unit?: string; + max?: number; + getColor?: GaugePropsGetColor; +}; + +// @public (undocumented) +export type GaugePropsGetColor = (args: GaugePropsGetColorOptions) => string; + +// @public (undocumented) +export type GaugePropsGetColorOptions = { + palette: BackstagePalette; + value: number; + inverse?: boolean; + max?: number; +}; + // @public (undocumented) export function GitHubIcon(props: IconComponentProps): JSX.Element; diff --git a/packages/core-components/src/components/ProgressBars/Gauge.tsx b/packages/core-components/src/components/ProgressBars/Gauge.tsx index ef8ec08378..00e7b3257b 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.tsx @@ -46,6 +46,7 @@ const useStyles = makeStyles( { name: 'BackstageGauge' }, ); +/** @public */ export type GaugeProps = { value: number; fractional?: boolean; @@ -55,6 +56,7 @@ export type GaugeProps = { getColor?: GaugePropsGetColor; }; +/** @public */ export type GaugePropsGetColorOptions = { palette: BackstagePalette; value: number; @@ -62,6 +64,7 @@ export type GaugePropsGetColorOptions = { max?: number; }; +/** @public */ export type GaugePropsGetColor = (args: GaugePropsGetColorOptions) => string; const defaultGaugeProps = { diff --git a/packages/core-components/src/components/ProgressBars/index.ts b/packages/core-components/src/components/ProgressBars/index.ts index 3d821eaac2..5e588ea696 100644 --- a/packages/core-components/src/components/ProgressBars/index.ts +++ b/packages/core-components/src/components/ProgressBars/index.ts @@ -17,5 +17,10 @@ export { GaugeCard } from './GaugeCard'; export type { GaugeCardClassKey } from './GaugeCard'; export { Gauge } from './Gauge'; +export type { + GaugeProps, + GaugePropsGetColor, + GaugePropsGetColorOptions, +} from './Gauge'; export type { GaugeClassKey } from './Gauge'; export { LinearGauge } from './LinearGauge'; From 88677aee18aeb59b2b2e1665bc61aa08a57bf20a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Nov 2021 04:09:19 +0000 Subject: [PATCH 025/181] build(deps): bump @spotify/eslint-config-react from 10.0.0 to 12.0.0 Bumps [@spotify/eslint-config-react](https://github.com/spotify/web-scripts) from 10.0.0 to 12.0.0. - [Release notes](https://github.com/spotify/web-scripts/releases) - [Changelog](https://github.com/spotify/web-scripts/blob/master/CHANGELOG.md) - [Commits](https://github.com/spotify/web-scripts/compare/v10.0.0...v12.0.0) --- updated-dependencies: - dependency-name: "@spotify/eslint-config-react" dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- packages/cli/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 23a2bef027..0fca97f317 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -42,7 +42,7 @@ "@rollup/plugin-node-resolve": "^13.0.0", "@rollup/plugin-yaml": "^3.0.0", "@spotify/eslint-config-base": "^12.0.0", - "@spotify/eslint-config-react": "^10.0.0", + "@spotify/eslint-config-react": "^12.0.0", "@spotify/eslint-config-typescript": "^12.0.0", "@sucrase/jest-plugin": "^2.1.1", "@sucrase/webpack-loader": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 42c43fe4b7..6c23c706b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5481,10 +5481,10 @@ resolved "https://registry.npmjs.org/@spotify/eslint-config-base/-/eslint-config-base-12.0.0.tgz#0b1e41bb436d5c1c20714703629514d64c3c0f06" integrity sha512-5Uud/TmzakqmdUNCZpD8JFQRa2VG3dVd3DanSMpU/nVdu6K5LyX8EMU3Tz1vGP18Wih8iAu/sBSJhntNzw7e6w== -"@spotify/eslint-config-react@^10.0.0": - version "10.0.0" - resolved "https://registry.npmjs.org/@spotify/eslint-config-react/-/eslint-config-react-10.0.0.tgz#6f83ada05f79b49c1f9def5b8815e3231ed24969" - integrity sha512-MozX6W3aMp7EQPliuUQYI58Ni5vh65mItXMG0CgZBj0v1ZEeZVM5XS/nqhsCaIHYXskmZM2O1qqLFaEg5PqGdg== +"@spotify/eslint-config-react@^12.0.0": + version "12.0.0" + resolved "https://registry.npmjs.org/@spotify/eslint-config-react/-/eslint-config-react-12.0.0.tgz#5b8d4bc3b81a8ec2824648f482f1f6c3cf711893" + integrity sha512-lNHZRtJesNA273OJHBVUGAg2JYyVDZ+bsT7h3OwnX1HYgejJ3YcKPSziPM8TGFAN8DruH3tHFfaM63uAIA1+uw== "@spotify/eslint-config-typescript@^12.0.0": version "12.0.0" From 8af66229e7535daa0269f8bf979d942163b457b1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Nov 2021 19:38:12 +0100 Subject: [PATCH 026/181] add changeset for @spotify/eslint-config-react bump Signed-off-by: Patrik Oldsberg --- .changeset/rich-meals-switch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rich-meals-switch.md diff --git a/.changeset/rich-meals-switch.md b/.changeset/rich-meals-switch.md new file mode 100644 index 0000000000..8a9467a84e --- /dev/null +++ b/.changeset/rich-meals-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Bumped `@spotify/eslint-config-react` from `v10` to `v12`, dropping support for Node.js v12. From 38d6df6bb9ecdce798ea9033f5b68490dcfe9823 Mon Sep 17 00:00:00 2001 From: Dominik Schwank Date: Thu, 4 Nov 2021 16:05:15 +0100 Subject: [PATCH 027/181] feat(catalog): remove 'View Api' link from AboutCard Signed-off-by: Dominik Schwank --- .changeset/big-months-float.md | 6 +++++ .../src/components/AboutCard/AboutCard.tsx | 25 +------------------ 2 files changed, 7 insertions(+), 24 deletions(-) create mode 100644 .changeset/big-months-float.md diff --git a/.changeset/big-months-float.md b/.changeset/big-months-float.md new file mode 100644 index 0000000000..9acb7002e7 --- /dev/null +++ b/.changeset/big-months-float.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Remove the "View Api" icon in the AboutCard, as the information is misleading for some users and is +duplicated in the tabs above. diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 68777d4109..7e44ae4efb 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -18,8 +18,6 @@ import { Entity, ENTITY_DEFAULT_NAMESPACE, LOCATION_ANNOTATION, - RELATION_CONSUMES_API, - RELATION_PROVIDES_API, stringifyEntityRef, } from '@backstage/catalog-model'; import { @@ -36,7 +34,6 @@ import { import { catalogApiRef, getEntityMetadataEditUrl, - getEntityRelations, getEntitySourceLocation, useEntity, } from '@backstage/plugin-catalog-react'; @@ -51,7 +48,6 @@ import { import CachedIcon from '@material-ui/icons/Cached'; import DocsIcon from '@material-ui/icons/Description'; import EditIcon from '@material-ui/icons/Edit'; -import ExtensionIcon from '@material-ui/icons/Extension'; import React, { useCallback } from 'react'; import { viewTechDocRouteRef } from '../../routes'; import { AboutContent } from './AboutContent'; @@ -95,16 +91,6 @@ export function AboutCard({ variant }: AboutCardProps) { scmIntegrationsApi, ); const entityMetadataEditUrl = getEntityMetadataEditUrl(entity); - const providesApiRelations = getEntityRelations( - entity, - RELATION_PROVIDES_API, - ); - const consumesApiRelations = getEntityRelations( - entity, - RELATION_CONSUMES_API, - ); - const hasApis = - providesApiRelations.length > 0 || consumesApiRelations.length > 0; const viewInSource: IconLinkVerticalProps = { label: 'View Source', @@ -126,13 +112,6 @@ export function AboutCard({ variant }: AboutCardProps) { name: entity.metadata.name, }), }; - const viewApi: IconLinkVerticalProps = { - title: hasApis ? '' : 'No APIs available', - label: 'View API', - disabled: !hasApis, - icon: , - href: 'api', - }; let cardClass = ''; if (variant === 'gridItem') { @@ -181,9 +160,7 @@ export function AboutCard({ variant }: AboutCardProps) { } - subheader={ - - } + subheader={} /> From 7e966461d09980b11cc634767b12cbc88eebb23c Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Wed, 10 Nov 2021 12:17:58 -0600 Subject: [PATCH 028/181] Use string for Date and let client handle display Signed-off-by: Andre Wanlin --- .../src/api/AzureDevOpsApi.test.ts | 32 +++++++++---------- .../src/api/AzureDevOpsApi.ts | 8 ++--- plugins/azure-devops-common/src/types.ts | 8 ++--- .../src/components/BuildTable/BuildTable.tsx | 2 +- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts index 429d608bd6..dc0ebb2920 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts @@ -72,9 +72,9 @@ describe('AzureDevOpsApi', () => { link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', status: BuildStatus.Completed, result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.9325232Z'), - startTime: new Date('2020-09-12T06:15:23.9325232Z'), - finishTime: new Date('2020-09-12T06:20:23.9325232Z'), + queueTime: new Date('2020-09-12T06:10:23.9325232Z').toISOString(), + startTime: new Date('2020-09-12T06:15:23.9325232Z').toISOString(), + finishTime: new Date('2020-09-12T06:20:23.9325232Z').toISOString(), source: 'refs/heads/develop (f4f78b31)', uniqueName: 'DOMAINjdoe', }; @@ -117,9 +117,9 @@ describe('AzureDevOpsApi', () => { link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', status: BuildStatus.Completed, result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.9325232Z'), - startTime: new Date('2020-09-12T06:15:23.9325232Z'), - finishTime: new Date('2020-09-12T06:20:23.9325232Z'), + queueTime: new Date('2020-09-12T06:10:23.9325232Z').toISOString(), + startTime: new Date('2020-09-12T06:15:23.9325232Z').toISOString(), + finishTime: new Date('2020-09-12T06:20:23.9325232Z').toISOString(), source: 'refs/heads/develop (f4f78b31)', uniqueName: 'DOMAINjdoe', }; @@ -162,9 +162,9 @@ describe('AzureDevOpsApi', () => { link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', status: BuildStatus.None, result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.9325232Z'), - startTime: new Date('2020-09-12T06:15:23.9325232Z'), - finishTime: new Date('2020-09-12T06:20:23.9325232Z'), + queueTime: new Date('2020-09-12T06:10:23.9325232Z').toISOString(), + startTime: new Date('2020-09-12T06:15:23.9325232Z').toISOString(), + finishTime: new Date('2020-09-12T06:20:23.9325232Z').toISOString(), source: 'refs/heads/develop (f4f78b31)', uniqueName: 'DOMAINjdoe', }; @@ -207,9 +207,9 @@ describe('AzureDevOpsApi', () => { link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', status: BuildStatus.InProgress, result: BuildResult.None, - queueTime: new Date('2020-09-12T06:10:23.9325232Z'), - startTime: new Date('2020-09-12T06:15:23.9325232Z'), - finishTime: new Date('2020-09-12T06:20:23.9325232Z'), + queueTime: new Date('2020-09-12T06:10:23.9325232Z').toISOString(), + startTime: new Date('2020-09-12T06:15:23.9325232Z').toISOString(), + finishTime: new Date('2020-09-12T06:20:23.9325232Z').toISOString(), source: 'refs/heads/develop (f4f78b31)', uniqueName: 'DOMAINjdoe', }; @@ -246,9 +246,9 @@ describe('AzureDevOpsApi', () => { link: '', status: BuildStatus.InProgress, result: BuildResult.None, - queueTime: new Date('2020-09-12T06:10:23.9325232Z'), - startTime: new Date('2020-09-12T06:15:23.9325232Z'), - finishTime: new Date('2020-09-12T06:20:23.9325232Z'), + queueTime: new Date('2020-09-12T06:10:23.9325232Z').toISOString(), + startTime: new Date('2020-09-12T06:15:23.9325232Z').toISOString(), + finishTime: new Date('2020-09-12T06:20:23.9325232Z').toISOString(), source: 'refs/heads/develop (f4f78b31)', uniqueName: 'DOMAINjdoe', }; @@ -289,7 +289,7 @@ describe('AzureDevOpsApi', () => { title: 'My Awesome New Feature', uniqueName: 'DOMAINjdoe', createdBy: 'Jane Doe', - creationDate: new Date('2020-09-12T06:10:23.9325232Z'), + creationDate: new Date('2020-09-12T06:10:23.9325232Z').toISOString(), sourceRefName: 'refs/heads/topic/super-awesome-feature', targetRefName: 'refs/heads/main', status: PullRequestStatus.Active, diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index 1ed1f1d5f5..23ea3311f0 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -149,9 +149,9 @@ export function mappedRepoBuild(build: Build): RepoBuild { link: build._links?.web.href ?? '', status: build.status ?? BuildStatus.None, result: build.result ?? BuildResult.None, - queueTime: build.queueTime, - startTime: build.startTime, - finishTime: build.finishTime, + queueTime: build.queueTime?.toISOString(), + startTime: build.startTime?.toISOString(), + finishTime: build.finishTime?.toISOString(), source: `${build.sourceBranch} (${build.sourceVersion?.substr(0, 8)})`, uniqueName: build.requestedFor?.uniqueName ?? 'N/A', }; @@ -167,7 +167,7 @@ export function mappedPullRequest( title: pullRequest.title, uniqueName: pullRequest.createdBy?.uniqueName ?? 'N/A', createdBy: pullRequest.createdBy?.displayName ?? 'N/A', - creationDate: pullRequest.creationDate, + creationDate: pullRequest.creationDate?.toISOString(), sourceRefName: pullRequest.sourceRefName, targetRefName: pullRequest.targetRefName, status: pullRequest.status, diff --git a/plugins/azure-devops-common/src/types.ts b/plugins/azure-devops-common/src/types.ts index eacc2af30d..a647f87300 100644 --- a/plugins/azure-devops-common/src/types.ts +++ b/plugins/azure-devops-common/src/types.ts @@ -74,9 +74,9 @@ export type RepoBuild = { link?: string; status?: BuildStatus; result?: BuildResult; - queueTime?: Date; - startTime?: Date; - finishTime?: Date; + queueTime?: string; + startTime?: string; + finishTime?: string; source: string; uniqueName?: string; }; @@ -114,7 +114,7 @@ export type PullRequest = { title?: string; uniqueName?: string; createdBy?: string; - creationDate?: Date; + creationDate?: string; sourceRefName?: string; targetRefName?: string; status?: PullRequestStatus; diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx index 5667c65711..99b23fe183 100644 --- a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx +++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx @@ -149,7 +149,7 @@ const columns: TableColumn[] = [ field: 'queueTime', width: 'auto', render: (row: Partial) => - DateTime.fromJSDate(row.queueTime ?? new Date()).toRelative(), + DateTime.fromISO(row.queueTime ?? new Date().toISOString()).toRelative(), }, ]; From 55ced4d6288ff08919882846adf79a9700a746e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Nov 2021 04:08:39 +0000 Subject: [PATCH 029/181] build(deps): bump @rollup/plugin-commonjs from 17.1.0 to 21.0.1 Bumps [@rollup/plugin-commonjs](https://github.com/rollup/plugins/tree/HEAD/packages/commonjs) from 17.1.0 to 21.0.1. - [Release notes](https://github.com/rollup/plugins/releases) - [Changelog](https://github.com/rollup/plugins/blob/master/packages/commonjs/CHANGELOG.md) - [Commits](https://github.com/rollup/plugins/commits/commonjs-v21.0.1/packages/commonjs) --- updated-dependencies: - dependency-name: "@rollup/plugin-commonjs" dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- packages/cli/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 23a2bef027..89a210290b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,7 +37,7 @@ "@lerna/package-graph": "^4.0.0", "@lerna/project": "^4.0.0", "@octokit/request": "^5.4.12", - "@rollup/plugin-commonjs": "^17.1.0", + "@rollup/plugin-commonjs": "^21.0.1", "@rollup/plugin-json": "^4.0.2", "@rollup/plugin-node-resolve": "^13.0.0", "@rollup/plugin-yaml": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 42c43fe4b7..5fc4c40365 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5309,10 +5309,10 @@ react-router-dom "6.0.0-beta.0" react-use "^17.2.4" -"@rollup/plugin-commonjs@^17.1.0": - version "17.1.0" - resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz#757ec88737dffa8aa913eb392fade2e45aef2a2d" - integrity sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew== +"@rollup/plugin-commonjs@^21.0.1": + version "21.0.1" + resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-21.0.1.tgz#1e57c81ae1518e4df0954d681c642e7d94588fee" + integrity sha512-EA+g22lbNJ8p5kuZJUYyhhDK7WgJckW5g4pNN7n4mAFUM96VuwUnNT3xr2Db2iCZPI1pJPbGyfT5mS9T1dHfMg== dependencies: "@rollup/pluginutils" "^3.1.0" commondir "^1.0.1" From 79dc3f9a29603dc7866a2907acb8f01bc0e55564 Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Thu, 11 Nov 2021 08:09:01 +0000 Subject: [PATCH 030/181] Pass the GH token as an ENV to the Github step of the Snyk workflow Signed-off-by: Harry Hogg --- .github/workflows/snyk-github-issue-sync.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/snyk-github-issue-sync.yml b/.github/workflows/snyk-github-issue-sync.yml index 77604a126d..0c50b2c505 100644 --- a/.github/workflows/snyk-github-issue-sync.yml +++ b/.github/workflows/snyk-github-issue-sync.yml @@ -57,3 +57,5 @@ jobs: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - name: Update Github issues run: yarn ts-node scripts/snyk-github-issue-sync.ts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 5dcea2586c79f1d41609d70c9139fdc204fdc2d1 Mon Sep 17 00:00:00 2001 From: Rod Machen Date: Thu, 11 Nov 2021 02:26:15 -0600 Subject: [PATCH 031/181] Add Search Modal component (#7316) Signed-off-by: rodmachen --- .changeset/flat-tomatoes-drop.md | 28 +++ .changeset/proud-eels-work.md | 17 ++ docs/features/search/getting-started.md | 19 ++ packages/app/src/components/Root/Root.tsx | 4 +- .../packages/app/src/components/Root/Root.tsx | 4 +- plugins/search/api-report.md | 23 +++ .../SearchModal/SearchModal.test.tsx | 77 +++++++++ .../components/SearchModal/SearchModal.tsx | 162 ++++++++++++++++++ .../src/components/SearchModal/index.tsx | 17 ++ .../SidebarSearchModal/SidebarSearchModal.tsx | 36 ++++ .../components/SidebarSearchModal/index.ts | 16 ++ plugins/search/src/components/index.tsx | 2 + plugins/search/src/index.ts | 5 +- plugins/search/src/plugin.ts | 12 ++ 14 files changed, 417 insertions(+), 5 deletions(-) create mode 100644 .changeset/flat-tomatoes-drop.md create mode 100644 .changeset/proud-eels-work.md create mode 100644 plugins/search/src/components/SearchModal/SearchModal.test.tsx create mode 100644 plugins/search/src/components/SearchModal/SearchModal.tsx create mode 100644 plugins/search/src/components/SearchModal/index.tsx create mode 100644 plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx create mode 100644 plugins/search/src/components/SidebarSearchModal/index.ts diff --git a/.changeset/flat-tomatoes-drop.md b/.changeset/flat-tomatoes-drop.md new file mode 100644 index 0000000000..97ea32d8fd --- /dev/null +++ b/.changeset/flat-tomatoes-drop.md @@ -0,0 +1,28 @@ +--- +'@backstage/create-app': patch +--- + +Integrated `SidebarSearchModal` component into default-app to use the `SearchModal`. + +The `SidebarSearchModal` component can also be used in other generated apps: + +```diff +import { +- SidebarSearch, ++ SidebarSearchModal +} from '@backstage/plugin-search'; +... + + + +- ++ + +... +``` + +If you only want to use the `SearchModal` you can import it from `'@backstage/plugin-search'`: + +```js +import { SearchModal } from '@backstage/plugin-search'; +``` diff --git a/.changeset/proud-eels-work.md b/.changeset/proud-eels-work.md new file mode 100644 index 0000000000..a4f2c86eb4 --- /dev/null +++ b/.changeset/proud-eels-work.md @@ -0,0 +1,17 @@ +--- +'@backstage/plugin-search': patch +--- + +Added `SearchModal` component. + +Now you can import `SearchModal` in your apps: + +```js +import { SearchModal } from '@backstage/plugin-search'; +``` + +You can also use the `SidebarSearchModal` component to integrate it into the sidebar of your sample apps: + +```js +import { SidebarSearchModal } from '@backstage/plugin-search'; +``` diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index aa40c1e539..27219e529a 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -109,6 +109,25 @@ const routes = ( ); ``` +### Using the Search Modal + +In `Root.tsx`, add the `SidebarSearchModal` component: + +```bash +import { SidebarSearchModal } from '@backstage/plugin-search'; + +export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + + + +... +``` + +For more information about using `Root.tsx`, please see +[the changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#0315). + ## Adding Search to the Backend Add the following plugins into your backend app: diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 7a666a020a..f365310595 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -29,7 +29,7 @@ import LogoIcon from './LogoIcon'; import { NavLink } from 'react-router-dom'; import { GraphiQLIcon } from '@backstage/plugin-graphiql'; import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; -import { SidebarSearch } from '@backstage/plugin-search'; +import { SidebarSearchModal } from '@backstage/plugin-search'; import { Shortcuts } from '@backstage/plugin-shortcuts'; import { Sidebar, @@ -79,7 +79,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( - + {/* Global nav, not org-specific */} diff --git a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx index 198e7ec2b1..ec59b0b116 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx @@ -25,7 +25,7 @@ import LogoFull from './LogoFull'; import LogoIcon from './LogoIcon'; import { NavLink } from 'react-router-dom'; import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; -import { SidebarSearch } from '@backstage/plugin-search'; +import { SidebarSearchModal } from '@backstage/plugin-search'; import { Sidebar, SidebarPage, @@ -74,7 +74,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( - + {/* Global nav, not org-specific */} diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index c638fb4af6..0e0ad01fca 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -126,6 +126,24 @@ export const SearchFilterNext: { Select(props: Omit & Component): JSX.Element; }; +// Warning: (ae-missing-release-tag) "SearchModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const SearchModal: ({ + open, + toggleModal, +}: SearchModalProps) => JSX.Element; + +// Warning: (ae-missing-release-tag) "SearchModalProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface SearchModalProps { + // (undocumented) + open?: boolean; + // (undocumented) + toggleModal: () => void; +} + // Warning: (ae-missing-release-tag) "SearchPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -179,6 +197,11 @@ export const SearchType: ({ // @public (undocumented) export const SidebarSearch: () => JSX.Element; +// Warning: (ae-missing-release-tag) "SidebarSearchModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const SidebarSearchModal: () => JSX.Element; + // Warning: (ae-forgotten-export) The symbol "SearchContextValue" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "useSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx new file mode 100644 index 0000000000..d7f873abf3 --- /dev/null +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; +import { configApiRef } from '@backstage/core-plugin-api'; +import { + ApiProvider, + ApiRegistry, + ConfigReader, +} from '@backstage/core-app-api'; +import { rootRouteRef } from '../../plugin'; +import { searchApiRef } from '../../apis'; + +import { SearchModal } from './SearchModal'; + +jest.mock('../SearchContext', () => ({ + ...jest.requireActual('../SearchContext'), + useSearch: jest.fn().mockReturnValue({ + result: {}, + }), +})); + +describe('SearchModal', () => { + const query = jest.fn().mockResolvedValue({}); + + const apiRegistry = ApiRegistry.from([ + [configApiRef, new ConfigReader({ app: { title: 'Mock app' } })], + [searchApiRef, { query }], + ]); + + const toggleModal = jest.fn(); + + it('Should render the Modal correctly', async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { + '/search': rootRouteRef, + }, + }, + ); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + it('Calls toggleModal handler', async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { + '/search': rootRouteRef, + }, + }, + ); + userEvent.keyboard('{esc}'); + expect(toggleModal).toHaveBeenCalledTimes(1); + }); +}); diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx new file mode 100644 index 0000000000..feabbc55af --- /dev/null +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -0,0 +1,162 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useEffect, useState } from 'react'; +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Grid, + List, + Paper, +} from '@material-ui/core'; +import { Launch } from '@material-ui/icons/'; +import { makeStyles } from '@material-ui/core/styles'; +import { SearchBarBase } from '../SearchBar'; +import { DefaultResultListItem } from '../DefaultResultListItem'; +import { SearchResult } from '../SearchResult'; +import { SearchContextProvider, useSearch } from '../SearchContext'; +import { SearchResultPager } from '../SearchResultPager'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { Link } from '@backstage/core-components'; +import { rootRouteRef } from '../../plugin'; + +import { useDebounce } from 'react-use'; + +export interface SearchModalProps { + open?: boolean; + toggleModal: () => void; +} + +const useStyles = makeStyles(theme => ({ + container: { + borderRadius: 30, + display: 'flex', + height: '2.4em', + }, + input: { + flex: 1, + }, + // Reduces default height of the modal, keeping a gap of 128px between the top and bottom of the page. + paperFullWidth: { height: 'calc(100% - 128px)' }, + dialogActionsContainer: { padding: theme.spacing(1, 3) }, + viewResultsLink: { verticalAlign: '0.5em' }, +})); + +export const Modal = ({ open = true, toggleModal }: SearchModalProps) => { + const getSearchLink = useRouteRef(rootRouteRef); + const classes = useStyles(); + + const { term, setTerm } = useSearch(); + const [value, setValue] = useState(term); + + useEffect(() => { + setValue(prevValue => (prevValue !== term ? term : prevValue)); + }, [term]); + + useDebounce(() => setTerm(value), 1000, [value]); + + const handleQuery = (newValue: string) => { + setValue(newValue); + }; + + const handleClear = () => setValue(''); + + const handleResultClick = () => { + toggleModal(); + handleClear(); + }; + + const handleKeyPress = () => { + handleResultClick(); + }; + + return ( + + + + + + + + + + + View Full Results + + + + + + + {({ results }) => ( + + {results.map(({ document }) => ( +
+ +
+ ))} +
+ )} +
+
+ + + + + + + +
+ ); +}; + +export const SearchModal = ({ open = true, toggleModal }: SearchModalProps) => { + return ( + + + + ); +}; diff --git a/plugins/search/src/components/SearchModal/index.tsx b/plugins/search/src/components/SearchModal/index.tsx new file mode 100644 index 0000000000..ec0053951d --- /dev/null +++ b/plugins/search/src/components/SearchModal/index.tsx @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { SearchModal } from './SearchModal'; +export type { SearchModalProps } from './SearchModal'; diff --git a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx new file mode 100644 index 0000000000..4d4fa1a9fc --- /dev/null +++ b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useState } from 'react'; +import SearchIcon from '@material-ui/icons/Search'; +import { SearchModal } from '../SearchModal'; +import { SidebarItem } from '@backstage/core-components'; + +export const SidebarSearchModal = () => { + const [open, setOpen] = useState(false); + const toggleModal = (): void => setOpen(prevState => !prevState); + + return ( + <> + + + + ); +}; diff --git a/plugins/search/src/components/SidebarSearchModal/index.ts b/plugins/search/src/components/SidebarSearchModal/index.ts new file mode 100644 index 0000000000..849180878d --- /dev/null +++ b/plugins/search/src/components/SidebarSearchModal/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { SidebarSearchModal } from './SidebarSearchModal'; diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index 8b6493ae9b..6a4eae4001 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -19,8 +19,10 @@ export * from './Filters'; export * from './SearchBar'; export * from './SearchContext'; export * from './SearchFilter'; +export * from './SearchModal'; export * from './SearchPage'; export * from './SearchResult'; export * from './SearchResultPager'; export * from './SearchType'; export * from './SidebarSearch'; +export * from './SidebarSearchModal'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 07e4443f04..09cf204017 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -28,20 +28,23 @@ export { SearchContextProvider, SearchFilter, SearchFilterNext, + SearchModal, SearchPage as Router, SearchResultPager, SearchType, SidebarSearch, useSearch, } from './components'; +export type { SearchModalProps } from './components'; export type { FiltersState } from './components'; export { DefaultResultListItem, + HomePageSearchBar, SearchBarNext, SearchPage, SearchPageNext, searchPlugin as plugin, searchPlugin, SearchResult, - HomePageSearchBar, + SidebarSearchModal, } from './plugin'; diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index 491582aead..b8c016fb36 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -122,6 +122,18 @@ export const SearchResultNext = searchPlugin.provide( }), ); +export const SidebarSearchModal = searchPlugin.provide( + createComponentExtension({ + name: 'SidebarSearchModal', + component: { + lazy: () => + import('./components/SidebarSearchModal').then( + m => m.SidebarSearchModal, + ), + }, + }), +); + export const DefaultResultListItem = searchPlugin.provide( createComponentExtension({ name: 'DefaultResultListItem', From fe5738fe1c8dd286f374bb0fe8f81e2009210e56 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 1 Nov 2021 16:25:46 +0100 Subject: [PATCH 032/181] Lazy load `LazyLog` as it is rarely used Signed-off-by: Oliver Sand --- .changeset/gold-cobras-stare.md | 6 ++ .../src/components/TaskPage/TaskPage.tsx | 60 +++++++++++-------- .../components/TechDocsBuildLogs.test.tsx | 21 ++++--- .../reader/components/TechDocsBuildLogs.tsx | 27 +++++---- 4 files changed, 66 insertions(+), 48 deletions(-) create mode 100644 .changeset/gold-cobras-stare.md diff --git a/.changeset/gold-cobras-stare.md b/.changeset/gold-cobras-stare.md new file mode 100644 index 0000000000..d3265c865a --- /dev/null +++ b/.changeset/gold-cobras-stare.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-techdocs': patch +--- + +Lazy load `LazyLog` as it is rarely used. diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index aeb47b2789..8d22293cb1 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -14,38 +14,40 @@ * limitations under the License. */ -import React, { useState, useEffect, memo, useMemo } from 'react'; -import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; -import Stepper from '@material-ui/core/Stepper'; -import Step from '@material-ui/core/Step'; -import StepLabel from '@material-ui/core/StepLabel'; -import Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; -import { useParams } from 'react-router'; -import { useTaskEventStream } from '../hooks/useEventStream'; -import LazyLog from 'react-lazylog/build/LazyLog'; +import { + Content, + ErrorPage, + Header, + Lifecycle, + Page, +} from '@backstage/core-components'; +import { BackstageTheme } from '@backstage/theme'; import { CircularProgress, + LinearProgress, Paper, StepButton, StepIconProps, } from '@material-ui/core'; -import { Status, TaskOutput } from '../../types'; -import { DateTime, Interval } from 'luxon'; -import { useInterval } from 'react-use'; -import Check from '@material-ui/icons/Check'; +import Grid from '@material-ui/core/Grid'; +import Step from '@material-ui/core/Step'; +import StepLabel from '@material-ui/core/StepLabel'; +import Stepper from '@material-ui/core/Stepper'; +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; import Cancel from '@material-ui/icons/Cancel'; +import Check from '@material-ui/icons/Check'; import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; import classNames from 'classnames'; -import { BackstageTheme } from '@backstage/theme'; +import { DateTime, Interval } from 'luxon'; +import React, { memo, Suspense, useEffect, useMemo, useState } from 'react'; +import { useParams } from 'react-router'; +import { useInterval } from 'react-use'; +import { Status, TaskOutput } from '../../types'; +import { useTaskEventStream } from '../hooks/useEventStream'; import { TaskPageLinks } from './TaskPageLinks'; -import { - Page, - Header, - Lifecycle, - Content, - ErrorPage, -} from '@backstage/core-components'; + +const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); // typings are wrong for this library, so fallback to not parsing types. const humanizeDuration = require('humanize-duration'); @@ -213,9 +215,17 @@ export const TaskStatusStepper = memo( const TaskLogger = memo(({ log }: { log: string }) => { return ( -
- -
+ }> +
+ +
+
); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx index d1bb99b76c..fa82527282 100644 --- a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx @@ -23,9 +23,9 @@ import { // react-lazylog is based on a react-virtualized component which doesn't // write the content to the dom, so we mock it. -jest.mock('react-lazylog', () => { +jest.mock('react-lazylog/build/LazyLog', () => { return { - LazyLog: ({ text }: { text: string }) => { + default: ({ text }: { text: string }) => { return

{text}

; }, }; @@ -46,18 +46,20 @@ describe('', () => { }); describe('', () => { - it('should render with empty log', () => { + it('should render with empty log', async () => { const onClose = jest.fn(); const rendered = render( , ); expect(rendered.getByText(/Build Details/i)).toBeInTheDocument(); - expect(rendered.getByText(/Waiting for logs.../i)).toBeInTheDocument(); + expect( + await rendered.findByText(/Waiting for logs.../i), + ).toBeInTheDocument(); expect(onClose).toBeCalledTimes(0); }); - it('should render with empty logs', () => { + it('should render logs', async () => { const onClose = jest.fn(); const rendered = render( ', () => { />, ); expect(rendered.getByText(/Build Details/i)).toBeInTheDocument(); - expect( - rendered.queryByText(/Waiting for logs.../i), - ).not.toBeInTheDocument(); - expect(rendered.getByText(/Line 1/i)).toBeInTheDocument(); - expect(rendered.getByText(/Line 2/i)).toBeInTheDocument(); + expect(await rendered.findByText(/Line 1/i)).toBeInTheDocument(); + expect(await rendered.findByText(/Line 2/i)).toBeInTheDocument(); expect(onClose).toBeCalledTimes(0); }); @@ -80,7 +79,7 @@ describe('', () => { const rendered = render( , ); - rendered.getByRole('button').click(); + rendered.getByTitle('Close the drawer').click(); expect(onClose).toBeCalledTimes(1); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx index 4283932fb3..0850c0e291 100644 --- a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx @@ -20,14 +20,15 @@ import { Drawer, Grid, IconButton, + LinearProgress, makeStyles, Theme, Typography, } from '@material-ui/core'; import Close from '@material-ui/icons/Close'; -import * as React from 'react'; -import { useState } from 'react'; -import { LazyLog } from 'react-lazylog'; +import React, { Suspense, useState } from 'react'; + +const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); const useDrawerStyles = makeStyles((theme: Theme) => createStyles({ @@ -83,15 +84,17 @@ export const TechDocsBuildLogsDrawerContent = ({ - + }> + + ); }; From 7e5bd18cf7774f07eb54c072d2e48de73ebee899 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 2 Nov 2021 09:58:11 +0100 Subject: [PATCH 033/181] Adjust to review comments Signed-off-by: Oliver Sand --- plugins/api-docs/api-report.md | 44 +++++++++++++------ .../AsyncApiDefinitionWidget.tsx | 8 ++-- .../AsyncApiDefinitionWidget/index.ts | 1 + .../GraphQlDefinitionWidget.tsx | 8 ++-- .../GraphQlDefinitionWidget/index.ts | 1 + .../OpenApiDefinitionWidget.tsx | 8 ++-- .../OpenApiDefinitionWidget/index.ts | 1 + .../PlainApiDefinitionWidget.tsx | 12 +++-- .../PlainApiDefinitionWidget/index.ts | 1 + .../src/components/TaskPage/TaskPage.tsx | 4 +- .../reader/components/TechDocsBuildLogs.tsx | 4 +- 11 files changed, 60 insertions(+), 32 deletions(-) diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md index 8ec6175a6d..111660ae07 100644 --- a/plugins/api-docs/api-report.md +++ b/plugins/api-docs/api-report.md @@ -84,13 +84,19 @@ export const ApiTypeTitle: ({ apiEntity: ApiEntity; }) => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "AsyncApiDefinitionWidgetProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "AsyncApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const AsyncApiDefinitionWidget: ({ - definition, -}: AsyncApiDefinitionWidgetProps) => JSX.Element; +export const AsyncApiDefinitionWidget: ( + props: AsyncApiDefinitionWidgetProps, +) => JSX.Element; + +// Warning: (ae-missing-release-tag) "AsyncApiDefinitionWidgetProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AsyncApiDefinitionWidgetProps = { + definition: string; +}; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ConsumedApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -171,22 +177,34 @@ export const EntityProvidingComponentsCard: ({ // @public (undocumented) export const HasApisCard: ({ variant }: Props_3) => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "OpenApiDefinitionWidgetProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "OpenApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const OpenApiDefinitionWidget: ({ - definition, -}: OpenApiDefinitionWidgetProps) => JSX.Element; +export const OpenApiDefinitionWidget: ( + props: OpenApiDefinitionWidgetProps, +) => JSX.Element; + +// Warning: (ae-missing-release-tag) "OpenApiDefinitionWidgetProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type OpenApiDefinitionWidgetProps = { + definition: string; +}; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PlainApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const PlainApiDefinitionWidget: ({ - definition, - language, -}: Props_7) => JSX.Element; +export const PlainApiDefinitionWidget: ( + props: PlainApiDefinitionWidgetProps, +) => JSX.Element; + +// Warning: (ae-missing-release-tag) "PlainApiDefinitionWidgetProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PlainApiDefinitionWidgetProps = { + definition: any; + language: string; +}; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ProvidedApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx index a3d9bc1f64..03bc6fa8a6 100644 --- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx @@ -29,12 +29,12 @@ export type AsyncApiDefinitionWidgetProps = { definition: string; }; -export const AsyncApiDefinitionWidget = ({ - definition, -}: AsyncApiDefinitionWidgetProps) => { +export const AsyncApiDefinitionWidget = ( + props: AsyncApiDefinitionWidgetProps, +) => { return ( }> - + ); }; diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/index.ts b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/index.ts index dcca901be2..7cc49e3138 100644 --- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/index.ts +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/index.ts @@ -15,3 +15,4 @@ */ export { AsyncApiDefinitionWidget } from './AsyncApiDefinitionWidget'; +export type { AsyncApiDefinitionWidgetProps } from './AsyncApiDefinitionWidget'; diff --git a/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx index e91387bffa..1f8a796249 100644 --- a/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx @@ -29,12 +29,12 @@ export type GraphQlDefinitionWidgetProps = { definition: string; }; -export const GraphQlDefinitionWidget = ({ - definition, -}: GraphQlDefinitionWidgetProps) => { +export const GraphQlDefinitionWidget = ( + props: GraphQlDefinitionWidgetProps, +) => { return ( }> - + ); }; diff --git a/plugins/api-docs/src/components/GraphQlDefinitionWidget/index.ts b/plugins/api-docs/src/components/GraphQlDefinitionWidget/index.ts index 9173f492b0..8645b3c372 100644 --- a/plugins/api-docs/src/components/GraphQlDefinitionWidget/index.ts +++ b/plugins/api-docs/src/components/GraphQlDefinitionWidget/index.ts @@ -15,3 +15,4 @@ */ export { GraphQlDefinitionWidget } from './GraphQlDefinitionWidget'; +export type { GraphQlDefinitionWidgetProps } from './GraphQlDefinitionWidget'; diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx index 2c223243d1..606e476454 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx @@ -29,12 +29,12 @@ export type OpenApiDefinitionWidgetProps = { definition: string; }; -export const OpenApiDefinitionWidget = ({ - definition, -}: OpenApiDefinitionWidgetProps) => { +export const OpenApiDefinitionWidget = ( + props: OpenApiDefinitionWidgetProps, +) => { return ( }> - + ); }; diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/index.ts b/plugins/api-docs/src/components/OpenApiDefinitionWidget/index.ts index 94401c92a1..e317d42a59 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/index.ts +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/index.ts @@ -15,3 +15,4 @@ */ export { OpenApiDefinitionWidget } from './OpenApiDefinitionWidget'; +export type { OpenApiDefinitionWidgetProps } from './OpenApiDefinitionWidget'; diff --git a/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.tsx b/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.tsx index efdd882564..b7f1e8afc0 100644 --- a/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.tsx @@ -17,13 +17,19 @@ import React from 'react'; import { CodeSnippet } from '@backstage/core-components'; -type Props = { +export type PlainApiDefinitionWidgetProps = { definition: any; language: string; }; -export const PlainApiDefinitionWidget = ({ definition, language }: Props) => { +export const PlainApiDefinitionWidget = ( + props: PlainApiDefinitionWidgetProps, +) => { return ( - + ); }; diff --git a/plugins/api-docs/src/components/PlainApiDefinitionWidget/index.ts b/plugins/api-docs/src/components/PlainApiDefinitionWidget/index.ts index c233652981..562a3e67c1 100644 --- a/plugins/api-docs/src/components/PlainApiDefinitionWidget/index.ts +++ b/plugins/api-docs/src/components/PlainApiDefinitionWidget/index.ts @@ -15,3 +15,4 @@ */ export { PlainApiDefinitionWidget } from './PlainApiDefinitionWidget'; +export type { PlainApiDefinitionWidgetProps } from './PlainApiDefinitionWidget'; diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index 8d22293cb1..11d12282d2 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -20,11 +20,11 @@ import { Header, Lifecycle, Page, + Progress, } from '@backstage/core-components'; import { BackstageTheme } from '@backstage/theme'; import { CircularProgress, - LinearProgress, Paper, StepButton, StepIconProps, @@ -215,7 +215,7 @@ export const TaskStatusStepper = memo( const TaskLogger = memo(({ log }: { log: string }) => { return ( - }> + }>
- }> + }> Date: Tue, 2 Nov 2021 10:00:10 +0100 Subject: [PATCH 034/181] Use `` instead of `` in combination with `Suspense` Signed-off-by: Oliver Sand --- .changeset/dull-mangos-count.md | 6 ++++++ .../lib/ActionOutput/ActionOutput.tsx | 4 ++-- .../WorkflowRunLogs/WorkflowRunLogs.tsx | 19 +++++++++---------- 3 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 .changeset/dull-mangos-count.md diff --git a/.changeset/dull-mangos-count.md b/.changeset/dull-mangos-count.md new file mode 100644 index 0000000000..64a01c121d --- /dev/null +++ b/.changeset/dull-mangos-count.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-circleci': patch +'@backstage/plugin-github-actions': patch +--- + +Use `` instead of `` in combination with `Suspense`. diff --git a/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx b/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx index bbfc1e468c..a4c2af241f 100644 --- a/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx +++ b/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ +import { Progress } from '@backstage/core-components'; import { Accordion, AccordionDetails, AccordionSummary, - LinearProgress, Typography, } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; @@ -85,7 +85,7 @@ export const ActionOutput = ({ {messages.length === 0 ? ( 'Nothing here...' ) : ( - }> + }>
diff --git a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx index ce33ae3781..399fed9557 100644 --- a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx +++ b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx @@ -14,12 +14,15 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; +import { Progress } from '@backstage/core-components'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { readGitHubIntegrationConfigs } from '@backstage/integration'; import { Accordion, AccordionSummary, CircularProgress, Fade, - LinearProgress, makeStyles, Modal, Theme, @@ -27,15 +30,11 @@ import { Typography, Zoom, } from '@material-ui/core'; - -import React, { Suspense } from 'react'; -import { useDownloadWorkflowRunLogs } from './useDownloadWorkflowRunLogs'; -import { useProjectName } from '../useProjectName'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import DescriptionIcon from '@material-ui/icons/Description'; -import { Entity } from '@backstage/catalog-model'; -import { readGitHubIntegrationConfigs } from '@backstage/integration'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import React, { Suspense } from 'react'; +import { useProjectName } from '../useProjectName'; +import { useDownloadWorkflowRunLogs } from './useDownloadWorkflowRunLogs'; const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); const LinePart = React.lazy(() => import('react-lazylog/build/LinePart')); @@ -72,7 +71,7 @@ const DisplayLog = ({ className: string; }) => { return ( - }> + }>
Date: Thu, 11 Nov 2021 11:33:12 +0100 Subject: [PATCH 035/181] Fix styling issues of swagger-ui Signed-off-by: Oliver Sand --- .../OpenApiDefinition.tsx | 108 ++++++++++++------ 1 file changed, 74 insertions(+), 34 deletions(-) diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx index cc3df40ae5..4a74b238f7 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx @@ -21,58 +21,98 @@ import 'swagger-ui-react/swagger-ui.css'; const useStyles = makeStyles(theme => ({ root: { - '& .swagger-ui, .info h1, .info h2, .info h3, .info h4, .info h': { - 'font-family': 'inherit', + '& .swagger-ui': { + fontFamily: 'inherit', color: theme.palette.text.primary, - }, - '& .scheme-container': { - 'background-color': theme.palette.background.default, - }, - '& .opblock-tag, .opblock-tag small, table thead tr td, table thead tr th': - { + + [`& .info h1, + .info h2, + .info h3, + .info h4, + .info h5, + .info h6`]: { + fontFamily: 'inherit', color: theme.palette.text.primary, - 'border-color': theme.palette.divider, }, - '& section.models, section.models.is-open h4': { - 'border-color': theme.palette.divider, - }, - '& .opblock .opblock-summary-description, .parameter__type, table.headers td, .model-title, .model .property.primitive, section h3': - { + [`& .scheme-container`]: { + backgroundColor: theme.palette.background.default, + }, + [`& .opblock-tag, + .opblock-tag small, + table thead tr td, + table thead tr th`]: { + fontFamily: 'inherit', + color: theme.palette.text.primary, + borderColor: theme.palette.divider, + }, + [`& section.models, + section.models.is-open h4`]: { + borderColor: theme.palette.divider, + }, + [`& .opblock .opblock-summary-description, + .parameter__type, + table.headers td, + .model-title, + .model .property.primitive, + section h3`]: { + fontFamily: 'inherit', color: theme.palette.text.secondary, }, - '& .opblock .opblock-summary-operation-id, .opblock .opblock-summary-path, .opblock .opblock-summary-path__deprecated, .opblock .opblock-section-header h4, .parameter__name, .response-col_status, .response-col_links, .responses-inner h4, .swagger-ui .responses-inner h5, .opblock-section-header .btn, .tab li, .info li, .info p, .info table, section.models h4, .info .title, table.model tr.description, .property-row': - { + [`& .opblock .opblock-summary-operation-id, + .opblock .opblock-summary-path, + .opblock .opblock-summary-path__deprecated, + .opblock .opblock-section-header h4, + .parameter__name, + .response-col_status, + .response-col_links, + .responses-inner h4, + .responses-inner h5, + .opblock-section-header .btn, + .tab li, + .info li, + .info p, + .info table, + section.models h4, + .info .title, + table.model tr.description, + .property-row`]: { + fontFamily: 'inherit', color: theme.palette.text.primary, }, - '& .opblock .opblock-section-header, .model-box, section.models .model-container': - { + [`& .opblock .opblock-section-header, + .model-box, + section.models .model-container`]: { background: theme.palette.background.default, }, - '& .prop-format, .parameter__in': { - color: theme.palette.text.disabled, - }, - '& ': { - color: theme.palette.text.primary, - 'border-color': theme.palette.divider, - }, - '& .opblock-description-wrapper p, .opblock-external-docs-wrapper p, .opblock-title_normal p, .response-control-media-type__accept-message, .opblock .opblock-section-header>label, .scheme-container .schemes>label, .info .base-url, .model': - { + [`& .prop-format, + .parameter__in`]: { + color: theme.palette.text.disabled, + }, + [`& .opblock-description-wrapper p, + .opblock-external-docs-wrapper p, + .opblock-title_normal p, + .response-control-media-type__accept-message, + .opblock .opblock-section-header>label, + .scheme-container .schemes>label, + .info .base-url, + .model`]: { color: theme.palette.text.hint, }, - '& .parameter__name.required:after': { - color: theme.palette.warning.dark, - }, - '& .prop-type': { - color: theme.palette.primary.main, + [`& .parameter__name.required:after`]: { + color: theme.palette.warning.dark, + }, + [`& .prop-type`]: { + color: theme.palette.primary.main, + }, }, }, })); -type Props = { +export type OpenApiDefinitionProps = { definition: string; }; -export const OpenApiDefinition = ({ definition }: Props) => { +export const OpenApiDefinition = ({ definition }: OpenApiDefinitionProps) => { const classes = useStyles(); // Due to a bug in the swagger-ui-react component, the component needs From 53c9ad7e04ae49bd17a0ef86f18b6c31cf6cec3b Mon Sep 17 00:00:00 2001 From: Kim Jin <28945404+kim5566@users.noreply.github.com> Date: Thu, 11 Nov 2021 21:45:40 +1100 Subject: [PATCH 036/181] Update TechDocs Headings css font weight (#7931) * update headings style Signed-off-by: kim5566 <28945404+kim5566@users.noreply.github.com> * add changeset Signed-off-by: kim5566 <28945404+kim5566@users.noreply.github.com> * update changeset Signed-off-by: kim5566 <28945404+kim5566@users.noreply.github.com> * rename changeset Signed-off-by: kim5566 <28945404+kim5566@users.noreply.github.com> --- .changeset/techdocs-friendly-chairs-wait.md | 5 +++++ plugins/techdocs/src/reader/components/Reader.tsx | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/techdocs-friendly-chairs-wait.md diff --git a/.changeset/techdocs-friendly-chairs-wait.md b/.changeset/techdocs-friendly-chairs-wait.md new file mode 100644 index 0000000000..b4881ade11 --- /dev/null +++ b/.changeset/techdocs-friendly-chairs-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Update font weight for headings in TechDocs diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 9bb0a7bbad..4e12ac18d1 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -198,6 +198,7 @@ export const useTechDocsReaderDom = (): Element | null => { .md-footer-nav__link { width: 20rem;} .md-content { margin-left: 20rem; max-width: calc(100% - 20rem * 2 - 3rem); } .md-typeset { font-size: 1rem; } + .md-typeset h1, .md-typeset h2, .md-typeset h3 { font-weight: bold; } .md-nav { font-size: 1rem; } .md-grid { max-width: 90vw; margin: 0 } .md-typeset table:not([class]) { From 3369ade8c09b3000a723ae40fa37eca8e924db00 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Nov 2021 11:07:58 +0000 Subject: [PATCH 037/181] Version Packages --- .changeset/angry-countries-cheat.md | 5 -- .changeset/chilled-pandas-leave.md | 5 -- .changeset/chilly-emus-roll.md | 5 -- .changeset/clean-fans-change.md | 5 -- .changeset/dry-spies-cover.md | 8 --- .changeset/dull-mangos-count.md | 6 --- .changeset/early-trains-hammer.md | 7 --- .changeset/few-waves-dream.md | 5 -- .changeset/flat-dodos-bake.md | 5 -- .changeset/flat-tomatoes-drop.md | 28 ---------- .changeset/four-years-develop.md | 5 -- .changeset/giant-years-help.md | 7 --- .changeset/gold-cobras-stare.md | 6 --- .changeset/grumpy-pugs-report.md | 23 --------- .changeset/late-walls-cry.md | 5 -- .changeset/light-knives-camp.md | 5 -- .changeset/long-bugs-kiss.md | 5 -- .changeset/loud-bats-flow.md | 5 -- .changeset/lovely-cars-sneeze.md | 5 -- .changeset/many-mayflies-notice.md | 9 ---- .changeset/mean-cars-wash.md | 5 -- .changeset/metal-impalas-allow.md | 15 ------ .changeset/mighty-gifts-visit.md | 5 -- .changeset/modern-beers-tickle.md | 5 -- .changeset/nasty-actors-push.md | 5 -- .changeset/neat-pugs-wait.md | 5 -- .changeset/nice-tools-cheat.md | 5 -- .changeset/nine-dots-suffer.md | 14 ----- .changeset/odd-rats-walk.md | 5 -- .changeset/perfect-avocados-mate.md | 5 -- .changeset/proud-eels-work.md | 17 ------- .changeset/quiet-clocks-push.md | 5 -- .changeset/real-mails-add.md | 5 -- .changeset/rich-meals-switch.md | 5 -- .changeset/selfish-bags-punch.md | 5 -- .changeset/selfish-countries-prove.md | 42 --------------- .changeset/seven-zoos-sparkle.md | 14 ----- .changeset/slimy-days-leave.md | 5 -- .changeset/smart-penguins-compete.md | 5 -- .changeset/spicy-panthers-thank.md | 5 -- .changeset/swift-turtles-provide.md | 7 --- .changeset/tall-boxes-sit.md | 5 -- .changeset/tall-lions-return.md | 5 -- .changeset/techdocs-buh-bump-ts.md | 5 -- .changeset/techdocs-friendly-chairs-wait.md | 5 -- .changeset/techdocs-tender-bugs-sort.md | 5 -- .changeset/tender-chefs-approve.md | 6 --- .changeset/tough-buckets-explain.md | 5 -- .changeset/wet-pears-thank.md | 5 -- .changeset/wicked-boats-lie.md | 7 --- .changeset/young-steaks-punch.md | 5 -- packages/backend-common/CHANGELOG.md | 9 ++++ packages/backend-common/package.json | 8 +-- packages/backend/CHANGELOG.md | 15 ++++++ packages/backend/package.json | 22 ++++---- packages/catalog-client/CHANGELOG.md | 6 +++ packages/catalog-client/package.json | 4 +- packages/cli/CHANGELOG.md | 13 +++++ packages/cli/package.json | 16 +++--- packages/codemods/CHANGELOG.md | 9 ++++ packages/codemods/package.json | 2 +- packages/config-loader/CHANGELOG.md | 7 +++ packages/config-loader/package.json | 2 +- packages/core-app-api/CHANGELOG.md | 11 ++++ packages/core-app-api/package.json | 12 ++--- packages/core-components/CHANGELOG.md | 34 +++++++++++++ packages/core-components/package.json | 12 ++--- packages/core-plugin-api/CHANGELOG.md | 10 ++++ packages/core-plugin-api/package.json | 10 ++-- packages/create-app/CHANGELOG.md | 51 +++++++++++++++++++ packages/create-app/package.json | 2 +- packages/techdocs-common/CHANGELOG.md | 21 ++++++++ packages/techdocs-common/package.json | 6 +-- packages/test-utils/CHANGELOG.md | 10 ++++ packages/test-utils/package.json | 10 ++-- packages/theme/CHANGELOG.md | 8 +++ packages/theme/package.json | 4 +- plugins/allure/package.json | 12 ++--- plugins/analytics-module-ga/package.json | 12 ++--- plugins/api-docs/CHANGELOG.md | 13 +++++ plugins/api-docs/package.json | 16 +++--- plugins/auth-backend/CHANGELOG.md | 11 ++++ plugins/auth-backend/package.json | 10 ++-- plugins/azure-devops-backend/CHANGELOG.md | 14 +++++ plugins/azure-devops-backend/package.json | 8 +-- plugins/azure-devops-common/CHANGELOG.md | 9 ++++ plugins/azure-devops-common/package.json | 4 +- plugins/azure-devops/CHANGELOG.md | 15 ++++++ plugins/azure-devops/package.json | 18 +++---- plugins/badges/package.json | 12 ++--- plugins/bazaar/CHANGELOG.md | 11 ++++ plugins/bazaar/package.json | 12 ++--- plugins/bitrise/package.json | 12 ++--- .../CHANGELOG.md | 11 ++++ .../package.json | 10 ++-- plugins/catalog-backend/CHANGELOG.md | 9 ++++ plugins/catalog-backend/package.json | 10 ++-- plugins/catalog-graph/package.json | 12 ++--- plugins/catalog-import/package.json | 10 ++-- plugins/catalog-react/CHANGELOG.md | 11 ++++ plugins/catalog-react/package.json | 14 ++--- plugins/catalog/package.json | 12 ++--- plugins/circleci/CHANGELOG.md | 11 ++++ plugins/circleci/package.json | 16 +++--- plugins/cloudbuild/package.json | 12 ++--- plugins/code-coverage/CHANGELOG.md | 12 +++++ plugins/code-coverage/package.json | 16 +++--- plugins/config-schema/package.json | 12 ++--- plugins/cost-insights/package.json | 12 ++--- plugins/explore/package.json | 12 ++--- plugins/firehydrant/package.json | 12 ++--- plugins/fossa/package.json | 12 ++--- plugins/gcp-projects/package.json | 12 ++--- plugins/git-release-manager/package.json | 12 ++--- plugins/github-actions/CHANGELOG.md | 11 ++++ plugins/github-actions/package.json | 16 +++--- plugins/github-deployments/package.json | 12 ++--- plugins/gitops-profiles/package.json | 12 ++--- plugins/graphiql/package.json | 12 ++--- plugins/home/CHANGELOG.md | 10 ++++ plugins/home/package.json | 14 ++--- plugins/ilert/CHANGELOG.md | 11 ++++ plugins/ilert/package.json | 16 +++--- plugins/jenkins-backend/CHANGELOG.md | 9 ++++ plugins/jenkins-backend/package.json | 8 +-- plugins/jenkins/package.json | 12 ++--- plugins/kafka/CHANGELOG.md | 11 ++++ plugins/kafka/package.json | 16 +++--- plugins/kubernetes/CHANGELOG.md | 11 ++++ plugins/kubernetes/package.json | 16 +++--- plugins/lighthouse/package.json | 12 ++--- plugins/newrelic/package.json | 12 ++--- plugins/org/package.json | 12 ++--- plugins/pagerduty/package.json | 12 ++--- plugins/rollbar/package.json | 12 ++--- .../CHANGELOG.md | 9 ++++ .../package.json | 8 +-- .../CHANGELOG.md | 9 ++++ .../package.json | 8 +-- .../CHANGELOG.md | 8 +++ .../package.json | 6 +-- plugins/scaffolder-backend/CHANGELOG.md | 50 ++++++++++++++++++ plugins/scaffolder-backend/package.json | 14 ++--- plugins/scaffolder/CHANGELOG.md | 50 ++++++++++++++++++ plugins/scaffolder/package.json | 18 +++---- .../CHANGELOG.md | 6 +++ .../package.json | 6 +-- plugins/search/CHANGELOG.md | 24 +++++++++ plugins/search/package.json | 16 +++--- plugins/sentry/CHANGELOG.md | 11 ++++ plugins/sentry/package.json | 16 +++--- plugins/shortcuts/package.json | 12 ++--- plugins/sonarqube/package.json | 12 ++--- plugins/splunk-on-call/package.json | 12 ++--- plugins/tech-radar/package.json | 12 ++--- plugins/techdocs/CHANGELOG.md | 13 +++++ plugins/techdocs/package.json | 18 +++---- plugins/todo/package.json | 12 ++--- plugins/user-settings/package.json | 12 ++--- plugins/xcmetrics/package.json | 12 ++--- 160 files changed, 979 insertions(+), 796 deletions(-) delete mode 100644 .changeset/angry-countries-cheat.md delete mode 100644 .changeset/chilled-pandas-leave.md delete mode 100644 .changeset/chilly-emus-roll.md delete mode 100644 .changeset/clean-fans-change.md delete mode 100644 .changeset/dry-spies-cover.md delete mode 100644 .changeset/dull-mangos-count.md delete mode 100644 .changeset/early-trains-hammer.md delete mode 100644 .changeset/few-waves-dream.md delete mode 100644 .changeset/flat-dodos-bake.md delete mode 100644 .changeset/flat-tomatoes-drop.md delete mode 100644 .changeset/four-years-develop.md delete mode 100644 .changeset/giant-years-help.md delete mode 100644 .changeset/gold-cobras-stare.md delete mode 100644 .changeset/grumpy-pugs-report.md delete mode 100644 .changeset/late-walls-cry.md delete mode 100644 .changeset/light-knives-camp.md delete mode 100644 .changeset/long-bugs-kiss.md delete mode 100644 .changeset/loud-bats-flow.md delete mode 100644 .changeset/lovely-cars-sneeze.md delete mode 100644 .changeset/many-mayflies-notice.md delete mode 100644 .changeset/mean-cars-wash.md delete mode 100644 .changeset/metal-impalas-allow.md delete mode 100644 .changeset/mighty-gifts-visit.md delete mode 100644 .changeset/modern-beers-tickle.md delete mode 100644 .changeset/nasty-actors-push.md delete mode 100644 .changeset/neat-pugs-wait.md delete mode 100644 .changeset/nice-tools-cheat.md delete mode 100644 .changeset/nine-dots-suffer.md delete mode 100644 .changeset/odd-rats-walk.md delete mode 100644 .changeset/perfect-avocados-mate.md delete mode 100644 .changeset/proud-eels-work.md delete mode 100644 .changeset/quiet-clocks-push.md delete mode 100644 .changeset/real-mails-add.md delete mode 100644 .changeset/rich-meals-switch.md delete mode 100644 .changeset/selfish-bags-punch.md delete mode 100644 .changeset/selfish-countries-prove.md delete mode 100644 .changeset/seven-zoos-sparkle.md delete mode 100644 .changeset/slimy-days-leave.md delete mode 100644 .changeset/smart-penguins-compete.md delete mode 100644 .changeset/spicy-panthers-thank.md delete mode 100644 .changeset/swift-turtles-provide.md delete mode 100644 .changeset/tall-boxes-sit.md delete mode 100644 .changeset/tall-lions-return.md delete mode 100644 .changeset/techdocs-buh-bump-ts.md delete mode 100644 .changeset/techdocs-friendly-chairs-wait.md delete mode 100644 .changeset/techdocs-tender-bugs-sort.md delete mode 100644 .changeset/tender-chefs-approve.md delete mode 100644 .changeset/tough-buckets-explain.md delete mode 100644 .changeset/wet-pears-thank.md delete mode 100644 .changeset/wicked-boats-lie.md delete mode 100644 .changeset/young-steaks-punch.md create mode 100644 plugins/azure-devops-common/CHANGELOG.md diff --git a/.changeset/angry-countries-cheat.md b/.changeset/angry-countries-cheat.md deleted file mode 100644 index e97e0ae56c..0000000000 --- a/.changeset/angry-countries-cheat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Switched to dynamically determining the packages that are unsafe to repack when executing the CLI within the Backstage main repo. diff --git a/.changeset/chilled-pandas-leave.md b/.changeset/chilled-pandas-leave.md deleted file mode 100644 index 7ad5953f7f..0000000000 --- a/.changeset/chilled-pandas-leave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Fixed the `config:check` command that was incorrectly only validating frontend configuration. Also added a `--frontend` flag to the command which maintains that behavior. diff --git a/.changeset/chilly-emus-roll.md b/.changeset/chilly-emus-roll.md deleted file mode 100644 index 1569820980..0000000000 --- a/.changeset/chilly-emus-roll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-home': patch ---- - -Add name option to `createCardExtension` to remove deprecation warnings for extensions without name. Name will be required for extensions in a future release of `core-plugin-api` and therefore also in `createCardExtension`. diff --git a/.changeset/clean-fans-change.md b/.changeset/clean-fans-change.md deleted file mode 100644 index fad4a9cfa6..0000000000 --- a/.changeset/clean-fans-change.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch ---- - -fix: kubernetes plugin shall pass id token on get clusters request if possible diff --git a/.changeset/dry-spies-cover.md b/.changeset/dry-spies-cover.md deleted file mode 100644 index aae683faaf..0000000000 --- a/.changeset/dry-spies-cover.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/core-components': patch -'@backstage/theme': patch ---- - -Added a warning variant to `DismissableBanner` component. If you are using a -custom theme, you will need to add the optional `palette.banner.warning` color, -otherwise this variant will fall back to the `palette.banner.error` color. diff --git a/.changeset/dull-mangos-count.md b/.changeset/dull-mangos-count.md deleted file mode 100644 index 64a01c121d..0000000000 --- a/.changeset/dull-mangos-count.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-circleci': patch -'@backstage/plugin-github-actions': patch ---- - -Use `` instead of `` in combination with `Suspense`. diff --git a/.changeset/early-trains-hammer.md b/.changeset/early-trains-hammer.md deleted file mode 100644 index 853788e015..0000000000 --- a/.changeset/early-trains-hammer.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-msgraph': patch ---- - -Tweak logic for msgraph catalog ingesting for display names with security groups - -Previously security groups that weren't mail enabled were imported with UUIDs, now they use the display name. diff --git a/.changeset/few-waves-dream.md b/.changeset/few-waves-dream.md deleted file mode 100644 index 72123171b8..0000000000 --- a/.changeset/few-waves-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -AWSS3UrlReader now throws a `NotModifiedError` (exported from @backstage/backend-common) when s3 returns a 304 response. diff --git a/.changeset/flat-dodos-bake.md b/.changeset/flat-dodos-bake.md deleted file mode 100644 index f6b52aac1a..0000000000 --- a/.changeset/flat-dodos-bake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-jenkins-backend': patch ---- - -Don't require a validation pattern for the Jenkins base URL. diff --git a/.changeset/flat-tomatoes-drop.md b/.changeset/flat-tomatoes-drop.md deleted file mode 100644 index 97ea32d8fd..0000000000 --- a/.changeset/flat-tomatoes-drop.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Integrated `SidebarSearchModal` component into default-app to use the `SearchModal`. - -The `SidebarSearchModal` component can also be used in other generated apps: - -```diff -import { -- SidebarSearch, -+ SidebarSearchModal -} from '@backstage/plugin-search'; -... - - - -- -+ - -... -``` - -If you only want to use the `SearchModal` you can import it from `'@backstage/plugin-search'`: - -```js -import { SearchModal } from '@backstage/plugin-search'; -``` diff --git a/.changeset/four-years-develop.md b/.changeset/four-years-develop.md deleted file mode 100644 index cfd3704f2f..0000000000 --- a/.changeset/four-years-develop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Allow for `cellStyle` property on `TableColumn` to be a function as well as `React.CSSProperties` as per the Material UI Table component diff --git a/.changeset/giant-years-help.md b/.changeset/giant-years-help.md deleted file mode 100644 index c6ca0def30..0000000000 --- a/.changeset/giant-years-help.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/cli': patch -'@backstage/config-loader': patch ---- - -Reading app config from a remote server diff --git a/.changeset/gold-cobras-stare.md b/.changeset/gold-cobras-stare.md deleted file mode 100644 index d3265c865a..0000000000 --- a/.changeset/gold-cobras-stare.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-techdocs': patch ---- - -Lazy load `LazyLog` as it is rarely used. diff --git a/.changeset/grumpy-pugs-report.md b/.changeset/grumpy-pugs-report.md deleted file mode 100644 index db915b7a79..0000000000 --- a/.changeset/grumpy-pugs-report.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Add new way to override color selection to progress bar/gauge components. - -`Gauge`, `LinearGauge` and `GaugeCard` all accept a `getColor` prop, -which is a function of the type: - -```ts -export type GaugePropsGetColor = (args: { - palette: Palette; - value: number; - inverse?: boolean; - max?: number; -}) => string; -``` - -Return a standard CSS color string (e.g. "red", "#f02020"), and the gauge will -be set to that color. - -If the prop is omitted, the default implementation is unchanged from previous -versions. diff --git a/.changeset/late-walls-cry.md b/.changeset/late-walls-cry.md deleted file mode 100644 index c9fcb3112a..0000000000 --- a/.changeset/late-walls-cry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend-module-elasticsearch': patch ---- - -Pinning version of elastic search client to 7.13.0 to prevent breaking change towards third party ElasticSearch clusters on 7.14.0. diff --git a/.changeset/light-knives-camp.md b/.changeset/light-knives-camp.md deleted file mode 100644 index 1e7fa37082..0000000000 --- a/.changeset/light-knives-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-ilert': patch ---- - -Change the version of `@date-io/luxon` from 2.x to 1.x to make it compatible with material-ui-pickers diff --git a/.changeset/long-bugs-kiss.md b/.changeset/long-bugs-kiss.md deleted file mode 100644 index 49e0edae72..0000000000 --- a/.changeset/long-bugs-kiss.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kafka': patch ---- - -Use IdentityApi to provide Auth Token for KafkaBackendClient Api calls diff --git a/.changeset/loud-bats-flow.md b/.changeset/loud-bats-flow.md deleted file mode 100644 index af4837dbbe..0000000000 --- a/.changeset/loud-bats-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/test-utils': patch ---- - -Update Keyboard deprecation with a link to the recommended successor diff --git a/.changeset/lovely-cars-sneeze.md b/.changeset/lovely-cars-sneeze.md deleted file mode 100644 index 8e6bfd691c..0000000000 --- a/.changeset/lovely-cars-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Expose some classes and interfaces public so TaskWorkers can run externally from the scaffolder API. diff --git a/.changeset/many-mayflies-notice.md b/.changeset/many-mayflies-notice.md deleted file mode 100644 index a96d1fcce5..0000000000 --- a/.changeset/many-mayflies-notice.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/plugin-azure-devops': patch -'@backstage/plugin-azure-devops-backend': minor -'@backstage/plugin-azure-devops-common': patch ---- - -refactor(`@backstage/plugin-azure-devops`): Consume types from `@backstage/plugin-azure-devops-common`. -Stop re-exporting types from `@backstage/plugin-azure-devops-backend`. -Added new types to `@backstage/plugin-azure-devops-common`. diff --git a/.changeset/mean-cars-wash.md b/.changeset/mean-cars-wash.md deleted file mode 100644 index 187f5d7e21..0000000000 --- a/.changeset/mean-cars-wash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/catalog-client': patch ---- - -Improved API documentation for catalog-client. diff --git a/.changeset/metal-impalas-allow.md b/.changeset/metal-impalas-allow.md deleted file mode 100644 index 8ad727f759..0000000000 --- a/.changeset/metal-impalas-allow.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -1. Techdocs publisher constructors now use parameter objects when being - instantiated - -2. Internal refactor of `LocalPublish` publisher to use `fromConfig` for - creation to be aligned with other publishers; this does not impact - `LocalPublish` usage. - -```diff -- const publisher = new LocalPublish(config, logger, discovery); -+ const publisher = LocalPublish.fromConfig(config, logger, discovery); -``` diff --git a/.changeset/mighty-gifts-visit.md b/.changeset/mighty-gifts-visit.md deleted file mode 100644 index 8ceab2906e..0000000000 --- a/.changeset/mighty-gifts-visit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-bazaar': patch ---- - -Name extension to remove deprecation warning diff --git a/.changeset/modern-beers-tickle.md b/.changeset/modern-beers-tickle.md deleted file mode 100644 index 6ad9cc872d..0000000000 --- a/.changeset/modern-beers-tickle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Only use settings that have a value when creating a new FirestoreKeyStore instance diff --git a/.changeset/nasty-actors-push.md b/.changeset/nasty-actors-push.md deleted file mode 100644 index 8f4d0d92a8..0000000000 --- a/.changeset/nasty-actors-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sentry': patch ---- - -fix: sentry-plugin can forward identity token to backend (for case when it requires authorization) diff --git a/.changeset/neat-pugs-wait.md b/.changeset/neat-pugs-wait.md deleted file mode 100644 index 8902f44c9d..0000000000 --- a/.changeset/neat-pugs-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-code-coverage': patch ---- - -Change represented test date from epoch to something more human friendly. Round test coverage to 2 decimal places. diff --git a/.changeset/nice-tools-cheat.md b/.changeset/nice-tools-cheat.md deleted file mode 100644 index c4f0e162bf..0000000000 --- a/.changeset/nice-tools-cheat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Make `ExitCodeError` call `super` early to avoid compiler warnings diff --git a/.changeset/nine-dots-suffer.md b/.changeset/nine-dots-suffer.md deleted file mode 100644 index 3efd307a10..0000000000 --- a/.changeset/nine-dots-suffer.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Updated the app template to no longer include the `--no-private` flag for the `create-plugin` command. - -To apply this change to an existing application, remove the `--no-private` flag from the `create-plugin` command in the root `package.json`: - -```diff - "prettier:check": "prettier --check .", -- "create-plugin": "backstage-cli create-plugin --scope internal --no-private", -+ "create-plugin": "backstage-cli create-plugin --scope internal", - "remove-plugin": "backstage-cli remove-plugin" -``` diff --git a/.changeset/odd-rats-walk.md b/.changeset/odd-rats-walk.md deleted file mode 100644 index 3a8710cc32..0000000000 --- a/.changeset/odd-rats-walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/config-loader': patch ---- - -Bump msw to the same version as the rest diff --git a/.changeset/perfect-avocados-mate.md b/.changeset/perfect-avocados-mate.md deleted file mode 100644 index 9394b57449..0000000000 --- a/.changeset/perfect-avocados-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-code-coverage': patch ---- - -Make dates in X-Axis sort in ascending order diff --git a/.changeset/proud-eels-work.md b/.changeset/proud-eels-work.md deleted file mode 100644 index a4f2c86eb4..0000000000 --- a/.changeset/proud-eels-work.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@backstage/plugin-search': patch ---- - -Added `SearchModal` component. - -Now you can import `SearchModal` in your apps: - -```js -import { SearchModal } from '@backstage/plugin-search'; -``` - -You can also use the `SidebarSearchModal` component to integrate it into the sidebar of your sample apps: - -```js -import { SidebarSearchModal } from '@backstage/plugin-search'; -``` diff --git a/.changeset/quiet-clocks-push.md b/.changeset/quiet-clocks-push.md deleted file mode 100644 index 2b469a5fc5..0000000000 --- a/.changeset/quiet-clocks-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-plugin-api': patch ---- - -Deprecate use of extensions without name. Adds a warning to the developer console to prompt integrators to provide names for extensions. diff --git a/.changeset/real-mails-add.md b/.changeset/real-mails-add.md deleted file mode 100644 index 30a9100699..0000000000 --- a/.changeset/real-mails-add.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -fixed route resolving (issue #7741) when user cannot select a tab in any of the tabbed pages (like the Catalog page) if it shares the same initial letters as a preceding tab. (i.e. where tab with a path of /ci is followed by a path of /ci-2, user cannot select /ci-2 as /ci will always be selected first). diff --git a/.changeset/rich-meals-switch.md b/.changeset/rich-meals-switch.md deleted file mode 100644 index 8a9467a84e..0000000000 --- a/.changeset/rich-meals-switch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Bumped `@spotify/eslint-config-react` from `v10` to `v12`, dropping support for Node.js v12. diff --git a/.changeset/selfish-bags-punch.md b/.changeset/selfish-bags-punch.md deleted file mode 100644 index 1950da9357..0000000000 --- a/.changeset/selfish-bags-punch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Added forwarding of the `audience` option for the SAML provider, making it possible to enable `audience` verification. diff --git a/.changeset/selfish-countries-prove.md b/.changeset/selfish-countries-prove.md deleted file mode 100644 index 03e36f6d80..0000000000 --- a/.changeset/selfish-countries-prove.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-scaffolder-backend': patch ---- - -Adds a new endpoint for consuming logs from the Scaffolder that uses long polling instead of Server Sent Events. - -This is useful if Backstage is accessed from an environment that doesn't support SSE correctly, which happens in combination with certain enterprise HTTP Proxy servers. - -It is intended to switch the endpoint globally for the whole instance. -If you want to use it, you can provide a reconfigured API to the `scaffolderApiRef`: - -```tsx -// packages/app/src/apis.ts - -// ... -import { - scaffolderApiRef, - ScaffolderClient, -} from '@backstage/plugin-scaffolder'; - -export const apis: AnyApiFactory[] = [ - // ... - - createApiFactory({ - api: scaffolderApiRef, - deps: { - discoveryApi: discoveryApiRef, - identityApi: identityApiRef, - scmIntegrationsApi: scmIntegrationsApiRef, - }, - factory: ({ discoveryApi, identityApi, scmIntegrationsApi }) => - new ScaffolderClient({ - discoveryApi, - identityApi, - scmIntegrationsApi, - // use long polling instead of an eventsource - useLongPollingLogs: true, - }), - }), -]; -``` diff --git a/.changeset/seven-zoos-sparkle.md b/.changeset/seven-zoos-sparkle.md deleted file mode 100644 index 1399c3254e..0000000000 --- a/.changeset/seven-zoos-sparkle.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Removed the version pinning of the packages `graphql-language-service-interface` and `graphql-language-service-parser`. This should no longer be necessary. - -You can apply the same change in your repository by ensuring that the following does _NOT_ appear in your root `package.json`. - -```json -"resolutions": { - "graphql-language-service-interface": "2.8.2", - "graphql-language-service-parser": "1.9.0" - }, -``` diff --git a/.changeset/slimy-days-leave.md b/.changeset/slimy-days-leave.md deleted file mode 100644 index df772d80d4..0000000000 --- a/.changeset/slimy-days-leave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Allow singleton and flexibly nested EntityFilters diff --git a/.changeset/smart-penguins-compete.md b/.changeset/smart-penguins-compete.md deleted file mode 100644 index d3518600a6..0000000000 --- a/.changeset/smart-penguins-compete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -export `loadIdentityOwnerRefs` and `loadCatalogOwnerRefs` all the way diff --git a/.changeset/spicy-panthers-thank.md b/.changeset/spicy-panthers-thank.md deleted file mode 100644 index 6c28b002f4..0000000000 --- a/.changeset/spicy-panthers-thank.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': patch ---- - -I have added default icons for the catalog, scaffolder, techdocs, and search. diff --git a/.changeset/swift-turtles-provide.md b/.changeset/swift-turtles-provide.md deleted file mode 100644 index 52cf9a7c8f..0000000000 --- a/.changeset/swift-turtles-provide.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch ---- - -Lazy load all API definition widgets. The widgets use libraries like -`swagger-ui`, `graphiql`, and `@asyncapi/react-component` which are quite heavy -weight. To improve initial load times, the widgets are only loaded once used. diff --git a/.changeset/tall-boxes-sit.md b/.changeset/tall-boxes-sit.md deleted file mode 100644 index a74e0400bd..0000000000 --- a/.changeset/tall-boxes-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Add Theme Overrides for Sidebar diff --git a/.changeset/tall-lions-return.md b/.changeset/tall-lions-return.md deleted file mode 100644 index 56bb0fc3dd..0000000000 --- a/.changeset/tall-lions-return.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Bumped `@spotify/eslint-config-typescript` from `v10` to `v12`, dropping support for Node.js v12. diff --git a/.changeset/techdocs-buh-bump-ts.md b/.changeset/techdocs-buh-bump-ts.md deleted file mode 100644 index 438843e04b..0000000000 --- a/.changeset/techdocs-buh-bump-ts.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -Default TechDocs container used at docs generation-time is now [v0.3.5](https://github.com/backstage/techdocs-container/releases/tag/v0.3.5). diff --git a/.changeset/techdocs-friendly-chairs-wait.md b/.changeset/techdocs-friendly-chairs-wait.md deleted file mode 100644 index b4881ade11..0000000000 --- a/.changeset/techdocs-friendly-chairs-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Update font weight for headings in TechDocs diff --git a/.changeset/techdocs-tender-bugs-sort.md b/.changeset/techdocs-tender-bugs-sort.md deleted file mode 100644 index be670b06ff..0000000000 --- a/.changeset/techdocs-tender-bugs-sort.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -Updated to properly join URL segments under any OS for both AWS S3 and GCP diff --git a/.changeset/tender-chefs-approve.md b/.changeset/tender-chefs-approve.md deleted file mode 100644 index 1cc618507c..0000000000 --- a/.changeset/tender-chefs-approve.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/core-app-api': patch -'@backstage/core-plugin-api': patch ---- - -Improve API documentation for @backstage/core-plugin-api diff --git a/.changeset/tough-buckets-explain.md b/.changeset/tough-buckets-explain.md deleted file mode 100644 index a2ca37d516..0000000000 --- a/.changeset/tough-buckets-explain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-azure-devops': patch ---- - -Simplified queue time calculation in `BuildTable`. diff --git a/.changeset/wet-pears-thank.md b/.changeset/wet-pears-thank.md deleted file mode 100644 index f9b5576fb3..0000000000 --- a/.changeset/wet-pears-thank.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Fix a bug where only file mode 775 is considered an executable diff --git a/.changeset/wicked-boats-lie.md b/.changeset/wicked-boats-lie.md deleted file mode 100644 index 8c5da92d9c..0000000000 --- a/.changeset/wicked-boats-lie.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch -'@backstage/plugin-scaffolder-backend-module-rails': patch -'@backstage/plugin-scaffolder-backend-module-yeoman': patch ---- - -Add missing API docs to scaffolder action plugins diff --git a/.changeset/young-steaks-punch.md b/.changeset/young-steaks-punch.md deleted file mode 100644 index 55997af565..0000000000 --- a/.changeset/young-steaks-punch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-plugin-api': patch ---- - -Deprecate unused ApiRef types diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index f0368b55d4..f3464917aa 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/backend-common +## 0.9.9 + +### Patch Changes + +- 8c4cad0bf2: AWSS3UrlReader now throws a `NotModifiedError` (exported from @backstage/backend-common) when s3 returns a 304 response. +- 0611f3b3e2: Reading app config from a remote server +- Updated dependencies + - @backstage/config-loader@0.7.2 + ## 0.9.8 ### Patch Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index a174862b4a..03ae1fbe62 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.9.8", + "version": "0.9.9", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -31,7 +31,7 @@ "dependencies": { "@backstage/cli-common": "^0.1.5", "@backstage/config": "^0.1.11", - "@backstage/config-loader": "^0.7.1", + "@backstage/config-loader": "^0.7.2", "@backstage/errors": "^0.1.4", "@backstage/integration": "^0.6.9", "@backstage/types": "^0.1.1", @@ -79,8 +79,8 @@ } }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/test-utils": "^0.1.20", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 19e9bcdd67..7cf7d9ddaa 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,20 @@ # example-backend +## 0.2.52 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/plugin-jenkins-backend@0.1.7 + - @backstage/plugin-search-backend-module-elasticsearch@0.0.5 + - @backstage/plugin-scaffolder-backend@0.15.12 + - @backstage/plugin-azure-devops-backend@0.2.0 + - @backstage/catalog-client@0.5.1 + - @backstage/plugin-auth-backend@0.4.7 + - @backstage/plugin-catalog-backend@0.17.3 + - @backstage/plugin-scaffolder-backend-module-rails@0.1.7 + ## 0.2.50 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index f1d58252a4..4c854241d4 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.50", + "version": "0.2.52", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,28 +24,28 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/catalog-client": "^0.5.0", + "@backstage/backend-common": "^0.9.9", + "@backstage/catalog-client": "^0.5.1", "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", "@backstage/integration": "^0.6.8", "@backstage/plugin-app-backend": "^0.3.17", - "@backstage/plugin-auth-backend": "^0.4.5", - "@backstage/plugin-azure-devops-backend": "^0.1.3", + "@backstage/plugin-auth-backend": "^0.4.7", + "@backstage/plugin-azure-devops-backend": "^0.2.0", "@backstage/plugin-badges-backend": "^0.1.11", - "@backstage/plugin-catalog-backend": "^0.17.1", + "@backstage/plugin-catalog-backend": "^0.17.3", "@backstage/plugin-code-coverage-backend": "^0.1.14", "@backstage/plugin-graphql-backend": "^0.1.9", - "@backstage/plugin-jenkins-backend": "^0.1.6", + "@backstage/plugin-jenkins-backend": "^0.1.7", "@backstage/plugin-kubernetes-backend": "^0.3.18", "@backstage/plugin-kafka-backend": "^0.2.10", "@backstage/plugin-proxy-backend": "^0.2.13", "@backstage/plugin-rollbar-backend": "^0.1.15", - "@backstage/plugin-scaffolder-backend": "^0.15.10", - "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.5", + "@backstage/plugin-scaffolder-backend": "^0.15.12", + "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.7", "@backstage/plugin-search-backend": "^0.2.6", "@backstage/plugin-search-backend-node": "^0.4.2", - "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.4", + "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.5", "@backstage/plugin-search-backend-module-pg": "^0.2.1", "@backstage/plugin-techdocs-backend": "^0.10.5", "@backstage/plugin-tech-insights-backend": "^0.1.0", @@ -68,7 +68,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.2", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 8243d09486..f5ec2116f7 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/catalog-client +## 0.5.1 + +### Patch Changes + +- 39e92897e4: Improved API documentation for catalog-client. + ## 0.5.0 ### Minor Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index d4ba2c20f6..efdaacc1e0 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-client", "description": "An isomorphic client for the catalog backend", - "version": "0.5.0", + "version": "0.5.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.2", "@types/jest": "^26.0.7", "msw": "^0.35.0" }, diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 48f38cbc8a..0fc702851d 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/cli +## 0.8.2 + +### Patch Changes + +- dd355bca46: Switched to dynamically determining the packages that are unsafe to repack when executing the CLI within the Backstage main repo. +- b393c4d4be: Fixed the `config:check` command that was incorrectly only validating frontend configuration. Also added a `--frontend` flag to the command which maintains that behavior. +- 0611f3b3e2: Reading app config from a remote server +- ec64d9590c: Make `ExitCodeError` call `super` early to avoid compiler warnings +- 8af66229e7: Bumped `@spotify/eslint-config-react` from `v10` to `v12`, dropping support for Node.js v12. +- a197708da9: Bumped `@spotify/eslint-config-typescript` from `v10` to `v12`, dropping support for Node.js v12. +- Updated dependencies + - @backstage/config-loader@0.7.2 + ## 0.8.1 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 0fca97f317..546a1597e1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.8.1", + "version": "0.8.2", "private": false, "publishConfig": { "access": "public" @@ -30,7 +30,7 @@ "dependencies": { "@backstage/cli-common": "^0.1.5", "@backstage/config": "^0.1.11", - "@backstage/config-loader": "^0.7.1", + "@backstage/config-loader": "^0.7.2", "@backstage/errors": "^0.1.4", "@backstage/types": "^0.1.1", "@hot-loader/react-dom": "^16.13.0", @@ -117,14 +117,14 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.8", + "@backstage/backend-common": "^0.9.9", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/core-app-api": "^0.1.19", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", - "@backstage/theme": "^0.2.12", + "@backstage/test-utils": "^0.1.21", + "@backstage/theme": "^0.2.13", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index fbb2076167..1043d9718f 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/codemods +## 0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/core-plugin-api@0.1.13 + - @backstage/core-app-api@0.1.20 + ## 0.1.20 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 0dc0e9a0d5..17e89c86f0 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.20", + "version": "0.1.21", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index dcaec00816..0f1bc27996 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/config-loader +## 0.7.2 + +### Patch Changes + +- 0611f3b3e2: Reading app config from a remote server +- 26c5659c97: Bump msw to the same version as the rest + ## 0.7.1 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 22fcedf486..2e25d91856 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.7.1", + "version": "0.7.2", "private": false, "publishConfig": { "access": "public", diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index ff75f5bf81..b9e60b7dab 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/core-app-api +## 0.1.20 + +### Patch Changes + +- 78c512ce8f: I have added default icons for the catalog, scaffolder, techdocs, and search. +- 8b4284cd5c: Improve API documentation for @backstage/core-plugin-api +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + ## 0.1.19 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index f40e3d5eb7..a0877cf933 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.1.19", + "version": "0.1.20", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.2", + "@backstage/core-components": "^0.7.3", "@backstage/config": "^0.1.11", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", @@ -46,8 +46,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/test-utils": "^0.1.20", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index af03762094..1e62df337d 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/core-components +## 0.7.3 + +### Patch Changes + +- c11a37710a: Added a warning variant to `DismissableBanner` component. If you are using a + custom theme, you will need to add the optional `palette.banner.warning` color, + otherwise this variant will fall back to the `palette.banner.error` color. +- 5826c17b7d: Allow for `cellStyle` property on `TableColumn` to be a function as well as `React.CSSProperties` as per the Material UI Table component +- e0861b92ff: Add new way to override color selection to progress bar/gauge components. + + `Gauge`, `LinearGauge` and `GaugeCard` all accept a `getColor` prop, + which is a function of the type: + + ```ts + export type GaugePropsGetColor = (args: { + palette: Palette; + value: number; + inverse?: boolean; + max?: number; + }) => string; + ``` + + Return a standard CSS color string (e.g. "red", "#f02020"), and the gauge will + be set to that color. + + If the prop is omitted, the default implementation is unchanged from previous + versions. + +- 021986e8a3: fixed route resolving (issue #7741) when user cannot select a tab in any of the tabbed pages (like the Catalog page) if it shares the same initial letters as a preceding tab. (i.e. where tab with a path of /ci is followed by a path of /ci-2, user cannot select /ci-2 as /ci will always be selected first). +- a39a2105ef: Add Theme Overrides for Sidebar +- Updated dependencies + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + ## 0.7.2 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 7f50af7488..ffbde0072a 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.7.2", + "version": "0.7.3", "private": false, "publishConfig": { "access": "public", @@ -30,9 +30,9 @@ }, "dependencies": { "@backstage/config": "^0.1.11", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.4", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-table/core": "^3.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -67,9 +67,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/core-app-api": "^0.1.19", - "@backstage/cli": "^0.8.1", - "@backstage/test-utils": "^0.1.20", + "@backstage/core-app-api": "^0.1.20", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 9d8246f931..d92922ddb4 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/core-plugin-api +## 0.1.13 + +### Patch Changes + +- 4a336fd292: Deprecate use of extensions without name. Adds a warning to the developer console to prompt integrators to provide names for extensions. +- 8b4284cd5c: Improve API documentation for @backstage/core-plugin-api +- e059aea7b9: Deprecate unused ApiRef types +- Updated dependencies + - @backstage/theme@0.2.13 + ## 0.1.12 ### Patch Changes diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 23bf0fc8b0..6679e89645 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "0.1.12", + "version": "0.1.13", "private": false, "publishConfig": { "access": "public", @@ -30,7 +30,7 @@ }, "dependencies": { "@backstage/config": "^0.1.11", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", @@ -43,9 +43,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", - "@backstage/test-utils": "^0.1.20", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 95d99d0532..7e53d0cde5 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,56 @@ # @backstage/create-app +## 0.4.3 + +### Patch Changes + +- 5dcea2586c: Integrated `SidebarSearchModal` component into default-app to use the `SearchModal`. + + The `SidebarSearchModal` component can also be used in other generated apps: + + ```diff + import { + - SidebarSearch, + + SidebarSearchModal + } from '@backstage/plugin-search'; + ... + + + + - + + + + ... + ``` + + If you only want to use the `SearchModal` you can import it from `'@backstage/plugin-search'`: + + ```js + import { SearchModal } from '@backstage/plugin-search'; + ``` + +- 5725f87e4c: Updated the app template to no longer include the `--no-private` flag for the `create-plugin` command. + + To apply this change to an existing application, remove the `--no-private` flag from the `create-plugin` command in the root `package.json`: + + ```diff + "prettier:check": "prettier --check .", + - "create-plugin": "backstage-cli create-plugin --scope internal --no-private", + + "create-plugin": "backstage-cli create-plugin --scope internal", + "remove-plugin": "backstage-cli remove-plugin" + ``` + +- 1921f70aa7: Removed the version pinning of the packages `graphql-language-service-interface` and `graphql-language-service-parser`. This should no longer be necessary. + + You can apply the same change in your repository by ensuring that the following does _NOT_ appear in your root `package.json`. + + ```json + "resolutions": { + "graphql-language-service-interface": "2.8.2", + "graphql-language-service-parser": "1.9.0" + }, + ``` + ## 0.4.2 ## 0.4.1 diff --git a/packages/create-app/package.json b/packages/create-app/package.json index ad4ab8dabd..2820d41ed6 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.4.2", + "version": "0.4.3", "private": false, "publishConfig": { "access": "public" diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index b540f74549..87e9e12193 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/techdocs-common +## 0.10.6 + +### Patch Changes + +- a2d4389587: 1. Techdocs publisher constructors now use parameter objects when being + instantiated + + 2. Internal refactor of `LocalPublish` publisher to use `fromConfig` for + creation to be aligned with other publishers; this does not impact + `LocalPublish` usage. + + ```diff + - const publisher = new LocalPublish(config, logger, discovery); + + const publisher = LocalPublish.fromConfig(config, logger, discovery); + ``` + +- 6129c89a47: Default TechDocs container used at docs generation-time is now [v0.3.5](https://github.com/backstage/techdocs-container/releases/tag/v0.3.5). +- f3c7eec64b: Updated to properly join URL segments under any OS for both AWS S3 and GCP +- Updated dependencies + - @backstage/backend-common@0.9.9 + ## 0.10.5 ### Patch Changes diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 152b9b04f1..25577df1dd 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.10.5", + "version": "0.10.6", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -38,7 +38,7 @@ "dependencies": { "@azure/identity": "^1.5.0", "@azure/storage-blob": "^12.5.0", - "@backstage/backend-common": "^0.9.8", + "@backstage/backend-common": "^0.9.9", "@backstage/catalog-model": "^0.9.6", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.4", @@ -60,7 +60,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.8.1", + "@backstage/cli": "^0.8.2", "@types/fs-extra": "^9.0.5", "@types/js-yaml": "^4.0.0", "@types/mime-types": "^2.1.0", diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index a1fa9d3b6b..6fcacc30fa 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/test-utils +## 0.1.21 + +### Patch Changes + +- 71fd5cd735: Update Keyboard deprecation with a link to the recommended successor +- Updated dependencies + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/core-app-api@0.1.20 + ## 0.1.20 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 269b7b2d38..ab78eab683 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.20", + "version": "0.1.21", "private": false, "publishConfig": { "access": "public", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-app-api": "^0.1.19", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-app-api": "^0.1.20", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@testing-library/jest-dom": "^5.10.1", @@ -45,7 +45,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.1", + "@backstage/cli": "^0.8.2", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index 8709484c79..1c8661d7cc 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/theme +## 0.2.13 + +### Patch Changes + +- c11a37710a: Added a warning variant to `DismissableBanner` component. If you are using a + custom theme, you will need to add the optional `palette.banner.warning` color, + otherwise this variant will fall back to the `palette.banner.error` color. + ## 0.2.12 ### Patch Changes diff --git a/packages/theme/package.json b/packages/theme/package.json index 1acc91de79..d9e89c969f 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.2.12", + "version": "0.2.13", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "@material-ui/core": "^4.12.2" }, "devDependencies": { - "@backstage/cli": "^0.8.1" + "@backstage/cli": "^0.8.2" }, "files": [ "dist" diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 63864a7239..b157795eec 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -23,10 +23,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -36,10 +36,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 25534e9577..4970f50c04 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -22,9 +22,9 @@ }, "dependencies": { "@backstage/config": "^0.1.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -34,10 +34,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 188d81fcd1..78f4b08bd7 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-api-docs +## 0.6.13 + +### Patch Changes + +- 044c38e739: Lazy load all API definition widgets. The widgets use libraries like + `swagger-ui`, `graphiql`, and `@asyncapi/react-component` which are quite heavy + weight. To improve initial load times, the widgets are only loaded once used. +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.6.12 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 67045c2043..61ae26c776 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.6.12", + "version": "0.6.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,11 +32,11 @@ "dependencies": { "@asyncapi/react-component": "^0.23.0", "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog": "^0.7.2", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -53,10 +53,10 @@ "swagger-ui-react": "^4.0.0-rc.3" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index c1ba2038aa..a11f561986 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-auth-backend +## 0.4.7 + +### Patch Changes + +- 5ee31f860b: Only use settings that have a value when creating a new FirestoreKeyStore instance +- 3e0e2f09d5: Added forwarding of the `audience` option for the SAML provider, making it possible to enable `audience` verification. +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/test-utils@0.1.21 + - @backstage/catalog-client@0.5.1 + ## 0.4.6 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 5c9d70ea10..e9ec29970b 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.4.6", + "version": "0.4.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,12 +30,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.8", - "@backstage/catalog-client": "^0.5.0", + "@backstage/backend-common": "^0.9.9", + "@backstage/catalog-client": "^0.5.1", "@backstage/catalog-model": "^0.9.6", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.4", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@google-cloud/firestore": "^4.15.1", "@types/express": "^4.17.6", "@types/passport": "^1.0.3", @@ -73,7 +73,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.1", + "@backstage/cli": "^0.8.2", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index b756e7ce4f..d2fe981eb0 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-azure-devops-backend +## 0.2.0 + +### Minor Changes + +- b85acc8c35: refactor(`@backstage/plugin-azure-devops`): Consume types from `@backstage/plugin-azure-devops-common`. + Stop re-exporting types from `@backstage/plugin-azure-devops-backend`. + Added new types to `@backstage/plugin-azure-devops-common`. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/plugin-azure-devops-common@0.0.2 + ## 0.1.4 ### Patch Changes diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 9dd79e9f8e..901ad0b960 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.1.4", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.8", + "@backstage/backend-common": "^0.9.9", "@backstage/config": "^0.1.11", - "@backstage/plugin-azure-devops-common": "^0.0.1", + "@backstage/plugin-azure-devops-common": "^0.0.2", "@types/express": "^4.17.6", "azure-devops-node-api": "^11.0.1", "express": "^4.17.1", @@ -31,7 +31,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.1", + "@backstage/cli": "^0.8.2", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", "msw": "^0.35.0" diff --git a/plugins/azure-devops-common/CHANGELOG.md b/plugins/azure-devops-common/CHANGELOG.md new file mode 100644 index 0000000000..b414cd3ce6 --- /dev/null +++ b/plugins/azure-devops-common/CHANGELOG.md @@ -0,0 +1,9 @@ +# @backstage/plugin-azure-devops-common + +## 0.0.2 + +### Patch Changes + +- b85acc8c35: refactor(`@backstage/plugin-azure-devops`): Consume types from `@backstage/plugin-azure-devops-common`. + Stop re-exporting types from `@backstage/plugin-azure-devops-backend`. + Added new types to `@backstage/plugin-azure-devops-common`. diff --git a/plugins/azure-devops-common/package.json b/plugins/azure-devops-common/package.json index 05f70f6596..a1ddddcbe0 100644 --- a/plugins/azure-devops-common/package.json +++ b/plugins/azure-devops-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-common", - "version": "0.0.1", + "version": "0.0.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "devDependencies": { - "@backstage/cli": "^0.8.1" + "@backstage/cli": "^0.8.2" }, "files": [ "dist" diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index 71904265dd..2f75f64236 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-azure-devops +## 0.1.3 + +### Patch Changes + +- b85acc8c35: refactor(`@backstage/plugin-azure-devops`): Consume types from `@backstage/plugin-azure-devops-common`. + Stop re-exporting types from `@backstage/plugin-azure-devops-backend`. + Added new types to `@backstage/plugin-azure-devops-common`. +- 84ace9a29c: Simplified queue time calculation in `BuildTable`. +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/plugin-azure-devops-common@0.0.2 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.1.2 ### Patch Changes diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 25328d9e81..235304c9fa 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,12 +28,12 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.6", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.4", - "@backstage/plugin-azure-devops-common": "^0.0.1", - "@backstage/plugin-catalog-react": "^0.6.2", - "@backstage/theme": "^0.2.12", + "@backstage/plugin-azure-devops-common": "^0.0.2", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -44,10 +44,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 0cc3a06509..0c792808bc 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -28,11 +28,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -42,10 +42,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index 3cd13cfa8a..731f62b9e9 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-bazaar +## 0.1.3 + +### Patch Changes + +- 4a336fd292: Name extension to remove deprecation warning +- Updated dependencies + - @backstage/cli@0.8.2 + - @backstage/core-components@0.7.3 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.1.2 ### Patch Changes diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 5a98abfc3a..4d18784d2d 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,11 +22,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/cli": "^0.8.0", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/cli": "^0.8.2", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog": "^0.7.2", - "@backstage/plugin-catalog-react": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.6.3", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,7 +39,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", + "@backstage/cli": "^0.8.2", "@backstage/dev-utils": "^0.2.12", "@testing-library/jest-dom": "^5.10.1", "cross-fetch": "^3.0.6" diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index c4403830f1..5ebfd453a2 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -22,10 +22,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,10 +39,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 7f3b8657f9..ee8aa49065 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.2.9 + +### Patch Changes + +- 779d7a2304: Tweak logic for msgraph catalog ingesting for display names with security groups + + Previously security groups that weren't mail enabled were imported with UUIDs, now they use the display name. + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.17.3 + ## 0.2.8 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 68e58d922d..b3c9494f78 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend modules that helps integrate towards Microsoft Graph", - "version": "0.2.8", + "version": "0.2.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "@azure/msal-node": "^1.1.0", "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/plugin-catalog-backend": "^0.17.1", + "@backstage/plugin-catalog-backend": "^0.17.3", "@microsoft/microsoft-graph-types": "^2.6.0", "cross-fetch": "^3.0.6", "lodash": "^4.17.21", @@ -41,9 +41,9 @@ "qs": "^6.9.4" }, "devDependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/backend-common": "^0.9.9", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@types/lodash": "^4.14.151", "msw": "^0.35.0" }, diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index a6bdc8407d..abb50a345c 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend +## 0.17.3 + +### Patch Changes + +- 86bef79ad1: Allow singleton and flexibly nested EntityFilters +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/catalog-client@0.5.1 + ## 0.17.2 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 121dd933f9..f13ff2d66c 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "0.17.2", + "version": "0.17.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.8", - "@backstage/catalog-client": "^0.5.0", + "@backstage/backend-common": "^0.9.9", + "@backstage/catalog-client": "^0.5.1", "@backstage/catalog-model": "^0.9.6", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.4", @@ -63,8 +63,8 @@ }, "devDependencies": { "@backstage/backend-test-utils": "^0.1.8", - "@backstage/cli": "^0.8.1", - "@backstage/test-utils": "^0.1.20", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index d5cacc12a6..e58d66b1f2 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -23,10 +23,10 @@ "dependencies": { "@backstage/catalog-client": "^0.5.0", "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -41,10 +41,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 0287be8e06..242231bf81 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -33,8 +33,8 @@ "dependencies": { "@backstage/catalog-client": "^0.5.0", "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/integration-react": "^0.1.12", @@ -55,10 +55,10 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index cdb80ceac4..79dfa5e72a 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-react +## 0.6.3 + +### Patch Changes + +- 03b47a476d: export `loadIdentityOwnerRefs` and `loadCatalogOwnerRefs` all the way +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/catalog-client@0.5.1 + - @backstage/core-plugin-api@0.1.13 + - @backstage/core-app-api@0.1.20 + ## 0.6.2 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 40c0d9c739..9778163391 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "0.6.2", + "version": "0.6.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,11 +29,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.5.0", + "@backstage/catalog-client": "^0.5.1", "@backstage/catalog-model": "^0.9.6", - "@backstage/core-app-api": "^0.1.19", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-app-api": "^0.1.20", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.4", "@backstage/integration": "^0.6.9", "@backstage/types": "^0.1.1", @@ -51,8 +51,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/test-utils": "^0.1.20", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 0bc7ac4034..9697741dde 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -33,12 +33,12 @@ "dependencies": { "@backstage/catalog-client": "^0.5.0", "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.3", "@backstage/integration-react": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -51,10 +51,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index a501d77ff3..78cd6d9711 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-circleci +## 0.2.28 + +### Patch Changes + +- e849f9cc0a: Use `` instead of `` in combination with `Suspense`. +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.2.27 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 9a7c428d33..c57d19ea00 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-circleci", "description": "A Backstage plugin that integrates towards Circle CI", - "version": "0.2.27", + "version": "0.2.28", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,10 +33,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -52,10 +52,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 376dd9f091..6521816f73 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -32,10 +32,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index 8a8c91a111..572e0f7800 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-code-coverage +## 0.1.17 + +### Patch Changes + +- a5512851a0: Change represented test date from epoch to something more human friendly. Round test coverage to 2 decimal places. +- 5a85b802f3: Make dates in X-Axis sort in ascending order +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.1.16 ### Patch Changes diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index d01e12dff5..0d6e998b69 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage", "description": "A Backstage plugin that helps you keep track of your code coverage", - "version": "0.1.16", + "version": "0.1.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,11 +23,11 @@ "dependencies": { "@backstage/catalog-model": "^0.9.6", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.4", - "@backstage/plugin-catalog-react": "^0.6.2", - "@backstage/theme": "^0.2.12", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -42,10 +42,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index e460878691..0708115270 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -22,10 +22,10 @@ }, "dependencies": { "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.4", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -37,10 +37,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 010498a272..064b24bd0c 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -32,9 +32,9 @@ }, "dependencies": { "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -55,10 +55,10 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 66bd3fdf7a..30890c7176 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -32,11 +32,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/plugin-explore-react": "^0.0.6", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 0738980613..4e3fbdd3c8 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -22,10 +22,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -35,10 +35,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index c5b6b8b62f..94528a3126 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -33,11 +33,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 65458ea88b..471b9d80f1 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -43,10 +43,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 11f6655c5d..b1b2c11732 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/integration": "^0.6.8", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,10 +39,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 222bc4c851..c8fce8e501 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-github-actions +## 0.4.23 + +### Patch Changes + +- e849f9cc0a: Use `` instead of `` in combination with `Suspense`. +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.4.22 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 087a596c64..1a4079a5ca 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-actions", "description": "A Backstage plugin that integrates towards GitHub Actions", - "version": "0.4.22", + "version": "0.4.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,11 +34,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/integration": "^0.6.8", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -52,10 +52,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index a405800387..78061951f5 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -22,13 +22,13 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/integration-react": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,10 +39,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index cdebce653f..c290599c2c 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -32,9 +32,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -44,10 +44,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index b21281ff80..df1ad6f887 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -44,10 +44,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index aa35cdf968..06beb827a6 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-home +## 0.4.5 + +### Patch Changes + +- 4a336fd292: Add name option to `createCardExtension` to remove deprecation warnings for extensions without name. Name will be required for extensions in a future release of `core-plugin-api` and therefore also in `createCardExtension`. +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + ## 0.4.4 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index e100dba9ca..8020d7af03 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-home", "description": "A Backstage plugin that helps you build a home page", - "version": "0.4.4", + "version": "0.4.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -35,10 +35,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index efa2d5d8f6..f43f2add09 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-ilert +## 0.1.16 + +### Patch Changes + +- fa325fd89d: Change the version of `@date-io/luxon` from 2.x to 1.x to make it compatible with material-ui-pickers +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.1.15 ### Patch Changes diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index f93a5b4870..69c4025181 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-ilert", "description": "A Backstage plugin that integrates towards iLert", - "version": "0.1.15", + "version": "0.1.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,11 +22,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.3", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -39,10 +39,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index 635fa080ce..48f4461f7f 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-jenkins-backend +## 0.1.7 + +### Patch Changes + +- e8a1c1afe2: Don't require a validation pattern for the Jenkins base URL. +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/catalog-client@0.5.1 + ## 0.1.6 ### Patch Changes diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 93f5fa84d2..7049fc0a32 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins-backend", "description": "A Backstage backend plugin that integrates towards Jenkins", - "version": "0.1.6", + "version": "0.1.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,8 +22,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/catalog-client": "^0.5.0", + "@backstage/backend-common": "^0.9.9", + "@backstage/catalog-client": "^0.5.1", "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", "@types/express": "^4.17.6", @@ -35,7 +35,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.2", "@types/jenkins": "^0.23.1", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 1bd6388fba..80d8b40322 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -33,10 +33,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index 3697f8de28..2fe42565da 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-kafka +## 0.2.20 + +### Patch Changes + +- 91f8644f17: Use IdentityApi to provide Auth Token for KafkaBackendClient Api calls +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.2.19 ### Patch Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index e9fe6bc4bf..888c79db59 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka", "description": "A Backstage plugin that integrates towards Kafka", - "version": "0.2.19", + "version": "0.2.20", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,10 +22,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -35,10 +35,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index ac7b19a1d9..a757041b32 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-kubernetes +## 0.4.19 + +### Patch Changes + +- 9f3dd76bc7: fix: kubernetes plugin shall pass id token on get clusters request if possible +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.4.18 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 5bce077278..9dbca8aff8 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.4.18", + "version": "0.4.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,11 +33,11 @@ "dependencies": { "@backstage/catalog-model": "^0.9.6", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/plugin-catalog-react": "^0.6.2", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/plugin-catalog-react": "^0.6.3", "@backstage/plugin-kubernetes-common": "^0.1.5", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@kubernetes/client-node": "^0.15.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -51,10 +51,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 1f3dd91d32..f0dca639fc 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -34,10 +34,10 @@ "dependencies": { "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -47,10 +47,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index c60f1ee9a5..271daa566e 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -32,9 +32,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -43,10 +43,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/org/package.json b/plugins/org/package.json index dc91b59816..5348f67523 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -22,10 +22,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -37,10 +37,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 2efc824746..7b42a15bc6 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -32,10 +32,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 38e295a37b..2e2da0a284 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -33,10 +33,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 9e870a9e0b..43a522482f 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.1.4 + +### Patch Changes + +- 290fbb3ec2: Add missing API docs to scaffolder action plugins +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/plugin-scaffolder-backend@0.15.12 + ## 0.1.3 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 38c2eb493c..36a6310aa2 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.8", + "@backstage/backend-common": "^0.9.9", "@backstage/errors": "^0.1.4", "@backstage/integration": "^0.6.9", - "@backstage/plugin-scaffolder-backend": "^0.15.11", + "@backstage/plugin-scaffolder-backend": "^0.15.12", "@backstage/config": "^0.1.11", "@backstage/types": "^0.1.1", "command-exists": "^1.2.9", @@ -33,7 +33,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.1", + "@backstage/cli": "^0.8.2", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/jest": "^26.0.7", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index f1e53abc5b..9dcd6bcb88 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.1.7 + +### Patch Changes + +- 290fbb3ec2: Add missing API docs to scaffolder action plugins +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/plugin-scaffolder-backend@0.15.12 + ## 0.1.6 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index e79b7fb818..f4ace02eca 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.1.6", + "version": "0.1.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.8", - "@backstage/plugin-scaffolder-backend": "^0.15.11", + "@backstage/backend-common": "^0.9.9", + "@backstage/plugin-scaffolder-backend": "^0.15.12", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.4", "@backstage/integration": "^0.6.9", @@ -31,7 +31,7 @@ "fs-extra": "^9.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.1", + "@backstage/cli": "^0.8.2", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/command-exists": "^1.2.0", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index b15614b0ba..9373564b1c 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.1.2 + +### Patch Changes + +- 290fbb3ec2: Add missing API docs to scaffolder action plugins +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.15.12 + ## 0.1.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index b853309c56..8d17df0239 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.1.1", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,13 +21,13 @@ }, "dependencies": { "@backstage/config": "^0.1.11", - "@backstage/plugin-scaffolder-backend": "^0.15.11", + "@backstage/plugin-scaffolder-backend": "^0.15.12", "@backstage/types": "^0.1.1", "winston": "^3.2.1", "yeoman-environment": "^3.6.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.8", + "@backstage/backend-common": "^0.9.9", "@types/jest": "^26.0.7" }, "files": [ diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 689cb51d24..de033eabbd 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/plugin-scaffolder-backend +## 0.15.12 + +### Patch Changes + +- 9990df8a1f: Expose some classes and interfaces public so TaskWorkers can run externally from the scaffolder API. +- b45a34fb15: Adds a new endpoint for consuming logs from the Scaffolder that uses long polling instead of Server Sent Events. + + This is useful if Backstage is accessed from an environment that doesn't support SSE correctly, which happens in combination with certain enterprise HTTP Proxy servers. + + It is intended to switch the endpoint globally for the whole instance. + If you want to use it, you can provide a reconfigured API to the `scaffolderApiRef`: + + ```tsx + // packages/app/src/apis.ts + + // ... + import { + scaffolderApiRef, + ScaffolderClient, + } from '@backstage/plugin-scaffolder'; + + export const apis: AnyApiFactory[] = [ + // ... + + createApiFactory({ + api: scaffolderApiRef, + deps: { + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + scmIntegrationsApi: scmIntegrationsApiRef, + }, + factory: ({ discoveryApi, identityApi, scmIntegrationsApi }) => + new ScaffolderClient({ + discoveryApi, + identityApi, + scmIntegrationsApi, + // use long polling instead of an eventsource + useLongPollingLogs: true, + }), + }), + ]; + ``` + +- a794c341ca: Fix a bug where only file mode 775 is considered an executable +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/catalog-client@0.5.1 + - @backstage/plugin-catalog-backend@0.17.3 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.1.4 + ## 0.15.11 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index fe5d61d4d3..e5d13801ba 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "0.15.11", + "version": "0.15.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,15 +30,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.8", - "@backstage/catalog-client": "^0.5.0", + "@backstage/backend-common": "^0.9.9", + "@backstage/catalog-client": "^0.5.1", "@backstage/catalog-model": "^0.9.6", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.4", "@backstage/integration": "^0.6.9", - "@backstage/plugin-catalog-backend": "^0.17.2", + "@backstage/plugin-catalog-backend": "^0.17.3", "@backstage/plugin-scaffolder-common": "^0.1.1", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.3", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.4", "@backstage/types": "^0.1.1", "@gitbeaker/core": "^30.2.0", "@gitbeaker/node": "^30.2.0", @@ -71,8 +71,8 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/test-utils": "^0.1.20", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index b9873b5357..dd5653240a 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/plugin-scaffolder +## 0.11.10 + +### Patch Changes + +- fe5738fe1c: Lazy load `LazyLog` as it is rarely used. +- b45a34fb15: Adds a new endpoint for consuming logs from the Scaffolder that uses long polling instead of Server Sent Events. + + This is useful if Backstage is accessed from an environment that doesn't support SSE correctly, which happens in combination with certain enterprise HTTP Proxy servers. + + It is intended to switch the endpoint globally for the whole instance. + If you want to use it, you can provide a reconfigured API to the `scaffolderApiRef`: + + ```tsx + // packages/app/src/apis.ts + + // ... + import { + scaffolderApiRef, + ScaffolderClient, + } from '@backstage/plugin-scaffolder'; + + export const apis: AnyApiFactory[] = [ + // ... + + createApiFactory({ + api: scaffolderApiRef, + deps: { + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + scmIntegrationsApi: scmIntegrationsApiRef, + }, + factory: ({ discoveryApi, identityApi, scmIntegrationsApi }) => + new ScaffolderClient({ + discoveryApi, + identityApi, + scmIntegrationsApi, + // use long polling instead of an eventsource + useLongPollingLogs: true, + }), + }), + ]; + ``` + +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/catalog-client@0.5.1 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.11.9 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 58bee32201..1d8bc4a0ea 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "0.11.9", + "version": "0.11.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,16 +31,16 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.5.0", + "@backstage/catalog-client": "^0.5.1", "@backstage/catalog-model": "^0.9.6", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.4", "@backstage/integration": "^0.6.9", "@backstage/integration-react": "^0.1.13", - "@backstage/plugin-catalog-react": "^0.6.2", - "@backstage/theme": "^0.2.12", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -66,10 +66,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index e950678cef..6bd39200c0 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 0.0.5 + +### Patch Changes + +- 36350bf8b3: Pinning version of elastic search client to 7.13.0 to prevent breaking change towards third party ElasticSearch clusters on 7.14.0. + ## 0.0.4 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 2d5cabf725..df0a783396 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", "description": "A module for the search backend that implements search using ElasticSearch", - "version": "0.0.4", + "version": "0.0.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/cli": "^0.8.0", + "@backstage/backend-common": "^0.9.9", + "@backstage/cli": "^0.8.2", "@elastic/elasticsearch-mock": "^0.3.0" }, "files": [ diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 66c4a61ead..808c847914 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-search +## 0.4.17 + +### Patch Changes + +- 5dcea2586c: Added `SearchModal` component. + + Now you can import `SearchModal` in your apps: + + ```js + import { SearchModal } from '@backstage/plugin-search'; + ``` + + You can also use the `SidebarSearchModal` component to integrate it into the sidebar of your sample apps: + + ```js + import { SidebarSearchModal } from '@backstage/plugin-search'; + ``` + +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.4.16 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 60e8a8eb3c..9d25ec723d 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search", "description": "The Backstage plugin that provides your backstage app with search", - "version": "0.4.16", + "version": "0.4.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,12 +32,12 @@ "dependencies": { "@backstage/catalog-model": "^0.9.6", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.4", - "@backstage/plugin-catalog-react": "^0.6.2", + "@backstage/plugin-catalog-react": "^0.6.3", "@backstage/search-common": "^0.2.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -51,10 +51,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 2f44314100..b0bcaad261 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-sentry +## 0.3.28 + +### Patch Changes + +- 33d7bd0baa: fix: sentry-plugin can forward identity token to backend (for case when it requires authorization) +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.3.27 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 005bfb2b3a..ee2421bf7f 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sentry", "description": "A Backstage plugin that integrates towards Sentry", - "version": "0.3.27", + "version": "0.3.28", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,10 +33,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.6", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/plugin-catalog-react": "^0.6.2", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 8b6f57bc52..e73e047bc1 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -38,10 +38,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 30cdd83368..bb55e68954 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -34,10 +34,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 632de7e413..9d9ffdb370 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -32,10 +32,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -47,10 +47,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 31b12a97a9..839ad1b399 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -31,9 +31,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -45,10 +45,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index a825b9a008..be100b9dea 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-techdocs +## 0.12.5 + +### Patch Changes + +- fe5738fe1c: Lazy load `LazyLog` as it is rarely used. +- 53c9ad7e04: Update font weight for headings in TechDocs +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/plugin-search@0.4.17 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.12.4 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index c72bcab499..79841a0621 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs", "description": "The Backstage plugin that renders technical documentation for your components", - "version": "0.12.4", + "version": "0.12.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,15 +34,15 @@ "dependencies": { "@backstage/catalog-model": "^0.9.6", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.4", "@backstage/integration": "^0.6.9", "@backstage/integration-react": "^0.1.13", "@backstage/plugin-catalog": "^0.7.2", - "@backstage/plugin-catalog-react": "^0.6.2", - "@backstage/plugin-search": "^0.4.16", - "@backstage/theme": "^0.2.12", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/plugin-search": "^0.4.17", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -61,10 +61,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 9f27679615..5e32d84497 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -28,11 +28,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -41,10 +41,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 9ec642892b..dc63957cb9 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -43,10 +43,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 1ef31ecd6d..bbab66e455 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.2", - "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.3", - "@backstage/theme": "^0.2.12", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -36,10 +36,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", From aae27a554e4a56b02a05c9905786e4424e3eb3a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 11 Nov 2021 13:38:56 +0100 Subject: [PATCH 038/181] bump uglify-js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- yarn.lock | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6c23c706b1..19c9f7e627 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11309,7 +11309,7 @@ commander@7.1.0: resolved "https://registry.npmjs.org/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff" integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== -commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.7.1, commander@~2.20.3: +commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.7.1: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -27718,12 +27718,9 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== uglify-js@^3.1.4: - version "3.8.0" - resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.0.tgz#f3541ae97b2f048d7e7e3aa4f39fd8a1f5d7a805" - integrity sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ== - dependencies: - commander "~2.20.3" - source-map "~0.6.1" + version "3.14.3" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.3.tgz#c0f25dfea1e8e5323eccf59610be08b6043c15cf" + integrity sha512-mic3aOdiq01DuSVx0TseaEzMIVqebMZ0Z3vaeDhFEh9bsc24hV1TFvN74reA2vs08D0ZWfNjAcJ3UbVLaBss+g== uid-number@0.0.6: version "0.0.6" From 33fa4487e6201fac5449100a0a10e7a4b3e04bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 11 Nov 2021 13:46:34 +0100 Subject: [PATCH 039/181] bump refractor and, hence, prismjs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6c23c706b1..57bbaa382e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23148,10 +23148,10 @@ printj@~1.1.0: resolved "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== -prismjs@^1.21.0, prismjs@^1.22.0, prismjs@~1.24.0: - version "1.24.1" - resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.24.1.tgz#c4d7895c4d6500289482fa8936d9cdd192684036" - integrity sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow== +prismjs@^1.21.0, prismjs@^1.22.0, prismjs@~1.25.0: + version "1.25.0" + resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756" + integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg== private@^0.1.8: version "0.1.8" @@ -24448,13 +24448,13 @@ reflect-metadata@^0.1.13: integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== refractor@^3.1.0, refractor@^3.2.0: - version "3.4.0" - resolved "https://registry.npmjs.org/refractor/-/refractor-3.4.0.tgz#62bd274b06c942041f390c371b676eb67cb0a678" - integrity sha512-dBeD02lC5eytm9Gld2Mx0cMcnR+zhSnsTfPpWqFaMgUMJfC9A6bcN3Br/NaXrnBJcuxnLFR90k1jrkaSyV8umg== + version "3.5.0" + resolved "https://registry.npmjs.org/refractor/-/refractor-3.5.0.tgz#334586f352dda4beaf354099b48c2d18e0819aec" + integrity sha512-QwPJd3ferTZ4cSPPjdP5bsYHMytwWYnAN5EEnLtGvkqp/FCCnGsBgxrm9EuIDnjUC3Uc/kETtvVi7fSIVC74Dg== dependencies: hastscript "^6.0.0" parse-entities "^2.0.0" - prismjs "~1.24.0" + prismjs "~1.25.0" regenerate-unicode-properties@^8.2.0: version "8.2.0" From 71264599e142516b7b35b83a8867e29417007bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 11 Nov 2021 14:05:45 +0100 Subject: [PATCH 040/181] bump xss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6c23c706b1..ba46cc3425 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29161,9 +29161,9 @@ xpath@0.0.32: integrity sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw== xss@^1.0.8: - version "1.0.9" - resolved "https://registry.npmjs.org/xss/-/xss-1.0.9.tgz#3ffd565571ff60d2e40db7f3b80b4677bec770d2" - integrity sha512-2t7FahYnGJys6DpHLhajusId7R0Pm2yTmuL0GV9+mV0ZlaLSnb2toBmppATfg5sWIhZQGlsTLoecSzya+l4EAQ== + version "1.0.10" + resolved "https://registry.npmjs.org/xss/-/xss-1.0.10.tgz#5cd63a9b147a755a14cb0455c7db8866120eb4d2" + integrity sha512-qmoqrRksmzqSKvgqzN0055UFWY7OKx1/9JWeRswwEVX9fCG5jcYRxa/A2DHcmZX6VJvjzHRQ2STeeVcQkrmLSw== dependencies: commander "^2.20.3" cssfilter "0.0.10" From c416ae18517795d97bc870118bd70cf5864f043a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 11 Nov 2021 14:14:44 +0100 Subject: [PATCH 041/181] bump @npmcli/git MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- yarn.lock | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6c23c706b1..87327f96f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4714,22 +4714,7 @@ "@gar/promisify" "^1.0.1" semver "^7.3.5" -"@npmcli/git@^2.0.1": - version "2.0.6" - resolved "https://registry.npmjs.org/@npmcli/git/-/git-2.0.6.tgz#47b97e96b2eede3f38379262fa3bdfa6eae57bf2" - integrity sha512-a1MnTfeRPBaKbFY07fd+6HugY1WAkKJzdiJvlRub/9o5xz2F/JtPacZZapx5zRJUQFIzSL677vmTSxEcDMrDbg== - dependencies: - "@npmcli/promise-spawn" "^1.1.0" - lru-cache "^6.0.0" - mkdirp "^1.0.3" - npm-pick-manifest "^6.0.0" - promise-inflight "^1.0.1" - promise-retry "^2.0.1" - semver "^7.3.2" - unique-filename "^1.1.1" - which "^2.0.2" - -"@npmcli/git@^2.1.0": +"@npmcli/git@^2.0.1", "@npmcli/git@^2.1.0": version "2.1.0" resolved "https://registry.npmjs.org/@npmcli/git/-/git-2.1.0.tgz#2fbd77e147530247d37f325930d457b3ebe894f6" integrity sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw== @@ -4802,7 +4787,7 @@ dependencies: json-parse-even-better-errors "^2.3.1" -"@npmcli/promise-spawn@^1.1.0", "@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": +"@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": version "1.3.2" resolved "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz#42d4e56a8e9274fba180dabc0aea6e38f29274f5" integrity sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg== From a1c4c8de3df2cd2987bafc5dcd3fb0cd7a130c7a Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Thu, 11 Nov 2021 13:25:38 +0000 Subject: [PATCH 042/181] Fixes a minor markdown foobar in the body output Signed-off-by: Harry Hogg --- scripts/snyk-github-issue-sync.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/snyk-github-issue-sync.ts b/scripts/snyk-github-issue-sync.ts index d7f64cf1ff..f1015f8139 100644 --- a/scripts/snyk-github-issue-sync.ts +++ b/scripts/snyk-github-issue-sync.ts @@ -82,9 +82,9 @@ const fetchSnykGithubIssueMap = async (): Promise> => { const generateIssueBody = (vulnerability: Vulnerability) => ` ## Affecting Packages/Plugins -${Array.from(vulnerability.packages).map( - ({ name, target }) => `* [${name}](${target})\n`, -)} +${Array.from(vulnerability.packages) + .map(({ name, target }) => `* [${name}](${target})`) + .join('\n')} ${vulnerability.description} `; From 03fdccacb7825955f8fd79b3d164968f62de60d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 11 Nov 2021 14:26:49 +0100 Subject: [PATCH 043/181] partially bump nth-check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- yarn.lock | 136 +++++++++++++++++++++++++++++------------------------- 1 file changed, 74 insertions(+), 62 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6c23c706b1..4bd65274fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12000,16 +12000,6 @@ css-select-base-adapter@^0.1.1: resolved "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== -css-select@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" - integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= - dependencies: - boolbase "~1.0.0" - css-what "2.1" - domutils "1.5.1" - nth-check "~1.0.1" - css-select@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" @@ -12020,6 +12010,17 @@ css-select@^2.0.0: domutils "^1.7.0" nth-check "^1.0.2" +css-select@^4.1.3: + version "4.1.3" + resolved "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067" + integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA== + dependencies: + boolbase "^1.0.0" + css-what "^5.0.0" + domhandler "^4.2.0" + domutils "^2.6.0" + nth-check "^2.0.0" + css-tree@1.0.0-alpha.37: version "1.0.0-alpha.37" resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" @@ -12049,16 +12050,16 @@ css-vendor@^2.0.8: "@babel/runtime" "^7.8.3" is-in-browser "^1.0.2" -css-what@2.1: - version "2.1.3" - resolved "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" - integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== - css-what@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1" integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw== +css-what@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" + integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== + css.escape@1.5.1, css.escape@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" @@ -13016,7 +13017,7 @@ dom-accessibility-api@^0.5.4, dom-accessibility-api@^0.5.6: resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz#3f5d43b52c7a3bd68b5fb63fa47b4e4c1fdf65a9" integrity sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw== -dom-converter@^0.2: +dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== @@ -13065,7 +13066,7 @@ domain-browser@^1.1.1: resolved "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@1, domelementtype@^1.3.1: +domelementtype@1: version "1.3.1" resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== @@ -13075,6 +13076,11 @@ domelementtype@^2.0.1, domelementtype@^2.1.0: resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== +domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + domexception@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" @@ -13082,13 +13088,6 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - domhandler@^3.0, domhandler@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz#51cd13efca31da95bbb0c5bee3a48300e333b3e9" @@ -13103,20 +13102,19 @@ domhandler@^4.0.0: dependencies: domelementtype "^2.1.0" +domhandler@^4.2.0: + version "4.2.2" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f" + integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w== + dependencies: + domelementtype "^2.2.0" + dompurify@^2.1.1, dompurify@^2.2.9: version "2.3.3" resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c" integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg== -domutils@1.5.1: - version "1.5.1" - resolved "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" - integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= - dependencies: - dom-serializer "0" - domelementtype "1" - -domutils@^1.5.1, domutils@^1.7.0: +domutils@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== @@ -13133,6 +13131,15 @@ domutils@^2.0.0: domelementtype "^2.0.1" domhandler "^4.0.0" +domutils@^2.5.2, domutils@^2.6.0: + version "2.8.0" + resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" @@ -13411,7 +13418,7 @@ ent@^2.2.0: resolved "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= -entities@^1.1.1, entities@^1.1.2: +entities@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== @@ -16167,18 +16174,6 @@ html-webpack-plugin@^5.3.1: pretty-error "^2.1.1" tapable "^2.0.0" -htmlparser2@^3.3.0: - version "3.10.1" - resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - htmlparser2@^4.0: version "4.1.0" resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" @@ -16189,6 +16184,16 @@ htmlparser2@^4.0: domutils "^2.0.0" entities "^2.0.0" +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" @@ -21312,13 +21317,20 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: gauge "~2.7.3" set-blocking "~2.0.0" -nth-check@^1.0.2, nth-check@~1.0.1: +nth-check@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== dependencies: boolbase "~1.0.0" +nth-check@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" + integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== + dependencies: + boolbase "^1.0.0" + nullthrows@^1.0.0, nullthrows@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" @@ -23111,12 +23123,12 @@ pretty-bytes@^5.3.0, pretty-bytes@^5.6.0: integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== pretty-error@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" - integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM= + version "2.1.2" + resolved "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6" + integrity sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw== dependencies: - renderkid "^2.0.1" - utila "~0.4" + lodash "^4.17.20" + renderkid "^2.0.4" pretty-format@^24.9.0: version "24.9.0" @@ -24705,16 +24717,16 @@ remove-trailing-spaces@^1.0.6: resolved "https://registry.npmjs.org/remove-trailing-spaces/-/remove-trailing-spaces-1.0.8.tgz#4354d22f3236374702f58ee373168f6d6887ada7" integrity sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA== -renderkid@^2.0.1: - version "2.0.3" - resolved "https://registry.npmjs.org/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149" - integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA== +renderkid@^2.0.4: + version "2.0.7" + resolved "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz#464f276a6bdcee606f4a15993f9b29fc74ca8609" + integrity sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ== dependencies: - css-select "^1.1.0" - dom-converter "^0.2" - htmlparser2 "^3.3.0" - strip-ansi "^3.0.0" - utila "^0.4.0" + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^3.0.1" repeat-element@^1.1.2: version "1.1.3" @@ -28281,7 +28293,7 @@ util@^0.12.3: safe-buffer "^5.1.2" which-typed-array "^1.1.2" -utila@^0.4.0, utila@~0.4: +utila@~0.4: version "0.4.0" resolved "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= From ec786075419e200d7b8e37b7aeebc51919e8d184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 11 Nov 2021 14:34:42 +0100 Subject: [PATCH 044/181] bump prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6c23c706b1..ebd1998bae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23245,7 +23245,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prompts@2.4.0, prompts@^2.0.1: +prompts@2.4.0: version "2.4.0" resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" integrity sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ== @@ -23253,10 +23253,10 @@ prompts@2.4.0, prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prompts@^2.4.0: - version "2.4.1" - resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz#befd3b1195ba052f9fd2fde8a486c4e82ee77f61" - integrity sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ== +prompts@^2.0.1, prompts@^2.4.0: + version "2.4.2" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== dependencies: kleur "^3.0.3" sisteransi "^1.0.5" From a2637ce8936b92e48275502b37c88dd6e82de0ec Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Thu, 11 Nov 2021 09:18:50 -0600 Subject: [PATCH 045/181] Cleaned up tests Signed-off-by: Andre Wanlin --- .../src/api/AzureDevOpsApi.test.ts | 88 +++++++++---------- .../src/service/router.test.ts | 12 +-- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts index dc0ebb2920..2cb7d93c0b 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts @@ -48,7 +48,7 @@ describe('AzureDevOpsApi', () => { const inputIdentityRef: IdentityRef = { displayName: 'Jane Doe', - uniqueName: 'DOMAINjdoe', + uniqueName: 'DOMAIN\\jdoe', }; const inputBuild: Build = { @@ -56,9 +56,9 @@ describe('AzureDevOpsApi', () => { buildNumber: 'Build-1', status: BuildStatus.Completed, result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.9325232Z'), - startTime: new Date('2020-09-12T06:15:23.9325232Z'), - finishTime: new Date('2020-09-12T06:20:23.9325232Z'), + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), sourceBranch: 'refs/heads/develop', sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', definition: inputBuildDefinition, @@ -72,11 +72,11 @@ describe('AzureDevOpsApi', () => { link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', status: BuildStatus.Completed, result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.9325232Z').toISOString(), - startTime: new Date('2020-09-12T06:15:23.9325232Z').toISOString(), - finishTime: new Date('2020-09-12T06:20:23.9325232Z').toISOString(), + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAINjdoe', + uniqueName: 'DOMAIN\\jdoe', }; expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); @@ -93,7 +93,7 @@ describe('AzureDevOpsApi', () => { const inputIdentityRef: IdentityRef = { displayName: 'Jane Doe', - uniqueName: 'DOMAINjdoe', + uniqueName: 'DOMAIN\\jdoe', }; const inputBuild: Build = { @@ -101,9 +101,9 @@ describe('AzureDevOpsApi', () => { buildNumber: 'Build-1', status: BuildStatus.Completed, result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.9325232Z'), - startTime: new Date('2020-09-12T06:15:23.9325232Z'), - finishTime: new Date('2020-09-12T06:20:23.9325232Z'), + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), sourceBranch: 'refs/heads/develop', sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', definition: undefined, @@ -117,11 +117,11 @@ describe('AzureDevOpsApi', () => { link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', status: BuildStatus.Completed, result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.9325232Z').toISOString(), - startTime: new Date('2020-09-12T06:15:23.9325232Z').toISOString(), - finishTime: new Date('2020-09-12T06:20:23.9325232Z').toISOString(), + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAINjdoe', + uniqueName: 'DOMAIN\\jdoe', }; expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); @@ -138,7 +138,7 @@ describe('AzureDevOpsApi', () => { const inputIdentityRef: IdentityRef = { displayName: 'Jane Doe', - uniqueName: 'DOMAINjdoe', + uniqueName: 'DOMAIN\\jdoe', }; const inputBuild: Build = { @@ -146,9 +146,9 @@ describe('AzureDevOpsApi', () => { buildNumber: 'Build-1', status: undefined, result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.9325232Z'), - startTime: new Date('2020-09-12T06:15:23.9325232Z'), - finishTime: new Date('2020-09-12T06:20:23.9325232Z'), + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), sourceBranch: 'refs/heads/develop', sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', definition: undefined, @@ -162,11 +162,11 @@ describe('AzureDevOpsApi', () => { link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', status: BuildStatus.None, result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.9325232Z').toISOString(), - startTime: new Date('2020-09-12T06:15:23.9325232Z').toISOString(), - finishTime: new Date('2020-09-12T06:20:23.9325232Z').toISOString(), + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAINjdoe', + uniqueName: 'DOMAIN\\jdoe', }; expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); @@ -183,7 +183,7 @@ describe('AzureDevOpsApi', () => { const inputIdentityRef: IdentityRef = { displayName: 'Jane Doe', - uniqueName: 'DOMAINjdoe', + uniqueName: 'DOMAIN\\jdoe', }; const inputBuild: Build = { @@ -191,9 +191,9 @@ describe('AzureDevOpsApi', () => { buildNumber: 'Build-1', status: BuildStatus.InProgress, result: undefined, - queueTime: new Date('2020-09-12T06:10:23.9325232Z'), - startTime: new Date('2020-09-12T06:15:23.9325232Z'), - finishTime: new Date('2020-09-12T06:20:23.9325232Z'), + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), sourceBranch: 'refs/heads/develop', sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', definition: undefined, @@ -207,11 +207,11 @@ describe('AzureDevOpsApi', () => { link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', status: BuildStatus.InProgress, result: BuildResult.None, - queueTime: new Date('2020-09-12T06:10:23.9325232Z').toISOString(), - startTime: new Date('2020-09-12T06:15:23.9325232Z').toISOString(), - finishTime: new Date('2020-09-12T06:20:23.9325232Z').toISOString(), + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAINjdoe', + uniqueName: 'DOMAIN\\jdoe', }; expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); @@ -222,7 +222,7 @@ describe('AzureDevOpsApi', () => { it('should return empty string for link', () => { const inputIdentityRef: IdentityRef = { displayName: 'Jane Doe', - uniqueName: 'DOMAINjdoe', + uniqueName: 'DOMAIN\\jdoe', }; const inputBuild: Build = { @@ -230,9 +230,9 @@ describe('AzureDevOpsApi', () => { buildNumber: 'Build-1', status: BuildStatus.InProgress, result: undefined, - queueTime: new Date('2020-09-12T06:10:23.9325232Z'), - startTime: new Date('2020-09-12T06:15:23.9325232Z'), - finishTime: new Date('2020-09-12T06:20:23.9325232Z'), + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), sourceBranch: 'refs/heads/develop', sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', definition: undefined, @@ -246,11 +246,11 @@ describe('AzureDevOpsApi', () => { link: '', status: BuildStatus.InProgress, result: BuildResult.None, - queueTime: new Date('2020-09-12T06:10:23.9325232Z').toISOString(), - startTime: new Date('2020-09-12T06:15:23.9325232Z').toISOString(), - finishTime: new Date('2020-09-12T06:20:23.9325232Z').toISOString(), + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAINjdoe', + uniqueName: 'DOMAIN\\jdoe', }; expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); @@ -265,7 +265,7 @@ describe('AzureDevOpsApi', () => { const inputIdentityRef: IdentityRef = { displayName: 'Jane Doe', - uniqueName: 'DOMAINjdoe', + uniqueName: 'DOMAIN\\jdoe', }; const inputPullRequest: GitPullRequest = { @@ -273,7 +273,7 @@ describe('AzureDevOpsApi', () => { repository: inputGitRepository, title: 'My Awesome New Feature', createdBy: inputIdentityRef, - creationDate: new Date('2020-09-12T06:10:23.9325232Z'), + creationDate: new Date('2020-09-12T06:10:23.932Z'), sourceRefName: 'refs/heads/topic/super-awesome-feature', targetRefName: 'refs/heads/main', status: PullRequestStatus.Active, @@ -287,9 +287,9 @@ describe('AzureDevOpsApi', () => { pullRequestId: 7181, repoName: 'super-feature-repo', title: 'My Awesome New Feature', - uniqueName: 'DOMAINjdoe', + uniqueName: 'DOMAIN\\jdoe', createdBy: 'Jane Doe', - creationDate: new Date('2020-09-12T06:10:23.9325232Z').toISOString(), + creationDate: '2020-09-12T06:10:23.932Z', sourceRefName: 'refs/heads/topic/super-awesome-feature', targetRefName: 'refs/heads/main', status: PullRequestStatus.Active, diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index f5d533d2d8..ce5f712392 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -152,7 +152,7 @@ describe('createRouter', () => { link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', status: BuildStatus.Completed, result: BuildResult.PartiallySucceeded, - queueTime: undefined, + queueTime: '2020-09-12T06:10:23.932Z', source: 'refs/heads/develop (f4f78b31)', }; @@ -162,7 +162,7 @@ describe('createRouter', () => { link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=2', status: BuildStatus.InProgress, result: BuildResult.None, - queueTime: undefined, + queueTime: '2020-09-12T06:10:23.932Z', source: 'refs/heads/develop (13c988d4)', }; @@ -172,7 +172,7 @@ describe('createRouter', () => { link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=3', status: BuildStatus.Completed, result: BuildResult.Succeeded, - queueTime: undefined, + queueTime: '2020-09-12T06:10:23.932Z', source: 'refs/heads/develop (9bedf678)', }; @@ -205,7 +205,7 @@ describe('createRouter', () => { repoName: 'super-feature-repo', title: 'My Awesome New Feature', createdBy: 'Jane Doe', - creationDate: undefined, + creationDate: '2020-09-12T06:10:23.932Z', sourceRefName: 'refs/heads/topic/super-awesome-feature', targetRefName: 'refs/heads/main', status: PullRequestStatus.Active, @@ -218,7 +218,7 @@ describe('createRouter', () => { repoName: 'super-feature-repo', title: 'Refactoring My Awesome New Feature', createdBy: 'Jane Doe', - creationDate: undefined, + creationDate: '2020-09-12T06:10:23.932Z', sourceRefName: 'refs/heads/topic/refactor-super-awesome-feature', targetRefName: 'refs/heads/main', status: PullRequestStatus.Active, @@ -231,7 +231,7 @@ describe('createRouter', () => { repoName: 'super-feature-repo', title: 'Bug Fix for My Awesome New Feature', createdBy: 'Jane Doe', - creationDate: undefined, + creationDate: '2020-09-12T06:10:23.932Z', sourceRefName: 'refs/heads/topic/fix-super-awesome-feature', targetRefName: 'refs/heads/main', status: PullRequestStatus.Active, From 94dc1fedee26f05583697d54c14f7c8a025372f0 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Thu, 11 Nov 2021 09:31:40 -0600 Subject: [PATCH 046/181] Improved readability and used DateTime consistently Signed-off-by: Andre Wanlin --- .../azure-devops/src/components/BuildTable/BuildTable.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx index 99b23fe183..c16c836393 100644 --- a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx +++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx @@ -149,7 +149,10 @@ const columns: TableColumn[] = [ field: 'queueTime', width: 'auto', render: (row: Partial) => - DateTime.fromISO(row.queueTime ?? new Date().toISOString()).toRelative(), + (row.queueTime + ? DateTime.fromISO(row.queueTime) + : DateTime.now() + ).toRelative(), }, ]; From 6275236aab0f896ed0f2266a3492efb3e7abb5ec Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Thu, 11 Nov 2021 10:01:58 -0600 Subject: [PATCH 047/181] Updated story to handle date changes Signed-off-by: Andre Wanlin --- .../src/components/BuildTable/BuildTable.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.stories.tsx b/plugins/azure-devops/src/components/BuildTable/BuildTable.stories.tsx index 76a19155ba..84980b0b51 100644 --- a/plugins/azure-devops/src/components/BuildTable/BuildTable.stories.tsx +++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.stories.tsx @@ -56,7 +56,7 @@ const generateTestData = (rows = 10): RepoBuild[] => { title: `backstage ci - 1.0.0-preview-${rows - i}`, status, result, - queueTime: new Date(Date.now() - i * 60000), + queueTime: new Date(Date.now() - i * 60000).toISOString(), source: 'refs/heads/main', link: '', }); From 2b5ccd2964ec271f68bc618bc75e4d09372ee95a Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Thu, 11 Nov 2021 10:02:29 -0600 Subject: [PATCH 048/181] Aded API Report and changeset Signed-off-by: Andre Wanlin --- .changeset/tame-buckets-move.md | 7 +++++++ plugins/azure-devops-common/api-report.md | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .changeset/tame-buckets-move.md diff --git a/.changeset/tame-buckets-move.md b/.changeset/tame-buckets-move.md new file mode 100644 index 0000000000..13adb92b5e --- /dev/null +++ b/.changeset/tame-buckets-move.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-azure-devops-backend': patch +'@backstage/plugin-azure-devops-common': patch +--- + +Improved Date handling for the Azure DevOps set of plugins by using strings and letting the frontend handle the conversion to DateTime diff --git a/plugins/azure-devops-common/api-report.md b/plugins/azure-devops-common/api-report.md index f03c0b4059..71f0b8b807 100644 --- a/plugins/azure-devops-common/api-report.md +++ b/plugins/azure-devops-common/api-report.md @@ -36,7 +36,7 @@ export type PullRequest = { title?: string; uniqueName?: string; createdBy?: string; - creationDate?: Date; + creationDate?: string; sourceRefName?: string; targetRefName?: string; status?: PullRequestStatus; @@ -72,9 +72,9 @@ export type RepoBuild = { link?: string; status?: BuildStatus; result?: BuildResult; - queueTime?: Date; - startTime?: Date; - finishTime?: Date; + queueTime?: string; + startTime?: string; + finishTime?: string; source: string; uniqueName?: string; }; From 0a7a2eba76febb07a613fb76ccf4ef6a8208ed02 Mon Sep 17 00:00:00 2001 From: Marley Powell Date: Thu, 11 Nov 2021 16:22:25 +0000 Subject: [PATCH 049/181] fix: Fixed issue with running a subset of stories caused by incorrectly resolved paths. Signed-off-by: Marley Powell --- packages/storybook/.storybook/main.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js index 5bdc6b3a2f..8d60899e48 100644 --- a/packages/storybook/.storybook/main.js +++ b/packages/storybook/.storybook/main.js @@ -15,8 +15,7 @@ module.exports = ({ args }) => { '../../../plugins/search/src/**/*.stories.tsx', ]; } else { - const rootDir = path.resolve(__dirname, '../../..'); - stories = args.map(arg => path.join(rootDir, arg, 'src/**/*.stories.tsx')); + stories = args.map(arg => `../../../${arg}/src/**/*.stories.tsx`); } return { From 014cbf8cb9f91bbf5961aca0a5b4839c806729dd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 11 Oct 2021 22:10:47 +0200 Subject: [PATCH 050/181] core-app-api: deprecate dependency on core-components Signed-off-by: Patrik Oldsberg --- .changeset/giant-drinks-wave.md | 5 ++ .changeset/hot-walls-fail.md | 33 ++++++++ .changeset/late-rice-sit.md | 5 ++ .changeset/twenty-swans-matter.md | 16 ++++ packages/app/src/App.tsx | 2 + .../core-app-api/src/app/createApp.test.tsx | 28 +------ packages/core-app-api/src/app/createApp.tsx | 84 ++++++------------- packages/core-components/api-report.md | 4 + .../src/defaultAppComponents.test.tsx | 38 +++++++++ .../src/defaultAppComponents.tsx | 83 ++++++++++++++++++ packages/core-components/src/index.ts | 1 + .../default-app/packages/app/src/App.tsx | 3 +- packages/dev-utils/src/devApp/render.tsx | 2 + 13 files changed, 219 insertions(+), 85 deletions(-) create mode 100644 .changeset/giant-drinks-wave.md create mode 100644 .changeset/hot-walls-fail.md create mode 100644 .changeset/late-rice-sit.md create mode 100644 .changeset/twenty-swans-matter.md create mode 100644 packages/core-components/src/defaultAppComponents.test.tsx create mode 100644 packages/core-components/src/defaultAppComponents.tsx diff --git a/.changeset/giant-drinks-wave.md b/.changeset/giant-drinks-wave.md new file mode 100644 index 0000000000..ab9df136fc --- /dev/null +++ b/.changeset/giant-drinks-wave.md @@ -0,0 +1,5 @@ +--- +'@backstage/dev-utils': patch +--- + +Migrated to explicit passing of components to `createApp`. diff --git a/.changeset/hot-walls-fail.md b/.changeset/hot-walls-fail.md new file mode 100644 index 0000000000..055613934d --- /dev/null +++ b/.changeset/hot-walls-fail.md @@ -0,0 +1,33 @@ +--- +'@backstage/create-app': patch +--- + +Migrated the app template to pass on explicit `components` to the `createApp` options, as not doing this has been deprecated and will need to be done in the future. + +To migrate an existing application, make the following change to `packages/app/src/App.tsx`: + +```diff ++import { defaultAppComponents } from '@backstage/core-components'; + + // ... + + const app = createApp({ + apis, ++ components: defaultAppComponents(), + bindRoutes({ bind }) { +``` + +If you already supply custom app components, you can use the following: + +```diff + + // ... + + const app = createApp({ + apis, ++ components: { + ...defaultAppComponents(), ++ Progress: MyCustomProgressComponent, + }, + bindRoutes({ bind }) { +``` diff --git a/.changeset/late-rice-sit.md b/.changeset/late-rice-sit.md new file mode 100644 index 0000000000..eb074cbb18 --- /dev/null +++ b/.changeset/late-rice-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Added a new `defaultAppComponents` method that creates a minimal set of components to pass on to `createApp` from `@backstage/core-app-api`. diff --git a/.changeset/twenty-swans-matter.md b/.changeset/twenty-swans-matter.md new file mode 100644 index 0000000000..0722913e05 --- /dev/null +++ b/.changeset/twenty-swans-matter.md @@ -0,0 +1,16 @@ +--- +'@backstage/core-app-api': patch +--- + +Deprecated the defaulting of the `components` options of `createApp`, meaning it will become required in the future. When not passing the required components options a deprecation warning is currently logged, and it will become required in a future release. + +The keep the existing components intact, migrate to using `defaultAppComponents` from `@backstage/core-components`: + +```ts +const app = createApp({ + components: { + ...defaultAppComponents(), + // Place any custom components here + }, +}); +``` diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 109ae7922e..f0b31fb4d0 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -31,6 +31,7 @@ import { AlertDisplay, OAuthRequestDialog, SignInPage, + defaultAppComponents, } from '@backstage/core-components'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { @@ -95,6 +96,7 @@ const app = createApp({ }, components: { + ...defaultAppComponents(), SignInPage: props => { return ( { }); }); -describe('OptionallyWrapInRouter', () => { - it('should wrap with router if not yet inside a router', async () => { - const { getByText } = render( - Test, - ); - - expect(getByText('Test')).toBeInTheDocument(); - }); - - it('should not wrap with router if already inside a router', async () => { - const { getByText } = render( - - Test - , - ); - - expect(getByText('Test')).toBeInTheDocument(); - }); -}); - describe('Optional ThemeProvider', () => { it('should render app with user-provided ThemeProvider', async () => { const components = { diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index 65aa15b50c..1e53103152 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -16,28 +16,25 @@ import { AppConfig } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -import { Button } from '@material-ui/core'; -import { ErrorPage, ErrorPanel, Progress } from '@backstage/core-components'; +import { defaultAppComponents } from '@backstage/core-components'; import { darkTheme, lightTheme } from '@backstage/theme'; import DarkIcon from '@material-ui/icons/Brightness2'; import LightIcon from '@material-ui/icons/WbSunny'; -import React, { PropsWithChildren } from 'react'; -import { - BrowserRouter, - MemoryRouter, - useInRouterContext, -} from 'react-router-dom'; +import React from 'react'; +import { BrowserRouter } from 'react-router-dom'; import { PrivateAppImpl } from './App'; import { AppThemeProvider } from './AppThemeProvider'; import { defaultApis } from './defaultApis'; import { defaultAppIcons } from './icons'; -import { - AppConfigLoader, - AppOptions, - BootErrorPageProps, - ErrorBoundaryFallbackProps, -} from './types'; -import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { AppConfigLoader, AppOptions } from './types'; +import { AppComponents, BackstagePlugin } from '@backstage/core-plugin-api'; + +const REQUIRED_APP_COMPONENTS: Array = [ + 'Progress', + 'NotFoundErrorPage', + 'BootErrorPage', + 'ErrorBoundaryFallback', +]; /** * The default config loader, which expects that config is available at compile-time @@ -93,63 +90,34 @@ export const defaultConfigLoader: AppConfigLoader = async ( return configs; }; -export function OptionallyWrapInRouter({ children }: PropsWithChildren<{}>) { - if (useInRouterContext()) { - return <>{children}; - } - return {children}; -} - /** * Creates a new Backstage App. * * @public */ export function createApp(options?: AppOptions) { - const DefaultNotFoundPage = () => ( - + const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter( + name => !options?.components?.[name], ); - const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => { - let message = ''; - if (step === 'load-config') { - message = `The configuration failed to load, someone should have a look at this error: ${error.message}`; - } else if (step === 'load-chunk') { - message = `Lazy loaded chunk failed to load, try to reload the page: ${error.message}`; - } - // TODO: figure out a nicer way to handle routing on the error page, when it can be done. - return ( - - - + if (missingRequiredComponents.length > 0) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp options will soon require a minimal set of ' + + 'components to be provided in the components option. These components can be ' + + 'created using defaultAppComponents from @backstage/core-components and ' + + 'passed along like this: createApp({ components: defaultAppComponents() }). ' + + `The following components are missing: ${missingRequiredComponents.join( + ', ', + )}`, ); - }; - const DefaultErrorBoundaryFallback = ({ - error, - resetError, - plugin, - }: ErrorBoundaryFallbackProps) => { - return ( - - - - ); - }; + } const apis = options?.apis ?? []; const icons = { ...defaultAppIcons, ...options?.icons }; const plugins = options?.plugins ?? []; const components = { - NotFoundErrorPage: DefaultNotFoundPage, - BootErrorPage: DefaultBootErrorPage, - Progress: Progress, + ...defaultAppComponents(), Router: BrowserRouter, - ErrorBoundaryFallback: DefaultErrorBoundaryFallback, ThemeProvider: AppThemeProvider, ...options?.components, }; diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 0f1e3510ae..63f35873f7 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -6,6 +6,7 @@ /// import { ApiRef } from '@backstage/core-plugin-api'; +import { AppComponents } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePalette } from '@backstage/theme'; import { BackstageTheme } from '@backstage/theme'; @@ -199,6 +200,9 @@ export type CustomProviderClassKey = 'form' | 'button'; // @public (undocumented) export function DashboardIcon(props: IconComponentProps): JSX.Element; +// @public +export function defaultAppComponents(): Omit; + // @public type DependencyEdge = T & { from: string; diff --git a/packages/core-components/src/defaultAppComponents.test.tsx b/packages/core-components/src/defaultAppComponents.test.tsx new file mode 100644 index 0000000000..db0e2d50c1 --- /dev/null +++ b/packages/core-components/src/defaultAppComponents.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { OptionallyWrapInRouter } from './defaultAppComponents'; + +describe('OptionallyWrapInRouter', () => { + it('should wrap with router if not yet inside a router', async () => { + render(Test); + + expect(screen.getByText('Test')).toBeInTheDocument(); + }); + + it('should not wrap with router if already inside a router', async () => { + render( + + Test + , + ); + + expect(screen.getByText('Test')).toBeInTheDocument(); + }); +}); diff --git a/packages/core-components/src/defaultAppComponents.tsx b/packages/core-components/src/defaultAppComponents.tsx new file mode 100644 index 0000000000..fb810a6272 --- /dev/null +++ b/packages/core-components/src/defaultAppComponents.tsx @@ -0,0 +1,83 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactNode } from 'react'; +import Button from '@material-ui/core/Button'; +import { + AppComponents, + BootErrorPageProps, + ErrorBoundaryFallbackProps, +} from '@backstage/core-plugin-api'; +import { ErrorPanel, Progress } from './components'; +import { ErrorPage } from './layout'; +import { MemoryRouter, useInRouterContext } from 'react-router'; + +export function OptionallyWrapInRouter({ children }: { children: ReactNode }) { + if (useInRouterContext()) { + return <>{children}; + } + return {children}; +} + +const DefaultNotFoundPage = () => ( + +); + +const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => { + let message = ''; + if (step === 'load-config') { + message = `The configuration failed to load, someone should have a look at this error: ${error.message}`; + } else if (step === 'load-chunk') { + message = `Lazy loaded chunk failed to load, try to reload the page: ${error.message}`; + } + // TODO: figure out a nicer way to handle routing on the error page, when it can be done. + return ( + + + + ); +}; +const DefaultErrorBoundaryFallback = ({ + error, + resetError, + plugin, +}: ErrorBoundaryFallbackProps) => { + return ( + + + + ); +}; + +/** + * Creates a set of default components to pass along to {@link @backstage/core-app-api#createApp}. + * + * @public + */ +export function defaultAppComponents(): Omit { + return { + Progress, + NotFoundErrorPage: DefaultNotFoundPage, + BootErrorPage: DefaultBootErrorPage, + ErrorBoundaryFallback: DefaultErrorBoundaryFallback, + }; +} diff --git a/packages/core-components/src/index.ts b/packages/core-components/src/index.ts index 3c5e708360..9bc2436559 100644 --- a/packages/core-components/src/index.ts +++ b/packages/core-components/src/index.ts @@ -25,3 +25,4 @@ export * from './hooks'; export * from './icons'; export * from './layout'; export * from './overridableComponents'; +export { defaultAppComponents } from './defaultAppComponents'; 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 4cd83685a6..7117b3ea07 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 @@ -25,11 +25,12 @@ import { entityPage } from './components/catalog/EntityPage'; import { searchPage } from './components/search/SearchPage'; import { Root } from './components/Root'; -import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; +import { AlertDisplay, defaultAppComponents, OAuthRequestDialog } from '@backstage/core-components'; import { createApp, FlatRoutes } from '@backstage/core-app-api'; const app = createApp({ apis, + components: defaultAppComponents(), bindRoutes({ bind }) { bind(catalogPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 755289a060..31b6ac6169 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -27,6 +27,7 @@ import { Route } from 'react-router'; import { AlertDisplay, + defaultAppComponents, OAuthRequestDialog, Sidebar, SidebarItem, @@ -177,6 +178,7 @@ export class DevAppBuilder { apis, plugins: this.plugins, themes: this.themes, + components: defaultAppComponents(), bindRoutes: ({ bind }) => { for (const plugin of this.plugins ?? []) { const targets: Record> = {}; From cb77f4599a2f9433bf0e87cc4350d912913ba216 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 22 Oct 2021 13:59:41 +0200 Subject: [PATCH 051/181] core-app-api: deprecate not passing in icons + add defaultAppIcons() Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- packages/app/src/App.tsx | 2 + packages/core-app-api/src/app/createApp.tsx | 24 ++++++-- packages/core-app-api/src/app/icons.tsx | 46 ++++++++-------- packages/core-app-api/src/app/index.ts | 1 + .../core-components/src/defaultAppIcons.tsx | 55 +++++++++++++++++++ packages/core-components/src/index.ts | 1 + 6 files changed, 102 insertions(+), 27 deletions(-) create mode 100644 packages/core-components/src/defaultAppIcons.tsx diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index f0b31fb4d0..c2273156ac 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -31,6 +31,7 @@ import { AlertDisplay, OAuthRequestDialog, SignInPage, + defaultAppIcons, defaultAppComponents, } from '@backstage/core-components'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; @@ -91,6 +92,7 @@ const app = createApp({ apis, plugins: Object.values(plugins), icons: { + ...defaultAppIcons(), // Custom icon example alert: AlarmIcon, }, diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index 1e53103152..7b93daf40f 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -16,7 +16,10 @@ import { AppConfig } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -import { defaultAppComponents } from '@backstage/core-components'; +import { + defaultAppComponents, + defaultAppIcons, +} from '@backstage/core-components'; import { darkTheme, lightTheme } from '@backstage/theme'; import DarkIcon from '@material-ui/icons/Brightness2'; import LightIcon from '@material-ui/icons/WbSunny'; @@ -25,7 +28,6 @@ import { BrowserRouter } from 'react-router-dom'; import { PrivateAppImpl } from './App'; import { AppThemeProvider } from './AppThemeProvider'; import { defaultApis } from './defaultApis'; -import { defaultAppIcons } from './icons'; import { AppConfigLoader, AppOptions } from './types'; import { AppComponents, BackstagePlugin } from '@backstage/core-plugin-api'; @@ -112,8 +114,22 @@ export function createApp(options?: AppOptions) { ); } + const appIcons = defaultAppIcons(); + const providedIconKeys = Object.keys(options?.icons ?? {}); + const missingIconKeys = Object.keys(appIcons).filter( + key => !providedIconKeys.includes(key), + ); + if (missingIconKeys.length > 0) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp options will soon require all app icons to be provided.' + + 'These icons can be created using defaultAppIcons from @backstage/core-components ' + + 'and then passed along like this: createApp({ icons: ...defaultAppIcons() })' + + `The following icons are missing: ${missingIconKeys.join(', ')}`, + ); + } + const apis = options?.apis ?? []; - const icons = { ...defaultAppIcons, ...options?.icons }; const plugins = options?.plugins ?? []; const components = { ...defaultAppComponents(), @@ -141,7 +157,7 @@ export function createApp(options?: AppOptions) { return new PrivateAppImpl({ apis, - icons, + icons: { ...appIcons, ...options?.icons }, plugins: plugins as BackstagePlugin[], components, themes, diff --git a/packages/core-app-api/src/app/icons.tsx b/packages/core-app-api/src/app/icons.tsx index b234a292b1..f06c9da989 100644 --- a/packages/core-app-api/src/app/icons.tsx +++ b/packages/core-app-api/src/app/icons.tsx @@ -35,30 +35,30 @@ import MuiPeopleIcon from '@material-ui/icons/People'; import MuiPersonIcon from '@material-ui/icons/Person'; import MuiWarningIcon from '@material-ui/icons/Warning'; -type AppIconsKey = - | 'brokenImage' - | 'catalog' - | 'scaffolder' - | 'techdocs' - | 'search' - | 'chat' - | 'dashboard' - | 'docs' - | 'email' - | 'github' - | 'group' - | 'help' - | 'kind:api' - | 'kind:component' - | 'kind:domain' - | 'kind:group' - | 'kind:location' - | 'kind:system' - | 'kind:user' - | 'user' - | 'warning'; +export type AppIcons = { + 'kind:api': IconComponent; + 'kind:component': IconComponent; + 'kind:domain': IconComponent; + 'kind:group': IconComponent; + 'kind:location': IconComponent; + 'kind:system': IconComponent; + 'kind:user': IconComponent; -export type AppIcons = { [key in AppIconsKey]: IconComponent }; + brokenImage: IconComponent; + catalog: IconComponent; + chat: IconComponent; + dashboard: IconComponent; + docs: IconComponent; + email: IconComponent; + github: IconComponent; + group: IconComponent; + help: IconComponent; + scaffolder: IconComponent; + search: IconComponent; + techdocs: IconComponent; + user: IconComponent; + warning: IconComponent; +}; export const defaultAppIcons: AppIcons = { brokenImage: MuiBrokenImageIcon, diff --git a/packages/core-app-api/src/app/index.ts b/packages/core-app-api/src/app/index.ts index ab82774cad..2a16a90bfa 100644 --- a/packages/core-app-api/src/app/index.ts +++ b/packages/core-app-api/src/app/index.ts @@ -15,4 +15,5 @@ */ export { createApp, defaultConfigLoader } from './createApp'; +export type { AppIcons } from './icons'; export * from './types'; diff --git a/packages/core-components/src/defaultAppIcons.tsx b/packages/core-components/src/defaultAppIcons.tsx new file mode 100644 index 0000000000..1d02dd5ad2 --- /dev/null +++ b/packages/core-components/src/defaultAppIcons.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IconComponent } from '@backstage/core-plugin-api'; +import MuiApartmentIcon from '@material-ui/icons/Apartment'; +import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; +import MuiCategoryIcon from '@material-ui/icons/Category'; +import MuiChatIcon from '@material-ui/icons/Chat'; +import MuiDashboardIcon from '@material-ui/icons/Dashboard'; +import MuiDocsIcon from '@material-ui/icons/Description'; +import MuiEmailIcon from '@material-ui/icons/Email'; +import MuiExtensionIcon from '@material-ui/icons/Extension'; +import MuiGitHubIcon from '@material-ui/icons/GitHub'; +import MuiHelpIcon from '@material-ui/icons/Help'; +import MuiLocationOnIcon from '@material-ui/icons/LocationOn'; +import MuiMemoryIcon from '@material-ui/icons/Memory'; +import MuiMenuBookIcon from '@material-ui/icons/MenuBook'; +import MuiPeopleIcon from '@material-ui/icons/People'; +import MuiPersonIcon from '@material-ui/icons/Person'; +import MuiWarningIcon from '@material-ui/icons/Warning'; + +export const defaultAppIcons = () => ({ + brokenImage: MuiBrokenImageIcon as IconComponent, + // To be confirmed: see https://github.com/backstage/backstage/issues/4970 + catalog: MuiMenuBookIcon as IconComponent, + chat: MuiChatIcon as IconComponent, + dashboard: MuiDashboardIcon as IconComponent, + docs: MuiDocsIcon as IconComponent, + email: MuiEmailIcon as IconComponent, + github: MuiGitHubIcon as IconComponent, + group: MuiPeopleIcon as IconComponent, + help: MuiHelpIcon as IconComponent, + 'kind:api': MuiExtensionIcon as IconComponent, + 'kind:component': MuiMemoryIcon as IconComponent, + 'kind:domain': MuiApartmentIcon as IconComponent, + 'kind:group': MuiPeopleIcon as IconComponent, + 'kind:location': MuiLocationOnIcon as IconComponent, + 'kind:system': MuiCategoryIcon as IconComponent, + 'kind:user': MuiPersonIcon as IconComponent, + user: MuiPersonIcon as IconComponent, + warning: MuiWarningIcon as IconComponent, +}); diff --git a/packages/core-components/src/index.ts b/packages/core-components/src/index.ts index 9bc2436559..3538cd4186 100644 --- a/packages/core-components/src/index.ts +++ b/packages/core-components/src/index.ts @@ -26,3 +26,4 @@ export * from './icons'; export * from './layout'; export * from './overridableComponents'; export { defaultAppComponents } from './defaultAppComponents'; +export { defaultAppIcons } from './defaultAppIcons'; From 9ddec7eaf14cfc77bc8244fd93023efecf25a86d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 22 Oct 2021 14:46:33 +0200 Subject: [PATCH 052/181] core-{app,plugin}-api: refactor AppTheme to use provider + defaultAppThemes Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- packages/core-app-api/src/app/App.tsx | 4 +- .../core-app-api/src/app/AppThemeProvider.tsx | 11 ++++ packages/core-app-api/src/app/createApp.tsx | 32 ++++-------- packages/core-app-api/src/app/types.ts | 14 +++-- .../core-components/src/defaultAppThemes.tsx | 52 +++++++++++++++++++ packages/core-components/src/index.ts | 1 + .../src/apis/definitions/AppThemeApi.ts | 4 ++ 7 files changed, 92 insertions(+), 26 deletions(-) create mode 100644 packages/core-components/src/defaultAppThemes.tsx diff --git a/packages/core-app-api/src/app/App.tsx b/packages/core-app-api/src/app/App.tsx index 8cb3f167fb..6fadd22808 100644 --- a/packages/core-app-api/src/app/App.tsx +++ b/packages/core-app-api/src/app/App.tsx @@ -126,7 +126,7 @@ type FullAppOptions = { icons: NonNullable; plugins: BackstagePlugin[]; components: AppComponents; - themes: AppTheme[]; + themes: (Partial & Omit)[]; configLoader?: AppConfigLoader; defaultApis: Iterable; bindRoutes?: AppOptions['bindRoutes']; @@ -206,7 +206,7 @@ export class PrivateAppImpl implements BackstageApp { this.icons = options.icons; this.plugins = new Set(options.plugins); this.components = options.components; - this.themes = options.themes; + this.themes = options.themes as AppTheme[]; this.configLoader = options.configLoader; this.defaultApis = options.defaultApis; this.bindRoutes = options.bindRoutes; diff --git a/packages/core-app-api/src/app/AppThemeProvider.tsx b/packages/core-app-api/src/app/AppThemeProvider.tsx index 4e3a8487a6..aa48a2187d 100644 --- a/packages/core-app-api/src/app/AppThemeProvider.tsx +++ b/packages/core-app-api/src/app/AppThemeProvider.tsx @@ -90,6 +90,17 @@ export function AppThemeProvider({ children }: PropsWithChildren<{}>) { throw new Error('App has no themes'); } + if (appTheme.Provider) { + return ; + } + + // eslint-disable-next-line no-console + console.warn( + "DEPRECATION WARNING: A provided app theme is using the deprecated 'theme' property " + + 'and should be migrated to use a Provider instead. ' + + 'See https://backstage.io/docs/deprecations/TODO for more info.', + ); + return ( {children} diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index 7b93daf40f..f5989219e8 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -19,11 +19,8 @@ import { JsonObject } from '@backstage/types'; import { defaultAppComponents, defaultAppIcons, + defaultAppThemes, } from '@backstage/core-components'; -import { darkTheme, lightTheme } from '@backstage/theme'; -import DarkIcon from '@material-ui/icons/Brightness2'; -import LightIcon from '@material-ui/icons/WbSunny'; -import React from 'react'; import { BrowserRouter } from 'react-router-dom'; import { PrivateAppImpl } from './App'; import { AppThemeProvider } from './AppThemeProvider'; @@ -129,6 +126,15 @@ export function createApp(options?: AppOptions) { ); } + if (!options?.themes) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp options will soon require themes to be provided. ' + + 'Themes can be created using defaultAppThemes from @backstage/core-components ' + + 'and then passed along like this: createApp({ theme: defaultAppThemes() })', + ); + } + const apis = options?.apis ?? []; const plugins = options?.plugins ?? []; const components = { @@ -137,22 +143,6 @@ export function createApp(options?: AppOptions) { ThemeProvider: AppThemeProvider, ...options?.components, }; - const themes = options?.themes ?? [ - { - id: 'light', - title: 'Light Theme', - variant: 'light', - theme: lightTheme, - icon: , - }, - { - id: 'dark', - title: 'Dark Theme', - variant: 'dark', - theme: darkTheme, - icon: , - }, - ]; const configLoader = options?.configLoader ?? defaultConfigLoader; return new PrivateAppImpl({ @@ -160,7 +150,7 @@ export function createApp(options?: AppOptions) { icons: { ...appIcons, ...options?.icons }, plugins: plugins as BackstagePlugin[], components, - themes, + themes: options?.themes ?? defaultAppThemes(), configLoader, defaultApis, bindRoutes: options?.bindRoutes, diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 407018111b..78ef277c90 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -226,18 +226,26 @@ export type AppOptions = { * id: 'light', * title: 'Light Theme', * variant: 'light', - * theme: lightTheme, * icon: , + * Provider: ({ children }) => ( + * + * {children} + * + * ), * }, { * id: 'dark', * title: 'Dark Theme', * variant: 'dark', - * theme: darkTheme, * icon: , + * Provider: ({ children }) => ( + * + * {children} + * + * ), * }] * ``` */ - themes?: AppTheme[]; + themes?: (Partial & Omit)[]; /** * A function that loads in App configuration that will be accessible via diff --git a/packages/core-components/src/defaultAppThemes.tsx b/packages/core-components/src/defaultAppThemes.tsx new file mode 100644 index 0000000000..894716942b --- /dev/null +++ b/packages/core-components/src/defaultAppThemes.tsx @@ -0,0 +1,52 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { darkTheme, lightTheme } from '@backstage/theme'; +import DarkIcon from '@material-ui/icons/Brightness2'; +import LightIcon from '@material-ui/icons/WbSunny'; +import { ThemeProvider } from '@material-ui/core/styles'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import { AppTheme } from '@backstage/core-plugin-api'; + +export function defaultAppThemes(): AppTheme[] { + return [ + { + id: 'light', + title: 'Light Theme', + variant: 'light', + icon: , + theme: lightTheme, + Provider: ({ children }) => ( + + {children} + + ), + }, + { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + theme: darkTheme, + Provider: ({ children }) => ( + + {children} + + ), + }, + ]; +} diff --git a/packages/core-components/src/index.ts b/packages/core-components/src/index.ts index 3538cd4186..305783dec8 100644 --- a/packages/core-components/src/index.ts +++ b/packages/core-components/src/index.ts @@ -27,3 +27,4 @@ export * from './layout'; export * from './overridableComponents'; export { defaultAppComponents } from './defaultAppComponents'; export { defaultAppIcons } from './defaultAppIcons'; +export { defaultAppThemes } from './defaultAppThemes'; diff --git a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts index de523663f2..52cf202422 100644 --- a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { ReactNode } from 'react'; import { ApiRef, createApiRef } from '../system'; import { BackstageTheme } from '@backstage/theme'; import { Observable } from '@backstage/types'; @@ -41,6 +42,7 @@ export type AppTheme = { /** * The specialized MaterialUI theme instance. + * @deprecated use Provider instead, see https://backstage.io/docs/deprecations/TODO */ theme: BackstageTheme; @@ -48,6 +50,8 @@ export type AppTheme = { * An Icon for the theme mode setting. */ icon?: React.ReactElement; + + Provider?(props: { children: ReactNode }): JSX.Element | null; }; /** From e551a44c65bb06a2e45c08706850925ba2e50de1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 Oct 2021 19:08:50 +0200 Subject: [PATCH 053/181] core-components: provide withDefaults and work around type duplication Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- packages/app/src/App.tsx | 82 ++++++------ packages/core-app-api/src/app/createApp.tsx | 40 +++--- .../src/appDefaults/AppThemeProvider.tsx | 109 ++++++++++++++++ .../defaultAppComponents.test.tsx | 0 .../defaultAppComponents.tsx | 12 +- .../src/{ => appDefaults}/defaultAppIcons.tsx | 0 .../{ => appDefaults}/defaultAppThemes.tsx | 0 .../core-components/src/appDefaults/index.ts | 17 +++ .../src/appDefaults/withDefaults.tsx | 64 ++++++++++ packages/core-components/src/index.ts | 4 +- packages/core-plugin-api/src/app/types.ts | 119 ++---------------- 11 files changed, 265 insertions(+), 182 deletions(-) create mode 100644 packages/core-components/src/appDefaults/AppThemeProvider.tsx rename packages/core-components/src/{ => appDefaults}/defaultAppComponents.test.tsx (100%) rename packages/core-components/src/{ => appDefaults}/defaultAppComponents.tsx (88%) rename packages/core-components/src/{ => appDefaults}/defaultAppIcons.tsx (100%) rename packages/core-components/src/{ => appDefaults}/defaultAppThemes.tsx (100%) create mode 100644 packages/core-components/src/appDefaults/index.ts create mode 100644 packages/core-components/src/appDefaults/withDefaults.tsx diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c2273156ac..d3ab0649e9 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -31,8 +31,7 @@ import { AlertDisplay, OAuthRequestDialog, SignInPage, - defaultAppIcons, - defaultAppComponents, + withDefaults, } from '@backstage/core-components'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { @@ -88,47 +87,46 @@ import * as plugins from './plugins'; import { techDocsPage } from './components/techdocs/TechDocsPage'; -const app = createApp({ - apis, - plugins: Object.values(plugins), - icons: { - ...defaultAppIcons(), - // Custom icon example - alert: AlarmIcon, - }, - - components: { - ...defaultAppComponents(), - SignInPage: props => { - return ( - - ); +const app = createApp( + withDefaults({ + apis, + plugins: Object.values(plugins), + icons: { + // Custom icon example + alert: AlarmIcon, }, - }, - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - viewTechDoc: techdocsPlugin.routes.docRoot, - }); - bind(catalogGraphPlugin.externalRoutes, { - catalogEntity: catalogPlugin.routes.catalogEntity, - }); - bind(apiDocsPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - }); - bind(explorePlugin.externalRoutes, { - catalogEntity: catalogPlugin.routes.catalogEntity, - }); - bind(scaffolderPlugin.externalRoutes, { - registerComponent: catalogImportPlugin.routes.importPage, - }); - }, -}); + components: { + SignInPage: props => { + return ( + + ); + }, + }, + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + viewTechDoc: techdocsPlugin.routes.docRoot, + }); + bind(catalogGraphPlugin.externalRoutes, { + catalogEntity: catalogPlugin.routes.catalogEntity, + }); + bind(apiDocsPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + }); + bind(explorePlugin.externalRoutes, { + catalogEntity: catalogPlugin.routes.catalogEntity, + }); + bind(scaffolderPlugin.externalRoutes, { + registerComponent: catalogImportPlugin.routes.importPage, + }); + }, + }), +); const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index f5989219e8..c1e9741ff1 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -16,20 +16,15 @@ import { AppConfig } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -import { - defaultAppComponents, - defaultAppIcons, - defaultAppThemes, -} from '@backstage/core-components'; -import { BrowserRouter } from 'react-router-dom'; +import { withDefaults } from '@backstage/core-components'; import { PrivateAppImpl } from './App'; -import { AppThemeProvider } from './AppThemeProvider'; +import { AppComponents, AppConfigLoader, AppOptions } from './types'; import { defaultApis } from './defaultApis'; -import { AppConfigLoader, AppOptions } from './types'; -import { AppComponents, BackstagePlugin } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; const REQUIRED_APP_COMPONENTS: Array = [ 'Progress', + 'Router', 'NotFoundErrorPage', 'BootErrorPage', 'ErrorBoundaryFallback', @@ -95,6 +90,8 @@ export const defaultConfigLoader: AppConfigLoader = async ( * @public */ export function createApp(options?: AppOptions) { + const optionsWithDefaults = withDefaults(options); + const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter( name => !options?.components?.[name], ); @@ -111,9 +108,8 @@ export function createApp(options?: AppOptions) { ); } - const appIcons = defaultAppIcons(); const providedIconKeys = Object.keys(options?.icons ?? {}); - const missingIconKeys = Object.keys(appIcons).filter( + const missingIconKeys = Object.keys(optionsWithDefaults.icons!).filter( key => !providedIconKeys.includes(key), ); if (missingIconKeys.length > 0) { @@ -135,24 +131,16 @@ export function createApp(options?: AppOptions) { ); } - const apis = options?.apis ?? []; - const plugins = options?.plugins ?? []; - const components = { - ...defaultAppComponents(), - Router: BrowserRouter, - ThemeProvider: AppThemeProvider, - ...options?.components, - }; - const configLoader = options?.configLoader ?? defaultConfigLoader; + const { icons, themes, components } = optionsWithDefaults; return new PrivateAppImpl({ - apis, - icons: { ...appIcons, ...options?.icons }, - plugins: plugins as BackstagePlugin[], - components, - themes: options?.themes ?? defaultAppThemes(), - configLoader, + icons: icons!, + themes: themes!, + components: components! as AppComponents, defaultApis, + apis: options?.apis ?? [], bindRoutes: options?.bindRoutes, + plugins: (options?.plugins as BackstagePlugin[]) ?? [], + configLoader: options?.configLoader ?? defaultConfigLoader, }); } diff --git a/packages/core-components/src/appDefaults/AppThemeProvider.tsx b/packages/core-components/src/appDefaults/AppThemeProvider.tsx new file mode 100644 index 0000000000..aa48a2187d --- /dev/null +++ b/packages/core-components/src/appDefaults/AppThemeProvider.tsx @@ -0,0 +1,109 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useMemo, useEffect, useState, PropsWithChildren } from 'react'; +import { ThemeProvider } from '@material-ui/core/styles'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import { useApi, appThemeApiRef, AppTheme } from '@backstage/core-plugin-api'; +import { useObservable } from 'react-use'; + +// This tries to find the most accurate match, but also falls back to less +// accurate results in order to avoid errors. +function resolveTheme( + themeId: string | undefined, + shouldPreferDark: boolean, + themes: AppTheme[], +) { + if (themeId !== undefined) { + const selectedTheme = themes.find(theme => theme.id === themeId); + if (selectedTheme) { + return selectedTheme; + } + } + + if (shouldPreferDark) { + const darkTheme = themes.find(theme => theme.variant === 'dark'); + if (darkTheme) { + return darkTheme; + } + } + + const lightTheme = themes.find(theme => theme.variant === 'light'); + if (lightTheme) { + return lightTheme; + } + + return themes[0]; +} + +const useShouldPreferDarkTheme = () => { + const mediaQuery = useMemo( + () => window.matchMedia('(prefers-color-scheme: dark)'), + [], + ); + const [shouldPreferDark, setPrefersDark] = useState(mediaQuery.matches); + + useEffect(() => { + const listener = (event: MediaQueryListEvent) => { + setPrefersDark(event.matches); + }; + mediaQuery.addListener(listener); + return () => { + mediaQuery.removeListener(listener); + }; + }, [mediaQuery]); + + return shouldPreferDark; +}; + +export function AppThemeProvider({ children }: PropsWithChildren<{}>) { + const appThemeApi = useApi(appThemeApiRef); + const themeId = useObservable( + appThemeApi.activeThemeId$(), + appThemeApi.getActiveThemeId(), + ); + + // Browser feature detection won't change over time, so ignore lint rule + const shouldPreferDark = Boolean(window.matchMedia) + ? useShouldPreferDarkTheme() // eslint-disable-line react-hooks/rules-of-hooks + : false; + + const appTheme = resolveTheme( + themeId, + shouldPreferDark, + appThemeApi.getInstalledThemes(), + ); + if (!appTheme) { + throw new Error('App has no themes'); + } + + if (appTheme.Provider) { + return ; + } + + // eslint-disable-next-line no-console + console.warn( + "DEPRECATION WARNING: A provided app theme is using the deprecated 'theme' property " + + 'and should be migrated to use a Provider instead. ' + + 'See https://backstage.io/docs/deprecations/TODO for more info.', + ); + + return ( + + {children} + + ); +} diff --git a/packages/core-components/src/defaultAppComponents.test.tsx b/packages/core-components/src/appDefaults/defaultAppComponents.test.tsx similarity index 100% rename from packages/core-components/src/defaultAppComponents.test.tsx rename to packages/core-components/src/appDefaults/defaultAppComponents.test.tsx diff --git a/packages/core-components/src/defaultAppComponents.tsx b/packages/core-components/src/appDefaults/defaultAppComponents.tsx similarity index 88% rename from packages/core-components/src/defaultAppComponents.tsx rename to packages/core-components/src/appDefaults/defaultAppComponents.tsx index fb810a6272..ab55a25840 100644 --- a/packages/core-components/src/defaultAppComponents.tsx +++ b/packages/core-components/src/appDefaults/defaultAppComponents.tsx @@ -16,14 +16,16 @@ import React, { ReactNode } from 'react'; import Button from '@material-ui/core/Button'; +import { ErrorPanel, Progress } from '../components'; +import { ErrorPage } from '../layout'; +import { MemoryRouter, useInRouterContext } from 'react-router'; +import { BrowserRouter } from 'react-router-dom'; import { AppComponents, BootErrorPageProps, ErrorBoundaryFallbackProps, } from '@backstage/core-plugin-api'; -import { ErrorPanel, Progress } from './components'; -import { ErrorPage } from './layout'; -import { MemoryRouter, useInRouterContext } from 'react-router'; +import { AppThemeProvider } from './AppThemeProvider'; export function OptionallyWrapInRouter({ children }: { children: ReactNode }) { if (useInRouterContext()) { @@ -73,9 +75,11 @@ const DefaultErrorBoundaryFallback = ({ * * @public */ -export function defaultAppComponents(): Omit { +export function defaultAppComponents(): AppComponents { return { Progress, + Router: BrowserRouter, + ThemeProvider: AppThemeProvider, NotFoundErrorPage: DefaultNotFoundPage, BootErrorPage: DefaultBootErrorPage, ErrorBoundaryFallback: DefaultErrorBoundaryFallback, diff --git a/packages/core-components/src/defaultAppIcons.tsx b/packages/core-components/src/appDefaults/defaultAppIcons.tsx similarity index 100% rename from packages/core-components/src/defaultAppIcons.tsx rename to packages/core-components/src/appDefaults/defaultAppIcons.tsx diff --git a/packages/core-components/src/defaultAppThemes.tsx b/packages/core-components/src/appDefaults/defaultAppThemes.tsx similarity index 100% rename from packages/core-components/src/defaultAppThemes.tsx rename to packages/core-components/src/appDefaults/defaultAppThemes.tsx diff --git a/packages/core-components/src/appDefaults/index.ts b/packages/core-components/src/appDefaults/index.ts new file mode 100644 index 0000000000..287ad3cac6 --- /dev/null +++ b/packages/core-components/src/appDefaults/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { withDefaults } from './withDefaults'; diff --git a/packages/core-components/src/appDefaults/withDefaults.tsx b/packages/core-components/src/appDefaults/withDefaults.tsx new file mode 100644 index 0000000000..51adaf22ef --- /dev/null +++ b/packages/core-components/src/appDefaults/withDefaults.tsx @@ -0,0 +1,64 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AppTheme, IconComponent } from '@backstage/core-plugin-api'; +// This is a bit of a hack that we use to avoid having to redeclare these types +// within this package or have an explicit dependency on core-app-api. +// These types end up being inlined and duplicated into this package at build time. +// eslint-disable-next-line no-restricted-imports +import { + AppIcons, + AppComponents, + AppOptions, +} from '../../../core-app-api/src/app'; +import { defaultAppComponents } from './defaultAppComponents'; +import { defaultAppIcons } from './defaultAppIcons'; +import { defaultAppThemes } from './defaultAppThemes'; + +interface OptionalAppOptions { + icons?: Partial & { + [key in string]: IconComponent; + }; + themes?: (Partial & Omit)[]; + components?: Partial; +} + +/** + * The options required by {@link @backstage/core-app-api#createApp}, but with + * many of the fields being optional + * + * @public + */ +type DefaultAppOptions = Omit & + OptionalAppOptions; + +/** + * Provides a set of default App options with the ability to override specific options. + * + * These options populate the theme, icons and components options of {@link @backstage/core-app-api#AppOptions}. + * + * @public + */ +export function withDefaults(options?: DefaultAppOptions): AppOptions { + const { themes, icons, components } = options ?? {}; + + return { + ...options, + themes: themes ?? defaultAppThemes(), + icons: { ...defaultAppIcons(), ...icons }, + components: { ...defaultAppComponents(), ...components }, + }; +} diff --git a/packages/core-components/src/index.ts b/packages/core-components/src/index.ts index 305783dec8..ea1fea9d5c 100644 --- a/packages/core-components/src/index.ts +++ b/packages/core-components/src/index.ts @@ -25,6 +25,4 @@ export * from './hooks'; export * from './icons'; export * from './layout'; export * from './overridableComponents'; -export { defaultAppComponents } from './defaultAppComponents'; -export { defaultAppIcons } from './defaultAppIcons'; -export { defaultAppThemes } from './defaultAppThemes'; +export * from './appDefaults'; diff --git a/packages/core-plugin-api/src/app/types.ts b/packages/core-plugin-api/src/app/types.ts index d072f0a2e6..df78728a21 100644 --- a/packages/core-plugin-api/src/app/types.ts +++ b/packages/core-plugin-api/src/app/types.ts @@ -14,110 +14,15 @@ * limitations under the License. */ -import { ComponentType } from 'react'; -import { ProfileInfo } from '../apis/definitions'; -import { IconComponent } from '../icons'; -import { BackstagePlugin } from '../plugin/types'; - -/** - * Props for the BootErrorPage. - * - * @public - */ -export type BootErrorPageProps = { - step: 'load-config' | 'load-chunk'; - error: Error; -}; - -/** - * Data and handlers associated with the user sign in event. - * - * @public - */ -export type SignInResult = { - /** - * User ID that will be returned by the IdentityApi - */ - userId: string; - - profile: ProfileInfo; - - /** - * Function used to retrieve an ID token for the signed in user. - */ - getIdToken?: () => Promise; - - /** - * Sign out handler that will be called if the user requests to sign out. - */ - signOut?: () => Promise; -}; - -/** - * Props for the SignInPage. - * - * @public - */ -export type SignInPageProps = { - /** - * Set the sign-in result for the app. This should only be called once. - */ - onResult(result: SignInResult): void; -}; - -/** - * Props for the ErrorBoundaryFallback. - * - * @public - */ -export type ErrorBoundaryFallbackProps = { - plugin?: BackstagePlugin; - error: Error; - resetError: () => void; -}; - -/** - * Basic app components. - * - * @public - */ -export type AppComponents = { - NotFoundErrorPage: ComponentType<{}>; - BootErrorPage: ComponentType; - Progress: ComponentType<{}>; - Router: ComponentType<{}>; - ErrorBoundaryFallback: ComponentType; - - /** - * An optional sign-in page that will be rendered instead of the AppRouter at startup. - * - * If a sign-in page is set, it will always be shown before the app, and it is up - * to the sign-in page to handle e.g. saving of login methods for subsequent visits. - * - * The sign-in page will be displayed until it has passed up a result to the parent, - * and which point the AppRouter and all of its children will be rendered instead. - */ - SignInPage?: ComponentType; -}; - -/** - * Provides plugins and components registered in the app. - * - * @public - */ -export type AppContext = { - /** - * Get a list of all plugins that are installed in the app. - */ - getPlugins(): BackstagePlugin[]; - - /** - * Get a common or custom icon for this app. - */ - getSystemIcon(key: string): IconComponent | undefined; - - /** - * Get the components registered for various purposes in the app. - */ - getComponents(): AppComponents; -}; +// This is a bit of a hack that we use to avoid having to redeclare these types +// within this package or have an explicit dependency on core-app-api. +// These types end up being inlined and duplicated into this package at build time. +// eslint-disable-next-line no-restricted-imports +export type { + BootErrorPageProps, + SignInResult, + SignInPageProps, + ErrorBoundaryFallbackProps, + AppComponents, + AppContext, +} from '../../../core-app-api/src/app/types'; From 44976c138cc491d577386a551ccd58e747e112ab Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 Oct 2021 19:46:26 +0200 Subject: [PATCH 054/181] core-app-api, create-app, dev-utils: updates to use withDefaults Signed-off-by: Patrik Oldsberg --- packages/core-app-api/src/app/createApp.tsx | 19 +++++---- .../default-app/packages/app/src/App.tsx | 39 +++++++++++-------- packages/dev-utils/src/devApp/render.tsx | 31 ++++++++------- 3 files changed, 47 insertions(+), 42 deletions(-) diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index c1e9741ff1..c3f477035f 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -98,10 +98,9 @@ export function createApp(options?: AppOptions) { if (missingRequiredComponents.length > 0) { // eslint-disable-next-line no-console console.warn( - 'DEPRECATION WARNING: The createApp options will soon require a minimal set of ' + - 'components to be provided in the components option. These components can be ' + - 'created using defaultAppComponents from @backstage/core-components and ' + - 'passed along like this: createApp({ components: defaultAppComponents() }). ' + + 'DEPRECATION WARNING: The createApp options will soon require a minimal set of components to ' + + 'be provided. You can use the default components by using withDefaults from @backstage/core-components ' + + 'like this: createApp(withDefaults({ ... })), or you can provide the components yourself. ' + `The following components are missing: ${missingRequiredComponents.join( ', ', )}`, @@ -115,9 +114,9 @@ export function createApp(options?: AppOptions) { if (missingIconKeys.length > 0) { // eslint-disable-next-line no-console console.warn( - 'DEPRECATION WARNING: The createApp options will soon require all app icons to be provided.' + - 'These icons can be created using defaultAppIcons from @backstage/core-components ' + - 'and then passed along like this: createApp({ icons: ...defaultAppIcons() })' + + 'DEPRECATION WARNING: The createApp options will soon require a minimal set of icons to ' + + 'be provided. You can use the default icons by using withDefaults from @backstage/core-components ' + + 'like this: createApp(withDefaults({ ... })), or you can provide the icons yourself. ' + `The following icons are missing: ${missingIconKeys.join(', ')}`, ); } @@ -125,9 +124,9 @@ export function createApp(options?: AppOptions) { if (!options?.themes) { // eslint-disable-next-line no-console console.warn( - 'DEPRECATION WARNING: The createApp options will soon require themes to be provided. ' + - 'Themes can be created using defaultAppThemes from @backstage/core-components ' + - 'and then passed along like this: createApp({ theme: defaultAppThemes() })', + 'DEPRECATION WARNING: The createApp options will soon require the themes to be provided. ' + + 'You can use the default themes by using withDefaults from @backstage/core-components ' + + 'like this: createApp(withDefaults({ ... })), or you can provide the themes yourself. ', ); } 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 7117b3ea07..a6d34e6687 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 @@ -25,25 +25,30 @@ import { entityPage } from './components/catalog/EntityPage'; import { searchPage } from './components/search/SearchPage'; import { Root } from './components/Root'; -import { AlertDisplay, defaultAppComponents, OAuthRequestDialog } from '@backstage/core-components'; +import { + AlertDisplay, + withDefaults, + OAuthRequestDialog, +} from '@backstage/core-components'; import { createApp, FlatRoutes } from '@backstage/core-app-api'; -const app = createApp({ - apis, - components: defaultAppComponents(), - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - viewTechDoc: techdocsPlugin.routes.docRoot, - }); - bind(apiDocsPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - }); - bind(scaffolderPlugin.externalRoutes, { - registerComponent: catalogImportPlugin.routes.importPage, - }); - }, -}); +const app = createApp( + withDefaults({ + apis, + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + viewTechDoc: techdocsPlugin.routes.docRoot, + }); + bind(apiDocsPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + }); + bind(scaffolderPlugin.externalRoutes, { + registerComponent: catalogImportPlugin.routes.importPage, + }); + }, + }), +); const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 31b6ac6169..bcaac891fe 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -27,12 +27,12 @@ import { Route } from 'react-router'; import { AlertDisplay, - defaultAppComponents, OAuthRequestDialog, Sidebar, SidebarItem, SidebarPage, SidebarSpacer, + withDefaults, } from '@backstage/core-components'; import { @@ -174,21 +174,22 @@ export class DevAppBuilder { ); } - const app = createApp({ - apis, - plugins: this.plugins, - themes: this.themes, - components: defaultAppComponents(), - bindRoutes: ({ bind }) => { - for (const plugin of this.plugins ?? []) { - const targets: Record> = {}; - for (const routeKey of Object.keys(plugin.externalRoutes)) { - targets[routeKey] = dummyRouteRef; + const app = createApp( + withDefaults({ + apis, + plugins: this.plugins, + themes: this.themes, + bindRoutes: ({ bind }) => { + for (const plugin of this.plugins ?? []) { + const targets: Record> = {}; + for (const routeKey of Object.keys(plugin.externalRoutes)) { + targets[routeKey] = dummyRouteRef; + } + bind(plugin.externalRoutes, targets); } - bind(plugin.externalRoutes, targets); - } - }, - }); + }, + }), + ); const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); From 2db99234966ab3e7c964de3d8acb4f21241ed350 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 Oct 2021 19:45:52 +0200 Subject: [PATCH 055/181] changesets: updated changesets to use withDefaults Signed-off-by: Patrik Oldsberg --- .changeset/giant-drinks-wave.md | 2 +- .changeset/hot-walls-fail.md | 25 +++++++------------------ .changeset/late-rice-sit.md | 2 +- .changeset/twenty-swans-matter.md | 15 +++++++-------- 4 files changed, 16 insertions(+), 28 deletions(-) diff --git a/.changeset/giant-drinks-wave.md b/.changeset/giant-drinks-wave.md index ab9df136fc..4c2f754c2c 100644 --- a/.changeset/giant-drinks-wave.md +++ b/.changeset/giant-drinks-wave.md @@ -2,4 +2,4 @@ '@backstage/dev-utils': patch --- -Migrated to explicit passing of components to `createApp`. +Migrated to using `withDefaults` to pass defaults to `createApp`. diff --git a/.changeset/hot-walls-fail.md b/.changeset/hot-walls-fail.md index 055613934d..c814354a56 100644 --- a/.changeset/hot-walls-fail.md +++ b/.changeset/hot-walls-fail.md @@ -2,32 +2,21 @@ '@backstage/create-app': patch --- -Migrated the app template to pass on explicit `components` to the `createApp` options, as not doing this has been deprecated and will need to be done in the future. +Migrated the app template use the new `withDefaults` to construct the `createApp` options, as not doing this has been deprecated and will need to be done in the future. To migrate an existing application, make the following change to `packages/app/src/App.tsx`: ```diff -+import { defaultAppComponents } from '@backstage/core-components'; ++import { withDefaults } from '@backstage/core-components'; // ... - const app = createApp({ +-const app = createApp({ ++const app = createApp(withDefaults({ apis, -+ components: defaultAppComponents(), bindRoutes({ bind }) { -``` - -If you already supply custom app components, you can use the following: - -```diff - - // ... - - const app = createApp({ - apis, -+ components: { - ...defaultAppComponents(), -+ Progress: MyCustomProgressComponent, + ... }, - bindRoutes({ bind }) { +-}); ++})); ``` diff --git a/.changeset/late-rice-sit.md b/.changeset/late-rice-sit.md index eb074cbb18..778215038f 100644 --- a/.changeset/late-rice-sit.md +++ b/.changeset/late-rice-sit.md @@ -2,4 +2,4 @@ '@backstage/core-components': patch --- -Added a new `defaultAppComponents` method that creates a minimal set of components to pass on to `createApp` from `@backstage/core-app-api`. +Added a new `withDefaults` method that accepts a set of `AppOptions` and add the default components, themes and icons. It is intended to be used together with `createApp` from `@backstage/core-app-api` like this: `createApp(withDefaults({ ... }))`. diff --git a/.changeset/twenty-swans-matter.md b/.changeset/twenty-swans-matter.md index 0722913e05..6dea7a089f 100644 --- a/.changeset/twenty-swans-matter.md +++ b/.changeset/twenty-swans-matter.md @@ -2,15 +2,14 @@ '@backstage/core-app-api': patch --- -Deprecated the defaulting of the `components` options of `createApp`, meaning it will become required in the future. When not passing the required components options a deprecation warning is currently logged, and it will become required in a future release. +Deprecated the defaulting of the `components`, `icons` and `themes` options of `createApp`, meaning it will become required in the future. When not passing the required options a deprecation warning is currently logged, and they will become required in a future release. -The keep the existing components intact, migrate to using `defaultAppComponents` from `@backstage/core-components`: +The keep using the default set of options, migrate to using `withDefaults` from `@backstage/core-components`: ```ts -const app = createApp({ - components: { - ...defaultAppComponents(), - // Place any custom components here - }, -}); +const app = createApp( + withDefaults({ + // ... + }), +); ``` From ab73a5216796a7ee62ca8af078b8b79a34292208 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 Oct 2021 19:59:47 +0200 Subject: [PATCH 056/181] core-*: update API reports Signed-off-by: Patrik Oldsberg --- packages/core-app-api/api-report.md | 27 ++++++++++++++++++++++++-- packages/core-components/api-report.md | 20 +++++++++++++++---- packages/core-plugin-api/api-report.md | 13 +++++++++---- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 8fb3d5c788..f517f284b4 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -164,6 +164,30 @@ export type AppContext = { getComponents(): AppComponents; }; +// Warning: (ae-missing-release-tag) "AppIcons" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AppIcons = { + 'kind:api': IconComponent; + 'kind:component': IconComponent; + 'kind:domain': IconComponent; + 'kind:group': IconComponent; + 'kind:location': IconComponent; + 'kind:system': IconComponent; + 'kind:user': IconComponent; + brokenImage: IconComponent; + catalog: IconComponent; + chat: IconComponent; + dashboard: IconComponent; + docs: IconComponent; + email: IconComponent; + github: IconComponent; + group: IconComponent; + help: IconComponent; + user: IconComponent; + warning: IconComponent; +}; + // @public export type AppOptions = { apis?: Iterable; @@ -172,7 +196,7 @@ export type AppOptions = { }; plugins?: BackstagePluginWithAnyOutput[]; components?: Partial; - themes?: AppTheme[]; + themes?: (Partial & Omit)[]; configLoader?: AppConfigLoader; bindRoutes?(context: { bind: AppRouteBinder }): void; }; @@ -608,5 +632,4 @@ export class WebStorage implements StorageApi { // Warnings were encountered during analysis: // // src/apis/system/ApiProvider.d.ts:15:5 - (ae-forgotten-export) The symbol "ApiProviderProps" needs to be exported by the entry point index.d.ts -// src/app/types.d.ts:152:5 - (ae-forgotten-export) The symbol "AppIcons" needs to be exported by the entry point index.d.ts ``` diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 63f35873f7..b0c3e2d9b2 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -5,21 +5,26 @@ ```ts /// +import { AnyApiFactory } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; -import { AppComponents } from '@backstage/core-plugin-api'; +import { AppConfig } from '@backstage/config'; +import { AppTheme } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePalette } from '@backstage/theme'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; import { ButtonProps as ButtonProps_2 } from '@material-ui/core/Button'; import { CardHeaderProps } from '@material-ui/core/CardHeader'; import { Column } from '@material-table/core'; import { ComponentClass } from 'react'; import { ComponentProps } from 'react'; +import { ComponentType } from 'react'; import { Context } from 'react'; import { default as CSS_2 } from 'csstype'; import { CSSProperties } from 'react'; import { ElementType } from 'react'; import { ErrorInfo } from 'react'; +import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { LinearProgressProps } from '@material-ui/core/LinearProgress'; import { LinkProps as LinkProps_2 } from '@material-ui/core/Link'; @@ -28,12 +33,15 @@ import MaterialBreadcrumbs from '@material-ui/core/Breadcrumbs'; import { MaterialTableProps } from '@material-table/core'; import { NavLinkProps } from 'react-router-dom'; import { Overrides } from '@material-ui/core/styles/overrides'; +import { PluginOutput } from '@backstage/core-plugin-api'; +import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import * as React_3 from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; import { SessionApi } from '@backstage/core-plugin-api'; import { SignInPageProps } from '@backstage/core-plugin-api'; import { SparklinesLineProps } from 'react-sparklines'; @@ -41,6 +49,7 @@ import { SparklinesProps } from 'react-sparklines'; import { StyledComponentProps } from '@material-ui/core/styles'; import { StyleRules } from '@material-ui/styles'; import { StyleRules as StyleRules_2 } from '@material-ui/core/styles/withStyles'; +import { SubRouteRef } from '@backstage/core-plugin-api'; import { TabProps } from '@material-ui/core/Tab'; import { TextTruncateProps } from 'react-text-truncate'; import { Theme } from '@material-ui/core/styles'; @@ -200,9 +209,6 @@ export type CustomProviderClassKey = 'form' | 'button'; // @public (undocumented) export function DashboardIcon(props: IconComponentProps): JSX.Element; -// @public -export function defaultAppComponents(): Omit; - // @public type DependencyEdge = T & { from: string; @@ -2322,6 +2328,12 @@ export type WarningPanelClassKey = | 'message' | 'details'; +// Warning: (ae-forgotten-export) The symbol "DefaultAppOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "AppOptions" needs to be exported by the entry point index.d.ts +// +// @public +export function withDefaults(options?: DefaultAppOptions): AppOptions; + // Warnings were encountered during analysis: // // src/components/DependencyGraph/types.d.ts:16:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index f0cee2a42c..433edc57ff 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -5,11 +5,14 @@ ```ts /// +import { BackstagePlugin as BackstagePlugin_2 } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; +import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api'; import { Observable as Observable_2 } from '@backstage/types'; import { Observer as Observer_2 } from '@backstage/types'; +import { ProfileInfo as ProfileInfo_2 } from '@backstage/core-plugin-api'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; @@ -161,13 +164,14 @@ export type AppComponents = { Progress: ComponentType<{}>; Router: ComponentType<{}>; ErrorBoundaryFallback: ComponentType; + ThemeProvider: ComponentType<{}>; SignInPage?: ComponentType; }; // @public export type AppContext = { - getPlugins(): BackstagePlugin[]; - getSystemIcon(key: string): IconComponent | undefined; + getPlugins(): BackstagePlugin_2[]; + getSystemIcon(key: string): IconComponent_2 | undefined; getComponents(): AppComponents; }; @@ -178,6 +182,7 @@ export type AppTheme = { variant: 'light' | 'dark'; theme: BackstageTheme; icon?: React.ReactElement; + Provider?(props: { children: ReactNode }): JSX.Element | null; }; // @public @@ -421,7 +426,7 @@ export const errorApiRef: ApiRef; // @public export type ErrorBoundaryFallbackProps = { - plugin?: BackstagePlugin; + plugin?: BackstagePlugin_2; error: Error; resetError: () => void; }; @@ -741,7 +746,7 @@ export type SignInPageProps = { // @public export type SignInResult = { userId: string; - profile: ProfileInfo; + profile: ProfileInfo_2; getIdToken?: () => Promise; signOut?: () => Promise; }; From 7e0b26dec09f87b535406b9e99c8a082fc5b60b3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 Oct 2021 21:02:52 +0200 Subject: [PATCH 057/181] core-app-api: remove duplicate AppThemeProvider Signed-off-by: Patrik Oldsberg --- packages/core-app-api/src/app/App.test.tsx | 6 +- .../core-app-api/src/app/AppThemeProvider.tsx | 109 ------------------ 2 files changed, 3 insertions(+), 112 deletions(-) delete mode 100644 packages/core-app-api/src/app/AppThemeProvider.tsx diff --git a/packages/core-app-api/src/app/App.test.tsx b/packages/core-app-api/src/app/App.test.tsx index 6ed5d45b86..198048f8cf 100644 --- a/packages/core-app-api/src/app/App.test.tsx +++ b/packages/core-app-api/src/app/App.test.tsx @@ -38,7 +38,7 @@ import { analyticsApiRef, } from '@backstage/core-plugin-api'; import { generateBoundRoutes, PrivateAppImpl } from './App'; -import { AppThemeProvider } from './AppThemeProvider'; +import { AppComponents } from './types'; describe('generateBoundRoutes', () => { it('runs happy path', () => { @@ -178,13 +178,13 @@ describe('Integration Test', () => { }), ); - const components = { + const components: AppComponents = { NotFoundErrorPage: () => null, BootErrorPage: () => null, Progress: () => null, Router: BrowserRouter, ErrorBoundaryFallback: () => null, - ThemeProvider: AppThemeProvider, + ThemeProvider: ({ children }) => <>{children}, }; it('runs happy paths', async () => { diff --git a/packages/core-app-api/src/app/AppThemeProvider.tsx b/packages/core-app-api/src/app/AppThemeProvider.tsx deleted file mode 100644 index aa48a2187d..0000000000 --- a/packages/core-app-api/src/app/AppThemeProvider.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useMemo, useEffect, useState, PropsWithChildren } from 'react'; -import { ThemeProvider } from '@material-ui/core/styles'; -import CssBaseline from '@material-ui/core/CssBaseline'; -import { useApi, appThemeApiRef, AppTheme } from '@backstage/core-plugin-api'; -import { useObservable } from 'react-use'; - -// This tries to find the most accurate match, but also falls back to less -// accurate results in order to avoid errors. -function resolveTheme( - themeId: string | undefined, - shouldPreferDark: boolean, - themes: AppTheme[], -) { - if (themeId !== undefined) { - const selectedTheme = themes.find(theme => theme.id === themeId); - if (selectedTheme) { - return selectedTheme; - } - } - - if (shouldPreferDark) { - const darkTheme = themes.find(theme => theme.variant === 'dark'); - if (darkTheme) { - return darkTheme; - } - } - - const lightTheme = themes.find(theme => theme.variant === 'light'); - if (lightTheme) { - return lightTheme; - } - - return themes[0]; -} - -const useShouldPreferDarkTheme = () => { - const mediaQuery = useMemo( - () => window.matchMedia('(prefers-color-scheme: dark)'), - [], - ); - const [shouldPreferDark, setPrefersDark] = useState(mediaQuery.matches); - - useEffect(() => { - const listener = (event: MediaQueryListEvent) => { - setPrefersDark(event.matches); - }; - mediaQuery.addListener(listener); - return () => { - mediaQuery.removeListener(listener); - }; - }, [mediaQuery]); - - return shouldPreferDark; -}; - -export function AppThemeProvider({ children }: PropsWithChildren<{}>) { - const appThemeApi = useApi(appThemeApiRef); - const themeId = useObservable( - appThemeApi.activeThemeId$(), - appThemeApi.getActiveThemeId(), - ); - - // Browser feature detection won't change over time, so ignore lint rule - const shouldPreferDark = Boolean(window.matchMedia) - ? useShouldPreferDarkTheme() // eslint-disable-line react-hooks/rules-of-hooks - : false; - - const appTheme = resolveTheme( - themeId, - shouldPreferDark, - appThemeApi.getInstalledThemes(), - ); - if (!appTheme) { - throw new Error('App has no themes'); - } - - if (appTheme.Provider) { - return ; - } - - // eslint-disable-next-line no-console - console.warn( - "DEPRECATION WARNING: A provided app theme is using the deprecated 'theme' property " + - 'and should be migrated to use a Provider instead. ' + - 'See https://backstage.io/docs/deprecations/TODO for more info.', - ); - - return ( - - {children} - - ); -} From 8921944da11d2f1d90c480c0e62a4d96455a6f1a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 Oct 2021 21:31:28 +0200 Subject: [PATCH 058/181] core-components: tweak exports and update api reports Signed-off-by: Patrik Oldsberg --- packages/core-app-api/api-report.md | 2 -- packages/core-app-api/src/app/icons.tsx | 1 + packages/core-components/api-report.md | 21 +++++++++++++-- .../core-components/src/appDefaults/index.ts | 1 + .../src/appDefaults/withDefaults.tsx | 26 ++++++++++--------- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index f517f284b4..b4b1b07ebe 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -164,8 +164,6 @@ export type AppContext = { getComponents(): AppComponents; }; -// Warning: (ae-missing-release-tag) "AppIcons" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type AppIcons = { 'kind:api': IconComponent; diff --git a/packages/core-app-api/src/app/icons.tsx b/packages/core-app-api/src/app/icons.tsx index f06c9da989..1d9bb12923 100644 --- a/packages/core-app-api/src/app/icons.tsx +++ b/packages/core-app-api/src/app/icons.tsx @@ -35,6 +35,7 @@ import MuiPeopleIcon from '@material-ui/icons/People'; import MuiPersonIcon from '@material-ui/icons/Person'; import MuiWarningIcon from '@material-ui/icons/Warning'; +/** @public */ export type AppIcons = { 'kind:api': IconComponent; 'kind:component': IconComponent; diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index b0c3e2d9b2..f517b1673b 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -681,6 +681,22 @@ export type OAuthRequestDialogClassKey = // @public (undocumented) export type OpenedDropdownClassKey = 'icon'; +// @public +export interface OptionalAppOptions { + // Warning: (ae-forgotten-export) The symbol "AppComponents" needs to be exported by the entry point index.d.ts + // + // (undocumented) + components?: Partial; + // Warning: (ae-forgotten-export) The symbol "AppIcons" needs to be exported by the entry point index.d.ts + // + // (undocumented) + icons?: Partial & { + [key in string]: IconComponent; + }; + // (undocumented) + themes?: (Partial & Omit)[]; +} + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "OverflowTooltip" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2328,11 +2344,12 @@ export type WarningPanelClassKey = | 'message' | 'details'; -// Warning: (ae-forgotten-export) The symbol "DefaultAppOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "AppOptions" needs to be exported by the entry point index.d.ts // // @public -export function withDefaults(options?: DefaultAppOptions): AppOptions; +export function withDefaults( + options?: Omit & OptionalAppOptions, +): AppOptions; // Warnings were encountered during analysis: // diff --git a/packages/core-components/src/appDefaults/index.ts b/packages/core-components/src/appDefaults/index.ts index 287ad3cac6..6ccc6a6257 100644 --- a/packages/core-components/src/appDefaults/index.ts +++ b/packages/core-components/src/appDefaults/index.ts @@ -15,3 +15,4 @@ */ export { withDefaults } from './withDefaults'; +export type { OptionalAppOptions } from './withDefaults'; diff --git a/packages/core-components/src/appDefaults/withDefaults.tsx b/packages/core-components/src/appDefaults/withDefaults.tsx index 51adaf22ef..9254053404 100644 --- a/packages/core-components/src/appDefaults/withDefaults.tsx +++ b/packages/core-components/src/appDefaults/withDefaults.tsx @@ -28,22 +28,22 @@ import { defaultAppComponents } from './defaultAppComponents'; import { defaultAppIcons } from './defaultAppIcons'; import { defaultAppThemes } from './defaultAppThemes'; -interface OptionalAppOptions { - icons?: Partial & { - [key in string]: IconComponent; - }; - themes?: (Partial & Omit)[]; - components?: Partial; -} +// NOTE: we don't re-export any of the types imported from core-app-api, as we +// want them to be imported from there rather than core-components. /** - * The options required by {@link @backstage/core-app-api#createApp}, but with - * many of the fields being optional + * The set of app options that will be populated by {@link withDefaults} if they + * are not passed in explicitly. * * @public */ -type DefaultAppOptions = Omit & - OptionalAppOptions; +export interface OptionalAppOptions { + icons?: Partial & { + [key in string]: IconComponent; + }; + themes?: (Partial & Omit)[]; // TODO: simplify once AppTheme is updated + components?: Partial; +} /** * Provides a set of default App options with the ability to override specific options. @@ -52,7 +52,9 @@ type DefaultAppOptions = Omit & * * @public */ -export function withDefaults(options?: DefaultAppOptions): AppOptions { +export function withDefaults( + options?: Omit & OptionalAppOptions, +): AppOptions { const { themes, icons, components } = options ?? {}; return { From a82aa54e272b03fb8b8d713909d5895f2b45e5f0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 Oct 2021 19:10:40 +0200 Subject: [PATCH 059/181] packages: new app-defaults package + move over withDefaults Signed-off-by: Patrik Oldsberg --- packages/app-defaults/.eslintrc.js | 3 + packages/app-defaults/README.md | 17 + packages/app-defaults/package.json | 64 ++++ .../src/createApp}/AppThemeProvider.tsx | 0 .../src/createApp/createApp.test.tsx | 119 +++++++ .../app-defaults/src/createApp/createApp.tsx | 145 ++++++++ .../app-defaults/src/createApp/defaultApis.ts | 264 ++++++++++++++ .../createApp}/defaultAppComponents.test.tsx | 0 .../src/createApp}/defaultAppComponents.tsx | 0 .../src/createApp}/defaultAppIcons.tsx | 0 .../src/createApp}/defaultAppThemes.tsx | 0 packages/app-defaults/src/createApp/icons.tsx | 78 ++++ packages/app-defaults/src/createApp/index.ts | 19 + packages/app-defaults/src/createApp/types.ts | 332 ++++++++++++++++++ .../src/createApp}/withDefaults.tsx | 0 packages/app-defaults/src/index.ts | 23 ++ .../src/setupTests.ts} | 6 +- packages/core-components/src/index.ts | 1 - 18 files changed, 1067 insertions(+), 4 deletions(-) create mode 100644 packages/app-defaults/.eslintrc.js create mode 100644 packages/app-defaults/README.md create mode 100644 packages/app-defaults/package.json rename packages/{core-components/src/appDefaults => app-defaults/src/createApp}/AppThemeProvider.tsx (100%) create mode 100644 packages/app-defaults/src/createApp/createApp.test.tsx create mode 100644 packages/app-defaults/src/createApp/createApp.tsx create mode 100644 packages/app-defaults/src/createApp/defaultApis.ts rename packages/{core-components/src/appDefaults => app-defaults/src/createApp}/defaultAppComponents.test.tsx (100%) rename packages/{core-components/src/appDefaults => app-defaults/src/createApp}/defaultAppComponents.tsx (100%) rename packages/{core-components/src/appDefaults => app-defaults/src/createApp}/defaultAppIcons.tsx (100%) rename packages/{core-components/src/appDefaults => app-defaults/src/createApp}/defaultAppThemes.tsx (100%) create mode 100644 packages/app-defaults/src/createApp/icons.tsx create mode 100644 packages/app-defaults/src/createApp/index.ts create mode 100644 packages/app-defaults/src/createApp/types.ts rename packages/{core-components/src/appDefaults => app-defaults/src/createApp}/withDefaults.tsx (100%) create mode 100644 packages/app-defaults/src/index.ts rename packages/{core-components/src/appDefaults/index.ts => app-defaults/src/setupTests.ts} (79%) diff --git a/packages/app-defaults/.eslintrc.js b/packages/app-defaults/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/packages/app-defaults/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/packages/app-defaults/README.md b/packages/app-defaults/README.md new file mode 100644 index 0000000000..15a0bd3d24 --- /dev/null +++ b/packages/app-defaults/README.md @@ -0,0 +1,17 @@ +# @backstage/app-defaults + +This package provides a default wiring of a Backstage app that avoids boilerplate when setting up a standard Backstage app. + +## Installation + +Install the package via Yarn: + +```sh +cd packages/app +yarn add @backstage/app-defaults +``` + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json new file mode 100644 index 0000000000..c7594470d6 --- /dev/null +++ b/packages/app-defaults/package.json @@ -0,0 +1,64 @@ +{ + "name": "@backstage/app-defaults", + "description": "Provides the default wiring of a Backstage App", + "version": "0.1.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/app-defaults" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "build": "backstage-cli build --outputs types,esm", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core-components": "^0.7.1", + "@backstage/config": "^0.1.10", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/theme": "^0.2.11", + "@backstage/types": "^0.1.1", + "@backstage/version-bridge": "^0.1.0", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@types/react": "*", + "@types/prop-types": "^15.7.3", + "prop-types": "^15.7.2", + "react": "^16.12.0", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^17.2.4", + "zen-observable": "^0.8.15" + }, + "devDependencies": { + "@backstage/cli": "^0.8.0", + "@backstage/test-utils": "^0.1.19", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/react-hooks": "^7.0.2", + "@testing-library/user-event": "^13.1.8", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "@types/zen-observable": "^0.8.0", + "cross-fetch": "^3.0.6", + "msw": "^0.29.0" + }, + "files": [ + "dist" + ] +} diff --git a/packages/core-components/src/appDefaults/AppThemeProvider.tsx b/packages/app-defaults/src/createApp/AppThemeProvider.tsx similarity index 100% rename from packages/core-components/src/appDefaults/AppThemeProvider.tsx rename to packages/app-defaults/src/createApp/AppThemeProvider.tsx diff --git a/packages/app-defaults/src/createApp/createApp.test.tsx b/packages/app-defaults/src/createApp/createApp.test.tsx new file mode 100644 index 0000000000..0bf90987db --- /dev/null +++ b/packages/app-defaults/src/createApp/createApp.test.tsx @@ -0,0 +1,119 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { screen } from '@testing-library/react'; +import { renderWithEffects } from '@backstage/test-utils'; +import React, { PropsWithChildren } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { defaultConfigLoader, createApp } from './createApp'; + +(process as any).env = { NODE_ENV: 'test' }; +const anyEnv = process.env as any; +const anyWindow = window as any; + +describe('defaultConfigLoader', () => { + afterEach(() => { + delete anyEnv.APP_CONFIG; + delete anyWindow.__APP_CONFIG__; + }); + + it('loads static config', async () => { + anyEnv.APP_CONFIG = [ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]; + + const configs = await defaultConfigLoader(); + expect(configs).toEqual([ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]); + }); + + it('loads runtime config', async () => { + anyEnv.APP_CONFIG = [ + { data: { my: 'override-config' }, context: 'a' }, + { data: { my: 'config' }, context: 'b' }, + ]; + + const configs = await (defaultConfigLoader as any)( + '{"my":"runtime-config"}', + ); + expect(configs).toEqual([ + { data: { my: 'override-config' }, context: 'a' }, + { data: { my: 'config' }, context: 'b' }, + { data: { my: 'runtime-config' }, context: 'env' }, + ]); + }); + + it('fails to load invalid missing config', async () => { + await expect(defaultConfigLoader()).rejects.toThrow( + 'No static configuration provided', + ); + }); + + it('fails to load invalid static config', async () => { + anyEnv.APP_CONFIG = { my: 'invalid-config' }; + await expect(defaultConfigLoader()).rejects.toThrow( + 'Static configuration has invalid format', + ); + }); + + it('fails to load bad runtime config', async () => { + anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }]; + + await expect((defaultConfigLoader as any)('}')).rejects.toThrow( + 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', + ); + }); + + it('loads config from window.__APP_CONFIG__', async () => { + anyEnv.APP_CONFIG = [ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]; + const windowConfig = { app: { configKey: 'config-value' } }; + anyWindow.__APP_CONFIG__ = windowConfig; + + const configs = await defaultConfigLoader(); + + expect(configs).toEqual([ + ...anyEnv.APP_CONFIG, + { context: 'window', data: windowConfig }, + ]); + }); +}); + +describe('Optional ThemeProvider', () => { + it('should render app with user-provided ThemeProvider', async () => { + const components = { + NotFoundErrorPage: () => null, + BootErrorPage: () => null, + Progress: () => null, + Router: MemoryRouter, + ErrorBoundaryFallback: () => null, + ThemeProvider: ({ children }: PropsWithChildren<{}>) => ( +
{children}
+ ), + }; + + const App = createApp({ components }).getProvider(); + + await renderWithEffects(); + + expect(screen.getByRole('main')).toBeInTheDocument(); + }); +}); diff --git a/packages/app-defaults/src/createApp/createApp.tsx b/packages/app-defaults/src/createApp/createApp.tsx new file mode 100644 index 0000000000..c3f477035f --- /dev/null +++ b/packages/app-defaults/src/createApp/createApp.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { withDefaults } from '@backstage/core-components'; +import { PrivateAppImpl } from './App'; +import { AppComponents, AppConfigLoader, AppOptions } from './types'; +import { defaultApis } from './defaultApis'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; + +const REQUIRED_APP_COMPONENTS: Array = [ + 'Progress', + 'Router', + 'NotFoundErrorPage', + 'BootErrorPage', + 'ErrorBoundaryFallback', +]; + +/** + * The default config loader, which expects that config is available at compile-time + * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as + * returned by the config loader. + * + * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string, + * which can be rewritten at runtime to contain an additional JSON config object. + * If runtime config is present, it will be placed first in the config array, overriding + * other config values. + * + * @public + */ +export const defaultConfigLoader: AppConfigLoader = async ( + // This string may be replaced at runtime to provide additional config. + // It should be replaced by a JSON-serialized config object. + // It's a param so we can test it, but at runtime this will always fall back to default. + runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', +) => { + const appConfig = process.env.APP_CONFIG; + if (!appConfig) { + throw new Error('No static configuration provided'); + } + if (!Array.isArray(appConfig)) { + throw new Error('Static configuration has invalid format'); + } + const configs = appConfig.slice() as unknown as AppConfig[]; + + // Avoiding this string also being replaced at runtime + if ( + runtimeConfigJson !== + '__app_injected_runtime_config__'.toLocaleUpperCase('en-US') + ) { + try { + const data = JSON.parse(runtimeConfigJson) as JsonObject; + if (Array.isArray(data)) { + configs.push(...data); + } else { + configs.push({ data, context: 'env' }); + } + } catch (error) { + throw new Error(`Failed to load runtime configuration, ${error}`); + } + } + + const windowAppConfig = (window as any).__APP_CONFIG__; + if (windowAppConfig) { + configs.push({ + context: 'window', + data: windowAppConfig, + }); + } + return configs; +}; + +/** + * Creates a new Backstage App. + * + * @public + */ +export function createApp(options?: AppOptions) { + const optionsWithDefaults = withDefaults(options); + + const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter( + name => !options?.components?.[name], + ); + if (missingRequiredComponents.length > 0) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp options will soon require a minimal set of components to ' + + 'be provided. You can use the default components by using withDefaults from @backstage/core-components ' + + 'like this: createApp(withDefaults({ ... })), or you can provide the components yourself. ' + + `The following components are missing: ${missingRequiredComponents.join( + ', ', + )}`, + ); + } + + const providedIconKeys = Object.keys(options?.icons ?? {}); + const missingIconKeys = Object.keys(optionsWithDefaults.icons!).filter( + key => !providedIconKeys.includes(key), + ); + if (missingIconKeys.length > 0) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp options will soon require a minimal set of icons to ' + + 'be provided. You can use the default icons by using withDefaults from @backstage/core-components ' + + 'like this: createApp(withDefaults({ ... })), or you can provide the icons yourself. ' + + `The following icons are missing: ${missingIconKeys.join(', ')}`, + ); + } + + if (!options?.themes) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp options will soon require the themes to be provided. ' + + 'You can use the default themes by using withDefaults from @backstage/core-components ' + + 'like this: createApp(withDefaults({ ... })), or you can provide the themes yourself. ', + ); + } + + const { icons, themes, components } = optionsWithDefaults; + + return new PrivateAppImpl({ + icons: icons!, + themes: themes!, + components: components! as AppComponents, + defaultApis, + apis: options?.apis ?? [], + bindRoutes: options?.bindRoutes, + plugins: (options?.plugins as BackstagePlugin[]) ?? [], + configLoader: options?.configLoader ?? defaultConfigLoader, + }); +} diff --git a/packages/app-defaults/src/createApp/defaultApis.ts b/packages/app-defaults/src/createApp/defaultApis.ts new file mode 100644 index 0000000000..fb02274cf0 --- /dev/null +++ b/packages/app-defaults/src/createApp/defaultApis.ts @@ -0,0 +1,264 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AlertApiForwarder, + NoOpAnalyticsApi, + ErrorApiForwarder, + ErrorAlerter, + GoogleAuth, + GithubAuth, + OAuth2, + OktaAuth, + GitlabAuth, + Auth0Auth, + MicrosoftAuth, + BitbucketAuth, + OAuthRequestManager, + WebStorage, + UrlPatternDiscovery, + SamlAuth, + OneLoginAuth, + UnhandledErrorForwarder, + AtlassianAuth, +} from '../apis'; + +import { + createApiFactory, + alertApiRef, + analyticsApiRef, + errorApiRef, + discoveryApiRef, + oauthRequestApiRef, + googleAuthApiRef, + githubAuthApiRef, + oauth2ApiRef, + oktaAuthApiRef, + gitlabAuthApiRef, + auth0AuthApiRef, + microsoftAuthApiRef, + storageApiRef, + configApiRef, + samlAuthApiRef, + oneloginAuthApiRef, + oidcAuthApiRef, + bitbucketAuthApiRef, + atlassianAuthApiRef, +} from '@backstage/core-plugin-api'; + +import OAuth2Icon from '@material-ui/icons/AcUnit'; + +export const defaultApis = [ + createApiFactory({ + api: discoveryApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => + UrlPatternDiscovery.compile( + `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`, + ), + }), + createApiFactory(alertApiRef, new AlertApiForwarder()), + createApiFactory(analyticsApiRef, new NoOpAnalyticsApi()), + createApiFactory({ + api: errorApiRef, + deps: { alertApi: alertApiRef }, + factory: ({ alertApi }) => { + const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder()); + UnhandledErrorForwarder.forward(errorApi, { hidden: false }); + return errorApi; + }, + }), + createApiFactory({ + api: storageApiRef, + deps: { errorApi: errorApiRef }, + factory: ({ errorApi }) => WebStorage.create({ errorApi }), + }), + createApiFactory(oauthRequestApiRef, new OAuthRequestManager()), + createApiFactory({ + api: googleAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GoogleAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: microsoftAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + MicrosoftAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: githubAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GithubAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['read:user'], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oktaAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OktaAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: gitlabAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GitlabAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: auth0AuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + Auth0Auth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oauth2ApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: samlAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, configApi }) => + SamlAuth.create({ + discoveryApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oneloginAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OneLoginAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oidcAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider: { + id: 'oidc', + title: 'Your Identity Provider', + icon: OAuth2Icon, + }, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: bitbucketAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + BitbucketAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['team'], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: atlassianAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => { + return AtlassianAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), +]; diff --git a/packages/core-components/src/appDefaults/defaultAppComponents.test.tsx b/packages/app-defaults/src/createApp/defaultAppComponents.test.tsx similarity index 100% rename from packages/core-components/src/appDefaults/defaultAppComponents.test.tsx rename to packages/app-defaults/src/createApp/defaultAppComponents.test.tsx diff --git a/packages/core-components/src/appDefaults/defaultAppComponents.tsx b/packages/app-defaults/src/createApp/defaultAppComponents.tsx similarity index 100% rename from packages/core-components/src/appDefaults/defaultAppComponents.tsx rename to packages/app-defaults/src/createApp/defaultAppComponents.tsx diff --git a/packages/core-components/src/appDefaults/defaultAppIcons.tsx b/packages/app-defaults/src/createApp/defaultAppIcons.tsx similarity index 100% rename from packages/core-components/src/appDefaults/defaultAppIcons.tsx rename to packages/app-defaults/src/createApp/defaultAppIcons.tsx diff --git a/packages/core-components/src/appDefaults/defaultAppThemes.tsx b/packages/app-defaults/src/createApp/defaultAppThemes.tsx similarity index 100% rename from packages/core-components/src/appDefaults/defaultAppThemes.tsx rename to packages/app-defaults/src/createApp/defaultAppThemes.tsx diff --git a/packages/app-defaults/src/createApp/icons.tsx b/packages/app-defaults/src/createApp/icons.tsx new file mode 100644 index 0000000000..df1e69da6a --- /dev/null +++ b/packages/app-defaults/src/createApp/icons.tsx @@ -0,0 +1,78 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IconComponent } from '@backstage/core-plugin-api'; +import MuiApartmentIcon from '@material-ui/icons/Apartment'; +import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; +import MuiCategoryIcon from '@material-ui/icons/Category'; +import MuiChatIcon from '@material-ui/icons/Chat'; +import MuiDashboardIcon from '@material-ui/icons/Dashboard'; +import MuiDocsIcon from '@material-ui/icons/Description'; +import MuiEmailIcon from '@material-ui/icons/Email'; +import MuiExtensionIcon from '@material-ui/icons/Extension'; +import MuiGitHubIcon from '@material-ui/icons/GitHub'; +import MuiHelpIcon from '@material-ui/icons/Help'; +import MuiLocationOnIcon from '@material-ui/icons/LocationOn'; +import MuiMemoryIcon from '@material-ui/icons/Memory'; +import MuiMenuBookIcon from '@material-ui/icons/MenuBook'; +import MuiPeopleIcon from '@material-ui/icons/People'; +import MuiPersonIcon from '@material-ui/icons/Person'; +import MuiWarningIcon from '@material-ui/icons/Warning'; + +/** @public */ +export type AppIcons = { + 'kind:api': IconComponent; + 'kind:component': IconComponent; + 'kind:domain': IconComponent; + 'kind:group': IconComponent; + 'kind:location': IconComponent; + 'kind:system': IconComponent; + 'kind:user': IconComponent; + + brokenImage: IconComponent; + catalog: IconComponent; + chat: IconComponent; + dashboard: IconComponent; + docs: IconComponent; + email: IconComponent; + github: IconComponent; + group: IconComponent; + help: IconComponent; + user: IconComponent; + warning: IconComponent; +}; + +export const defaultAppIcons: AppIcons = { + brokenImage: MuiBrokenImageIcon, + // To be confirmed: see https://github.com/backstage/backstage/issues/4970 + catalog: MuiMenuBookIcon, + chat: MuiChatIcon, + dashboard: MuiDashboardIcon, + docs: MuiDocsIcon, + email: MuiEmailIcon, + github: MuiGitHubIcon, + group: MuiPeopleIcon, + help: MuiHelpIcon, + 'kind:api': MuiExtensionIcon, + 'kind:component': MuiMemoryIcon, + 'kind:domain': MuiApartmentIcon, + 'kind:group': MuiPeopleIcon, + 'kind:location': MuiLocationOnIcon, + 'kind:system': MuiCategoryIcon, + 'kind:user': MuiPersonIcon, + user: MuiPersonIcon, + warning: MuiWarningIcon, +}; diff --git a/packages/app-defaults/src/createApp/index.ts b/packages/app-defaults/src/createApp/index.ts new file mode 100644 index 0000000000..2a16a90bfa --- /dev/null +++ b/packages/app-defaults/src/createApp/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createApp, defaultConfigLoader } from './createApp'; +export type { AppIcons } from './icons'; +export * from './types'; diff --git a/packages/app-defaults/src/createApp/types.ts b/packages/app-defaults/src/createApp/types.ts new file mode 100644 index 0000000000..78ef277c90 --- /dev/null +++ b/packages/app-defaults/src/createApp/types.ts @@ -0,0 +1,332 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentType } from 'react'; +import { + AnyApiFactory, + AppTheme, + ProfileInfo, + IconComponent, + BackstagePlugin, + RouteRef, + SubRouteRef, + ExternalRouteRef, + PluginOutput, +} from '@backstage/core-plugin-api'; +import { AppConfig } from '@backstage/config'; +import { AppIcons } from './icons'; + +/** + * Props for the `BootErrorPage` component of {@link AppComponents}. + * + * @public + */ +export type BootErrorPageProps = { + step: 'load-config' | 'load-chunk'; + error: Error; +}; + +/** + * The outcome of signing in on the sign-in page. + * + * @public + */ +export type SignInResult = { + /** + * User ID that will be returned by the IdentityApi + */ + userId: string; + + profile: ProfileInfo; + + /** + * Function used to retrieve an ID token for the signed in user. + */ + getIdToken?: () => Promise; + + /** + * Sign out handler that will be called if the user requests to sign out. + */ + signOut?: () => Promise; +}; + +/** + * Props for the `SignInPage` component of {@link AppComponents}. + * + * @public + */ +export type SignInPageProps = { + /** + * Set the sign-in result for the app. This should only be called once. + */ + onResult(result: SignInResult): void; +}; + +/** + * Props for the fallback error boundary. + * + * @public + */ +export type ErrorBoundaryFallbackProps = { + plugin?: BackstagePlugin; + error: Error; + resetError: () => void; +}; + +/** + * A set of replaceable core components that are part of every Backstage app. + * + * @public + */ +export type AppComponents = { + NotFoundErrorPage: ComponentType<{}>; + BootErrorPage: ComponentType; + Progress: ComponentType<{}>; + Router: ComponentType<{}>; + ErrorBoundaryFallback: ComponentType; + ThemeProvider: ComponentType<{}>; + + /** + * An optional sign-in page that will be rendered instead of the AppRouter at startup. + * + * If a sign-in page is set, it will always be shown before the app, and it is up + * to the sign-in page to handle e.g. saving of login methods for subsequent visits. + * + * The sign-in page will be displayed until it has passed up a result to the parent, + * and which point the AppRouter and all of its children will be rendered instead. + */ + SignInPage?: ComponentType; +}; + +/** + * A function that loads in the App config that will be accessible via the ConfigApi. + * + * If multiple config objects are returned in the array, values in the earlier configs + * will override later ones. + * + * @public + */ +export type AppConfigLoader = () => Promise; + +/** + * Extracts a union of the keys in a map whose value extends the given type + */ +type KeysWithType = { + [key in keyof Obj]: Obj[key] extends Type ? key : never; +}[keyof Obj]; + +/** + * Takes a map Map required values and makes all keys matching Keys optional + */ +type PartialKeys< + Map extends { [name in string]: any }, + Keys extends keyof Map, +> = Partial> & Required>; + +/** + * Creates a map of target routes with matching parameters based on a map of external routes. + */ +type TargetRouteMap< + ExternalRoutes extends { [name: string]: ExternalRouteRef }, +> = { + [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef< + infer Params, + any + > + ? RouteRef | SubRouteRef + : never; +}; + +/** + * A function that can bind from external routes of a given plugin, to concrete + * routes of other plugins. See {@link createApp}. + * + * @public + */ +export type AppRouteBinder = < + ExternalRoutes extends { [name: string]: ExternalRouteRef }, +>( + externalRoutes: ExternalRoutes, + targetRoutes: PartialKeys< + TargetRouteMap, + KeysWithType> + >, +) => void; + +/** + * Internal helper type that represents a plugin with any type of output. + * + * @public + * @remarks + * + * The `type: string` type is there to handle output from newer or older plugin + * API versions that might not be supported by this version of the app API, but + * we don't want to break at the type checking level. We only use this more + * permissive type for the `createApp` options, as we otherwise want to stick + * to using the type for the outputs that we know about in this version of the + * app api. + * + * TODO(freben): This should be marked internal but that's not supported by the api report generation tools yet + */ +export type BackstagePluginWithAnyOutput = Omit< + BackstagePlugin, + 'output' +> & { + output(): (PluginOutput | { type: string })[]; +}; + +/** + * The options accepted by {@link createApp}. + * + * @public + */ +export type AppOptions = { + /** + * A collection of ApiFactories to register in the application to either + * add add new ones, or override factories provided by default or by plugins. + */ + apis?: Iterable; + + /** + * Supply icons to override the default ones. + */ + icons?: Partial & { [key in string]: IconComponent }; + + /** + * A list of all plugins to include in the app. + */ + plugins?: BackstagePluginWithAnyOutput[]; + + /** + * Supply components to the app to override the default ones. + */ + components?: Partial; + + /** + * Themes provided as a part of the app. By default two themes are included, one + * light variant of the default backstage theme, and one dark. + * + * This is the default config: + * + * ``` + * [{ + * id: 'light', + * title: 'Light Theme', + * variant: 'light', + * icon: , + * Provider: ({ children }) => ( + * + * {children} + * + * ), + * }, { + * id: 'dark', + * title: 'Dark Theme', + * variant: 'dark', + * icon: , + * Provider: ({ children }) => ( + * + * {children} + * + * ), + * }] + * ``` + */ + themes?: (Partial & Omit)[]; + + /** + * A function that loads in App configuration that will be accessible via + * the ConfigApi. + * + * Defaults to an empty config. + * + * TODO(Rugvip): Omitting this should instead default to loading in configuration + * that was packaged by the backstage-cli and default docker container boot script. + */ + configLoader?: AppConfigLoader; + + /** + * A function that is used to register associations between cross-plugin route + * references, enabling plugins to navigate between each other. + * + * The `bind` function that is passed in should be used to bind all external + * routes of all used plugins. + * + * ```ts + * bindRoutes({ bind }) { + * bind(docsPlugin.externalRoutes, { + * homePage: managePlugin.routes.managePage, + * }) + * bind(homePagePlugin.externalRoutes, { + * settingsPage: settingsPlugin.routes.settingsPage, + * }) + * } + * ``` + */ + bindRoutes?(context: { bind: AppRouteBinder }): void; +}; + +/** + * The public API of the output of {@link createApp}. + * + * @public + */ +export type BackstageApp = { + /** + * Returns all plugins registered for the app. + */ + getPlugins(): BackstagePlugin[]; + + /** + * Get a common or custom icon for this app. + */ + getSystemIcon(key: string): IconComponent | undefined; + + /** + * Provider component that should wrap the Router created with getRouter() + * and any other components that need to be within the app context. + */ + getProvider(): ComponentType<{}>; + + /** + * Router component that should wrap the App Routes create with getRoutes() + * and any other components that should only be available while signed in. + */ + getRouter(): ComponentType<{}>; +}; + +/** + * The central context providing runtime app specific state that plugin views + * want to consume. + * + * @public + */ +export type AppContext = { + /** + * Get a list of all plugins that are installed in the app. + */ + getPlugins(): BackstagePlugin[]; + + /** + * Get a common or custom icon for this app. + */ + getSystemIcon(key: string): IconComponent | undefined; + + /** + * Get the components registered for various purposes in the app. + */ + getComponents(): AppComponents; +}; diff --git a/packages/core-components/src/appDefaults/withDefaults.tsx b/packages/app-defaults/src/createApp/withDefaults.tsx similarity index 100% rename from packages/core-components/src/appDefaults/withDefaults.tsx rename to packages/app-defaults/src/createApp/withDefaults.tsx diff --git a/packages/app-defaults/src/index.ts b/packages/app-defaults/src/index.ts new file mode 100644 index 0000000000..62ef4d9077 --- /dev/null +++ b/packages/app-defaults/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Provides the default wiring of a Backstage App + * + * @packageDocumentation + */ + +export * from './createApp'; diff --git a/packages/core-components/src/appDefaults/index.ts b/packages/app-defaults/src/setupTests.ts similarity index 79% rename from packages/core-components/src/appDefaults/index.ts rename to packages/app-defaults/src/setupTests.ts index 6ccc6a6257..c1d649f2ad 100644 --- a/packages/core-components/src/appDefaults/index.ts +++ b/packages/app-defaults/src/setupTests.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * Copyright 2020 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,5 +14,5 @@ * limitations under the License. */ -export { withDefaults } from './withDefaults'; -export type { OptionalAppOptions } from './withDefaults'; +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/packages/core-components/src/index.ts b/packages/core-components/src/index.ts index ea1fea9d5c..3c5e708360 100644 --- a/packages/core-components/src/index.ts +++ b/packages/core-components/src/index.ts @@ -25,4 +25,3 @@ export * from './hooks'; export * from './icons'; export * from './layout'; export * from './overridableComponents'; -export * from './appDefaults'; From 60d1b30c215d6a0e7843447b7833229d7a7769e2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 Oct 2021 19:35:39 +0200 Subject: [PATCH 060/181] app-defaults: refactor things into folders and move things closer to home Signed-off-by: Patrik Oldsberg --- packages/app-defaults/package.json | 1 + .../{ => components}/AppThemeProvider.tsx | 0 .../src/createApp/createApp.test.tsx | 79 +---- .../app-defaults/src/createApp/createApp.tsx | 183 ++++------ .../{defaultApis.ts => defaults/apis.ts} | 4 +- .../components.test.tsx} | 2 +- .../components.tsx} | 31 +- .../createApp/defaults/configLoader.test.ts | 92 +++++ .../src/createApp/defaults/configLoader.ts | 73 ++++ .../icons.tsx} | 2 +- .../src/createApp/defaults/index.ts | 21 ++ .../themes.tsx} | 54 ++- packages/app-defaults/src/createApp/icons.tsx | 78 ---- packages/app-defaults/src/createApp/index.ts | 4 +- packages/app-defaults/src/createApp/types.ts | 332 ------------------ .../src/createApp/withDefaults.tsx | 66 ---- 16 files changed, 301 insertions(+), 721 deletions(-) rename packages/app-defaults/src/createApp/{ => components}/AppThemeProvider.tsx (100%) rename packages/app-defaults/src/createApp/{defaultApis.ts => defaults/apis.ts} (99%) rename packages/app-defaults/src/createApp/{defaultAppComponents.test.tsx => defaults/components.test.tsx} (95%) rename packages/app-defaults/src/createApp/{defaultAppComponents.tsx => defaults/components.tsx} (79%) create mode 100644 packages/app-defaults/src/createApp/defaults/configLoader.test.ts create mode 100644 packages/app-defaults/src/createApp/defaults/configLoader.ts rename packages/app-defaults/src/createApp/{defaultAppIcons.tsx => defaults/icons.tsx} (98%) create mode 100644 packages/app-defaults/src/createApp/defaults/index.ts rename packages/app-defaults/src/createApp/{defaultAppThemes.tsx => defaults/themes.tsx} (59%) delete mode 100644 packages/app-defaults/src/createApp/icons.tsx delete mode 100644 packages/app-defaults/src/createApp/types.ts delete mode 100644 packages/app-defaults/src/createApp/withDefaults.tsx diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index c7594470d6..19f4e1432e 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -31,6 +31,7 @@ "dependencies": { "@backstage/core-components": "^0.7.1", "@backstage/config": "^0.1.10", + "@backstage/core-app-api": "^0.1.11", "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", "@backstage/types": "^0.1.1", diff --git a/packages/app-defaults/src/createApp/AppThemeProvider.tsx b/packages/app-defaults/src/createApp/components/AppThemeProvider.tsx similarity index 100% rename from packages/app-defaults/src/createApp/AppThemeProvider.tsx rename to packages/app-defaults/src/createApp/components/AppThemeProvider.tsx diff --git a/packages/app-defaults/src/createApp/createApp.test.tsx b/packages/app-defaults/src/createApp/createApp.test.tsx index 0bf90987db..45988a4840 100644 --- a/packages/app-defaults/src/createApp/createApp.test.tsx +++ b/packages/app-defaults/src/createApp/createApp.test.tsx @@ -18,84 +18,7 @@ import { screen } from '@testing-library/react'; import { renderWithEffects } from '@backstage/test-utils'; import React, { PropsWithChildren } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { defaultConfigLoader, createApp } from './createApp'; - -(process as any).env = { NODE_ENV: 'test' }; -const anyEnv = process.env as any; -const anyWindow = window as any; - -describe('defaultConfigLoader', () => { - afterEach(() => { - delete anyEnv.APP_CONFIG; - delete anyWindow.__APP_CONFIG__; - }); - - it('loads static config', async () => { - anyEnv.APP_CONFIG = [ - { data: { my: 'config' }, context: 'a' }, - { data: { my: 'override-config' }, context: 'b' }, - ]; - - const configs = await defaultConfigLoader(); - expect(configs).toEqual([ - { data: { my: 'config' }, context: 'a' }, - { data: { my: 'override-config' }, context: 'b' }, - ]); - }); - - it('loads runtime config', async () => { - anyEnv.APP_CONFIG = [ - { data: { my: 'override-config' }, context: 'a' }, - { data: { my: 'config' }, context: 'b' }, - ]; - - const configs = await (defaultConfigLoader as any)( - '{"my":"runtime-config"}', - ); - expect(configs).toEqual([ - { data: { my: 'override-config' }, context: 'a' }, - { data: { my: 'config' }, context: 'b' }, - { data: { my: 'runtime-config' }, context: 'env' }, - ]); - }); - - it('fails to load invalid missing config', async () => { - await expect(defaultConfigLoader()).rejects.toThrow( - 'No static configuration provided', - ); - }); - - it('fails to load invalid static config', async () => { - anyEnv.APP_CONFIG = { my: 'invalid-config' }; - await expect(defaultConfigLoader()).rejects.toThrow( - 'Static configuration has invalid format', - ); - }); - - it('fails to load bad runtime config', async () => { - anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }]; - - await expect((defaultConfigLoader as any)('}')).rejects.toThrow( - 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', - ); - }); - - it('loads config from window.__APP_CONFIG__', async () => { - anyEnv.APP_CONFIG = [ - { data: { my: 'config' }, context: 'a' }, - { data: { my: 'override-config' }, context: 'b' }, - ]; - const windowConfig = { app: { configKey: 'config-value' } }; - anyWindow.__APP_CONFIG__ = windowConfig; - - const configs = await defaultConfigLoader(); - - expect(configs).toEqual([ - ...anyEnv.APP_CONFIG, - { context: 'window', data: windowConfig }, - ]); - }); -}); +import { createApp } from './createApp'; describe('Optional ThemeProvider', () => { it('should render app with user-provided ThemeProvider', async () => { diff --git a/packages/app-defaults/src/createApp/createApp.tsx b/packages/app-defaults/src/createApp/createApp.tsx index c3f477035f..417c41d3af 100644 --- a/packages/app-defaults/src/createApp/createApp.tsx +++ b/packages/app-defaults/src/createApp/createApp.tsx @@ -14,132 +14,81 @@ * limitations under the License. */ -import { AppConfig } from '@backstage/config'; -import { JsonObject } from '@backstage/types'; -import { withDefaults } from '@backstage/core-components'; -import { PrivateAppImpl } from './App'; -import { AppComponents, AppConfigLoader, AppOptions } from './types'; -import { defaultApis } from './defaultApis'; -import { BackstagePlugin } from '@backstage/core-plugin-api'; - -const REQUIRED_APP_COMPONENTS: Array = [ - 'Progress', - 'Router', - 'NotFoundErrorPage', - 'BootErrorPage', - 'ErrorBoundaryFallback', -]; +import { apis, components, configLoader, icons, themes } from './defaults'; +import { + AppTheme, + BackstagePlugin, + IconComponent, +} from '@backstage/core-plugin-api'; +import { + AppComponents, + AppOptions, + AppIcons, + PrivateAppImpl, +} from '@backstage/core-app-api'; /** - * The default config loader, which expects that config is available at compile-time - * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as - * returned by the config loader. - * - * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string, - * which can be rewritten at runtime to contain an additional JSON config object. - * If runtime config is present, it will be placed first in the config array, overriding - * other config values. - * - * @public - */ -export const defaultConfigLoader: AppConfigLoader = async ( - // This string may be replaced at runtime to provide additional config. - // It should be replaced by a JSON-serialized config object. - // It's a param so we can test it, but at runtime this will always fall back to default. - runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', -) => { - const appConfig = process.env.APP_CONFIG; - if (!appConfig) { - throw new Error('No static configuration provided'); - } - if (!Array.isArray(appConfig)) { - throw new Error('Static configuration has invalid format'); - } - const configs = appConfig.slice() as unknown as AppConfig[]; - - // Avoiding this string also being replaced at runtime - if ( - runtimeConfigJson !== - '__app_injected_runtime_config__'.toLocaleUpperCase('en-US') - ) { - try { - const data = JSON.parse(runtimeConfigJson) as JsonObject; - if (Array.isArray(data)) { - configs.push(...data); - } else { - configs.push({ data, context: 'env' }); - } - } catch (error) { - throw new Error(`Failed to load runtime configuration, ${error}`); - } - } - - const windowAppConfig = (window as any).__APP_CONFIG__; - if (windowAppConfig) { - configs.push({ - context: 'window', - data: windowAppConfig, - }); - } - return configs; -}; - -/** - * Creates a new Backstage App. + * Creates a new Backstage App using a default set of components, icons and themes unless + * they are explicitly provided. * * @public */ export function createApp(options?: AppOptions) { - const optionsWithDefaults = withDefaults(options); - - const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter( - name => !options?.components?.[name], - ); - if (missingRequiredComponents.length > 0) { - // eslint-disable-next-line no-console - console.warn( - 'DEPRECATION WARNING: The createApp options will soon require a minimal set of components to ' + - 'be provided. You can use the default components by using withDefaults from @backstage/core-components ' + - 'like this: createApp(withDefaults({ ... })), or you can provide the components yourself. ' + - `The following components are missing: ${missingRequiredComponents.join( - ', ', - )}`, - ); - } - - const providedIconKeys = Object.keys(options?.icons ?? {}); - const missingIconKeys = Object.keys(optionsWithDefaults.icons!).filter( - key => !providedIconKeys.includes(key), - ); - if (missingIconKeys.length > 0) { - // eslint-disable-next-line no-console - console.warn( - 'DEPRECATION WARNING: The createApp options will soon require a minimal set of icons to ' + - 'be provided. You can use the default icons by using withDefaults from @backstage/core-components ' + - 'like this: createApp(withDefaults({ ... })), or you can provide the icons yourself. ' + - `The following icons are missing: ${missingIconKeys.join(', ')}`, - ); - } - - if (!options?.themes) { - // eslint-disable-next-line no-console - console.warn( - 'DEPRECATION WARNING: The createApp options will soon require the themes to be provided. ' + - 'You can use the default themes by using withDefaults from @backstage/core-components ' + - 'like this: createApp(withDefaults({ ... })), or you can provide the themes yourself. ', - ); - } - - const { icons, themes, components } = optionsWithDefaults; - return new PrivateAppImpl({ - icons: icons!, - themes: themes!, - components: components! as AppComponents, - defaultApis, + ...options, apis: options?.apis ?? [], bindRoutes: options?.bindRoutes, + components: { + ...components, + ...options?.components, + }, + configLoader: options?.configLoader ?? configLoader, + defaultApis: apis, + icons: { + ...icons, + ...options?.icons, + }, plugins: (options?.plugins as BackstagePlugin[]) ?? [], - configLoader: options?.configLoader ?? defaultConfigLoader, + themes: options?.themes ?? themes, }); } + +// NOTE: we don't re-export any of the types imported from core-app-api, as we +// want them to be imported from there rather than core-components. + +/** + * The set of app options that will be populated by {@link withDefaults} if they + * are not passed in explicitly. + * + * @public + */ +export interface OptionalAppOptions { + /** + * A set of icons to override the default icons with. + * + * The override is applied for each icon individually. + * + * @public + */ + icons?: Partial & { + [key in string]: IconComponent; + }; + + /** + * A set of themes that override all of the default app themes. + * + * If this option is provided none of the default themes will be used. + * + * @public + */ + themes?: (Partial & Omit)[]; // TODO: simplify once AppTheme is updated + + /** + * A set of components to override the default components with. + * + * The override is applied for each icon individually. + * + * @public + */ + components?: Partial; +} diff --git a/packages/app-defaults/src/createApp/defaultApis.ts b/packages/app-defaults/src/createApp/defaults/apis.ts similarity index 99% rename from packages/app-defaults/src/createApp/defaultApis.ts rename to packages/app-defaults/src/createApp/defaults/apis.ts index fb02274cf0..2596ac2593 100644 --- a/packages/app-defaults/src/createApp/defaultApis.ts +++ b/packages/app-defaults/src/createApp/defaults/apis.ts @@ -34,7 +34,7 @@ import { OneLoginAuth, UnhandledErrorForwarder, AtlassianAuth, -} from '../apis'; +} from '@backstage/core-app-api'; import { createApiFactory, @@ -61,7 +61,7 @@ import { import OAuth2Icon from '@material-ui/icons/AcUnit'; -export const defaultApis = [ +export const apis = [ createApiFactory({ api: discoveryApiRef, deps: { configApi: configApiRef }, diff --git a/packages/app-defaults/src/createApp/defaultAppComponents.test.tsx b/packages/app-defaults/src/createApp/defaults/components.test.tsx similarity index 95% rename from packages/app-defaults/src/createApp/defaultAppComponents.test.tsx rename to packages/app-defaults/src/createApp/defaults/components.test.tsx index db0e2d50c1..865eeed8a7 100644 --- a/packages/app-defaults/src/createApp/defaultAppComponents.test.tsx +++ b/packages/app-defaults/src/createApp/defaults/components.test.tsx @@ -17,7 +17,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { OptionallyWrapInRouter } from './defaultAppComponents'; +import { OptionallyWrapInRouter } from './components'; describe('OptionallyWrapInRouter', () => { it('should wrap with router if not yet inside a router', async () => { diff --git a/packages/app-defaults/src/createApp/defaultAppComponents.tsx b/packages/app-defaults/src/createApp/defaults/components.tsx similarity index 79% rename from packages/app-defaults/src/createApp/defaultAppComponents.tsx rename to packages/app-defaults/src/createApp/defaults/components.tsx index ab55a25840..659af21cd7 100644 --- a/packages/app-defaults/src/createApp/defaultAppComponents.tsx +++ b/packages/app-defaults/src/createApp/defaults/components.tsx @@ -16,16 +16,18 @@ import React, { ReactNode } from 'react'; import Button from '@material-ui/core/Button'; -import { ErrorPanel, Progress } from '../components'; -import { ErrorPage } from '../layout'; -import { MemoryRouter, useInRouterContext } from 'react-router'; -import { BrowserRouter } from 'react-router-dom'; +import { ErrorPanel, Progress, ErrorPage } from '@backstage/core-components'; +import { + MemoryRouter, + useInRouterContext, + BrowserRouter, +} from 'react-router-dom'; import { AppComponents, BootErrorPageProps, ErrorBoundaryFallbackProps, } from '@backstage/core-plugin-api'; -import { AppThemeProvider } from './AppThemeProvider'; +import { AppThemeProvider } from '../components/AppThemeProvider'; export function OptionallyWrapInRouter({ children }: { children: ReactNode }) { if (useInRouterContext()) { @@ -52,6 +54,7 @@ const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => { ); }; + const DefaultErrorBoundaryFallback = ({ error, resetError, @@ -75,13 +78,11 @@ const DefaultErrorBoundaryFallback = ({ * * @public */ -export function defaultAppComponents(): AppComponents { - return { - Progress, - Router: BrowserRouter, - ThemeProvider: AppThemeProvider, - NotFoundErrorPage: DefaultNotFoundPage, - BootErrorPage: DefaultBootErrorPage, - ErrorBoundaryFallback: DefaultErrorBoundaryFallback, - }; -} +export const components: AppComponents = { + Progress, + Router: BrowserRouter, + ThemeProvider: AppThemeProvider, + NotFoundErrorPage: DefaultNotFoundPage, + BootErrorPage: DefaultBootErrorPage, + ErrorBoundaryFallback: DefaultErrorBoundaryFallback, +}; diff --git a/packages/app-defaults/src/createApp/defaults/configLoader.test.ts b/packages/app-defaults/src/createApp/defaults/configLoader.test.ts new file mode 100644 index 0000000000..0246a1cf11 --- /dev/null +++ b/packages/app-defaults/src/createApp/defaults/configLoader.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { configLoader } from './configLoader'; + +(process as any).env = { NODE_ENV: 'test' }; +const anyEnv = process.env as any; +const anyWindow = window as any; + +describe('configLoader', () => { + afterEach(() => { + delete anyEnv.APP_CONFIG; + delete anyWindow.__APP_CONFIG__; + }); + + it('loads static config', async () => { + anyEnv.APP_CONFIG = [ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]; + + const configs = await configLoader(); + expect(configs).toEqual([ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]); + }); + + it('loads runtime config', async () => { + anyEnv.APP_CONFIG = [ + { data: { my: 'override-config' }, context: 'a' }, + { data: { my: 'config' }, context: 'b' }, + ]; + + const configs = await (configLoader as any)('{"my":"runtime-config"}'); + expect(configs).toEqual([ + { data: { my: 'override-config' }, context: 'a' }, + { data: { my: 'config' }, context: 'b' }, + { data: { my: 'runtime-config' }, context: 'env' }, + ]); + }); + + it('fails to load invalid missing config', async () => { + await expect(configLoader()).rejects.toThrow( + 'No static configuration provided', + ); + }); + + it('fails to load invalid static config', async () => { + anyEnv.APP_CONFIG = { my: 'invalid-config' }; + await expect(configLoader()).rejects.toThrow( + 'Static configuration has invalid format', + ); + }); + + it('fails to load bad runtime config', async () => { + anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }]; + + await expect((configLoader as any)('}')).rejects.toThrow( + 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', + ); + }); + + it('loads config from window.__APP_CONFIG__', async () => { + anyEnv.APP_CONFIG = [ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]; + const windowConfig = { app: { configKey: 'config-value' } }; + anyWindow.__APP_CONFIG__ = windowConfig; + + const configs = await configLoader(); + + expect(configs).toEqual([ + ...anyEnv.APP_CONFIG, + { context: 'window', data: windowConfig }, + ]); + }); +}); diff --git a/packages/app-defaults/src/createApp/defaults/configLoader.ts b/packages/app-defaults/src/createApp/defaults/configLoader.ts new file mode 100644 index 0000000000..3cf10ef72f --- /dev/null +++ b/packages/app-defaults/src/createApp/defaults/configLoader.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { AppConfigLoader } from '@backstage/core-app-api'; + +/** + * The default config loader, which expects that config is available at compile-time + * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as + * returned by the config loader. + * + * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string, + * which can be rewritten at runtime to contain an additional JSON config object. + * If runtime config is present, it will be placed first in the config array, overriding + * other config values. + * + * @public + */ +export const configLoader: AppConfigLoader = async ( + // This string may be replaced at runtime to provide additional config. + // It should be replaced by a JSON-serialized config object. + // It's a param so we can test it, but at runtime this will always fall back to default. + runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', +) => { + const appConfig = process.env.APP_CONFIG; + if (!appConfig) { + throw new Error('No static configuration provided'); + } + if (!Array.isArray(appConfig)) { + throw new Error('Static configuration has invalid format'); + } + const configs = appConfig.slice() as unknown as AppConfig[]; + + // Avoiding this string also being replaced at runtime + if ( + runtimeConfigJson !== + '__app_injected_runtime_config__'.toLocaleUpperCase('en-US') + ) { + try { + const data = JSON.parse(runtimeConfigJson) as JsonObject; + if (Array.isArray(data)) { + configs.push(...data); + } else { + configs.push({ data, context: 'env' }); + } + } catch (error) { + throw new Error(`Failed to load runtime configuration, ${error}`); + } + } + + const windowAppConfig = (window as any).__APP_CONFIG__; + if (windowAppConfig) { + configs.push({ + context: 'window', + data: windowAppConfig, + }); + } + return configs; +}; diff --git a/packages/app-defaults/src/createApp/defaultAppIcons.tsx b/packages/app-defaults/src/createApp/defaults/icons.tsx similarity index 98% rename from packages/app-defaults/src/createApp/defaultAppIcons.tsx rename to packages/app-defaults/src/createApp/defaults/icons.tsx index 1d02dd5ad2..ba2531213d 100644 --- a/packages/app-defaults/src/createApp/defaultAppIcons.tsx +++ b/packages/app-defaults/src/createApp/defaults/icons.tsx @@ -32,7 +32,7 @@ import MuiPeopleIcon from '@material-ui/icons/People'; import MuiPersonIcon from '@material-ui/icons/Person'; import MuiWarningIcon from '@material-ui/icons/Warning'; -export const defaultAppIcons = () => ({ +export const icons = () => ({ brokenImage: MuiBrokenImageIcon as IconComponent, // To be confirmed: see https://github.com/backstage/backstage/issues/4970 catalog: MuiMenuBookIcon as IconComponent, diff --git a/packages/app-defaults/src/createApp/defaults/index.ts b/packages/app-defaults/src/createApp/defaults/index.ts new file mode 100644 index 0000000000..a8a11e78ac --- /dev/null +++ b/packages/app-defaults/src/createApp/defaults/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { apis } from './apis'; +export { components } from './components'; +export { configLoader } from './configLoader'; +export { icons } from './icons'; +export { themes } from './themes'; diff --git a/packages/app-defaults/src/createApp/defaultAppThemes.tsx b/packages/app-defaults/src/createApp/defaults/themes.tsx similarity index 59% rename from packages/app-defaults/src/createApp/defaultAppThemes.tsx rename to packages/app-defaults/src/createApp/defaults/themes.tsx index 894716942b..90a4a0f47f 100644 --- a/packages/app-defaults/src/createApp/defaultAppThemes.tsx +++ b/packages/app-defaults/src/createApp/defaults/themes.tsx @@ -22,31 +22,29 @@ import { ThemeProvider } from '@material-ui/core/styles'; import CssBaseline from '@material-ui/core/CssBaseline'; import { AppTheme } from '@backstage/core-plugin-api'; -export function defaultAppThemes(): AppTheme[] { - return [ - { - id: 'light', - title: 'Light Theme', - variant: 'light', - icon: , - theme: lightTheme, - Provider: ({ children }) => ( - - {children} - - ), - }, - { - id: 'dark', - title: 'Dark Theme', - variant: 'dark', - icon: , - theme: darkTheme, - Provider: ({ children }) => ( - - {children} - - ), - }, - ]; -} +export const themes: AppTheme[] = [ + { + id: 'light', + title: 'Light Theme', + variant: 'light', + icon: , + theme: lightTheme, + Provider: ({ children }) => ( + + {children} + + ), + }, + { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + theme: darkTheme, + Provider: ({ children }) => ( + + {children} + + ), + }, +]; diff --git a/packages/app-defaults/src/createApp/icons.tsx b/packages/app-defaults/src/createApp/icons.tsx deleted file mode 100644 index df1e69da6a..0000000000 --- a/packages/app-defaults/src/createApp/icons.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { IconComponent } from '@backstage/core-plugin-api'; -import MuiApartmentIcon from '@material-ui/icons/Apartment'; -import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; -import MuiCategoryIcon from '@material-ui/icons/Category'; -import MuiChatIcon from '@material-ui/icons/Chat'; -import MuiDashboardIcon from '@material-ui/icons/Dashboard'; -import MuiDocsIcon from '@material-ui/icons/Description'; -import MuiEmailIcon from '@material-ui/icons/Email'; -import MuiExtensionIcon from '@material-ui/icons/Extension'; -import MuiGitHubIcon from '@material-ui/icons/GitHub'; -import MuiHelpIcon from '@material-ui/icons/Help'; -import MuiLocationOnIcon from '@material-ui/icons/LocationOn'; -import MuiMemoryIcon from '@material-ui/icons/Memory'; -import MuiMenuBookIcon from '@material-ui/icons/MenuBook'; -import MuiPeopleIcon from '@material-ui/icons/People'; -import MuiPersonIcon from '@material-ui/icons/Person'; -import MuiWarningIcon from '@material-ui/icons/Warning'; - -/** @public */ -export type AppIcons = { - 'kind:api': IconComponent; - 'kind:component': IconComponent; - 'kind:domain': IconComponent; - 'kind:group': IconComponent; - 'kind:location': IconComponent; - 'kind:system': IconComponent; - 'kind:user': IconComponent; - - brokenImage: IconComponent; - catalog: IconComponent; - chat: IconComponent; - dashboard: IconComponent; - docs: IconComponent; - email: IconComponent; - github: IconComponent; - group: IconComponent; - help: IconComponent; - user: IconComponent; - warning: IconComponent; -}; - -export const defaultAppIcons: AppIcons = { - brokenImage: MuiBrokenImageIcon, - // To be confirmed: see https://github.com/backstage/backstage/issues/4970 - catalog: MuiMenuBookIcon, - chat: MuiChatIcon, - dashboard: MuiDashboardIcon, - docs: MuiDocsIcon, - email: MuiEmailIcon, - github: MuiGitHubIcon, - group: MuiPeopleIcon, - help: MuiHelpIcon, - 'kind:api': MuiExtensionIcon, - 'kind:component': MuiMemoryIcon, - 'kind:domain': MuiApartmentIcon, - 'kind:group': MuiPeopleIcon, - 'kind:location': MuiLocationOnIcon, - 'kind:system': MuiCategoryIcon, - 'kind:user': MuiPersonIcon, - user: MuiPersonIcon, - warning: MuiWarningIcon, -}; diff --git a/packages/app-defaults/src/createApp/index.ts b/packages/app-defaults/src/createApp/index.ts index 2a16a90bfa..dea36bb3e7 100644 --- a/packages/app-defaults/src/createApp/index.ts +++ b/packages/app-defaults/src/createApp/index.ts @@ -14,6 +14,4 @@ * limitations under the License. */ -export { createApp, defaultConfigLoader } from './createApp'; -export type { AppIcons } from './icons'; -export * from './types'; +export { createApp } from './createApp'; diff --git a/packages/app-defaults/src/createApp/types.ts b/packages/app-defaults/src/createApp/types.ts deleted file mode 100644 index 78ef277c90..0000000000 --- a/packages/app-defaults/src/createApp/types.ts +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ComponentType } from 'react'; -import { - AnyApiFactory, - AppTheme, - ProfileInfo, - IconComponent, - BackstagePlugin, - RouteRef, - SubRouteRef, - ExternalRouteRef, - PluginOutput, -} from '@backstage/core-plugin-api'; -import { AppConfig } from '@backstage/config'; -import { AppIcons } from './icons'; - -/** - * Props for the `BootErrorPage` component of {@link AppComponents}. - * - * @public - */ -export type BootErrorPageProps = { - step: 'load-config' | 'load-chunk'; - error: Error; -}; - -/** - * The outcome of signing in on the sign-in page. - * - * @public - */ -export type SignInResult = { - /** - * User ID that will be returned by the IdentityApi - */ - userId: string; - - profile: ProfileInfo; - - /** - * Function used to retrieve an ID token for the signed in user. - */ - getIdToken?: () => Promise; - - /** - * Sign out handler that will be called if the user requests to sign out. - */ - signOut?: () => Promise; -}; - -/** - * Props for the `SignInPage` component of {@link AppComponents}. - * - * @public - */ -export type SignInPageProps = { - /** - * Set the sign-in result for the app. This should only be called once. - */ - onResult(result: SignInResult): void; -}; - -/** - * Props for the fallback error boundary. - * - * @public - */ -export type ErrorBoundaryFallbackProps = { - plugin?: BackstagePlugin; - error: Error; - resetError: () => void; -}; - -/** - * A set of replaceable core components that are part of every Backstage app. - * - * @public - */ -export type AppComponents = { - NotFoundErrorPage: ComponentType<{}>; - BootErrorPage: ComponentType; - Progress: ComponentType<{}>; - Router: ComponentType<{}>; - ErrorBoundaryFallback: ComponentType; - ThemeProvider: ComponentType<{}>; - - /** - * An optional sign-in page that will be rendered instead of the AppRouter at startup. - * - * If a sign-in page is set, it will always be shown before the app, and it is up - * to the sign-in page to handle e.g. saving of login methods for subsequent visits. - * - * The sign-in page will be displayed until it has passed up a result to the parent, - * and which point the AppRouter and all of its children will be rendered instead. - */ - SignInPage?: ComponentType; -}; - -/** - * A function that loads in the App config that will be accessible via the ConfigApi. - * - * If multiple config objects are returned in the array, values in the earlier configs - * will override later ones. - * - * @public - */ -export type AppConfigLoader = () => Promise; - -/** - * Extracts a union of the keys in a map whose value extends the given type - */ -type KeysWithType = { - [key in keyof Obj]: Obj[key] extends Type ? key : never; -}[keyof Obj]; - -/** - * Takes a map Map required values and makes all keys matching Keys optional - */ -type PartialKeys< - Map extends { [name in string]: any }, - Keys extends keyof Map, -> = Partial> & Required>; - -/** - * Creates a map of target routes with matching parameters based on a map of external routes. - */ -type TargetRouteMap< - ExternalRoutes extends { [name: string]: ExternalRouteRef }, -> = { - [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef< - infer Params, - any - > - ? RouteRef | SubRouteRef - : never; -}; - -/** - * A function that can bind from external routes of a given plugin, to concrete - * routes of other plugins. See {@link createApp}. - * - * @public - */ -export type AppRouteBinder = < - ExternalRoutes extends { [name: string]: ExternalRouteRef }, ->( - externalRoutes: ExternalRoutes, - targetRoutes: PartialKeys< - TargetRouteMap, - KeysWithType> - >, -) => void; - -/** - * Internal helper type that represents a plugin with any type of output. - * - * @public - * @remarks - * - * The `type: string` type is there to handle output from newer or older plugin - * API versions that might not be supported by this version of the app API, but - * we don't want to break at the type checking level. We only use this more - * permissive type for the `createApp` options, as we otherwise want to stick - * to using the type for the outputs that we know about in this version of the - * app api. - * - * TODO(freben): This should be marked internal but that's not supported by the api report generation tools yet - */ -export type BackstagePluginWithAnyOutput = Omit< - BackstagePlugin, - 'output' -> & { - output(): (PluginOutput | { type: string })[]; -}; - -/** - * The options accepted by {@link createApp}. - * - * @public - */ -export type AppOptions = { - /** - * A collection of ApiFactories to register in the application to either - * add add new ones, or override factories provided by default or by plugins. - */ - apis?: Iterable; - - /** - * Supply icons to override the default ones. - */ - icons?: Partial & { [key in string]: IconComponent }; - - /** - * A list of all plugins to include in the app. - */ - plugins?: BackstagePluginWithAnyOutput[]; - - /** - * Supply components to the app to override the default ones. - */ - components?: Partial; - - /** - * Themes provided as a part of the app. By default two themes are included, one - * light variant of the default backstage theme, and one dark. - * - * This is the default config: - * - * ``` - * [{ - * id: 'light', - * title: 'Light Theme', - * variant: 'light', - * icon: , - * Provider: ({ children }) => ( - * - * {children} - * - * ), - * }, { - * id: 'dark', - * title: 'Dark Theme', - * variant: 'dark', - * icon: , - * Provider: ({ children }) => ( - * - * {children} - * - * ), - * }] - * ``` - */ - themes?: (Partial & Omit)[]; - - /** - * A function that loads in App configuration that will be accessible via - * the ConfigApi. - * - * Defaults to an empty config. - * - * TODO(Rugvip): Omitting this should instead default to loading in configuration - * that was packaged by the backstage-cli and default docker container boot script. - */ - configLoader?: AppConfigLoader; - - /** - * A function that is used to register associations between cross-plugin route - * references, enabling plugins to navigate between each other. - * - * The `bind` function that is passed in should be used to bind all external - * routes of all used plugins. - * - * ```ts - * bindRoutes({ bind }) { - * bind(docsPlugin.externalRoutes, { - * homePage: managePlugin.routes.managePage, - * }) - * bind(homePagePlugin.externalRoutes, { - * settingsPage: settingsPlugin.routes.settingsPage, - * }) - * } - * ``` - */ - bindRoutes?(context: { bind: AppRouteBinder }): void; -}; - -/** - * The public API of the output of {@link createApp}. - * - * @public - */ -export type BackstageApp = { - /** - * Returns all plugins registered for the app. - */ - getPlugins(): BackstagePlugin[]; - - /** - * Get a common or custom icon for this app. - */ - getSystemIcon(key: string): IconComponent | undefined; - - /** - * Provider component that should wrap the Router created with getRouter() - * and any other components that need to be within the app context. - */ - getProvider(): ComponentType<{}>; - - /** - * Router component that should wrap the App Routes create with getRoutes() - * and any other components that should only be available while signed in. - */ - getRouter(): ComponentType<{}>; -}; - -/** - * The central context providing runtime app specific state that plugin views - * want to consume. - * - * @public - */ -export type AppContext = { - /** - * Get a list of all plugins that are installed in the app. - */ - getPlugins(): BackstagePlugin[]; - - /** - * Get a common or custom icon for this app. - */ - getSystemIcon(key: string): IconComponent | undefined; - - /** - * Get the components registered for various purposes in the app. - */ - getComponents(): AppComponents; -}; diff --git a/packages/app-defaults/src/createApp/withDefaults.tsx b/packages/app-defaults/src/createApp/withDefaults.tsx deleted file mode 100644 index 9254053404..0000000000 --- a/packages/app-defaults/src/createApp/withDefaults.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AppTheme, IconComponent } from '@backstage/core-plugin-api'; -// This is a bit of a hack that we use to avoid having to redeclare these types -// within this package or have an explicit dependency on core-app-api. -// These types end up being inlined and duplicated into this package at build time. -// eslint-disable-next-line no-restricted-imports -import { - AppIcons, - AppComponents, - AppOptions, -} from '../../../core-app-api/src/app'; -import { defaultAppComponents } from './defaultAppComponents'; -import { defaultAppIcons } from './defaultAppIcons'; -import { defaultAppThemes } from './defaultAppThemes'; - -// NOTE: we don't re-export any of the types imported from core-app-api, as we -// want them to be imported from there rather than core-components. - -/** - * The set of app options that will be populated by {@link withDefaults} if they - * are not passed in explicitly. - * - * @public - */ -export interface OptionalAppOptions { - icons?: Partial & { - [key in string]: IconComponent; - }; - themes?: (Partial & Omit)[]; // TODO: simplify once AppTheme is updated - components?: Partial; -} - -/** - * Provides a set of default App options with the ability to override specific options. - * - * These options populate the theme, icons and components options of {@link @backstage/core-app-api#AppOptions}. - * - * @public - */ -export function withDefaults( - options?: Omit & OptionalAppOptions, -): AppOptions { - const { themes, icons, components } = options ?? {}; - - return { - ...options, - themes: themes ?? defaultAppThemes(), - icons: { ...defaultAppIcons(), ...icons }, - components: { ...defaultAppComponents(), ...components }, - }; -} From 5bfe8917c79531f2f1ab7f969390819f7ad6e48b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 Oct 2021 21:20:57 +0200 Subject: [PATCH 061/181] app-defaults: move AppThemeProvider back to core-app-api Signed-off-by: Patrik Oldsberg --- packages/app-defaults/src/createApp/defaults/components.tsx | 2 -- packages/core-app-api/src/app/App.tsx | 5 +++-- .../components => core-app-api/src/app}/AppThemeProvider.tsx | 0 packages/core-app-api/src/app/types.ts | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) rename packages/{app-defaults/src/createApp/components => core-app-api/src/app}/AppThemeProvider.tsx (100%) diff --git a/packages/app-defaults/src/createApp/defaults/components.tsx b/packages/app-defaults/src/createApp/defaults/components.tsx index 659af21cd7..139bea4862 100644 --- a/packages/app-defaults/src/createApp/defaults/components.tsx +++ b/packages/app-defaults/src/createApp/defaults/components.tsx @@ -27,7 +27,6 @@ import { BootErrorPageProps, ErrorBoundaryFallbackProps, } from '@backstage/core-plugin-api'; -import { AppThemeProvider } from '../components/AppThemeProvider'; export function OptionallyWrapInRouter({ children }: { children: ReactNode }) { if (useInRouterContext()) { @@ -81,7 +80,6 @@ const DefaultErrorBoundaryFallback = ({ export const components: AppComponents = { Progress, Router: BrowserRouter, - ThemeProvider: AppThemeProvider, NotFoundErrorPage: DefaultNotFoundPage, BootErrorPage: DefaultBootErrorPage, ErrorBoundaryFallback: DefaultErrorBoundaryFallback, diff --git a/packages/core-app-api/src/app/App.tsx b/packages/core-app-api/src/app/App.tsx index 6fadd22808..81da370b6c 100644 --- a/packages/core-app-api/src/app/App.tsx +++ b/packages/core-app-api/src/app/App.tsx @@ -77,6 +77,7 @@ import { SignInPageProps, SignInResult, } from './types'; +import { AppThemeProvider } from './AppThemeProvider'; export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) { const result = new Map(); @@ -151,7 +152,7 @@ function useConfigLoader( noConfigNode = ; } - const { ThemeProvider } = components; + const { ThemeProvider = AppThemeProvider } = components; // Before the config is loaded we can't use a router, so exit early if (noConfigNode) { @@ -307,7 +308,7 @@ export class PrivateAppImpl implements BackstageApp { return loadedConfig.node; } - const { ThemeProvider } = this.components; + const { ThemeProvider = AppThemeProvider } = this.components; return ( diff --git a/packages/app-defaults/src/createApp/components/AppThemeProvider.tsx b/packages/core-app-api/src/app/AppThemeProvider.tsx similarity index 100% rename from packages/app-defaults/src/createApp/components/AppThemeProvider.tsx rename to packages/core-app-api/src/app/AppThemeProvider.tsx diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 78ef277c90..41e997e179 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -97,7 +97,7 @@ export type AppComponents = { Progress: ComponentType<{}>; Router: ComponentType<{}>; ErrorBoundaryFallback: ComponentType; - ThemeProvider: ComponentType<{}>; + ThemeProvider?: ComponentType<{}>; /** * An optional sign-in page that will be rendered instead of the AppRouter at startup. From 3fb98cfcbf63d7b71dba14df33513dd20ca319ec Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 Oct 2021 21:21:58 +0200 Subject: [PATCH 062/181] app-defaults: move defaultConfigLoader back to core-app-api Signed-off-by: Patrik Oldsberg --- .../src/createApp/defaults/index.ts | 1 - .../src/app/defaultConfigLoader.test.ts} | 18 ++++++++++-------- .../src/app/defaultConfigLoader.ts} | 4 ++-- packages/core-app-api/src/app/index.ts | 3 ++- 4 files changed, 14 insertions(+), 12 deletions(-) rename packages/{app-defaults/src/createApp/defaults/configLoader.test.ts => core-app-api/src/app/defaultConfigLoader.test.ts} (83%) rename packages/{app-defaults/src/createApp/defaults/configLoader.ts => core-app-api/src/app/defaultConfigLoader.ts} (95%) diff --git a/packages/app-defaults/src/createApp/defaults/index.ts b/packages/app-defaults/src/createApp/defaults/index.ts index a8a11e78ac..d9ff18bc8c 100644 --- a/packages/app-defaults/src/createApp/defaults/index.ts +++ b/packages/app-defaults/src/createApp/defaults/index.ts @@ -16,6 +16,5 @@ export { apis } from './apis'; export { components } from './components'; -export { configLoader } from './configLoader'; export { icons } from './icons'; export { themes } from './themes'; diff --git a/packages/app-defaults/src/createApp/defaults/configLoader.test.ts b/packages/core-app-api/src/app/defaultConfigLoader.test.ts similarity index 83% rename from packages/app-defaults/src/createApp/defaults/configLoader.test.ts rename to packages/core-app-api/src/app/defaultConfigLoader.test.ts index 0246a1cf11..aebba92c43 100644 --- a/packages/app-defaults/src/createApp/defaults/configLoader.test.ts +++ b/packages/core-app-api/src/app/defaultConfigLoader.test.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { configLoader } from './configLoader'; +import { defaultConfigLoader } from './defaultConfigLoader'; (process as any).env = { NODE_ENV: 'test' }; const anyEnv = process.env as any; const anyWindow = window as any; -describe('configLoader', () => { +describe('defaultConfigLoader', () => { afterEach(() => { delete anyEnv.APP_CONFIG; delete anyWindow.__APP_CONFIG__; @@ -32,7 +32,7 @@ describe('configLoader', () => { { data: { my: 'override-config' }, context: 'b' }, ]; - const configs = await configLoader(); + const configs = await defaultConfigLoader(); expect(configs).toEqual([ { data: { my: 'config' }, context: 'a' }, { data: { my: 'override-config' }, context: 'b' }, @@ -45,7 +45,9 @@ describe('configLoader', () => { { data: { my: 'config' }, context: 'b' }, ]; - const configs = await (configLoader as any)('{"my":"runtime-config"}'); + const configs = await (defaultConfigLoader as any)( + '{"my":"runtime-config"}', + ); expect(configs).toEqual([ { data: { my: 'override-config' }, context: 'a' }, { data: { my: 'config' }, context: 'b' }, @@ -54,14 +56,14 @@ describe('configLoader', () => { }); it('fails to load invalid missing config', async () => { - await expect(configLoader()).rejects.toThrow( + await expect(defaultConfigLoader()).rejects.toThrow( 'No static configuration provided', ); }); it('fails to load invalid static config', async () => { anyEnv.APP_CONFIG = { my: 'invalid-config' }; - await expect(configLoader()).rejects.toThrow( + await expect(defaultConfigLoader()).rejects.toThrow( 'Static configuration has invalid format', ); }); @@ -69,7 +71,7 @@ describe('configLoader', () => { it('fails to load bad runtime config', async () => { anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }]; - await expect((configLoader as any)('}')).rejects.toThrow( + await expect((defaultConfigLoader as any)('}')).rejects.toThrow( 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', ); }); @@ -82,7 +84,7 @@ describe('configLoader', () => { const windowConfig = { app: { configKey: 'config-value' } }; anyWindow.__APP_CONFIG__ = windowConfig; - const configs = await configLoader(); + const configs = await defaultConfigLoader(); expect(configs).toEqual([ ...anyEnv.APP_CONFIG, diff --git a/packages/app-defaults/src/createApp/defaults/configLoader.ts b/packages/core-app-api/src/app/defaultConfigLoader.ts similarity index 95% rename from packages/app-defaults/src/createApp/defaults/configLoader.ts rename to packages/core-app-api/src/app/defaultConfigLoader.ts index 3cf10ef72f..00aee2d367 100644 --- a/packages/app-defaults/src/createApp/defaults/configLoader.ts +++ b/packages/core-app-api/src/app/defaultConfigLoader.ts @@ -16,7 +16,7 @@ import { AppConfig } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -import { AppConfigLoader } from '@backstage/core-app-api'; +import { AppConfigLoader } from './types'; /** * The default config loader, which expects that config is available at compile-time @@ -30,7 +30,7 @@ import { AppConfigLoader } from '@backstage/core-app-api'; * * @public */ -export const configLoader: AppConfigLoader = async ( +export const defaultConfigLoader: AppConfigLoader = async ( // This string may be replaced at runtime to provide additional config. // It should be replaced by a JSON-serialized config object. // It's a param so we can test it, but at runtime this will always fall back to default. diff --git a/packages/core-app-api/src/app/index.ts b/packages/core-app-api/src/app/index.ts index 2a16a90bfa..5514d7ee76 100644 --- a/packages/core-app-api/src/app/index.ts +++ b/packages/core-app-api/src/app/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ -export { createApp, defaultConfigLoader } from './createApp'; +export { createApp } from './createApp'; +export { defaultConfigLoader } from './defaultConfigLoader'; export type { AppIcons } from './icons'; export * from './types'; From 42175496486ed1f79bb4f1a0adf5eba4a1f484ef Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 Oct 2021 21:26:32 +0200 Subject: [PATCH 063/181] core-app-api: rename PrivateAppImpl to AppManager Signed-off-by: Patrik Oldsberg --- .../src/app/{App.test.tsx => AppManager.test.tsx} | 12 ++++++------ .../core-app-api/src/app/{App.tsx => AppManager.tsx} | 4 ++-- packages/core-app-api/src/app/createApp.tsx | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) rename packages/core-app-api/src/app/{App.test.tsx => AppManager.test.tsx} (98%) rename packages/core-app-api/src/app/{App.tsx => AppManager.tsx} (99%) diff --git a/packages/core-app-api/src/app/App.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx similarity index 98% rename from packages/core-app-api/src/app/App.test.tsx rename to packages/core-app-api/src/app/AppManager.test.tsx index 198048f8cf..934ca3295f 100644 --- a/packages/core-app-api/src/app/App.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -37,7 +37,7 @@ import { createRoutableExtension, analyticsApiRef, } from '@backstage/core-plugin-api'; -import { generateBoundRoutes, PrivateAppImpl } from './App'; +import { generateBoundRoutes, AppManager } from './AppManager'; import { AppComponents } from './types'; describe('generateBoundRoutes', () => { @@ -188,7 +188,7 @@ describe('Integration Test', () => { }; it('runs happy paths', async () => { - const app = new PrivateAppImpl({ + const app = new AppManager({ apis: [noOpAnalyticsApi], defaultApis: [], themes: [ @@ -242,7 +242,7 @@ describe('Integration Test', () => { }); it('runs happy paths without optional routes', async () => { - const app = new PrivateAppImpl({ + const app = new AppManager({ apis: [noOpAnalyticsApi], defaultApis: [], themes: [ @@ -299,7 +299,7 @@ describe('Integration Test', () => { }), ]; - const app = new PrivateAppImpl({ + const app = new AppManager({ apis, defaultApis: [], themes: [ @@ -349,7 +349,7 @@ describe('Integration Test', () => { it('should track route changes via analytics api', async () => { const mockAnalyticsApi = new MockAnalyticsApi(); const apis = [createApiFactory(analyticsApiRef, mockAnalyticsApi)]; - const app = new PrivateAppImpl({ + const app = new AppManager({ apis, defaultApis: [], themes: [ @@ -409,7 +409,7 @@ describe('Integration Test', () => { }); it('should throw some error when the route has duplicate params', () => { - const app = new PrivateAppImpl({ + const app = new AppManager({ apis: [], defaultApis: [], themes: [ diff --git a/packages/core-app-api/src/app/App.tsx b/packages/core-app-api/src/app/AppManager.tsx similarity index 99% rename from packages/core-app-api/src/app/App.tsx rename to packages/core-app-api/src/app/AppManager.tsx index 81da370b6c..ff501aa100 100644 --- a/packages/core-app-api/src/app/App.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -171,7 +171,7 @@ function useConfigLoader( } class AppContextImpl implements AppContext { - constructor(private readonly app: PrivateAppImpl) {} + constructor(private readonly app: AppManager) {} getPlugins(): BackstagePlugin[] { return this.app.getPlugins(); @@ -186,7 +186,7 @@ class AppContextImpl implements AppContext { } } -export class PrivateAppImpl implements BackstageApp { +export class AppManager implements BackstageApp { private apiHolder?: ApiHolder; private configApi?: ConfigApi; diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index c3f477035f..5bb5607a37 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -17,7 +17,7 @@ import { AppConfig } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { withDefaults } from '@backstage/core-components'; -import { PrivateAppImpl } from './App'; +import { AppManager } from './AppManager'; import { AppComponents, AppConfigLoader, AppOptions } from './types'; import { defaultApis } from './defaultApis'; import { BackstagePlugin } from '@backstage/core-plugin-api'; @@ -132,7 +132,7 @@ export function createApp(options?: AppOptions) { const { icons, themes, components } = optionsWithDefaults; - return new PrivateAppImpl({ + return new AppManager({ icons: icons!, themes: themes!, components: components! as AppComponents, From 1c3413bab07bdb45ea8ddca78aaf2a3f9c4e074c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 Oct 2021 13:49:25 +0200 Subject: [PATCH 064/181] app-defaults,core-app-api: refactor into createApp/createSpecializedApp split Signed-off-by: Patrik Oldsberg --- .../src/{createApp => }/createApp.test.tsx | 0 .../src/{createApp => }/createApp.tsx | 23 +- .../src/{createApp => }/defaults/apis.ts | 0 .../defaults/components.test.tsx | 0 .../{createApp => }/defaults/components.tsx | 0 .../src/{createApp => }/defaults/icons.tsx | 4 +- .../src/{createApp => }/defaults/index.ts | 0 .../src/{createApp => }/defaults/themes.tsx | 0 packages/app-defaults/src/index.ts | 3 +- packages/core-app-api/package.json | 1 + packages/core-app-api/src/app/AppManager.tsx | 24 +- .../core-app-api/src/app/createApp.test.tsx | 119 -------- packages/core-app-api/src/app/createApp.tsx | 135 +-------- .../src/app/createSpecializedApp.tsx} | 13 +- packages/core-app-api/src/app/defaultApis.ts | 264 ------------------ packages/core-app-api/src/app/index.ts | 1 + packages/core-app-api/src/app/types.ts | 15 +- 17 files changed, 63 insertions(+), 539 deletions(-) rename packages/app-defaults/src/{createApp => }/createApp.test.tsx (100%) rename packages/app-defaults/src/{createApp => }/createApp.tsx (78%) rename packages/app-defaults/src/{createApp => }/defaults/apis.ts (100%) rename packages/app-defaults/src/{createApp => }/defaults/components.test.tsx (100%) rename packages/app-defaults/src/{createApp => }/defaults/components.tsx (100%) rename packages/app-defaults/src/{createApp => }/defaults/icons.tsx (98%) rename packages/app-defaults/src/{createApp => }/defaults/index.ts (100%) rename packages/app-defaults/src/{createApp => }/defaults/themes.tsx (100%) delete mode 100644 packages/core-app-api/src/app/createApp.test.tsx rename packages/{app-defaults/src/createApp/index.ts => core-app-api/src/app/createSpecializedApp.tsx} (64%) delete mode 100644 packages/core-app-api/src/app/defaultApis.ts diff --git a/packages/app-defaults/src/createApp/createApp.test.tsx b/packages/app-defaults/src/createApp.test.tsx similarity index 100% rename from packages/app-defaults/src/createApp/createApp.test.tsx rename to packages/app-defaults/src/createApp.test.tsx diff --git a/packages/app-defaults/src/createApp/createApp.tsx b/packages/app-defaults/src/createApp.tsx similarity index 78% rename from packages/app-defaults/src/createApp/createApp.tsx rename to packages/app-defaults/src/createApp.tsx index 417c41d3af..d85dfa04eb 100644 --- a/packages/app-defaults/src/createApp/createApp.tsx +++ b/packages/app-defaults/src/createApp.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { apis, components, configLoader, icons, themes } from './defaults'; +import { apis, components, icons, themes } from './defaults'; import { AppTheme, BackstagePlugin, @@ -24,7 +24,7 @@ import { AppComponents, AppOptions, AppIcons, - PrivateAppImpl, + createSpecializedApp, } from '@backstage/core-app-api'; /** @@ -33,8 +33,10 @@ import { * * @public */ -export function createApp(options?: AppOptions) { - return new PrivateAppImpl({ +export function createApp( + options?: Omit & OptionalAppOptions, +) { + return createSpecializedApp({ ...options, apis: options?.apis ?? [], bindRoutes: options?.bindRoutes, @@ -42,7 +44,7 @@ export function createApp(options?: AppOptions) { ...components, ...options?.components, }, - configLoader: options?.configLoader ?? configLoader, + configLoader: options?.configLoader, defaultApis: apis, icons: { ...icons, @@ -53,16 +55,13 @@ export function createApp(options?: AppOptions) { }); } -// NOTE: we don't re-export any of the types imported from core-app-api, as we -// want them to be imported from there rather than core-components. - /** - * The set of app options that will be populated by {@link withDefaults} if they - * are not passed in explicitly. + * The set of app options that {@link createApp} will provide defaults for + * if they are not passed in explicitly. * * @public */ -export interface OptionalAppOptions { +export type OptionalAppOptions = { /** * A set of icons to override the default icons with. * @@ -91,4 +90,4 @@ export interface OptionalAppOptions { * @public */ components?: Partial; -} +}; diff --git a/packages/app-defaults/src/createApp/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts similarity index 100% rename from packages/app-defaults/src/createApp/defaults/apis.ts rename to packages/app-defaults/src/defaults/apis.ts diff --git a/packages/app-defaults/src/createApp/defaults/components.test.tsx b/packages/app-defaults/src/defaults/components.test.tsx similarity index 100% rename from packages/app-defaults/src/createApp/defaults/components.test.tsx rename to packages/app-defaults/src/defaults/components.test.tsx diff --git a/packages/app-defaults/src/createApp/defaults/components.tsx b/packages/app-defaults/src/defaults/components.tsx similarity index 100% rename from packages/app-defaults/src/createApp/defaults/components.tsx rename to packages/app-defaults/src/defaults/components.tsx diff --git a/packages/app-defaults/src/createApp/defaults/icons.tsx b/packages/app-defaults/src/defaults/icons.tsx similarity index 98% rename from packages/app-defaults/src/createApp/defaults/icons.tsx rename to packages/app-defaults/src/defaults/icons.tsx index ba2531213d..fa578a263a 100644 --- a/packages/app-defaults/src/createApp/defaults/icons.tsx +++ b/packages/app-defaults/src/defaults/icons.tsx @@ -32,7 +32,7 @@ import MuiPeopleIcon from '@material-ui/icons/People'; import MuiPersonIcon from '@material-ui/icons/Person'; import MuiWarningIcon from '@material-ui/icons/Warning'; -export const icons = () => ({ +export const icons = { brokenImage: MuiBrokenImageIcon as IconComponent, // To be confirmed: see https://github.com/backstage/backstage/issues/4970 catalog: MuiMenuBookIcon as IconComponent, @@ -52,4 +52,4 @@ export const icons = () => ({ 'kind:user': MuiPersonIcon as IconComponent, user: MuiPersonIcon as IconComponent, warning: MuiWarningIcon as IconComponent, -}); +}; diff --git a/packages/app-defaults/src/createApp/defaults/index.ts b/packages/app-defaults/src/defaults/index.ts similarity index 100% rename from packages/app-defaults/src/createApp/defaults/index.ts rename to packages/app-defaults/src/defaults/index.ts diff --git a/packages/app-defaults/src/createApp/defaults/themes.tsx b/packages/app-defaults/src/defaults/themes.tsx similarity index 100% rename from packages/app-defaults/src/createApp/defaults/themes.tsx rename to packages/app-defaults/src/defaults/themes.tsx diff --git a/packages/app-defaults/src/index.ts b/packages/app-defaults/src/index.ts index 62ef4d9077..16a6a693a5 100644 --- a/packages/app-defaults/src/index.ts +++ b/packages/app-defaults/src/index.ts @@ -20,4 +20,5 @@ * @packageDocumentation */ -export * from './createApp'; +export { createApp } from './createApp'; +export type { OptionalAppOptions } from './createApp'; diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index a0877cf933..fc6c103109 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -29,6 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/app-defaults": "^0.1.0", "@backstage/core-components": "^0.7.3", "@backstage/config": "^0.1.11", "@backstage/core-plugin-api": "^0.1.13", diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index ff501aa100..950f57bb7d 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -78,6 +78,7 @@ import { SignInResult, } from './types'; import { AppThemeProvider } from './AppThemeProvider'; +import { defaultConfigLoader } from './defaultConfigLoader'; export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) { const result = new Map(); @@ -122,17 +123,6 @@ function getBasePath(configApi: Config) { return pathname; } -type FullAppOptions = { - apis: Iterable; - icons: NonNullable; - plugins: BackstagePlugin[]; - components: AppComponents; - themes: (Partial & Omit)[]; - configLoader?: AppConfigLoader; - defaultApis: Iterable; - bindRoutes?: AppOptions['bindRoutes']; -}; - function useConfigLoader( configLoader: AppConfigLoader | undefined, components: AppComponents, @@ -202,14 +192,16 @@ export class AppManager implements BackstageApp { private readonly identityApi = new AppIdentity(); private readonly apiFactoryRegistry: ApiFactoryRegistry; - constructor(options: FullAppOptions) { - this.apis = options.apis; + constructor(options: AppOptions) { + this.apis = options.apis ?? []; this.icons = options.icons; - this.plugins = new Set(options.plugins); + this.plugins = new Set( + (options.plugins as BackstagePlugin[]) ?? [], + ); this.components = options.components; this.themes = options.themes as AppTheme[]; - this.configLoader = options.configLoader; - this.defaultApis = options.defaultApis; + this.configLoader = options.configLoader ?? defaultConfigLoader; + this.defaultApis = options.defaultApis ?? []; this.bindRoutes = options.bindRoutes; this.apiFactoryRegistry = new ApiFactoryRegistry(); } diff --git a/packages/core-app-api/src/app/createApp.test.tsx b/packages/core-app-api/src/app/createApp.test.tsx deleted file mode 100644 index 0bf90987db..0000000000 --- a/packages/core-app-api/src/app/createApp.test.tsx +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { screen } from '@testing-library/react'; -import { renderWithEffects } from '@backstage/test-utils'; -import React, { PropsWithChildren } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { defaultConfigLoader, createApp } from './createApp'; - -(process as any).env = { NODE_ENV: 'test' }; -const anyEnv = process.env as any; -const anyWindow = window as any; - -describe('defaultConfigLoader', () => { - afterEach(() => { - delete anyEnv.APP_CONFIG; - delete anyWindow.__APP_CONFIG__; - }); - - it('loads static config', async () => { - anyEnv.APP_CONFIG = [ - { data: { my: 'config' }, context: 'a' }, - { data: { my: 'override-config' }, context: 'b' }, - ]; - - const configs = await defaultConfigLoader(); - expect(configs).toEqual([ - { data: { my: 'config' }, context: 'a' }, - { data: { my: 'override-config' }, context: 'b' }, - ]); - }); - - it('loads runtime config', async () => { - anyEnv.APP_CONFIG = [ - { data: { my: 'override-config' }, context: 'a' }, - { data: { my: 'config' }, context: 'b' }, - ]; - - const configs = await (defaultConfigLoader as any)( - '{"my":"runtime-config"}', - ); - expect(configs).toEqual([ - { data: { my: 'override-config' }, context: 'a' }, - { data: { my: 'config' }, context: 'b' }, - { data: { my: 'runtime-config' }, context: 'env' }, - ]); - }); - - it('fails to load invalid missing config', async () => { - await expect(defaultConfigLoader()).rejects.toThrow( - 'No static configuration provided', - ); - }); - - it('fails to load invalid static config', async () => { - anyEnv.APP_CONFIG = { my: 'invalid-config' }; - await expect(defaultConfigLoader()).rejects.toThrow( - 'Static configuration has invalid format', - ); - }); - - it('fails to load bad runtime config', async () => { - anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }]; - - await expect((defaultConfigLoader as any)('}')).rejects.toThrow( - 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', - ); - }); - - it('loads config from window.__APP_CONFIG__', async () => { - anyEnv.APP_CONFIG = [ - { data: { my: 'config' }, context: 'a' }, - { data: { my: 'override-config' }, context: 'b' }, - ]; - const windowConfig = { app: { configKey: 'config-value' } }; - anyWindow.__APP_CONFIG__ = windowConfig; - - const configs = await defaultConfigLoader(); - - expect(configs).toEqual([ - ...anyEnv.APP_CONFIG, - { context: 'window', data: windowConfig }, - ]); - }); -}); - -describe('Optional ThemeProvider', () => { - it('should render app with user-provided ThemeProvider', async () => { - const components = { - NotFoundErrorPage: () => null, - BootErrorPage: () => null, - Progress: () => null, - Router: MemoryRouter, - ErrorBoundaryFallback: () => null, - ThemeProvider: ({ children }: PropsWithChildren<{}>) => ( -
{children}
- ), - }; - - const App = createApp({ components }).getProvider(); - - await renderWithEffects(); - - expect(screen.getByRole('main')).toBeInTheDocument(); - }); -}); diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index 5bb5607a37..a020308d91 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -14,132 +14,25 @@ * limitations under the License. */ -import { AppConfig } from '@backstage/config'; -import { JsonObject } from '@backstage/types'; -import { withDefaults } from '@backstage/core-components'; -import { AppManager } from './AppManager'; -import { AppComponents, AppConfigLoader, AppOptions } from './types'; -import { defaultApis } from './defaultApis'; -import { BackstagePlugin } from '@backstage/core-plugin-api'; - -const REQUIRED_APP_COMPONENTS: Array = [ - 'Progress', - 'Router', - 'NotFoundErrorPage', - 'BootErrorPage', - 'ErrorBoundaryFallback', -]; - -/** - * The default config loader, which expects that config is available at compile-time - * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as - * returned by the config loader. - * - * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string, - * which can be rewritten at runtime to contain an additional JSON config object. - * If runtime config is present, it will be placed first in the config array, overriding - * other config values. - * - * @public - */ -export const defaultConfigLoader: AppConfigLoader = async ( - // This string may be replaced at runtime to provide additional config. - // It should be replaced by a JSON-serialized config object. - // It's a param so we can test it, but at runtime this will always fall back to default. - runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', -) => { - const appConfig = process.env.APP_CONFIG; - if (!appConfig) { - throw new Error('No static configuration provided'); - } - if (!Array.isArray(appConfig)) { - throw new Error('Static configuration has invalid format'); - } - const configs = appConfig.slice() as unknown as AppConfig[]; - - // Avoiding this string also being replaced at runtime - if ( - runtimeConfigJson !== - '__app_injected_runtime_config__'.toLocaleUpperCase('en-US') - ) { - try { - const data = JSON.parse(runtimeConfigJson) as JsonObject; - if (Array.isArray(data)) { - configs.push(...data); - } else { - configs.push({ data, context: 'env' }); - } - } catch (error) { - throw new Error(`Failed to load runtime configuration, ${error}`); - } - } - - const windowAppConfig = (window as any).__APP_CONFIG__; - if (windowAppConfig) { - configs.push({ - context: 'window', - data: windowAppConfig, - }); - } - return configs; -}; +import { + createApp as createDefaultApp, + OptionalAppOptions, +} from '@backstage/app-defaults'; /** * Creates a new Backstage App. * + * @deprecated Use createApp from @backstage/app-defaults instead + * @param options - A set of options for creating the app * @public */ -export function createApp(options?: AppOptions) { - const optionsWithDefaults = withDefaults(options); - - const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter( - name => !options?.components?.[name], +export function createApp(options?: OptionalAppOptions) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp function from @backstage/core-app-api will soon be removed, ' + + 'migrate to importing createApp from the @backstage/app-defaults package instead. ' + + 'If you do not wish to use a standard app configuration but instead supply all options yourself ' + + ' you can use createSpecializedApp from @backstage/core-app-api instead.', ); - if (missingRequiredComponents.length > 0) { - // eslint-disable-next-line no-console - console.warn( - 'DEPRECATION WARNING: The createApp options will soon require a minimal set of components to ' + - 'be provided. You can use the default components by using withDefaults from @backstage/core-components ' + - 'like this: createApp(withDefaults({ ... })), or you can provide the components yourself. ' + - `The following components are missing: ${missingRequiredComponents.join( - ', ', - )}`, - ); - } - - const providedIconKeys = Object.keys(options?.icons ?? {}); - const missingIconKeys = Object.keys(optionsWithDefaults.icons!).filter( - key => !providedIconKeys.includes(key), - ); - if (missingIconKeys.length > 0) { - // eslint-disable-next-line no-console - console.warn( - 'DEPRECATION WARNING: The createApp options will soon require a minimal set of icons to ' + - 'be provided. You can use the default icons by using withDefaults from @backstage/core-components ' + - 'like this: createApp(withDefaults({ ... })), or you can provide the icons yourself. ' + - `The following icons are missing: ${missingIconKeys.join(', ')}`, - ); - } - - if (!options?.themes) { - // eslint-disable-next-line no-console - console.warn( - 'DEPRECATION WARNING: The createApp options will soon require the themes to be provided. ' + - 'You can use the default themes by using withDefaults from @backstage/core-components ' + - 'like this: createApp(withDefaults({ ... })), or you can provide the themes yourself. ', - ); - } - - const { icons, themes, components } = optionsWithDefaults; - - return new AppManager({ - icons: icons!, - themes: themes!, - components: components! as AppComponents, - defaultApis, - apis: options?.apis ?? [], - bindRoutes: options?.bindRoutes, - plugins: (options?.plugins as BackstagePlugin[]) ?? [], - configLoader: options?.configLoader ?? defaultConfigLoader, - }); + return createDefaultApp(options); } diff --git a/packages/app-defaults/src/createApp/index.ts b/packages/core-app-api/src/app/createSpecializedApp.tsx similarity index 64% rename from packages/app-defaults/src/createApp/index.ts rename to packages/core-app-api/src/app/createSpecializedApp.tsx index dea36bb3e7..54e8d6cddf 100644 --- a/packages/app-defaults/src/createApp/index.ts +++ b/packages/core-app-api/src/app/createSpecializedApp.tsx @@ -14,4 +14,15 @@ * limitations under the License. */ -export { createApp } from './createApp'; +import { AppManager } from './AppManager'; +import { AppOptions } from './types'; + +/** + * Creates a new Backstage App where the full set of options are required. + * + * @param options - A set of options for creating the app + * @returns + */ +export function createSpecializedApp(options: AppOptions) { + return new AppManager(options); +} diff --git a/packages/core-app-api/src/app/defaultApis.ts b/packages/core-app-api/src/app/defaultApis.ts deleted file mode 100644 index fb02274cf0..0000000000 --- a/packages/core-app-api/src/app/defaultApis.ts +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - AlertApiForwarder, - NoOpAnalyticsApi, - ErrorApiForwarder, - ErrorAlerter, - GoogleAuth, - GithubAuth, - OAuth2, - OktaAuth, - GitlabAuth, - Auth0Auth, - MicrosoftAuth, - BitbucketAuth, - OAuthRequestManager, - WebStorage, - UrlPatternDiscovery, - SamlAuth, - OneLoginAuth, - UnhandledErrorForwarder, - AtlassianAuth, -} from '../apis'; - -import { - createApiFactory, - alertApiRef, - analyticsApiRef, - errorApiRef, - discoveryApiRef, - oauthRequestApiRef, - googleAuthApiRef, - githubAuthApiRef, - oauth2ApiRef, - oktaAuthApiRef, - gitlabAuthApiRef, - auth0AuthApiRef, - microsoftAuthApiRef, - storageApiRef, - configApiRef, - samlAuthApiRef, - oneloginAuthApiRef, - oidcAuthApiRef, - bitbucketAuthApiRef, - atlassianAuthApiRef, -} from '@backstage/core-plugin-api'; - -import OAuth2Icon from '@material-ui/icons/AcUnit'; - -export const defaultApis = [ - createApiFactory({ - api: discoveryApiRef, - deps: { configApi: configApiRef }, - factory: ({ configApi }) => - UrlPatternDiscovery.compile( - `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`, - ), - }), - createApiFactory(alertApiRef, new AlertApiForwarder()), - createApiFactory(analyticsApiRef, new NoOpAnalyticsApi()), - createApiFactory({ - api: errorApiRef, - deps: { alertApi: alertApiRef }, - factory: ({ alertApi }) => { - const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder()); - UnhandledErrorForwarder.forward(errorApi, { hidden: false }); - return errorApi; - }, - }), - createApiFactory({ - api: storageApiRef, - deps: { errorApi: errorApiRef }, - factory: ({ errorApi }) => WebStorage.create({ errorApi }), - }), - createApiFactory(oauthRequestApiRef, new OAuthRequestManager()), - createApiFactory({ - api: googleAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - GoogleAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: microsoftAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - MicrosoftAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: githubAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - GithubAuth.create({ - discoveryApi, - oauthRequestApi, - defaultScopes: ['read:user'], - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oktaAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OktaAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: gitlabAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - GitlabAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: auth0AuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - Auth0Auth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oauth2ApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: samlAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, configApi }) => - SamlAuth.create({ - discoveryApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oneloginAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OneLoginAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oidcAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider: { - id: 'oidc', - title: 'Your Identity Provider', - icon: OAuth2Icon, - }, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: bitbucketAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - BitbucketAuth.create({ - discoveryApi, - oauthRequestApi, - defaultScopes: ['team'], - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: atlassianAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => { - return AtlassianAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }); - }, - }), -]; diff --git a/packages/core-app-api/src/app/index.ts b/packages/core-app-api/src/app/index.ts index 5514d7ee76..ba6a8917a4 100644 --- a/packages/core-app-api/src/app/index.ts +++ b/packages/core-app-api/src/app/index.ts @@ -15,6 +15,7 @@ */ export { createApp } from './createApp'; +export { createSpecializedApp } from './createSpecializedApp'; export { defaultConfigLoader } from './defaultConfigLoader'; export type { AppIcons } from './icons'; export * from './types'; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 41e997e179..89a70ed566 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -200,10 +200,19 @@ export type AppOptions = { */ apis?: Iterable; + /** + * A collection of ApiFactories to register in the application as default APIs. + * Theses APIs can not be overridden by plugin factories, but can be overridden + * by plugin APIs provided through the + * A collection of ApiFactories to register in the application to either + * add add new ones, or override factories provided by default or by plugins. + */ + defaultApis?: Iterable; + /** * Supply icons to override the default ones. */ - icons?: Partial & { [key in string]: IconComponent }; + icons: AppIcons & { [key in string]: IconComponent }; /** * A list of all plugins to include in the app. @@ -213,7 +222,7 @@ export type AppOptions = { /** * Supply components to the app to override the default ones. */ - components?: Partial; + components: AppComponents; /** * Themes provided as a part of the app. By default two themes are included, one @@ -245,7 +254,7 @@ export type AppOptions = { * }] * ``` */ - themes?: (Partial & Omit)[]; + themes: (Partial & Omit)[]; /** * A function that loads in App configuration that will be accessible via From 0fb57d28e36d6c26be419e431c61665daf4eb944 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 Oct 2021 15:31:59 +0200 Subject: [PATCH 065/181] dev-utils,test-utils,app: update to use app-defaults Signed-off-by: Patrik Oldsberg --- packages/app/package.json | 1 + packages/app/src/App.tsx | 80 +++++++++---------- packages/dev-utils/package.json | 1 + packages/dev-utils/src/devApp/render.tsx | 32 ++++---- .../test-utils/src/testUtils/appWrappers.tsx | 27 ++++++- 5 files changed, 81 insertions(+), 60 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 833e146d45..9a1fb108f6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -4,6 +4,7 @@ "private": true, "bundled": true, "dependencies": { + "@backstage/app-defaults": "^0.1.0", "@backstage/catalog-model": "^0.9.5", "@backstage/cli": "^0.8.0", "@backstage/core-app-api": "^0.1.18", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index d3ab0649e9..401665b62e 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -26,12 +26,12 @@ import { RELATION_PART_OF, RELATION_PROVIDES_API, } from '@backstage/catalog-model'; -import { createApp, FlatRoutes } from '@backstage/core-app-api'; +import { createApp } from '@backstage/app-defaults'; +import { FlatRoutes } from '@backstage/core-app-api'; import { AlertDisplay, OAuthRequestDialog, SignInPage, - withDefaults, } from '@backstage/core-components'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { @@ -87,46 +87,44 @@ import * as plugins from './plugins'; import { techDocsPage } from './components/techdocs/TechDocsPage'; -const app = createApp( - withDefaults({ - apis, - plugins: Object.values(plugins), - icons: { - // Custom icon example - alert: AlarmIcon, +const app = createApp({ + apis, + plugins: Object.values(plugins), + icons: { + // Custom icon example + alert: AlarmIcon, + }, + components: { + SignInPage: props => { + return ( + + ); }, - components: { - SignInPage: props => { - return ( - - ); - }, - }, - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - viewTechDoc: techdocsPlugin.routes.docRoot, - }); - bind(catalogGraphPlugin.externalRoutes, { - catalogEntity: catalogPlugin.routes.catalogEntity, - }); - bind(apiDocsPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - }); - bind(explorePlugin.externalRoutes, { - catalogEntity: catalogPlugin.routes.catalogEntity, - }); - bind(scaffolderPlugin.externalRoutes, { - registerComponent: catalogImportPlugin.routes.importPage, - }); - }, - }), -); + }, + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + viewTechDoc: techdocsPlugin.routes.docRoot, + }); + bind(catalogGraphPlugin.externalRoutes, { + catalogEntity: catalogPlugin.routes.catalogEntity, + }); + bind(apiDocsPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + }); + bind(explorePlugin.externalRoutes, { + catalogEntity: catalogPlugin.routes.catalogEntity, + }); + bind(scaffolderPlugin.externalRoutes, { + registerComponent: catalogImportPlugin.routes.importPage, + }); + }, +}); const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index e01390e169..0846ee8e4f 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -29,6 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/app-defaults": "^0.1.0", "@backstage/core-app-api": "^0.1.18", "@backstage/core-components": "^0.7.1", "@backstage/core-plugin-api": "^0.1.11", diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index bcaac891fe..d4b1c8db49 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -32,7 +32,6 @@ import { SidebarItem, SidebarPage, SidebarSpacer, - withDefaults, } from '@backstage/core-components'; import { @@ -48,7 +47,8 @@ import { BackstagePlugin, } from '@backstage/core-plugin-api'; -import { createApp, FlatRoutes } from '@backstage/core-app-api'; +import { createApp } from '@backstage/app-defaults'; +import { FlatRoutes } from '@backstage/core-app-api'; const GatheringRoute: (props: { path: string; @@ -174,22 +174,20 @@ export class DevAppBuilder { ); } - const app = createApp( - withDefaults({ - apis, - plugins: this.plugins, - themes: this.themes, - bindRoutes: ({ bind }) => { - for (const plugin of this.plugins ?? []) { - const targets: Record> = {}; - for (const routeKey of Object.keys(plugin.externalRoutes)) { - targets[routeKey] = dummyRouteRef; - } - bind(plugin.externalRoutes, targets); + const app = createApp({ + apis, + plugins: this.plugins, + themes: this.themes, + bindRoutes: ({ bind }) => { + for (const plugin of this.plugins ?? []) { + const targets: Record> = {}; + for (const routeKey of Object.keys(plugin.externalRoutes)) { + targets[routeKey] = dummyRouteRef; } - }, - }), - ); + bind(plugin.externalRoutes, targets); + } + }, + }); const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 6f55cc6190..890d89e3fe 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -18,7 +18,7 @@ import React, { ComponentType, ReactNode, ReactElement } from 'react'; import { MemoryRouter } from 'react-router'; import { Route } from 'react-router-dom'; import { lightTheme } from '@backstage/theme'; -import { createApp } from '@backstage/core-app-api'; +import { createSpecializedApp } from '@backstage/core-app-api'; import { BootErrorPageProps, RouteRef, @@ -30,6 +30,28 @@ import { RenderResult } from '@testing-library/react'; import { renderWithEffects } from './testingLibrary'; import { mockApis } from './mockApis'; +const mockIcons = { + 'kind:api': () => , + 'kind:component': () => , + 'kind:domain': () => , + 'kind:group': () => , + 'kind:location': () => , + 'kind:system': () => , + 'kind:user': () => , + + brokenImage: () => , + catalog: () => , + chat: () => , + dashboard: () => , + docs: () => , + email: () => , + github: () => , + group: () => , + help: () => , + user: () => , + warning: () => , +}; + const ErrorBoundaryFallback = ({ error }: { error: Error }) => { throw new Error(`Reached ErrorBoundaryFallback Page with error, ${error}`); }; @@ -90,7 +112,7 @@ export function wrapInTestApp( const { routeEntries = ['/'] } = options; const boundRoutes = new Map(); - const app = createApp({ + const app = createSpecializedApp({ apis: mockApis, // Bit of a hack to make sure that the default config loader isn't used // as that would force every single test to wait for config loading. @@ -104,6 +126,7 @@ export function wrapInTestApp( ), }, + icons: mockIcons, plugins: [], themes: [ { From 687fca4ff76d9a7bf8fb9edf20f3ce39d421ee0b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Oct 2021 16:06:42 +0200 Subject: [PATCH 066/181] changesets,create-app: update for createApp split Signed-off-by: Patrik Oldsberg --- .changeset/giant-drinks-wave.md | 2 +- .changeset/hot-walls-fail.md | 17 ++------ .changeset/late-rice-sit.md | 5 --- .changeset/twenty-swans-matter.md | 12 ++---- .../default-app/packages/app/src/App.tsx | 41 ++++++++----------- 5 files changed, 26 insertions(+), 51 deletions(-) delete mode 100644 .changeset/late-rice-sit.md diff --git a/.changeset/giant-drinks-wave.md b/.changeset/giant-drinks-wave.md index 4c2f754c2c..ecf4c8fbdd 100644 --- a/.changeset/giant-drinks-wave.md +++ b/.changeset/giant-drinks-wave.md @@ -2,4 +2,4 @@ '@backstage/dev-utils': patch --- -Migrated to using `withDefaults` to pass defaults to `createApp`. +Migrated to using `@backstage/app-defaults`. diff --git a/.changeset/hot-walls-fail.md b/.changeset/hot-walls-fail.md index c814354a56..b0854e868c 100644 --- a/.changeset/hot-walls-fail.md +++ b/.changeset/hot-walls-fail.md @@ -2,21 +2,12 @@ '@backstage/create-app': patch --- -Migrated the app template use the new `withDefaults` to construct the `createApp` options, as not doing this has been deprecated and will need to be done in the future. +Migrated the app template use the new `@backstage/app-defaults` for the `createApp` import, since the `createApp` exported by `@backstage/app-core-api` will be removed in the future. To migrate an existing application, make the following change to `packages/app/src/App.tsx`: ```diff -+import { withDefaults } from '@backstage/core-components'; - - // ... - --const app = createApp({ -+const app = createApp(withDefaults({ - apis, - bindRoutes({ bind }) { - ... - }, --}); -+})); +-import { createApp, FlatRoutes } from '@backstage/core-app-api'; ++import { createApp } from '@backstage/app-defaults'; ++import { FlatRoutes } from '@backstage/core-app-api'; ``` diff --git a/.changeset/late-rice-sit.md b/.changeset/late-rice-sit.md deleted file mode 100644 index 778215038f..0000000000 --- a/.changeset/late-rice-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Added a new `withDefaults` method that accepts a set of `AppOptions` and add the default components, themes and icons. It is intended to be used together with `createApp` from `@backstage/core-app-api` like this: `createApp(withDefaults({ ... }))`. diff --git a/.changeset/twenty-swans-matter.md b/.changeset/twenty-swans-matter.md index 6dea7a089f..55cd3d6dc3 100644 --- a/.changeset/twenty-swans-matter.md +++ b/.changeset/twenty-swans-matter.md @@ -2,14 +2,8 @@ '@backstage/core-app-api': patch --- -Deprecated the defaulting of the `components`, `icons` and `themes` options of `createApp`, meaning it will become required in the future. When not passing the required options a deprecation warning is currently logged, and they will become required in a future release. +The `createApp` function from `@backstage/core-app-api` has been deprecated, with two new options being provided as a replacement. -The keep using the default set of options, migrate to using `withDefaults` from `@backstage/core-components`: +The first and most commonly used one is `createApp` from the new `@backstage/app-defaults` package, which behaves just like the existing `createApp`. In the future this method is likely to be expanded to add more APIs and other pieces into the default setup, for example the Utility APIs from `@backstage/integration-react`. -```ts -const app = createApp( - withDefaults({ - // ... - }), -); -``` +The other option that we now provide is to use `createSpecializedApp` from `@backstage/core-app-api`. This is a more low-level API where you need to provide a full set of options, including your own `components`, `icons`, `defaultApis`, and `themes`. The `createSpecializedApp` way of creating an app is particularly useful if you are not using `@backstage/core-components` or MUI, as it allows you to avoid those dependencies completely. 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 a6d34e6687..8a535835b4 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 @@ -25,30 +25,25 @@ import { entityPage } from './components/catalog/EntityPage'; import { searchPage } from './components/search/SearchPage'; import { Root } from './components/Root'; -import { - AlertDisplay, - withDefaults, - OAuthRequestDialog, -} from '@backstage/core-components'; -import { createApp, FlatRoutes } from '@backstage/core-app-api'; +import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; +import { createApp } from '@backstage/app-defaults'; +import { FlatRoutes } from '@backstage/core-app-api'; -const app = createApp( - withDefaults({ - apis, - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - viewTechDoc: techdocsPlugin.routes.docRoot, - }); - bind(apiDocsPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - }); - bind(scaffolderPlugin.externalRoutes, { - registerComponent: catalogImportPlugin.routes.importPage, - }); - }, - }), -); +const app = createApp({ + apis, + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + viewTechDoc: techdocsPlugin.routes.docRoot, + }); + bind(apiDocsPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + }); + bind(scaffolderPlugin.externalRoutes, { + registerComponent: catalogImportPlugin.routes.importPage, + }); + }, +}); const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); From b7958c36dbc2608fb18c75b2f87ed7823b064b7f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Oct 2021 16:46:37 +0200 Subject: [PATCH 067/181] core-components: wrap page stories in test app Signed-off-by: Patrik Oldsberg --- .../src/layout/Page/Page.stories.tsx | 82 ++++++++----------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/packages/core-components/src/layout/Page/Page.stories.tsx b/packages/core-components/src/layout/Page/Page.stories.tsx index 7386d198a3..12ddd51d04 100644 --- a/packages/core-components/src/layout/Page/Page.stories.tsx +++ b/packages/core-components/src/layout/Page/Page.stories.tsx @@ -20,8 +20,7 @@ import Grid from '@material-ui/core/Grid'; import Link from '@material-ui/core/Link'; import Typography from '@material-ui/core/Typography'; import React, { useState } from 'react'; -import { MemoryRouter } from 'react-router'; -import { createApp } from '@backstage/core-app-api'; +import { wrapInTestApp } from '@backstage/test-utils'; import { Content, ContentHeader, @@ -197,53 +196,44 @@ const ExampleContentHeader = ({ selectedTab }: { selectedTab?: number }) => ( ); -const app = createApp({ configLoader: async () => [] }); -const AppProvider = app.getProvider(); - export const PluginWithData = () => { const [selectedTab, setSelectedTab] = useState(2); - return ( - - -
- - - setSelectedTab(index)} - tabs={tabs.map(({ label }, index) => ({ - id: index.toString(), - label, - }))} - /> - - - - - -
-
-
- ); + return wrapInTestApp(() => ( +
+ + + setSelectedTab(index)} + tabs={tabs.map(({ label }, index) => ({ + id: index.toString(), + label, + }))} + /> + + + + + +
+ )); }; export const PluginWithTable = () => { - return ( - -
- - - - - - - - - - ); + return wrapInTestApp(() => ( +
+ + + + +
+ + + + )); }; From c1578d609b1daf42f3aaece1eb69a9fbdf391f15 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Oct 2021 16:49:45 +0200 Subject: [PATCH 068/181] update API reports for createApp split Signed-off-by: Patrik Oldsberg --- packages/app-defaults/api-report.md | 26 ++++++++++++++++++++ packages/core-app-api/api-report.md | 20 +++++++++++----- packages/core-components/api-report.md | 33 -------------------------- packages/core-plugin-api/api-report.md | 2 +- 4 files changed, 41 insertions(+), 40 deletions(-) create mode 100644 packages/app-defaults/api-report.md diff --git a/packages/app-defaults/api-report.md b/packages/app-defaults/api-report.md new file mode 100644 index 0000000000..6b35dab81b --- /dev/null +++ b/packages/app-defaults/api-report.md @@ -0,0 +1,26 @@ +## API Report File for "@backstage/app-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AppComponents } from '@backstage/core-app-api'; +import { AppIcons } from '@backstage/core-app-api'; +import { AppManager } from '@backstage/core-app-api/src/app/AppManager'; +import { AppOptions } from '@backstage/core-app-api'; +import { AppTheme } from '@backstage/core-plugin-api'; +import { IconComponent } from '@backstage/core-plugin-api'; + +// @public +export function createApp( + options?: Omit & OptionalAppOptions, +): AppManager; + +// @public +export type OptionalAppOptions = { + icons?: Partial & { + [key in string]: IconComponent; + }; + themes?: (Partial & Omit)[]; + components?: Partial; +}; +``` diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index b4b1b07ebe..b3b54ff1a0 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -45,6 +45,7 @@ import { Observable } from '@backstage/types'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { OptionalAppOptions } from '@backstage/app-defaults'; import { PendingAuthRequest } from '@backstage/core-plugin-api'; import { PluginOutput } from '@backstage/core-plugin-api'; import { ProfileInfo } from '@backstage/core-plugin-api'; @@ -150,7 +151,7 @@ export type AppComponents = { Progress: ComponentType<{}>; Router: ComponentType<{}>; ErrorBoundaryFallback: ComponentType; - ThemeProvider: ComponentType<{}>; + ThemeProvider?: ComponentType<{}>; SignInPage?: ComponentType; }; @@ -189,12 +190,13 @@ export type AppIcons = { // @public export type AppOptions = { apis?: Iterable; - icons?: Partial & { + defaultApis?: Iterable; + icons: AppIcons & { [key in string]: IconComponent; }; plugins?: BackstagePluginWithAnyOutput[]; - components?: Partial; - themes?: (Partial & Omit)[]; + components: AppComponents; + themes: (Partial & Omit)[]; configLoader?: AppConfigLoader; bindRoutes?(context: { bind: AppRouteBinder }): void; }; @@ -308,10 +310,16 @@ export type BootErrorPageProps = { export { ConfigReader }; -// Warning: (ae-forgotten-export) The symbol "PrivateAppImpl" needs to be exported by the entry point index.d.ts +// Warning: (tsdoc-characters-after-block-tag) The token "@backstage" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@" +// Warning: (ae-forgotten-export) The symbol "AppManager" needs to be exported by the entry point index.d.ts +// +// @public @deprecated +export function createApp(options?: OptionalAppOptions): AppManager; + +// Warning: (ae-missing-release-tag) "createSpecializedApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function createApp(options?: AppOptions): PrivateAppImpl; +export function createSpecializedApp(options: AppOptions): AppManager; // @public export const defaultConfigLoader: AppConfigLoader; diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index f517b1673b..0f1e3510ae 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -5,26 +5,20 @@ ```ts /// -import { AnyApiFactory } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; -import { AppConfig } from '@backstage/config'; -import { AppTheme } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePalette } from '@backstage/theme'; -import { BackstagePlugin } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; import { ButtonProps as ButtonProps_2 } from '@material-ui/core/Button'; import { CardHeaderProps } from '@material-ui/core/CardHeader'; import { Column } from '@material-table/core'; import { ComponentClass } from 'react'; import { ComponentProps } from 'react'; -import { ComponentType } from 'react'; import { Context } from 'react'; import { default as CSS_2 } from 'csstype'; import { CSSProperties } from 'react'; import { ElementType } from 'react'; import { ErrorInfo } from 'react'; -import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { LinearProgressProps } from '@material-ui/core/LinearProgress'; import { LinkProps as LinkProps_2 } from '@material-ui/core/Link'; @@ -33,15 +27,12 @@ import MaterialBreadcrumbs from '@material-ui/core/Breadcrumbs'; import { MaterialTableProps } from '@material-table/core'; import { NavLinkProps } from 'react-router-dom'; import { Overrides } from '@material-ui/core/styles/overrides'; -import { PluginOutput } from '@backstage/core-plugin-api'; -import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import * as React_3 from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; -import { RouteRef } from '@backstage/core-plugin-api'; import { SessionApi } from '@backstage/core-plugin-api'; import { SignInPageProps } from '@backstage/core-plugin-api'; import { SparklinesLineProps } from 'react-sparklines'; @@ -49,7 +40,6 @@ import { SparklinesProps } from 'react-sparklines'; import { StyledComponentProps } from '@material-ui/core/styles'; import { StyleRules } from '@material-ui/styles'; import { StyleRules as StyleRules_2 } from '@material-ui/core/styles/withStyles'; -import { SubRouteRef } from '@backstage/core-plugin-api'; import { TabProps } from '@material-ui/core/Tab'; import { TextTruncateProps } from 'react-text-truncate'; import { Theme } from '@material-ui/core/styles'; @@ -681,22 +671,6 @@ export type OAuthRequestDialogClassKey = // @public (undocumented) export type OpenedDropdownClassKey = 'icon'; -// @public -export interface OptionalAppOptions { - // Warning: (ae-forgotten-export) The symbol "AppComponents" needs to be exported by the entry point index.d.ts - // - // (undocumented) - components?: Partial; - // Warning: (ae-forgotten-export) The symbol "AppIcons" needs to be exported by the entry point index.d.ts - // - // (undocumented) - icons?: Partial & { - [key in string]: IconComponent; - }; - // (undocumented) - themes?: (Partial & Omit)[]; -} - // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "OverflowTooltip" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2344,13 +2318,6 @@ export type WarningPanelClassKey = | 'message' | 'details'; -// Warning: (ae-forgotten-export) The symbol "AppOptions" needs to be exported by the entry point index.d.ts -// -// @public -export function withDefaults( - options?: Omit & OptionalAppOptions, -): AppOptions; - // Warnings were encountered during analysis: // // src/components/DependencyGraph/types.d.ts:16:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 433edc57ff..2765b02bca 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -164,7 +164,7 @@ export type AppComponents = { Progress: ComponentType<{}>; Router: ComponentType<{}>; ErrorBoundaryFallback: ComponentType; - ThemeProvider: ComponentType<{}>; + ThemeProvider?: ComponentType<{}>; SignInPage?: ComponentType; }; From 60e5d356cbab4f5049111cacd57e374bca1ec74f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 01:30:07 +0100 Subject: [PATCH 069/181] core-app-api: add missing core feature icons + move icon things around a bit Signed-off-by: Patrik Oldsberg --- packages/app-defaults/src/defaults/icons.tsx | 6 ++ packages/core-app-api/api-report.md | 5 +- .../core-app-api/src/app/AppManager.test.tsx | 15 ++-- packages/core-app-api/src/app/icons.tsx | 87 ------------------- packages/core-app-api/src/app/index.ts | 1 - packages/core-app-api/src/app/types.ts | 31 ++++++- .../test-utils/src/testUtils/appWrappers.tsx | 3 + 7 files changed, 51 insertions(+), 97 deletions(-) delete mode 100644 packages/core-app-api/src/app/icons.tsx diff --git a/packages/app-defaults/src/defaults/icons.tsx b/packages/app-defaults/src/defaults/icons.tsx index fa578a263a..c4f2d2b7e4 100644 --- a/packages/app-defaults/src/defaults/icons.tsx +++ b/packages/app-defaults/src/defaults/icons.tsx @@ -18,6 +18,9 @@ import { IconComponent } from '@backstage/core-plugin-api'; import MuiApartmentIcon from '@material-ui/icons/Apartment'; import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; import MuiCategoryIcon from '@material-ui/icons/Category'; +import MuiCreateNewFolderIcon from '@material-ui/icons/CreateNewFolder'; +import MuiSubjectIcon from '@material-ui/icons/Subject'; +import MuiSearchIcon from '@material-ui/icons/Search'; import MuiChatIcon from '@material-ui/icons/Chat'; import MuiDashboardIcon from '@material-ui/icons/Dashboard'; import MuiDocsIcon from '@material-ui/icons/Description'; @@ -36,6 +39,9 @@ export const icons = { brokenImage: MuiBrokenImageIcon as IconComponent, // To be confirmed: see https://github.com/backstage/backstage/issues/4970 catalog: MuiMenuBookIcon as IconComponent, + scaffolder: MuiCreateNewFolderIcon as IconComponent, + techdocs: MuiSubjectIcon as IconComponent, + search: MuiSearchIcon as IconComponent, chat: MuiChatIcon as IconComponent, dashboard: MuiDashboardIcon as IconComponent, docs: MuiDocsIcon as IconComponent, diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index b3b54ff1a0..f45fc349b2 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -165,7 +165,7 @@ export type AppContext = { getComponents(): AppComponents; }; -// @public (undocumented) +// @public export type AppIcons = { 'kind:api': IconComponent; 'kind:component': IconComponent; @@ -183,6 +183,9 @@ export type AppIcons = { github: IconComponent; group: IconComponent; help: IconComponent; + scaffolder: IconComponent; + search: IconComponent; + techdocs: IconComponent; user: IconComponent; warning: IconComponent; }; diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 934ca3295f..71df5219d1 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -24,7 +24,6 @@ import { lightTheme } from '@backstage/theme'; import { render, screen } from '@testing-library/react'; import React, { PropsWithChildren } from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; -import { defaultAppIcons } from './icons'; import { configApiRef, createApiFactory, @@ -38,7 +37,7 @@ import { analyticsApiRef, } from '@backstage/core-plugin-api'; import { generateBoundRoutes, AppManager } from './AppManager'; -import { AppComponents } from './types'; +import { AppComponents, AppIcons } from './types'; describe('generateBoundRoutes', () => { it('runs happy path', () => { @@ -187,6 +186,8 @@ describe('Integration Test', () => { ThemeProvider: ({ children }) => <>{children}, }; + const icons = {} as AppIcons; + it('runs happy paths', async () => { const app = new AppManager({ apis: [noOpAnalyticsApi], @@ -199,7 +200,7 @@ describe('Integration Test', () => { theme: lightTheme, }, ], - icons: defaultAppIcons, + icons, plugins: [], components, bindRoutes: ({ bind }) => { @@ -253,7 +254,7 @@ describe('Integration Test', () => { theme: lightTheme, }, ], - icons: defaultAppIcons, + icons, plugins: [], components, bindRoutes: ({ bind }) => { @@ -310,7 +311,7 @@ describe('Integration Test', () => { theme: lightTheme, }, ], - icons: defaultAppIcons, + icons, plugins: [ createPlugin({ id: 'test', @@ -360,7 +361,7 @@ describe('Integration Test', () => { theme: lightTheme, }, ], - icons: defaultAppIcons, + icons, plugins: [], components, bindRoutes: ({ bind }) => { @@ -420,7 +421,7 @@ describe('Integration Test', () => { theme: lightTheme, }, ], - icons: defaultAppIcons, + icons, plugins: [], components, bindRoutes: ({ bind }) => { diff --git a/packages/core-app-api/src/app/icons.tsx b/packages/core-app-api/src/app/icons.tsx deleted file mode 100644 index 1d9bb12923..0000000000 --- a/packages/core-app-api/src/app/icons.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { IconComponent } from '@backstage/core-plugin-api'; -import MuiApartmentIcon from '@material-ui/icons/Apartment'; -import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; -import MuiCategoryIcon from '@material-ui/icons/Category'; -import MuiCreateNewFolderIcon from '@material-ui/icons/CreateNewFolder'; -import MuiSubjectIcon from '@material-ui/icons/Subject'; -import MuiSearchIcon from '@material-ui/icons/Search'; -import MuiChatIcon from '@material-ui/icons/Chat'; -import MuiDashboardIcon from '@material-ui/icons/Dashboard'; -import MuiDocsIcon from '@material-ui/icons/Description'; -import MuiEmailIcon from '@material-ui/icons/Email'; -import MuiExtensionIcon from '@material-ui/icons/Extension'; -import MuiGitHubIcon from '@material-ui/icons/GitHub'; -import MuiHelpIcon from '@material-ui/icons/Help'; -import MuiLocationOnIcon from '@material-ui/icons/LocationOn'; -import MuiMemoryIcon from '@material-ui/icons/Memory'; -import MuiMenuBookIcon from '@material-ui/icons/MenuBook'; -import MuiPeopleIcon from '@material-ui/icons/People'; -import MuiPersonIcon from '@material-ui/icons/Person'; -import MuiWarningIcon from '@material-ui/icons/Warning'; - -/** @public */ -export type AppIcons = { - 'kind:api': IconComponent; - 'kind:component': IconComponent; - 'kind:domain': IconComponent; - 'kind:group': IconComponent; - 'kind:location': IconComponent; - 'kind:system': IconComponent; - 'kind:user': IconComponent; - - brokenImage: IconComponent; - catalog: IconComponent; - chat: IconComponent; - dashboard: IconComponent; - docs: IconComponent; - email: IconComponent; - github: IconComponent; - group: IconComponent; - help: IconComponent; - scaffolder: IconComponent; - search: IconComponent; - techdocs: IconComponent; - user: IconComponent; - warning: IconComponent; -}; - -export const defaultAppIcons: AppIcons = { - brokenImage: MuiBrokenImageIcon, - // To be confirmed: see https://github.com/backstage/backstage/issues/4970 - catalog: MuiMenuBookIcon, - scaffolder: MuiCreateNewFolderIcon, - techdocs: MuiSubjectIcon, - search: MuiSearchIcon, - chat: MuiChatIcon, - dashboard: MuiDashboardIcon, - docs: MuiDocsIcon, - email: MuiEmailIcon, - github: MuiGitHubIcon, - group: MuiPeopleIcon, - help: MuiHelpIcon, - 'kind:api': MuiExtensionIcon, - 'kind:component': MuiMemoryIcon, - 'kind:domain': MuiApartmentIcon, - 'kind:group': MuiPeopleIcon, - 'kind:location': MuiLocationOnIcon, - 'kind:system': MuiCategoryIcon, - 'kind:user': MuiPersonIcon, - user: MuiPersonIcon, - warning: MuiWarningIcon, -}; diff --git a/packages/core-app-api/src/app/index.ts b/packages/core-app-api/src/app/index.ts index ba6a8917a4..5ea5405632 100644 --- a/packages/core-app-api/src/app/index.ts +++ b/packages/core-app-api/src/app/index.ts @@ -17,5 +17,4 @@ export { createApp } from './createApp'; export { createSpecializedApp } from './createSpecializedApp'; export { defaultConfigLoader } from './defaultConfigLoader'; -export type { AppIcons } from './icons'; export * from './types'; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 89a70ed566..86389a0105 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -27,7 +27,6 @@ import { PluginOutput, } from '@backstage/core-plugin-api'; import { AppConfig } from '@backstage/config'; -import { AppIcons } from './icons'; /** * Props for the `BootErrorPage` component of {@link AppComponents}. @@ -111,6 +110,36 @@ export type AppComponents = { SignInPage?: ComponentType; }; +/** + * A set of well-known icons that should be available within an app. + * + * @public + */ +export type AppIcons = { + 'kind:api': IconComponent; + 'kind:component': IconComponent; + 'kind:domain': IconComponent; + 'kind:group': IconComponent; + 'kind:location': IconComponent; + 'kind:system': IconComponent; + 'kind:user': IconComponent; + + brokenImage: IconComponent; + catalog: IconComponent; + chat: IconComponent; + dashboard: IconComponent; + docs: IconComponent; + email: IconComponent; + github: IconComponent; + group: IconComponent; + help: IconComponent; + scaffolder: IconComponent; + search: IconComponent; + techdocs: IconComponent; + user: IconComponent; + warning: IconComponent; +}; + /** * A function that loads in the App config that will be accessible via the ConfigApi. * diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 890d89e3fe..b84c06c77b 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -41,6 +41,9 @@ const mockIcons = { brokenImage: () => , catalog: () => , + scaffolder: () => , + techdocs: () => , + search: () => , chat: () => , dashboard: () => , docs: () => , From a3b6707eb2f6a6e793ccd27906ebd62c2beba800 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 10:25:29 +0100 Subject: [PATCH 070/181] app-defaults: bump internal deps Signed-off-by: Patrik Oldsberg --- packages/app-defaults/package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 19f4e1432e..6c06927daf 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -29,11 +29,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/config": "^0.1.10", - "@backstage/core-app-api": "^0.1.11", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.2", + "@backstage/config": "^0.1.11", + "@backstage/core-app-api": "^0.1.19", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", @@ -47,8 +47,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.1", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", From 7ca03b85ab02792e2af5c5909c01610de0166123 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 10:25:53 +0100 Subject: [PATCH 071/181] core-app-api: createSpecializedApp returns a BackstageApp Signed-off-by: Patrik Oldsberg --- packages/core-app-api/src/app/createSpecializedApp.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core-app-api/src/app/createSpecializedApp.tsx b/packages/core-app-api/src/app/createSpecializedApp.tsx index 54e8d6cddf..a437835f39 100644 --- a/packages/core-app-api/src/app/createSpecializedApp.tsx +++ b/packages/core-app-api/src/app/createSpecializedApp.tsx @@ -15,7 +15,7 @@ */ import { AppManager } from './AppManager'; -import { AppOptions } from './types'; +import { AppOptions, BackstageApp } from './types'; /** * Creates a new Backstage App where the full set of options are required. @@ -23,6 +23,6 @@ import { AppOptions } from './types'; * @param options - A set of options for creating the app * @returns */ -export function createSpecializedApp(options: AppOptions) { +export function createSpecializedApp(options: AppOptions): BackstageApp { return new AppManager(options); } From c98c5c3d000ad5452a85b8054afca1e1fd3b0f1d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 10:40:41 +0100 Subject: [PATCH 072/181] core-app-api: switch auth provider icons to be empty by default Signed-off-by: Patrik Oldsberg --- packages/app-defaults/src/defaults/apis.ts | 4 +--- .../implementations/OAuthRequestApi/MockOAuthApi.test.ts | 9 ++++----- .../OAuthRequestApi/OAuthRequestManager.test.ts | 3 +-- .../apis/implementations/auth/atlassian/AtlassianAuth.ts | 3 +-- .../src/apis/implementations/auth/auth0/Auth0Auth.ts | 3 +-- .../apis/implementations/auth/bitbucket/BitbucketAuth.ts | 3 +-- .../src/apis/implementations/auth/github/GithubAuth.ts | 3 +-- .../src/apis/implementations/auth/gitlab/GitlabAuth.ts | 3 +-- .../src/apis/implementations/auth/google/GoogleAuth.ts | 3 +-- .../apis/implementations/auth/microsoft/MicrosoftAuth.ts | 3 +-- .../src/apis/implementations/auth/oauth2/OAuth2.ts | 3 +-- .../src/apis/implementations/auth/okta/OktaAuth.ts | 3 +-- .../apis/implementations/auth/onelogin/OneLoginAuth.ts | 3 +-- .../src/apis/implementations/auth/saml/SamlAuth.ts | 3 +-- .../src/lib/AuthConnector/DefaultAuthConnector.test.ts | 3 +-- 15 files changed, 18 insertions(+), 34 deletions(-) diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 2596ac2593..85c004eece 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -59,8 +59,6 @@ import { atlassianAuthApiRef, } from '@backstage/core-plugin-api'; -import OAuth2Icon from '@material-ui/icons/AcUnit'; - export const apis = [ createApiFactory({ api: discoveryApiRef, @@ -226,7 +224,7 @@ export const apis = [ provider: { id: 'oidc', title: 'Your Identity Provider', - icon: OAuth2Icon, + icon: () => null, }, environment: configApi.getOptionalString('auth.environment'), }), diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts index d0f137f0be..76cf01d20a 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts @@ -15,7 +15,6 @@ */ import MockOAuthApi from './MockOAuthApi'; -import PowerIcon from '@material-ui/icons/Power'; describe('MockOAuthApi', () => { it('should trigger all requests', async () => { @@ -24,13 +23,13 @@ describe('MockOAuthApi', () => { const authHandler1 = jest.fn().mockImplementation(() => authResult); const requester1 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, + provider: { icon: () => null, title: 'Test' }, onAuthRequest: authHandler1, }); const authHandler2 = jest.fn().mockResolvedValue('other'); const requester2 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, + provider: { icon: () => null, title: 'Test' }, onAuthRequest: authHandler2, }); @@ -67,13 +66,13 @@ describe('MockOAuthApi', () => { const authHandler1 = jest.fn(); const requester1 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, + provider: { icon: () => null, title: 'Test' }, onAuthRequest: authHandler1, }); const authHandler2 = jest.fn(); const requester2 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, + provider: { icon: () => null, title: 'Test' }, onAuthRequest: authHandler2, }); diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts index d8faf90229..3110197ad9 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import ProviderIcon from '@material-ui/icons/AcUnit'; import { OAuthRequestManager } from './OAuthRequestManager'; describe('OAuthRequestManager', () => { @@ -27,7 +26,7 @@ describe('OAuthRequestManager', () => { const requester = manager.createAuthRequester({ provider: { title: 'My Provider', - icon: ProviderIcon, + icon: () => null, }, onAuthRequest: async () => 'hello', }); diff --git a/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts index 227d70e494..caad40ddbf 100644 --- a/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import AtlassianIcon from '@material-ui/icons/AcUnit'; import { atlassianAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'atlassian', title: 'Atlassian', - icon: AtlassianIcon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts index 69462942e4..0a158ccb9e 100644 --- a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import Auth0Icon from '@material-ui/icons/AcUnit'; import { auth0AuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'auth0', title: 'Auth0', - icon: Auth0Icon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts index c9db93906a..e488580c4d 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import BitbucketIcon from '@material-ui/icons/FormatBold'; import { BackstageIdentity, bitbucketAuthApiRef, @@ -37,7 +36,7 @@ export type BitbucketAuthResponse = { const DEFAULT_PROVIDER = { id: 'bitbucket', title: 'Bitbucket', - icon: BitbucketIcon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts index 01e7e77b8e..885e80da7c 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import GithubIcon from '@material-ui/icons/AcUnit'; import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; import { GithubSession } from './types'; import { @@ -48,7 +47,7 @@ export type GithubAuthResponse = { const DEFAULT_PROVIDER = { id: 'github', title: 'GitHub', - icon: GithubIcon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts index af581d0018..f46f2ef72e 100644 --- a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import GitlabIcon from '@material-ui/icons/AcUnit'; import { gitlabAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'gitlab', title: 'GitLab', - icon: GitlabIcon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts index 916c52382e..a6f37b250f 100644 --- a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import GoogleIcon from '@material-ui/icons/AcUnit'; import { googleAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'google', title: 'Google', - icon: GoogleIcon, + icon: () => null, }; const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts index 09bd7f67a2..148be66873 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import MicrosoftIcon from '@material-ui/icons/AcUnit'; import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'microsoft', title: 'Microsoft', - icon: MicrosoftIcon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts index 6c0759d967..45937a68f8 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import OAuth2Icon from '@material-ui/icons/AcUnit'; import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; @@ -51,7 +50,7 @@ export type OAuth2Response = { const DEFAULT_PROVIDER = { id: 'oauth2', title: 'Your Identity Provider', - icon: OAuth2Icon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts index 294fb496af..465a124051 100644 --- a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import OktaIcon from '@material-ui/icons/AcUnit'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'okta', title: 'Okta', - icon: OktaIcon, + icon: () => null, }; const OKTA_OIDC_SCOPES: Set = new Set([ diff --git a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts index f49ea4cde8..5f933b9c6e 100644 --- a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import OneLoginIcon from '@material-ui/icons/AcUnit'; import { oneloginAuthApiRef, OAuthRequestApi, @@ -33,7 +32,7 @@ type CreateOptions = { const DEFAULT_PROVIDER = { id: 'onelogin', title: 'onelogin', - icon: OneLoginIcon, + icon: () => null, }; const OIDC_SCOPES: Set = new Set([ diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts index f2668dedd2..63985979de 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import SamlIcon from '@material-ui/icons/AcUnit'; import { DirectAuthConnector } from '../../../../lib/AuthConnector'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { @@ -42,7 +41,7 @@ export type SamlAuthResponse = { const DEFAULT_PROVIDER = { id: 'saml', title: 'SAML', - icon: SamlIcon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index 7e448586e3..baf89a59d8 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import ProviderIcon from '@material-ui/icons/AcUnit'; import { DefaultAuthConnector } from './DefaultAuthConnector'; import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi'; import * as loginPopup from '../loginPopup'; @@ -29,7 +28,7 @@ const defaultOptions = { provider: { id: 'my-provider', title: 'My Provider', - icon: ProviderIcon, + icon: () => null, }, oauthRequestApi: new MockOAuthApi(), sessionTransform: ({ expiresInSeconds, ...res }: any) => ({ From 03da4c09469ddf94760925d5b9d20f233eedfa78 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 11:23:46 +0100 Subject: [PATCH 073/181] create-app: add app-defaults dependency to app + notice in changeset Signed-off-by: Patrik Oldsberg --- .changeset/hot-walls-fail.md | 2 +- packages/create-app/src/lib/versions.ts | 2 ++ .../templates/default-app/packages/app/package.json.hbs | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.changeset/hot-walls-fail.md b/.changeset/hot-walls-fail.md index b0854e868c..3728e10ff6 100644 --- a/.changeset/hot-walls-fail.md +++ b/.changeset/hot-walls-fail.md @@ -4,7 +4,7 @@ Migrated the app template use the new `@backstage/app-defaults` for the `createApp` import, since the `createApp` exported by `@backstage/app-core-api` will be removed in the future. -To migrate an existing application, make the following change to `packages/app/src/App.tsx`: +To migrate an existing application, add the latest version of `@backstage/app-defaults` as a dependency in `packages/app/package.json`, and make the following change to `packages/app/src/App.tsx`: ```diff -import { createApp, FlatRoutes } from '@backstage/core-app-api'; diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 97904cc15b..6c85652c64 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -30,6 +30,7 @@ Rollup will extract the value of the version field in each package at build time leaving any imports in place. */ +import { version as appDefaults } from '../../../app-defaults/package.json'; import { version as backendCommon } from '../../../backend-common/package.json'; import { version as catalogClient } from '../../../catalog-client/package.json'; import { version as catalogModel } from '../../../catalog-model/package.json'; @@ -68,6 +69,7 @@ import { version as pluginTechdocsBackend } from '../../../../plugins/techdocs-b import { version as pluginUserSettings } from '../../../../plugins/user-settings/package.json'; export const packageVersions = { + '@backstage/app-defaults': appDefaults, '@backstage/backend-common': backendCommon, '@backstage/catalog-client': catalogClient, '@backstage/catalog-model': catalogModel, diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs index c443b45dc7..9e15608a01 100644 --- a/packages/create-app/templates/default-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs @@ -4,6 +4,7 @@ "private": true, "bundled": true, "dependencies": { + "@backstage/app-defaults": "^{{version '@backstage/app-defaults'}}", "@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}", "@backstage/cli": "^{{version '@backstage/cli'}}", "@backstage/core-app-api": "^{{version '@backstage/core-app-api'}}", From f2bd2419726a0025b1af699f437f628556bb1e3b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 11:41:31 +0100 Subject: [PATCH 074/181] docs: introduce deprecation docs + app-theme deprecation Signed-off-by: Patrik Oldsberg --- docs/api/deprecations.md | 56 +++++++++++++++++++ microsite/sidebars.json | 3 +- mkdocs.yml | 1 + .../core-app-api/src/app/AppThemeProvider.tsx | 2 +- .../src/apis/definitions/AppThemeApi.ts | 2 +- 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 docs/api/deprecations.md diff --git a/docs/api/deprecations.md b/docs/api/deprecations.md new file mode 100644 index 0000000000..774c8ef03d --- /dev/null +++ b/docs/api/deprecations.md @@ -0,0 +1,56 @@ +--- +id: deprecations +title: Deprecations +description: A list of active and past deprecations +--- + +## Introduction + +This page contains extended documentation for some of the deprecations in +various parts of Backstage. It is not an exhaustive list as most deprecation +only come in the form of a changelog notice and a console warning. The +deprecations listed here are the ones that need a bit more guidance than what +fits in a console message. + +### App Theme + +`Released 2021-11-12 in @backstage/core-plugin-api v0.1.13` + +In order to provide more flexibility in what types of themes can be used and how +they are applied, the `theme` property on the `AppTheme` type is being +deprecated and replaced by a `Provider` property instead. The `Provider` +property is a React component that will be mounted at the root of the app +whenever that theme is active. This also removes the tight connection to MUI and +opens up for other type of themes, and removes the hardcoded usage of +``. + +To migrate an existing theme, remove the `theme` property and move it over to a +new `Provider` component, using `ThemeProvider` from MUI to provide the new +theme, along with ``. For example a theme that currently looks like +this: + +```tsx +const darkTheme = { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + theme: darkTheme, +}; +``` + +Would be migrated to the following: + +```tsx +const darkTheme = { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + Provider: ({ children }) => ( + + {children} + + ), +}; +``` diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 91525d4938..ec5c5d8d68 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -256,7 +256,8 @@ "type": "subcategory", "label": "API Reference", "ids": ["reference/index"] - } + }, + "api/deprecations" ], "Tutorials": [ "tutorials/journey", diff --git a/mkdocs.yml b/mkdocs.yml index 0ddae446e6..8c7114abbc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -164,6 +164,7 @@ nav: - API Reference: - Guides: - Utility APIs: 'api/utility-apis.md' + - Deprecations: 'api/deprecations.md' - Tutorials: - Future developer journey: 'tutorials/journey.md' - Migrating away from @backstage/core: 'tutorials/migrating-away-from-core.md' diff --git a/packages/core-app-api/src/app/AppThemeProvider.tsx b/packages/core-app-api/src/app/AppThemeProvider.tsx index aa48a2187d..2e145025d5 100644 --- a/packages/core-app-api/src/app/AppThemeProvider.tsx +++ b/packages/core-app-api/src/app/AppThemeProvider.tsx @@ -98,7 +98,7 @@ export function AppThemeProvider({ children }: PropsWithChildren<{}>) { console.warn( "DEPRECATION WARNING: A provided app theme is using the deprecated 'theme' property " + 'and should be migrated to use a Provider instead. ' + - 'See https://backstage.io/docs/deprecations/TODO for more info.', + 'See https://backstage.io/docs/api/deprecations#app-theme for more info.', ); return ( diff --git a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts index 52cf202422..8053c449a1 100644 --- a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts @@ -42,7 +42,7 @@ export type AppTheme = { /** * The specialized MaterialUI theme instance. - * @deprecated use Provider instead, see https://backstage.io/docs/deprecations/TODO + * @deprecated use Provider instead, see https://backstage.io/docs/api/deprecations#app-theme */ theme: BackstageTheme; From 465dffb05c704d8922acd2ea58383b357be93a41 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 11:48:52 +0100 Subject: [PATCH 075/181] core-app-api,app-defaults: API report and doc fixups Signed-off-by: Patrik Oldsberg --- packages/app-defaults/api-report.md | 4 ++-- packages/core-app-api/api-report.md | 9 ++------- packages/core-app-api/src/app/createApp.tsx | 2 +- packages/core-app-api/src/app/createSpecializedApp.tsx | 1 + 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/app-defaults/api-report.md b/packages/app-defaults/api-report.md index 6b35dab81b..57ea61ad8b 100644 --- a/packages/app-defaults/api-report.md +++ b/packages/app-defaults/api-report.md @@ -5,15 +5,15 @@ ```ts import { AppComponents } from '@backstage/core-app-api'; import { AppIcons } from '@backstage/core-app-api'; -import { AppManager } from '@backstage/core-app-api/src/app/AppManager'; import { AppOptions } from '@backstage/core-app-api'; import { AppTheme } from '@backstage/core-plugin-api'; +import { BackstageApp } from '@backstage/core-app-api'; import { IconComponent } from '@backstage/core-plugin-api'; // @public export function createApp( options?: Omit & OptionalAppOptions, -): AppManager; +): BackstageApp; // @public export type OptionalAppOptions = { diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index f45fc349b2..d98b5ef5f8 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -313,16 +313,11 @@ export type BootErrorPageProps = { export { ConfigReader }; -// Warning: (tsdoc-characters-after-block-tag) The token "@backstage" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@" -// Warning: (ae-forgotten-export) The symbol "AppManager" needs to be exported by the entry point index.d.ts -// // @public @deprecated -export function createApp(options?: OptionalAppOptions): AppManager; +export function createApp(options?: OptionalAppOptions): BackstageApp; -// Warning: (ae-missing-release-tag) "createSpecializedApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export function createSpecializedApp(options: AppOptions): AppManager; +export function createSpecializedApp(options: AppOptions): BackstageApp; // @public export const defaultConfigLoader: AppConfigLoader; diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index a020308d91..e11076099b 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -22,7 +22,7 @@ import { /** * Creates a new Backstage App. * - * @deprecated Use createApp from @backstage/app-defaults instead + * @deprecated Use {@link @backstage/app-defaults#createApp} from `@backstage/app-defaults` instead * @param options - A set of options for creating the app * @public */ diff --git a/packages/core-app-api/src/app/createSpecializedApp.tsx b/packages/core-app-api/src/app/createSpecializedApp.tsx index a437835f39..9bda223722 100644 --- a/packages/core-app-api/src/app/createSpecializedApp.tsx +++ b/packages/core-app-api/src/app/createSpecializedApp.tsx @@ -20,6 +20,7 @@ import { AppOptions, BackstageApp } from './types'; /** * Creates a new Backstage App where the full set of options are required. * + * @public * @param options - A set of options for creating the app * @returns */ From cdbddf196f6ac9e4aeca2bc854ffc425b8ff937d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 11:56:29 +0100 Subject: [PATCH 076/181] app-defaults: clean up dependencies Signed-off-by: Patrik Oldsberg --- packages/app-defaults/package.json | 16 ++-------------- packages/app-defaults/src/setupTests.ts | 1 - 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 6c06927daf..76101f0ad4 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -30,34 +30,22 @@ }, "dependencies": { "@backstage/core-components": "^0.7.2", - "@backstage/config": "^0.1.11", "@backstage/core-app-api": "^0.1.19", "@backstage/core-plugin-api": "^0.1.12", "@backstage/theme": "^0.2.12", - "@backstage/types": "^0.1.1", - "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", - "@types/react": "*", - "@types/prop-types": "^15.7.3", - "prop-types": "^15.7.2", "react": "^16.12.0", - "react-router-dom": "6.0.0-beta.0", - "react-use": "^17.2.4", - "zen-observable": "^0.8.15" + "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { "@backstage/cli": "^0.8.1", "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/react-hooks": "^7.0.2", - "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/zen-observable": "^0.8.0", - "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "@types/react": "*" }, "files": [ "dist" diff --git a/packages/app-defaults/src/setupTests.ts b/packages/app-defaults/src/setupTests.ts index c1d649f2ad..963c0f188b 100644 --- a/packages/app-defaults/src/setupTests.ts +++ b/packages/app-defaults/src/setupTests.ts @@ -15,4 +15,3 @@ */ import '@testing-library/jest-dom'; -import 'cross-fetch/polyfill'; From 2dd2a7b2ccc2cd063d086b93ae2b7b8d68661104 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 12:04:27 +0100 Subject: [PATCH 077/181] changesets: add changesets for test-utils createApp and AppTheme deprecation Signed-off-by: Patrik Oldsberg --- .changeset/sharp-moons-jog.md | 5 +++++ .changeset/slow-moles-act.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/sharp-moons-jog.md create mode 100644 .changeset/slow-moles-act.md diff --git a/.changeset/sharp-moons-jog.md b/.changeset/sharp-moons-jog.md new file mode 100644 index 0000000000..d3329af08e --- /dev/null +++ b/.changeset/sharp-moons-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': patch +--- + +Deprecated the `theme` property on `AppTheme`, replacing it with `Provider`. See https://backstage.io/docs/api/deprecations#app-theme for more details. diff --git a/.changeset/slow-moles-act.md b/.changeset/slow-moles-act.md new file mode 100644 index 0000000000..ab920bfa5d --- /dev/null +++ b/.changeset/slow-moles-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/test-utils': patch +--- + +Migrated to using `createSpecializedApp`. From 29eb8d7dbee876056be94268ea68ff152a083612 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 15:08:32 +0100 Subject: [PATCH 078/181] test-utils: switch mock icons to be plain svg elements Signed-off-by: Patrik Oldsberg --- .../test-utils/src/testUtils/appWrappers.tsx | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index b84c06c77b..7a8d913273 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -31,28 +31,28 @@ import { renderWithEffects } from './testingLibrary'; import { mockApis } from './mockApis'; const mockIcons = { - 'kind:api': () => , - 'kind:component': () => , - 'kind:domain': () => , - 'kind:group': () => , - 'kind:location': () => , - 'kind:system': () => , - 'kind:user': () => , + 'kind:api': () => , + 'kind:component': () => , + 'kind:domain': () => , + 'kind:group': () => , + 'kind:location': () => , + 'kind:system': () => , + 'kind:user': () => , - brokenImage: () => , - catalog: () => , - scaffolder: () => , - techdocs: () => , - search: () => , - chat: () => , - dashboard: () => , - docs: () => , - email: () => , - github: () => , - group: () => , - help: () => , - user: () => , - warning: () => , + brokenImage: () => , + catalog: () => , + scaffolder: () => , + techdocs: () => , + search: () => , + chat: () => , + dashboard: () => , + docs: () => , + email: () => , + github: () => , + group: () => , + help: () => , + user: () => , + warning: () => , }; const ErrorBoundaryFallback = ({ error }: { error: Error }) => { From 569acdb8f8bd7b352d02274466390044c4085725 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 15:19:45 +0100 Subject: [PATCH 079/181] core-app-api: fix fix docs docs Signed-off-by: Patrik Oldsberg --- packages/core-app-api/src/app/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 86389a0105..f2d1976f87 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -225,7 +225,7 @@ export type BackstagePluginWithAnyOutput = Omit< export type AppOptions = { /** * A collection of ApiFactories to register in the application to either - * add add new ones, or override factories provided by default or by plugins. + * add new ones, or override factories provided by default or by plugins. */ apis?: Iterable; @@ -234,7 +234,7 @@ export type AppOptions = { * Theses APIs can not be overridden by plugin factories, but can be overridden * by plugin APIs provided through the * A collection of ApiFactories to register in the application to either - * add add new ones, or override factories provided by default or by plugins. + * add new ones, or override factories provided by default or by plugins. */ defaultApis?: Iterable; From 6d790f0f637c9c12e1f0ab4a38dc675aeaeefc14 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 15:21:08 +0100 Subject: [PATCH 080/181] test-utils: reintroduce defaultApis Signed-off-by: Patrik Oldsberg --- .../test-utils/src/testUtils/appWrappers.tsx | 2 + .../test-utils/src/testUtils/defaultApis.ts | 264 ++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 packages/test-utils/src/testUtils/defaultApis.ts diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 7a8d913273..ab96887a78 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -28,6 +28,7 @@ import { } from '@backstage/core-plugin-api'; import { RenderResult } from '@testing-library/react'; import { renderWithEffects } from './testingLibrary'; +import { defaultApis } from './defaultApis'; import { mockApis } from './mockApis'; const mockIcons = { @@ -117,6 +118,7 @@ export function wrapInTestApp( const app = createSpecializedApp({ apis: mockApis, + defaultApis, // Bit of a hack to make sure that the default config loader isn't used // as that would force every single test to wait for config loading. configLoader: false as unknown as undefined, diff --git a/packages/test-utils/src/testUtils/defaultApis.ts b/packages/test-utils/src/testUtils/defaultApis.ts new file mode 100644 index 0000000000..f06e1ba6b7 --- /dev/null +++ b/packages/test-utils/src/testUtils/defaultApis.ts @@ -0,0 +1,264 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AlertApiForwarder, + NoOpAnalyticsApi, + ErrorApiForwarder, + ErrorAlerter, + GoogleAuth, + GithubAuth, + OAuth2, + OktaAuth, + GitlabAuth, + Auth0Auth, + MicrosoftAuth, + BitbucketAuth, + OAuthRequestManager, + WebStorage, + UrlPatternDiscovery, + SamlAuth, + OneLoginAuth, + UnhandledErrorForwarder, + AtlassianAuth, +} from '@backstage/core-app-api'; + +import { + createApiFactory, + alertApiRef, + analyticsApiRef, + errorApiRef, + discoveryApiRef, + oauthRequestApiRef, + googleAuthApiRef, + githubAuthApiRef, + oauth2ApiRef, + oktaAuthApiRef, + gitlabAuthApiRef, + auth0AuthApiRef, + microsoftAuthApiRef, + storageApiRef, + configApiRef, + samlAuthApiRef, + oneloginAuthApiRef, + oidcAuthApiRef, + bitbucketAuthApiRef, + atlassianAuthApiRef, +} from '@backstage/core-plugin-api'; + +// TODO(Rugvip): This is just a copy of the createApp default APIs for now, but +// we should clean up this list a bit move more things over to mocks. +export const defaultApis = [ + createApiFactory({ + api: discoveryApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => + UrlPatternDiscovery.compile( + `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`, + ), + }), + createApiFactory(alertApiRef, new AlertApiForwarder()), + createApiFactory(analyticsApiRef, new NoOpAnalyticsApi()), + createApiFactory({ + api: errorApiRef, + deps: { alertApi: alertApiRef }, + factory: ({ alertApi }) => { + const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder()); + UnhandledErrorForwarder.forward(errorApi, { hidden: false }); + return errorApi; + }, + }), + createApiFactory({ + api: storageApiRef, + deps: { errorApi: errorApiRef }, + factory: ({ errorApi }) => WebStorage.create({ errorApi }), + }), + createApiFactory(oauthRequestApiRef, new OAuthRequestManager()), + createApiFactory({ + api: googleAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GoogleAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: microsoftAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + MicrosoftAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: githubAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GithubAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['read:user'], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oktaAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OktaAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: gitlabAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GitlabAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: auth0AuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + Auth0Auth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oauth2ApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: samlAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, configApi }) => + SamlAuth.create({ + discoveryApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oneloginAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OneLoginAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oidcAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider: { + id: 'oidc', + title: 'Your Identity Provider', + icon: () => null, + }, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: bitbucketAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + BitbucketAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['team'], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: atlassianAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => { + return AtlassianAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), +]; From eb489db0c1169dcc356d39683eb096fcbe888535 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 17:31:15 +0100 Subject: [PATCH 081/181] app-defaults: lazier api initialization to work around import cycle Signed-off-by: Patrik Oldsberg --- packages/app-defaults/src/defaults/apis.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 85c004eece..d3c76d52bd 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -68,8 +68,16 @@ export const apis = [ `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`, ), }), - createApiFactory(alertApiRef, new AlertApiForwarder()), - createApiFactory(analyticsApiRef, new NoOpAnalyticsApi()), + createApiFactory({ + api: alertApiRef, + deps: {}, + factory: () => new AlertApiForwarder(), + }), + createApiFactory({ + api: analyticsApiRef, + deps: {}, + factory: () => new NoOpAnalyticsApi(), + }), createApiFactory({ api: errorApiRef, deps: { alertApi: alertApiRef }, @@ -84,7 +92,11 @@ export const apis = [ deps: { errorApi: errorApiRef }, factory: ({ errorApi }) => WebStorage.create({ errorApi }), }), - createApiFactory(oauthRequestApiRef, new OAuthRequestManager()), + createApiFactory({ + api: oauthRequestApiRef, + deps: {}, + factory: () => new OAuthRequestManager(), + }), createApiFactory({ api: googleAuthApiRef, deps: { From 1be02e09dc73fe1e747101d9bf802816cd9e762c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 18:20:47 +0100 Subject: [PATCH 082/181] core-app-api: fix AppManager tests Signed-off-by: Patrik Oldsberg --- .../core-app-api/src/app/AppManager.test.tsx | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 71df5219d1..07401660e8 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -20,7 +20,6 @@ import { renderWithEffects, withLogCollector, } from '@backstage/test-utils'; -import { lightTheme } from '@backstage/theme'; import { render, screen } from '@testing-library/react'; import React, { PropsWithChildren } from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; @@ -127,7 +126,7 @@ describe('Integration Test', () => { const HiddenComponent = plugin2.provide( createRoutableExtension({ name: 'HiddenComponent', - component: () => Promise.resolve((_: { path?: string }) =>
), + component: () => Promise.resolve(() =>
), mountPoint: plugin2RouteRef, }), ); @@ -136,7 +135,7 @@ describe('Integration Test', () => { createRoutableExtension({ name: 'ExposedComponent', component: () => - Promise.resolve((_: PropsWithChildren<{ path?: string }>) => { + Promise.resolve(() => { const link1 = useRouteRef(plugin1RouteRef); const link2 = useRouteRef(plugin2RouteRef); const subLink1 = useRouteRef(subRouteRef1); @@ -197,12 +196,13 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], icons, plugins: [], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -220,8 +220,8 @@ describe('Integration Test', () => { - - + } /> + } /> , @@ -251,12 +251,13 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], icons, plugins: [], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -272,8 +273,8 @@ describe('Integration Test', () => { - - + } /> + } /> , @@ -308,7 +309,7 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], icons, @@ -319,6 +320,7 @@ describe('Integration Test', () => { }), ], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -334,8 +336,8 @@ describe('Integration Test', () => { - - + } /> + } /> , @@ -358,12 +360,13 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], icons, plugins: [], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -380,7 +383,7 @@ describe('Integration Test', () => { } /> - } /> + } /> , @@ -418,12 +421,13 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], icons, plugins: [], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -440,9 +444,9 @@ describe('Integration Test', () => { - - - + }> + } /> + , From 46570fecbc1076cf9b2d1c13241c05924e2c20d6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 18:50:22 +0100 Subject: [PATCH 083/181] test-utils: migrate theme usage and switch to actual MUI icons Signed-off-by: Patrik Oldsberg --- packages/test-utils/package.json | 1 + .../test-utils/src/testUtils/appWrappers.tsx | 51 +++++++++++-------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index ab78eab683..6f5f78de8c 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -34,6 +34,7 @@ "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.11.2", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index ab96887a78..cad1df4ecc 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -18,6 +18,9 @@ import React, { ComponentType, ReactNode, ReactElement } from 'react'; import { MemoryRouter } from 'react-router'; import { Route } from 'react-router-dom'; import { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core/styles'; +import { CssBaseline } from '@material-ui/core'; +import MockIcon from '@material-ui/icons/AcUnit'; import { createSpecializedApp } from '@backstage/core-app-api'; import { BootErrorPageProps, @@ -32,28 +35,28 @@ import { defaultApis } from './defaultApis'; import { mockApis } from './mockApis'; const mockIcons = { - 'kind:api': () => , - 'kind:component': () => , - 'kind:domain': () => , - 'kind:group': () => , - 'kind:location': () => , - 'kind:system': () => , - 'kind:user': () => , + 'kind:api': MockIcon, + 'kind:component': MockIcon, + 'kind:domain': MockIcon, + 'kind:group': MockIcon, + 'kind:location': MockIcon, + 'kind:system': MockIcon, + 'kind:user': MockIcon, - brokenImage: () => , - catalog: () => , - scaffolder: () => , - techdocs: () => , - search: () => , - chat: () => , - dashboard: () => , - docs: () => , - email: () => , - github: () => , - group: () => , - help: () => , - user: () => , - warning: () => , + brokenImage: MockIcon, + catalog: MockIcon, + scaffolder: MockIcon, + techdocs: MockIcon, + search: MockIcon, + chat: MockIcon, + dashboard: MockIcon, + docs: MockIcon, + email: MockIcon, + github: MockIcon, + group: MockIcon, + help: MockIcon, + user: MockIcon, + warning: MockIcon, }; const ErrorBoundaryFallback = ({ error }: { error: Error }) => { @@ -136,9 +139,13 @@ export function wrapInTestApp( themes: [ { id: 'light', - theme: lightTheme, title: 'Test App Theme', variant: 'light', + Provider: ({ children }) => ( + + {children} + + ), }, ], bindRoutes: ({ bind }) => { From b85ccce84df919c10d01cacce536211bb3e46a02 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Nov 2021 17:28:52 +0100 Subject: [PATCH 084/181] app-defaults: update internal deps Signed-off-by: Patrik Oldsberg --- packages/app-defaults/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 76101f0ad4..9438966b3b 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -29,18 +29,18 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.2", - "@backstage/core-app-api": "^0.1.19", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-app-api": "^0.1.20", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "react": "^16.12.0", "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/test-utils": "^0.1.20", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@types/jest": "^26.0.7", From 25f637f39f5066b2c7d3bc21118f712b7ac2a974 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Nov 2021 18:15:39 +0100 Subject: [PATCH 085/181] cli: make sure JSS styles receive the highest priority Signed-off-by: Patrik Oldsberg --- .changeset/moody-snails-admire.md | 5 ++++ packages/cli/src/lib/bundler/transforms.ts | 27 +++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .changeset/moody-snails-admire.md diff --git a/.changeset/moody-snails-admire.md b/.changeset/moody-snails-admire.md new file mode 100644 index 0000000000..7622546989 --- /dev/null +++ b/.changeset/moody-snails-admire.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': minor +--- + +Tweaked style insertion logic to make sure that JSS stylesheets always receive the highest priority. diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index cf9e873a66..863728b8f8 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -32,6 +32,23 @@ export const transforms = (options: TransformOptions): Transforms => { const extraTransforms = isDev ? ['react-hot-loader'] : []; + // This ensures that styles inserted from the style-loader and any + // async style chunks are always given lower priority than JSS styles. + // Note that this function is stringified and executed in the browser + // after transpilation, so stick to simple syntax + function insertBeforeJssStyles(element: any) { + const head = document.head; + // This makes sure that any style elements we insert get put before the + // dynamic styles from JSS, such as the ones from `makeStyles()`. + // TODO(Rugvip): This will likely break in material-ui v5, keep an eye on it. + const firstJssNode = head.querySelector('style[data-jss]'); + if (!firstJssNode) { + head.appendChild(element); + } else { + head.insertBefore(element, firstJssNode); + } + } + const loaders = [ { test: /\.(tsx?)$/, @@ -112,7 +129,14 @@ export const transforms = (options: TransformOptions): Transforms => { { test: /\.css$/i, use: [ - isDev ? require.resolve('style-loader') : MiniCssExtractPlugin.loader, + isDev + ? { + loader: require.resolve('style-loader'), + options: { + insert: insertBeforeJssStyles, + }, + } + : MiniCssExtractPlugin.loader, { loader: require.resolve('css-loader'), options: { @@ -132,6 +156,7 @@ export const transforms = (options: TransformOptions): Transforms => { new MiniCssExtractPlugin({ filename: 'static/[name].[contenthash:8].css', chunkFilename: 'static/[name].[id].[contenthash:8].css', + insert: insertBeforeJssStyles, // Only applies to async chunks }), ); } From 2163e83fa22e12b2f1727b6b9cbc49c6bd42028e Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Thu, 28 Oct 2021 19:57:23 -0400 Subject: [PATCH 086/181] Refactor create-app tasks and introduce regression tests - Tasks were moved from the script entrypoint to the lib directory - Doc comments were added for each task function defined in `src/lib/tasks.ts` - The `yarn test` script was added to package.json - Unit tests were written for each task -- verying file operations using fs-mock Signed-off-by: Colton Padden --- .changeset/many-sloths-cross.md | 5 + packages/create-app/package.json | 2 + packages/create-app/src/createApp.ts | 92 ++-------- packages/create-app/src/lib/tasks.test.ts | 208 ++++++++++++++++++++++ packages/create-app/src/lib/tasks.ts | 114 +++++++++++- 5 files changed, 342 insertions(+), 79 deletions(-) create mode 100644 .changeset/many-sloths-cross.md create mode 100644 packages/create-app/src/lib/tasks.test.ts diff --git a/.changeset/many-sloths-cross.md b/.changeset/many-sloths-cross.md new file mode 100644 index 0000000000..07cb6d2f61 --- /dev/null +++ b/.changeset/many-sloths-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Refactor and add regression tests for create-app tasks diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 2820d41ed6..7c1feec683 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -23,6 +23,7 @@ "scripts": { "build": "backstage-cli build --outputs cjs", "lint": "backstage-cli lint", + "test": "backstage-cli test", "clean": "backstage-cli clean", "start": "nodemon --" }, @@ -40,6 +41,7 @@ "@types/fs-extra": "^9.0.1", "@types/inquirer": "^7.3.1", "@types/recursive-readdir": "^2.2.0", + "mock-fs": "^5.1.1", "ts-node": "^10.0.0" }, "peerDependencies": { diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index d9719742fb..8823e6c858 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -14,85 +14,21 @@ * limitations under the License. */ -import fs from 'fs-extra'; -import { promisify } from 'util'; import chalk from 'chalk'; import { Command } from 'commander'; import inquirer, { Answers, Question } from 'inquirer'; -import { exec as execCb } from 'child_process'; import { resolve as resolvePath } from 'path'; import { findPaths } from '@backstage/cli-common'; import os from 'os'; -import { Task, templatingTask } from './lib/tasks'; - -const exec = promisify(execCb); - -async function checkAppExists(rootDir: string, name: string) { - await Task.forItem('checking', name, async () => { - const destination = resolvePath(rootDir, name); - - if (await fs.pathExists(destination)) { - const existing = chalk.cyan(destination.replace(`${rootDir}/`, '')); - throw new Error( - `A directory with the same name already exists: ${existing}\nPlease try again with a different app name`, - ); - } - }); -} - -async function checkPathExists(path: string) { - await Task.forItem('checking', path, async () => { - try { - await fs.mkdirs(path); - } catch (error) { - // will fail if a file already exists at given `path` - throw new Error(`Failed to create app directory: ${error.message}`); - } - }); -} - -async function createTemporaryAppFolder(tempDir: string) { - await Task.forItem('creating', 'temporary directory', async () => { - try { - await fs.mkdir(tempDir); - } catch (error) { - throw new Error(`Failed to create temporary app directory, ${error}`); - } - }); -} - -async function buildApp(appDir: string) { - const runCmd = async (cmd: string) => { - await Task.forItem('executing', cmd, async () => { - process.chdir(appDir); - - await exec(cmd).catch(error => { - process.stdout.write(error.stderr); - process.stdout.write(error.stdout); - throw new Error(`Could not execute command ${chalk.cyan(cmd)}`); - }); - }); - }; - - await runCmd('yarn install'); - await runCmd('yarn tsc'); -} - -async function moveApp(tempDir: string, destination: string, id: string) { - await Task.forItem('moving', id, async () => { - await fs - .move(tempDir, destination) - .catch(error => { - throw new Error( - `Failed to move app from ${tempDir} to ${destination}: ${error.message}`, - ); - }) - .finally(() => { - // remove temporary files on both success and failure - fs.removeSync(tempDir); - }); - }); -} +import { + Task, + buildAppTask, + checkAppExistsTask, + checkPathExistsTask, + createTemporaryAppFolderTask, + moveAppTask, + templatingTask, +} from './lib/tasks'; export default async (cmd: Command): Promise => { /* eslint-disable-next-line no-restricted-syntax */ @@ -143,7 +79,7 @@ export default async (cmd: Command): Promise => { // Template directly to specified path Task.section('Checking that supplied path exists'); - await checkPathExists(appDir); + await checkPathExistsTask(appDir); Task.section('Preparing files'); await templatingTask(templateDir, cmd.path, answers); @@ -151,21 +87,21 @@ export default async (cmd: Command): Promise => { // Template to temporary location, and then move files Task.section('Checking if the directory is available'); - await checkAppExists(paths.targetDir, answers.name); + await checkAppExistsTask(paths.targetDir, answers.name); Task.section('Creating a temporary app directory'); - await createTemporaryAppFolder(tempDir); + await createTemporaryAppFolderTask(tempDir); Task.section('Preparing files'); await templatingTask(templateDir, tempDir, answers); Task.section('Moving to final location'); - await moveApp(tempDir, appDir, answers.name); + await moveAppTask(tempDir, appDir, answers.name); } if (!cmd.skipInstall) { Task.section('Building the app'); - await buildApp(appDir); + await buildAppTask(appDir); } Task.log(); diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts new file mode 100644 index 0000000000..0abbb8e0a2 --- /dev/null +++ b/packages/create-app/src/lib/tasks.test.ts @@ -0,0 +1,208 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import child_process, { ChildProcess } from 'child_process'; +import path from 'path'; +import { + buildAppTask, + checkAppExistsTask, + checkPathExistsTask, + createTemporaryAppFolderTask, + moveAppTask, + templatingTask, +} from './tasks'; + +jest.mock('child_process'); + +beforeEach(() => { + mockFs({ + 'projects/my-module.ts': '', + 'projects/dir/my-file.txt': '', + 'tmp/mockApp/.gitignore': '', + 'tmp/mockApp/package.json': '', + 'tmp/mockApp/packages/app/package.json': '', + // load templates into mock filesystem + 'templates/': mockFs.load(path.resolve(__dirname, '../../templates/')), + }); +}); + +afterEach(() => { + mockFs.restore(); +}); + +describe('checkAppExistsTask', () => { + it('should do nothing if the directory does not exist', async () => { + const dir = 'projects/'; + const name = 'MyNewApp'; + await expect(checkAppExistsTask(dir, name)).resolves.not.toThrow(); + }); + + it('should throw an error when a file of the same name exists', async () => { + const dir = 'projects/'; + const name = 'my-module.ts'; + await expect(checkAppExistsTask(dir, name)).rejects.toThrow( + 'already exists', + ); + }); + + it('should throw an error when a directory of the same name exists', async () => { + const dir = 'projects/'; + const name = 'dir'; + await expect(checkAppExistsTask(dir, name)).rejects.toThrow( + 'already exists', + ); + }); +}); + +describe('checkPathExistsTask', () => { + it('should create a directory at the given path', async () => { + const appDir = 'projects/newProject'; + await expect(checkPathExistsTask(appDir)).resolves.not.toThrow(); + expect(fs.existsSync(appDir)).toBe(true); + }); + + it('should do nothing if a directory of the same name exists', async () => { + const appDir = 'projects/dir'; + await expect(checkPathExistsTask(appDir)).resolves.not.toThrow(); + expect(fs.existsSync(appDir)).toBe(true); + }); + + it('should fail if a file of the same name exists', async () => { + await expect(checkPathExistsTask('projects/my-module.ts')).rejects.toThrow( + 'already exists', + ); + }); +}); + +describe('createTemporaryAppFolderTask', () => { + it('should create a directory at a given path', async () => { + const tempDir = 'projects/tmpFolder'; + await expect(createTemporaryAppFolderTask(tempDir)).resolves.not.toThrow(); + expect(fs.existsSync(tempDir)).toBe(true); + }); + + it('should fail if a directory of the same name exists', async () => { + const tempDir = 'projects/dir'; + await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow( + 'file already exists', + ); + }); + + it('should fail if a file of the same name exists', async () => { + const tempDir = 'projects/dir/my-file.txt'; + await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow( + 'file already exists', + ); + }); +}); + +describe('buildAppTask', () => { + it('should change to `appDir` and run `yarn install` and `yarn tsc`', async () => { + const mockChdir = jest.spyOn(process, 'chdir'); + const mockExec = jest.spyOn(child_process, 'exec'); + + // requires callback implementation to support `promisify` wrapper + // https://stackoverflow.com/a/60579617/10044859 + mockExec.mockImplementation((_: string, callback?: any): ChildProcess => { + callback(null, 'stdout', 'stderr'); + return; + }); + + const appDir = 'projects/dir'; + await expect(buildAppTask(appDir)).resolves.not.toThrow(); + + expect(mockChdir).toBeCalledTimes(2); + expect(mockChdir).toHaveBeenNthCalledWith(1, appDir); + expect(mockChdir).toHaveBeenNthCalledWith(2, appDir); + + expect(mockExec).toBeCalledTimes(2); + expect(mockExec).toHaveBeenNthCalledWith( + 1, + 'yarn install', + expect.any(Function), + ); + expect(mockExec).toHaveBeenNthCalledWith( + 2, + 'yarn tsc', + expect.any(Function), + ); + }); + + it('should fail if project directory does not exist', async () => { + const appDir = 'projects/missingProject'; + await expect(buildAppTask(appDir)).rejects.toThrow( + 'no such file or directory', + ); + }); +}); + +describe('moveAppTask', () => { + const tempDir = 'tmp/mockApp/'; + const id = 'myApp'; + + it('should move all files in the temp dir to the target dir', async () => { + const destination = 'projects/mockApp'; + await moveAppTask(tempDir, destination, id); + expect(fs.existsSync('projects/mockApp/.gitignore')).toBe(true); + expect(fs.existsSync('projects/mockApp/package.json')).toBe(true); + expect(fs.existsSync('projects/mockApp/packages/app/package.json')).toBe( + true, + ); + }); + + it('should fail to move files if destination already exists', async () => { + const destination = 'projects'; + await expect(moveAppTask(tempDir, destination, id)).rejects.toThrow( + 'dest already exists', + ); + }); + + it('should remove temporary files if move succeeded', async () => { + const destination = 'projects/mockApp'; + await moveAppTask(tempDir, destination, id); + expect(fs.existsSync('tmp/mockApp')).toBe(false); + }); + + it('should remove temporary files if move failed', async () => { + const destination = 'projects'; + await expect(moveAppTask(tempDir, destination, id)).rejects.toThrow(); + expect(fs.existsSync('tmp/mockApp')).toBe(false); + }); +}); + +describe('templatingTask', () => { + it('should generate a project populating context parameters', async () => { + const templateDir = 'templates/default-app'; + const destinationDir = 'templatedApp'; + const context = { + name: 'SuperCoolBackstageInstance', + dbTypeSqlite: true, + }; + await templatingTask(templateDir, destinationDir, context); + expect(fs.existsSync('templatedApp/package.json')).toBe(true); + expect(fs.existsSync('templatedApp/.dockerignore')).toBe(true); + // catalog was populated with `context.name` + expect( + fs.readFileSync('templatedApp/catalog-info.yaml', 'utf-8'), + ).toContain('name: SuperCoolBackstageInstance'); + // backend dependencies include `sqlite3` from `context.SQLite` + expect( + fs.readFileSync('templatedApp/packages/backend/package.json', 'utf-8'), + ).toContain('"sqlite3"'); + }); +}); diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 19f0ed66b8..0b3ca63728 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -18,11 +18,14 @@ import chalk from 'chalk'; import fs from 'fs-extra'; import handlebars from 'handlebars'; import ora from 'ora'; -import { basename, dirname } from 'path'; import recursive from 'recursive-readdir'; +import { basename, dirname, resolve as resolvePath } from 'path'; +import { exec as execCb } from 'child_process'; import { packageVersions } from './versions'; +import { promisify } from 'util'; const TASK_NAME_MAX_LENGTH = 14; +const exec = promisify(execCb); export class Task { static log(name: string = '') { @@ -65,6 +68,13 @@ export class Task { } } +/** + * Generate a templated backstage project + * + * @param templateDir - location containing template files + * @param destinationDir - location to save templated project + * @param context - template parameters + */ export async function templatingTask( templateDir: string, destinationDir: string, @@ -116,3 +126,105 @@ export async function templatingTask( } } } + +/** + * Verify that application target does not already exist + * + * @param rootDir - The directory to create application folder `name` + * @param name - The specified name of the application + * @Throws Error - If directory with name of `destination` already exists + */ +export async function checkAppExistsTask(rootDir: string, name: string) { + await Task.forItem('checking', name, async () => { + const destination = resolvePath(rootDir, name); + + if (await fs.pathExists(destination)) { + const existing = chalk.cyan(destination.replace(`${rootDir}/`, '')); + throw new Error( + `A directory with the same name already exists: ${existing}\nPlease try again with a different app name`, + ); + } + }); +} + +/** + * Verify that application `path` exists, otherwise create the directory + * + * @param {string} path - target to create directory + * @throws {Error} if `path` is a file, or `fs.mkdir` fails + */ +export async function checkPathExistsTask(path: string) { + await Task.forItem('checking', path, async () => { + try { + await fs.mkdirs(path); + } catch (error) { + // will fail if a file already exists at given `path` + throw new Error(`Failed to create app directory: ${error.message}`); + } + }); +} + +/** + * Create a folder to store templated files + * + * @param {string} tempDir - target temporary directory + * @throws {Error} if `fs.mkdir` fails + */ +export async function createTemporaryAppFolderTask(tempDir: string) { + await Task.forItem('creating', 'temporary directory', async () => { + try { + await fs.mkdir(tempDir); + } catch (error) { + throw new Error(`Failed to create temporary app directory, ${error}`); + } + }); +} + +/** + * Run `yarn install` and `run tsc` in application directory + * + * @param {string} appDir - location of application to build + */ +export async function buildAppTask(appDir: string) { + const runCmd = async (cmd: string) => { + await Task.forItem('executing', cmd, async () => { + process.chdir(appDir); + await exec(cmd).catch(error => { + process.stdout.write(error.stderr); + process.stdout.write(error.stdout); + throw new Error(`Could not execute command ${chalk.cyan(cmd)}`); + }); + }); + }; + + await runCmd('yarn install'); + await runCmd('yarn tsc'); +} + +/** + * Move temporary directory to destination application folder + * + * @param {string} tempDir source path to copy files from + * @param {string} destination target path to copy files + * @param {string} id + * @throws {Error} if `fs.move` fails + */ +export async function moveAppTask( + tempDir: string, + destination: string, + id: string, +) { + await Task.forItem('moving', id, async () => { + await fs + .move(tempDir, destination) + .catch(error => { + throw new Error( + `Failed to move app from ${tempDir} to ${destination}: ${error.message}`, + ); + }) + .finally(() => { + // remove temporary files on both success and failure + fs.removeSync(tempDir); + }); + }); +} From 1d666cb67e28900c3ada3d1937708df7cdd1f1df Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Fri, 29 Oct 2021 10:44:32 -0400 Subject: [PATCH 087/181] Add regression tests to createApp entrypoint Ensure that appropriate tasks are called depending on inquirer prompt answers, and command-line argument options (--path, and --skip-install). Signed-off-by: Colton Padden --- packages/create-app/src/createApp.test.ts | 70 +++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 packages/create-app/src/createApp.test.ts diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts new file mode 100644 index 0000000000..b2d946f307 --- /dev/null +++ b/packages/create-app/src/createApp.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import inquirer from 'inquirer'; +import { Command } from 'commander'; +import * as tasks from './lib/tasks'; +import createApp from './createApp'; + +jest.mock('./lib/tasks'); + +const promptMock = jest.spyOn(inquirer, 'prompt'); +const checkPathExistsMock = jest.spyOn(tasks, 'checkPathExistsTask'); +const templatingMock = jest.spyOn(tasks, 'templatingTask'); +const checkAppExistsMock = jest.spyOn(tasks, 'checkAppExistsTask'); +const createTemporaryAppFolderMock = jest.spyOn( + tasks, + 'createTemporaryAppFolderTask', +); +const moveAppMock = jest.spyOn(tasks, 'moveAppTask'); +const buildAppMock = jest.spyOn(tasks, 'buildAppTask'); + +describe('command entrypoint', () => { + beforeEach(() => { + promptMock.mockResolvedValueOnce({ + name: 'MyApp', + dbType: 'PostgreSQL', + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('should call expected tasks with no path option', async () => { + const cmd = {} as unknown as Command; + await createApp(cmd); + expect(checkAppExistsMock).toHaveBeenCalled(); + expect(createTemporaryAppFolderMock).toHaveBeenCalled(); + expect(templatingMock).toHaveBeenCalled(); + expect(moveAppMock).toHaveBeenCalled(); + expect(buildAppMock).toHaveBeenCalled(); + }); + + it('should call expected tasks with path option', async () => { + const cmd = { path: 'myDirectory' } as unknown as Command; + await createApp(cmd); + expect(checkPathExistsMock).toHaveBeenCalled(); + expect(templatingMock).toHaveBeenCalled(); + expect(buildAppMock).toHaveBeenCalled(); + }); + + it('should not call `buildAppTask` when `skipInstall` is supplied', async () => { + const cmd = { skipInstall: true } as unknown as Command; + await createApp(cmd); + expect(buildAppMock).not.toHaveBeenCalled(); + }); +}); From bd90ded8b7d93c140ade4802cdacda9ccc22a465 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Fri, 29 Oct 2021 11:32:23 -0400 Subject: [PATCH 088/181] Add type annotations for mocked child_process.exec Signed-off-by: Colton Padden --- packages/create-app/src/lib/tasks.test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 0abbb8e0a2..d69526cb49 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import child_process, { ChildProcess } from 'child_process'; +import child_process from 'child_process'; import path from 'path'; import { buildAppTask, @@ -114,22 +114,24 @@ describe('createTemporaryAppFolderTask', () => { describe('buildAppTask', () => { it('should change to `appDir` and run `yarn install` and `yarn tsc`', async () => { const mockChdir = jest.spyOn(process, 'chdir'); - const mockExec = jest.spyOn(child_process, 'exec'); + const mockExec = child_process.exec as unknown as jest.MockedFunction< + ( + command: string, + callback: (error: null, stdout: string, stderr: string) => void, + ) => void + >; // requires callback implementation to support `promisify` wrapper // https://stackoverflow.com/a/60579617/10044859 - mockExec.mockImplementation((_: string, callback?: any): ChildProcess => { - callback(null, 'stdout', 'stderr'); - return; + mockExec.mockImplementation((_command, callback) => { + callback(null, 'standard out', 'standard error'); }); const appDir = 'projects/dir'; await expect(buildAppTask(appDir)).resolves.not.toThrow(); - expect(mockChdir).toBeCalledTimes(2); expect(mockChdir).toHaveBeenNthCalledWith(1, appDir); expect(mockChdir).toHaveBeenNthCalledWith(2, appDir); - expect(mockExec).toBeCalledTimes(2); expect(mockExec).toHaveBeenNthCalledWith( 1, From 1b3fab7193b7b6c7bda547c12ed763ac144f210b Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Fri, 29 Oct 2021 11:45:01 -0400 Subject: [PATCH 089/181] Use mock-fs during createApp to prevent filesystem modifications Signed-off-by: Colton Padden --- packages/create-app/src/createApp.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index b2d946f307..7b3b8f124c 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -15,12 +15,25 @@ */ import inquirer from 'inquirer'; +import mockFs from 'mock-fs'; +import path from 'path'; import { Command } from 'commander'; import * as tasks from './lib/tasks'; import createApp from './createApp'; jest.mock('./lib/tasks'); +beforeAll(() => { + mockFs({ + 'package.json': '', // required by `findPaths(__dirname)` + 'templates/': mockFs.load(path.resolve(__dirname, '../templates/')), + }); +}); + +afterAll(() => { + mockFs.restore(); +}); + const promptMock = jest.spyOn(inquirer, 'prompt'); const checkPathExistsMock = jest.spyOn(tasks, 'checkPathExistsTask'); const templatingMock = jest.spyOn(tasks, 'templatingTask'); From 9010cdc4d718f6ab46c88681b9e8e5188117a08d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 11 Nov 2021 15:48:26 +0100 Subject: [PATCH 090/181] add snyk ignores for the node-gyp tar vuln MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-test-utils/.snyk | 47 +++++++++++++++++++++++++++++++ packages/backend/.snyk | 47 +++++++++++++++++++++++++++++++ plugins/catalog-backend/.snyk | 47 +++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 packages/backend-test-utils/.snyk create mode 100644 packages/backend/.snyk create mode 100644 plugins/catalog-backend/.snyk diff --git a/packages/backend-test-utils/.snyk b/packages/backend-test-utils/.snyk new file mode 100644 index 0000000000..ad62599e66 --- /dev/null +++ b/packages/backend-test-utils/.snyk @@ -0,0 +1,47 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.22.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-TAR-1579155: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1579152: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1579147: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536758: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536531: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536528: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z +patch: {} diff --git a/packages/backend/.snyk b/packages/backend/.snyk new file mode 100644 index 0000000000..ad62599e66 --- /dev/null +++ b/packages/backend/.snyk @@ -0,0 +1,47 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.22.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-TAR-1579155: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1579152: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1579147: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536758: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536531: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536528: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z +patch: {} diff --git a/plugins/catalog-backend/.snyk b/plugins/catalog-backend/.snyk new file mode 100644 index 0000000000..ad62599e66 --- /dev/null +++ b/plugins/catalog-backend/.snyk @@ -0,0 +1,47 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.22.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-TAR-1579155: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1579152: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1579147: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536758: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536531: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536528: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z +patch: {} From a8732a1200bc4dfa90de84841f98e7a0d057fd2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 11 Nov 2021 11:58:33 +0100 Subject: [PATCH 091/181] Make sure that the server builder propagates init errors properly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/rich-pillows-cough.md | 12 ++++++ .../src/service/lib/ServiceBuilderImpl.ts | 38 ++++++++++--------- packages/backend/src/index.ts | 2 +- 3 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 .changeset/rich-pillows-cough.md diff --git a/.changeset/rich-pillows-cough.md b/.changeset/rich-pillows-cough.md new file mode 100644 index 0000000000..959eb69214 --- /dev/null +++ b/.changeset/rich-pillows-cough.md @@ -0,0 +1,12 @@ +--- +'@backstage/backend-common': patch +--- + +Make sure that server builder `start()` propagates errors (such as failing to bind to the required port) properly and doesn't resolve the promise prematurely. + +After this change, the backend logger will be able to actually capture the error as it happens: + +``` +2021-11-11T10:54:21.334Z backstage info Initializing http server +2021-11-11T10:54:21.335Z backstage error listen EADDRINUSE: address already in use :::7000 code=EADDRINUSE errno=-48 syscall=listen address=:: port=7000 +``` diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index ed16c9c9a7..a50fed9e93 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -176,25 +176,29 @@ export class ServiceBuilderImpl implements ServiceBuilder { : createHttpServer(app, logger); return new Promise((resolve, reject) => { - app.on('error', e => { - logger.error(`Failed to start up on port ${port}, ${e}`); + function handleStartupError(e: unknown) { + server.close(); reject(e); + } + + app.on('error', handleStartupError); + server.on('error', handleStartupError); + + server.listen(port, host, () => { + app.off('error', handleStartupError); + server.off('error', handleStartupError); + + const stoppableServer = stoppable(server, 0); + + useHotCleanup(this.module, () => + stoppableServer.stop((e: any) => { + if (e) console.error(e); + }), + ); + + logger.info(`Listening on ${host}:${port}`); + resolve(stoppableServer); }); - - const stoppableServer = stoppable( - server.listen(port, host, () => { - logger.info(`Listening on ${host}:${port}`); - }), - 0, - ); - - useHotCleanup(this.module, () => - stoppableServer.stop((e: any) => { - if (e) console.error(e); - }), - ); - - resolve(stoppableServer); }); } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index ffdce949b7..f3e4547e16 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -140,7 +140,7 @@ async function main() { await service.start().catch(err => { logger.error(err); - process.exit(1); + throw err; }); } From 6f276707e37187a45a875709f349369e3ce7aa16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 11 Nov 2021 20:06:45 +0100 Subject: [PATCH 092/181] review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../src/service/lib/ServiceBuilderImpl.ts | 18 +++++++----------- packages/backend/src/index.ts | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index a50fed9e93..885baba54b 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -174,6 +174,13 @@ export class ServiceBuilderImpl implements ServiceBuilder { const server: http.Server = httpsSettings ? await createHttpsServer(app, httpsSettings, logger) : createHttpServer(app, logger); + const stoppableServer = stoppable(server, 0); + + useHotCleanup(this.module, () => + stoppableServer.stop((e: any) => { + if (e) console.error(e); + }), + ); return new Promise((resolve, reject) => { function handleStartupError(e: unknown) { @@ -181,21 +188,10 @@ export class ServiceBuilderImpl implements ServiceBuilder { reject(e); } - app.on('error', handleStartupError); server.on('error', handleStartupError); server.listen(port, host, () => { - app.off('error', handleStartupError); server.off('error', handleStartupError); - - const stoppableServer = stoppable(server, 0); - - useHotCleanup(this.module, () => - stoppableServer.stop((e: any) => { - if (e) console.error(e); - }), - ); - logger.info(`Listening on ${host}:${port}`); resolve(stoppableServer); }); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index f3e4547e16..ffdce949b7 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -140,7 +140,7 @@ async function main() { await service.start().catch(err => { logger.error(err); - throw err; + process.exit(1); }); } From d088c7f0a3d98ead6101f27d1c017e639e0c3c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 23 Aug 2021 15:29:03 +0200 Subject: [PATCH 093/181] Implement the locks part of the task manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-common/knexfile.js | 26 ++++ .../migrations/20210928160613_init.js | 71 +++++++++ packages/backend-common/package.json | 5 + .../src/database/DatabaseManager.ts | 5 +- .../src/database/migrateBackendCommon.ts | 30 ++++ .../backend-common/src/database/tables.ts | 22 +++ packages/backend-common/src/index.ts | 1 + .../src/tasks/TaskManager.test.ts | 73 +++++++++ .../backend-common/src/tasks/TaskManager.ts | 144 ++++++++++++++++++ packages/backend-common/src/tasks/index.ts | 18 +++ packages/backend-common/src/tasks/types.ts | 45 ++++++ packages/backend-common/src/tasks/util.ts | 26 ++++ packages/backend/src/index.ts | 31 ++-- packages/backend/src/types.ts | 6 +- yarn.lock | 5 + 15 files changed, 490 insertions(+), 18 deletions(-) create mode 100644 packages/backend-common/knexfile.js create mode 100644 packages/backend-common/migrations/20210928160613_init.js create mode 100644 packages/backend-common/src/database/migrateBackendCommon.ts create mode 100644 packages/backend-common/src/database/tables.ts create mode 100644 packages/backend-common/src/tasks/TaskManager.test.ts create mode 100644 packages/backend-common/src/tasks/TaskManager.ts create mode 100644 packages/backend-common/src/tasks/index.ts create mode 100644 packages/backend-common/src/tasks/types.ts create mode 100644 packages/backend-common/src/tasks/util.ts diff --git a/packages/backend-common/knexfile.js b/packages/backend-common/knexfile.js new file mode 100644 index 0000000000..4c8be42673 --- /dev/null +++ b/packages/backend-common/knexfile.js @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This file makes it possible to run "yarn knex migrate:make some_file_name" +// to assist in making new migrations +module.exports = { + client: 'sqlite3', + connection: ':memory:', + useNullAsDefault: true, + migrations: { + directory: './migrations', + }, +}; diff --git a/packages/backend-common/migrations/20210928160613_init.js b/packages/backend-common/migrations/20210928160613_init.js new file mode 100644 index 0000000000..663e2f8aeb --- /dev/null +++ b/packages/backend-common/migrations/20210928160613_init.js @@ -0,0 +1,71 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + // + // locking + // + await knex.schema.createTable( + 'backstage_backend_common__task_locks', + table => { + table.comment('Locks used for mutual exclusion among multiple workers'); + table + .text('id') + .primary() + .notNullable() + .comment('The unique id of this particular lock'); + table + .text('acquired_ticket') + .nullable() + .comment('A unique ticket for the current lock acquiral, if any'); + table + .dateTime('acquired_at') + .nullable() + .comment('The time when the lock was acquired, if locked'); + table + .dateTime('expires_at') + .nullable() + .comment('The time when an acquired lock will time out and expire'); + table.index('id', 'task_locks_id_idx'); + }, + ); + // + // tasks + // + await knex.schema.createTable('backstage_backend_common__tasks', table => { + table.comment('Tasks used for scheduling work on multiple workers'); + table + .text('id') + .primary() + .notNullable() + .comment('The unique id of this particular task'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('task_locks', table => { + table.dropIndex([], 'task_locks_id_idx'); + }); + await knex.schema.dropTable('task_locks'); +}; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 03ae1fbe62..c8e8d69ab5 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -59,14 +59,17 @@ "knex": "^0.95.1", "lodash": "^4.17.21", "logform": "^2.1.1", + "luxon": "^2.0.2", "minimatch": "^3.0.4", "minimist": "^1.2.5", "morgan": "^1.10.0", + "node-abort-controller": "^3.0.0", "raw-body": "^2.4.1", "selfsigned": "^1.10.7", "stoppable": "^1.1.0", "tar": "^6.1.2", "unzipper": "^0.10.11", + "uuid": "^8.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, @@ -79,6 +82,7 @@ } }, "devDependencies": { + "@backstage/backend-test-utils": "^0.1.8", "@backstage/cli": "^0.8.2", "@backstage/test-utils": "^0.1.21", "@types/archiver": "^5.1.0", @@ -107,6 +111,7 @@ }, "files": [ "dist", + "migrations/**/*.{js,d.ts}", "config.d.ts" ], "configSchema": "config.d.ts" diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index c0511c6b78..f8cdab1413 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -13,14 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Knex } from 'knex'; import { omit } from 'lodash'; import { Config, ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { createDatabaseClient, - ensureDatabaseExists, createNameOverride, + ensureDatabaseExists, normalizeConnection, } from './connection'; import { PluginDatabaseManager } from './types'; @@ -165,7 +166,7 @@ export class DatabaseManager { ); return { - // include base connection if client type has not been overriden + // include base connection if client type has not been overridden ...(overridden ? {} : baseConnection), ...connection, }; diff --git a/packages/backend-common/src/database/migrateBackendCommon.ts b/packages/backend-common/src/database/migrateBackendCommon.ts new file mode 100644 index 0000000000..6283f3a96c --- /dev/null +++ b/packages/backend-common/src/database/migrateBackendCommon.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Knex } from 'knex'; +import { resolvePackagePath } from '../paths'; + +const migrationsDir = resolvePackagePath( + '@backstage/backend-common', + 'migrations', +); + +export async function migrateBackendCommon(knex: Knex): Promise { + await knex.migrate.latest({ + directory: migrationsDir, + tableName: 'knex_migrations_backstage_backend_common', + }); +} diff --git a/packages/backend-common/src/database/tables.ts b/packages/backend-common/src/database/tables.ts new file mode 100644 index 0000000000..8efa393bb7 --- /dev/null +++ b/packages/backend-common/src/database/tables.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type DbTaskLocksRow = { + id: string; + acquired_ticket?: string; + acquired_at?: Date; + expires_at?: Date; +}; diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index c214961a2e..a906d7bc61 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -31,4 +31,5 @@ export * from './paths'; export * from './reading'; export * from './scm'; export * from './service'; +export * from './tasks'; export * from './util'; diff --git a/packages/backend-common/src/tasks/TaskManager.test.ts b/packages/backend-common/src/tasks/TaskManager.test.ts new file mode 100644 index 0000000000..da9adb70f6 --- /dev/null +++ b/packages/backend-common/src/tasks/TaskManager.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { Duration } from 'luxon'; +import { DatabaseManager } from '../database'; +import { TaskManager } from './TaskManager'; + +describe('TaskManager', () => { + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + + async function createDatabase( + databaseId: TestDatabaseId, + ): Promise { + const knex = await databases.init(databaseId); + const databaseManager: Partial = { + forPlugin: () => ({ + getClient: async () => knex, + }), + }; + return databaseManager as DatabaseManager; + } + + describe('locking', () => { + it.each(databases.eachSupportedId())( + 'can run the happy path, %p', + async databaseId => { + const database = await createDatabase(databaseId); + const manager = new TaskManager(database).forPlugin('test'); + + const lock1 = await manager.acquireLock('lock1', { + timeout: Duration.fromMillis(5000), + }); + const lock2 = await manager.acquireLock('lock2', { + timeout: Duration.fromMillis(5000), + }); + + expect(lock1.acquired).toBe(true); + expect(lock2.acquired).toBe(true); + + await expect( + manager.acquireLock('lock1', { + timeout: Duration.fromMillis(5000), + }), + ).resolves.toEqual({ acquired: false }); + + await (lock1 as any).release(); + await (lock2 as any).release(); + + const lock1Again = await manager.acquireLock('lock1', { + timeout: Duration.fromMillis(5000), + }); + expect(lock1Again.acquired).toBe(true); + await (lock1Again as any).release(); + }, + ); + }); +}); diff --git a/packages/backend-common/src/tasks/TaskManager.ts b/packages/backend-common/src/tasks/TaskManager.ts new file mode 100644 index 0000000000..29741e1594 --- /dev/null +++ b/packages/backend-common/src/tasks/TaskManager.ts @@ -0,0 +1,144 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { Knex } from 'knex'; +import { memoize } from 'lodash'; +import { Duration } from 'luxon'; +import { v4 as uuid } from 'uuid'; +import { DatabaseManager } from '../database'; +import { migrateBackendCommon } from '../database/migrateBackendCommon'; +import { DbTaskLocksRow } from '../database/tables'; +import { PluginTaskManager } from './types'; +import { validateId } from './util'; + +export class PluginTaskManagerImpl implements PluginTaskManager { + constructor( + private readonly pluginId: string, + private readonly databaseFactory: () => Promise, + ) {} + + async acquireLock( + idWithoutPrefix: string, + options: { + timeout: Duration; + }, + ): Promise< + | { acquired: false } + | { acquired: true; release: () => void | Promise } + > { + validateId(idWithoutPrefix); + + const knex = await this.databaseFactory(); + const id = `plugin:${this.pluginId}:${idWithoutPrefix}`; + const ticket = uuid(); + const timeout = options.timeout.as('seconds'); + + const release = async () => { + try { + await knex('backstage_backend_common__task_locks') + .where('id', '=', id) + .where('acquired_ticket', '=', ticket) + .delete(); + } catch { + // fail silently + } + }; + + // First try to overwrite an existing lock, that has timed out + const stolen = await knex( + 'backstage_backend_common__task_locks', + ) + .where('id', '=', id) + .whereNotNull('acquired_ticket') + .where('expires_at', '<', knex.fn.now()) + .update({ + acquired_ticket: ticket, + acquired_at: knex.fn.now(), + expires_at: + knex.client.config.client === 'sqlite3' + ? knex.raw(`datetime('now', ?)`, [`${timeout} seconds`]) + : knex.raw(`now() + interval '${timeout} seconds'`), + }); + + if (stolen) { + return { acquired: true, release }; + } + + try { + await knex('backstage_backend_common__task_locks').insert( + { + id, + acquired_ticket: ticket, + acquired_at: knex.fn.now(), + expires_at: + knex.client.config.client === 'sqlite3' + ? knex.raw(`datetime('now', ?)`, [`${timeout} seconds`]) + : knex.raw(`now() + interval '${timeout} seconds'`), + }, + ); + return { acquired: true, release }; + } catch { + return { acquired: false }; + } + } + + async scheduleTask( + idWithoutPrefix: string, + options: { + timeout: Duration; + frequency: Duration; + initialDelay?: Duration; + }, + fn: () => Promise, + ): Promise<{ release: () => Promise }> { + validateId(idWithoutPrefix); + + const knex = await this.databaseFactory(); + const id = `plugin:${this.pluginId}:${idWithoutPrefix}`; + + return {}; + } +} + +/** + * Deals with management and locking related to distributed tasks. + * + * @public + */ +export class TaskManager { + static fromConfig( + config: Config, + options?: { databaseManager?: DatabaseManager }, + ): TaskManager { + const databaseManager = + options?.databaseManager ?? DatabaseManager.fromConfig(config); + return new TaskManager(databaseManager); + } + + constructor(private readonly databaseManager: DatabaseManager) {} + + forPlugin(pluginId: string): PluginTaskManager { + return new PluginTaskManagerImpl( + pluginId, + memoize(async () => { + const knex = await this.databaseManager.forPlugin(pluginId).getClient(); + await migrateBackendCommon(knex); + return knex; + }), + ); + } +} diff --git a/packages/backend-common/src/tasks/index.ts b/packages/backend-common/src/tasks/index.ts new file mode 100644 index 0000000000..60329788b3 --- /dev/null +++ b/packages/backend-common/src/tasks/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { PluginTaskManager } from './types'; +export { TaskManager } from './TaskManager'; diff --git a/packages/backend-common/src/tasks/types.ts b/packages/backend-common/src/tasks/types.ts new file mode 100644 index 0000000000..4a4fcb474e --- /dev/null +++ b/packages/backend-common/src/tasks/types.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Duration } from 'luxon'; + +/** + * Deals with management and locking related to distributed tasks, for a given + * plugin. + * + * @public + */ +export interface PluginTaskManager { + acquireLock( + id: string, + options: { + timeout: Duration; + }, + ): Promise< + | { acquired: false } + | { acquired: true; release: () => void | Promise } + >; + + scheduleTask( + id: string, + options: { + timeout: Duration; + frequency: Duration; + initialDelay?: Duration; + }, + fn: () => Promise, + ): Promise<{ release: () => Promise }>; +} diff --git a/packages/backend-common/src/tasks/util.ts b/packages/backend-common/src/tasks/util.ts new file mode 100644 index 0000000000..7bd4ff61b5 --- /dev/null +++ b/packages/backend-common/src/tasks/util.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; + +// Keep the IDs compatible with e.g. Prometheus +export function validateId(id: string) { + if (typeof id !== 'string' || !/^[a-z0-9]+(?:_[a-z0-9]+)*$/.test(id)) { + throw new InputError( + `${id} is not a valid ID, expected string of lowercase characters and digits separated by underscores`, + ); + } +} diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index ffdce949b7..554c9a7d99 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -22,41 +22,42 @@ * Happy hacking! */ -import Router from 'express-promise-router'; import { CacheManager, createServiceBuilder, + DatabaseManager, getRootLogger, loadBackendConfig, notFoundHandler, - DatabaseManager, SingleHostDiscovery, + TaskManager, UrlReaders, useHotMemoize, } from '@backstage/backend-common'; import { Config } from '@backstage/config'; -import healthcheck from './plugins/healthcheck'; -import { metricsInit, metricsHandler } from './metrics'; +import Router from 'express-promise-router'; +import { metricsHandler, metricsInit } from './metrics'; +import app from './plugins/app'; import auth from './plugins/auth'; import azureDevOps from './plugins/azure-devops'; +import badges from './plugins/badges'; import catalog from './plugins/catalog'; import codeCoverage from './plugins/codecoverage'; -import kubernetes from './plugins/kubernetes'; +import graphql from './plugins/graphql'; +import healthcheck from './plugins/healthcheck'; +import jenkins from './plugins/jenkins'; import kafka from './plugins/kafka'; +import kubernetes from './plugins/kubernetes'; +import proxy from './plugins/proxy'; import rollbar from './plugins/rollbar'; import scaffolder from './plugins/scaffolder'; -import proxy from './plugins/proxy'; import search from './plugins/search'; import techdocs from './plugins/techdocs'; -import todo from './plugins/todo'; -import graphql from './plugins/graphql'; -import app from './plugins/app'; -import badges from './plugins/badges'; -import jenkins from './plugins/jenkins'; import techInsights from './plugins/techInsights'; +import todo from './plugins/todo'; import { PluginEnvironment } from './types'; -function makeCreateEnv(config: Config) { +async function makeCreateEnv(config: Config) { const root = getRootLogger(); const reader = UrlReaders.default({ logger: root, config }); const discovery = SingleHostDiscovery.fromConfig(config); @@ -64,13 +65,15 @@ function makeCreateEnv(config: Config) { root.info(`Created UrlReader ${reader}`); const databaseManager = DatabaseManager.fromConfig(config); + const taskManager = TaskManager.fromConfig(config); const cacheManager = CacheManager.fromConfig(config); return (plugin: string): PluginEnvironment => { const logger = root.child({ type: 'plugin', plugin }); const database = databaseManager.forPlugin(plugin); + const tasks = taskManager.forPlugin(plugin); const cache = cacheManager.forPlugin(plugin); - return { logger, cache, database, config, reader, discovery }; + return { logger, cache, database, tasks, config, reader, discovery }; }; } @@ -87,7 +90,7 @@ async function main() { argv: process.argv, logger, }); - const createEnv = makeCreateEnv(config); + const createEnv = await makeCreateEnv(config); const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck')); const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 8290e569ef..aafd25bc3f 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -14,19 +14,21 @@ * limitations under the License. */ -import { Logger } from 'winston'; -import { Config } from '@backstage/config'; import { PluginCacheManager, PluginDatabaseManager, PluginEndpointDiscovery, + PluginTaskManager, UrlReader, } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; +import { Logger } from 'winston'; export type PluginEnvironment = { logger: Logger; cache: PluginCacheManager; database: PluginDatabaseManager; + tasks: PluginTaskManager; config: Config; reader: UrlReader; discovery: PluginEndpointDiscovery; diff --git a/yarn.lock b/yarn.lock index 4c42941ad9..caba3db5fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20895,6 +20895,11 @@ node-abi@^2.21.0: dependencies: semver "^5.4.1" +node-abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.0.tgz#e7b1f2b72f4c5a74b5594cc23fa8fe3b62010ab5" + integrity sha512-IqMCPbihDpbHV4bNws015hU0svIBGyzPjJearwXMGJyungWdblbBcboNojTz9bWOrrJD3zIwmcr5w5c+NH+2+A== + node-addon-api@^3.0.0: version "3.2.1" resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" From d4f412fcd3fbef498eeebfd2ea1647da56d5ced2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 30 Sep 2021 09:35:49 +0200 Subject: [PATCH 094/181] Get most of the task worker code into place MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-common/api-report.md | 47 +++ .../migrations/20210928160613_init.js | 89 ++++-- packages/backend-common/package.json | 6 +- .../backend-common/src/database/tables.ts | 20 +- .../backend-common/src/tasks/CancelToken.ts | 48 +++ .../src/tasks/PluginTaskManagerImpl.test.ts | 67 +++++ .../src/tasks/PluginTaskManagerImpl.ts | 132 +++++++++ .../src/tasks/TaskManager.test.ts | 46 +-- .../backend-common/src/tasks/TaskManager.ts | 126 ++------ .../src/tasks/TaskWorker.test.ts | 257 ++++++++++++++++ .../backend-common/src/tasks/TaskWorker.ts | 277 ++++++++++++++++++ packages/backend-common/src/tasks/types.ts | 32 +- packages/backend-common/src/tasks/util.ts | 19 ++ packages/backend/src/index.ts | 4 +- yarn.lock | 5 - 15 files changed, 996 insertions(+), 179 deletions(-) create mode 100644 packages/backend-common/src/tasks/CancelToken.ts create mode 100644 packages/backend-common/src/tasks/PluginTaskManagerImpl.test.ts create mode 100644 packages/backend-common/src/tasks/PluginTaskManagerImpl.ts create mode 100644 packages/backend-common/src/tasks/TaskWorker.test.ts create mode 100644 packages/backend-common/src/tasks/TaskWorker.ts diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index a19447034e..93c25bca24 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -12,6 +12,7 @@ import { BitbucketIntegration } from '@backstage/integration'; import { Config } from '@backstage/config'; import cors from 'cors'; import Docker from 'dockerode'; +import { Duration } from 'luxon'; import { ErrorRequestHandler } from 'express'; import express from 'express'; import { GithubCredentialsProvider } from '@backstage/integration'; @@ -399,6 +400,37 @@ export type PluginEndpointDiscovery = { getExternalBaseUrl(pluginId: string): Promise; }; +// @public +export interface PluginTaskManager { + // (undocumented) + acquireLock( + id: string, + options: { + timeout: Duration; + }, + ): Promise< + | { + acquired: false; + } + | { + acquired: true; + release: () => void | Promise; + } + >; + // (undocumented) + scheduleTask( + id: string, + options: { + timeout: Duration; + frequency: Duration; + initialDelay?: Duration; + }, + fn: () => Promise, + ): Promise<{ + unschedule: () => Promise; + }>; +} + // @public export type ReaderFactory = (options: { config: Config; @@ -576,6 +608,21 @@ export interface StatusCheckHandlerOptions { statusCheck?: StatusCheck; } +// @public +export class TaskManager { + constructor(databaseManager: DatabaseManager, logger: Logger_2); + // (undocumented) + forPlugin(pluginId: string): PluginTaskManager; + // (undocumented) + static fromConfig( + config: Config, + options?: { + databaseManager?: DatabaseManager; + logger?: Logger_2; + }, + ): TaskManager; +} + // @public export type UrlReader = { read(url: string): Promise; diff --git a/packages/backend-common/migrations/20210928160613_init.js b/packages/backend-common/migrations/20210928160613_init.js index 663e2f8aeb..7a7590d903 100644 --- a/packages/backend-common/migrations/20210928160613_init.js +++ b/packages/backend-common/migrations/20210928160613_init.js @@ -21,32 +21,29 @@ */ exports.up = async function up(knex) { // - // locking + // mutexes // - await knex.schema.createTable( - 'backstage_backend_common__task_locks', - table => { - table.comment('Locks used for mutual exclusion among multiple workers'); - table - .text('id') - .primary() - .notNullable() - .comment('The unique id of this particular lock'); - table - .text('acquired_ticket') - .nullable() - .comment('A unique ticket for the current lock acquiral, if any'); - table - .dateTime('acquired_at') - .nullable() - .comment('The time when the lock was acquired, if locked'); - table - .dateTime('expires_at') - .nullable() - .comment('The time when an acquired lock will time out and expire'); - table.index('id', 'task_locks_id_idx'); - }, - ); + await knex.schema.createTable('backstage_backend_common__mutexes', table => { + table.comment('Locks used for mutual exclusion among multiple workers'); + table + .text('id') + .primary() + .notNullable() + .comment('The unique ID of this particular mutex'); + table + .text('current_lock_ticket') + .nullable() + .comment('A unique ticket for the current mutex lock'); + table + .dateTime('current_lock_acquired_at') + .nullable() + .comment('The time when the mutex was locked'); + table + .dateTime('current_lock_expires_at') + .nullable() + .comment('The time when a locked mutex will time out and auto-release'); + table.index(['id'], 'backstage_backend_common__mutexes__id_idx'); + }); // // tasks // @@ -56,7 +53,28 @@ exports.up = async function up(knex) { .text('id') .primary() .notNullable() - .comment('The unique id of this particular task'); + .comment('The unique ID of this particular task'); + table + .text('settings_json') + .notNullable() + .comment('JSON serialized object with properties for this task'); + table + .dateTime('next_run_start_at') + .nullable() + .comment('The next time that the task should be started'); + table + .text('current_run_ticket') + .nullable() + .comment('A unique ticket for the current task run'); + table + .dateTime('current_run_started_at') + .nullable() + .comment('The time that the current task run started'); + table + .dateTime('current_run_expires_at') + .nullable() + .comment('The time that the current task run will time out'); + table.index(['id'], 'backstage_backend_common__tasks__id_idx'); }); }; @@ -64,8 +82,21 @@ exports.up = async function up(knex) { * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { - await knex.schema.alterTable('task_locks', table => { - table.dropIndex([], 'task_locks_id_idx'); + // + // tasks + // + await knex.schema.alterTable('backstage_backend_common__tasks', table => { + table.dropIndex([], 'backstage_backend_common__tasks__id_idx'); }); - await knex.schema.dropTable('task_locks'); + await knex.schema.dropTable('backstage_backend_common__tasks'); + // + // locks + // + await knex.schema.alterTable( + 'backstage_backend_common__task_locks', + table => { + table.dropIndex([], 'backstage_backend_common__task_locks__id_idx'); + }, + ); + await knex.schema.dropTable('backstage_backend_common__task_locks'); }; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index c8e8d69ab5..a2255926e9 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -71,7 +71,8 @@ "unzipper": "^0.10.11", "uuid": "^8.0.0", "winston": "^3.2.1", - "yn": "^4.0.0" + "yn": "^4.0.0", + "zod": "^3.9.5" }, "peerDependencies": { "pg-connection-string": "^2.3.0" @@ -107,7 +108,8 @@ "msw": "^0.35.0", "mysql2": "^2.2.5", "recursive-readdir": "^2.2.2", - "supertest": "^6.1.3" + "supertest": "^6.1.3", + "wait-for-expect": "^3.0.2" }, "files": [ "dist", diff --git a/packages/backend-common/src/database/tables.ts b/packages/backend-common/src/database/tables.ts index 8efa393bb7..e566272ea6 100644 --- a/packages/backend-common/src/database/tables.ts +++ b/packages/backend-common/src/database/tables.ts @@ -14,9 +14,21 @@ * limitations under the License. */ -export type DbTaskLocksRow = { +export const DB_MUTEXES_TABLE = 'backstage_backend_common__mutexes'; +export const DB_TASKS_TABLE = 'backstage_backend_common__tasks'; + +export type DbMutexesRow = { id: string; - acquired_ticket?: string; - acquired_at?: Date; - expires_at?: Date; + current_lock_ticket?: string; + current_lock_acquired_at?: Date | string; + current_lock_expires_at?: Date | string; +}; + +export type DbTasksRow = { + id: string; + settings_json: string; + next_run_start_at?: Date | string; + current_run_ticket?: string; + current_run_started_at?: Date | string; + current_run_expires_at?: Date | string; }; diff --git a/packages/backend-common/src/tasks/CancelToken.ts b/packages/backend-common/src/tasks/CancelToken.ts new file mode 100644 index 0000000000..9ea9802e82 --- /dev/null +++ b/packages/backend-common/src/tasks/CancelToken.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class CancelToken { + // @ts-ignore: is actually assigned by the Promise constructor + #cancel: () => void; + #isCancelled: boolean; + #cancelPromise: Promise; + + static create(): CancelToken { + return new CancelToken(); + } + + private constructor() { + this.#isCancelled = false; + this.#cancelPromise = new Promise(resolve => { + this.#cancel = () => { + this.#isCancelled = true; + resolve(); + }; + }); + } + + cancel(): void { + this.#cancel(); + } + + get isCancelled(): boolean { + return this.#isCancelled; + } + + get promise(): Promise { + return this.#cancelPromise; + } +} diff --git a/packages/backend-common/src/tasks/PluginTaskManagerImpl.test.ts b/packages/backend-common/src/tasks/PluginTaskManagerImpl.test.ts new file mode 100644 index 0000000000..ed5bf0911e --- /dev/null +++ b/packages/backend-common/src/tasks/PluginTaskManagerImpl.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestDatabases } from '@backstage/backend-test-utils'; +import { Duration } from 'luxon'; +import { migrateBackendCommon } from '../database/migrateBackendCommon'; +import { getVoidLogger } from '../logging'; +import { PluginTaskManagerImpl } from './PluginTaskManagerImpl'; + +describe('PluginTaskManagerImpl', () => { + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + + describe('locking', () => { + it.each(databases.eachSupportedId())( + 'can run the happy path, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendCommon(knex); + + const manager = new PluginTaskManagerImpl( + async () => knex, + getVoidLogger(), + ); + + const lock1 = await manager.acquireLock('lock1', { + timeout: Duration.fromMillis(5000), + }); + const lock2 = await manager.acquireLock('lock2', { + timeout: Duration.fromMillis(5000), + }); + + expect(lock1.acquired).toBe(true); + expect(lock2.acquired).toBe(true); + + await expect( + manager.acquireLock('lock1', { + timeout: Duration.fromMillis(5000), + }), + ).resolves.toEqual({ acquired: false }); + + await (lock1 as any).release(); + await (lock2 as any).release(); + + const lock1Again = await manager.acquireLock('lock1', { + timeout: Duration.fromMillis(5000), + }); + expect(lock1Again.acquired).toBe(true); + await (lock1Again as any).release(); + }, + ); + }); +}); diff --git a/packages/backend-common/src/tasks/PluginTaskManagerImpl.ts b/packages/backend-common/src/tasks/PluginTaskManagerImpl.ts new file mode 100644 index 0000000000..8b9aad4388 --- /dev/null +++ b/packages/backend-common/src/tasks/PluginTaskManagerImpl.ts @@ -0,0 +1,132 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Knex } from 'knex'; +import { Duration } from 'luxon'; +import { v4 as uuid } from 'uuid'; +import { Logger } from 'winston'; +import { DbMutexesRow, DB_MUTEXES_TABLE } from '../database/tables'; +import { PluginTaskManagerJanitor } from './PluginTaskManagerJanitor'; +import { TaskWorker } from './TaskWorker'; +import { PluginTaskManager } from './types'; +import { nowPlus, validateId } from './util'; + +/** + * Implements the actual task management. + */ +export class PluginTaskManagerImpl implements PluginTaskManager { + private janitor: PluginTaskManagerJanitor | undefined; + + constructor( + private readonly databaseFactory: () => Promise, + private readonly logger: Logger, + ) {} + + async acquireLock( + id: string, + options: { + timeout: Duration; + }, + ): Promise< + | { acquired: false } + | { acquired: true; release: () => void | Promise } + > { + validateId(id); + + const knex = await this.databaseFactory(); + await this.ensureJanitor(knex); + + const ticket = uuid(); + + async function release() { + try { + await knex(DB_MUTEXES_TABLE) + .where('id', '=', id) + .where('current_lock_ticket', '=', ticket) + .delete(); + } catch { + // fail silently + } + } + + // First try to overwrite an existing lock, that has timed out + const stolen = await knex(DB_MUTEXES_TABLE) + .where('id', '=', id) + .whereNotNull('current_lock_ticket') + .where('current_lock_expires_at', '<', knex.fn.now()) + .update({ + current_lock_ticket: ticket, + current_lock_acquired_at: knex.fn.now(), + current_lock_expires_at: nowPlus(options.timeout, knex), + }); + + if (stolen) { + return { acquired: true, release }; + } + + try { + await knex(DB_MUTEXES_TABLE).insert({ + id, + current_lock_ticket: ticket, + current_lock_acquired_at: knex.fn.now(), + current_lock_expires_at: nowPlus(options.timeout, knex), + }); + return { acquired: true, release }; + } catch { + return { acquired: false }; + } + } + + async scheduleTask( + id: string, + options: { + timeout?: Duration; + frequency?: Duration; + initialDelay?: Duration; + }, + fn: () => void | Promise, + ): Promise<{ unschedule: () => Promise }> { + validateId(id); + + const knex = await this.databaseFactory(); + await this.ensureJanitor(knex); + + const task = new TaskWorker(id, fn, knex, this.logger); + await task.start({ + version: 1, + initialDelayDuration: options.initialDelay?.toISO(), + recurringAtMostEveryDuration: options.frequency?.toISO(), + timeoutAfterDuration: options.timeout?.toISO(), + }); + + return { + async unschedule() { + await task.stop(); + }, + }; + } + + private async ensureJanitor(knex: Knex) { + if (!this.janitor) { + this.janitor = new PluginTaskManagerJanitor({ + knex, + waitBetweenRuns: Duration.fromObject({ minutes: 1 }), + logger: this.logger, + }); + this.janitor.start(); + } + } +} diff --git a/packages/backend-common/src/tasks/TaskManager.test.ts b/packages/backend-common/src/tasks/TaskManager.test.ts index da9adb70f6..e6a34e58c1 100644 --- a/packages/backend-common/src/tasks/TaskManager.test.ts +++ b/packages/backend-common/src/tasks/TaskManager.test.ts @@ -17,9 +17,11 @@ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Duration } from 'luxon'; import { DatabaseManager } from '../database'; +import { getVoidLogger } from '../logging'; import { TaskManager } from './TaskManager'; describe('TaskManager', () => { + const logger = getVoidLogger(); const databases = TestDatabases.create({ ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); @@ -36,38 +38,16 @@ describe('TaskManager', () => { return databaseManager as DatabaseManager; } - describe('locking', () => { - it.each(databases.eachSupportedId())( - 'can run the happy path, %p', - async databaseId => { - const database = await createDatabase(databaseId); - const manager = new TaskManager(database).forPlugin('test'); + it.each(databases.eachSupportedId())( + 'can return a working plugin impl, %p', + async databaseId => { + const database = await createDatabase(databaseId); + const manager = new TaskManager(database, logger).forPlugin('test'); - const lock1 = await manager.acquireLock('lock1', { - timeout: Duration.fromMillis(5000), - }); - const lock2 = await manager.acquireLock('lock2', { - timeout: Duration.fromMillis(5000), - }); - - expect(lock1.acquired).toBe(true); - expect(lock2.acquired).toBe(true); - - await expect( - manager.acquireLock('lock1', { - timeout: Duration.fromMillis(5000), - }), - ).resolves.toEqual({ acquired: false }); - - await (lock1 as any).release(); - await (lock2 as any).release(); - - const lock1Again = await manager.acquireLock('lock1', { - timeout: Duration.fromMillis(5000), - }); - expect(lock1Again.acquired).toBe(true); - await (lock1Again as any).release(); - }, - ); - }); + const lock = await manager.acquireLock('lock1', { + timeout: Duration.fromMillis(5000), + }); + expect(lock.acquired).toBe(true); + }, + ); }); diff --git a/packages/backend-common/src/tasks/TaskManager.ts b/packages/backend-common/src/tasks/TaskManager.ts index 29741e1594..edb8e46609 100644 --- a/packages/backend-common/src/tasks/TaskManager.ts +++ b/packages/backend-common/src/tasks/TaskManager.ts @@ -15,104 +15,13 @@ */ import { Config } from '@backstage/config'; -import { Knex } from 'knex'; import { memoize } from 'lodash'; -import { Duration } from 'luxon'; -import { v4 as uuid } from 'uuid'; +import { Logger } from 'winston'; import { DatabaseManager } from '../database'; import { migrateBackendCommon } from '../database/migrateBackendCommon'; -import { DbTaskLocksRow } from '../database/tables'; +import { getRootLogger } from '../logging'; +import { PluginTaskManagerImpl } from './PluginTaskManagerImpl'; import { PluginTaskManager } from './types'; -import { validateId } from './util'; - -export class PluginTaskManagerImpl implements PluginTaskManager { - constructor( - private readonly pluginId: string, - private readonly databaseFactory: () => Promise, - ) {} - - async acquireLock( - idWithoutPrefix: string, - options: { - timeout: Duration; - }, - ): Promise< - | { acquired: false } - | { acquired: true; release: () => void | Promise } - > { - validateId(idWithoutPrefix); - - const knex = await this.databaseFactory(); - const id = `plugin:${this.pluginId}:${idWithoutPrefix}`; - const ticket = uuid(); - const timeout = options.timeout.as('seconds'); - - const release = async () => { - try { - await knex('backstage_backend_common__task_locks') - .where('id', '=', id) - .where('acquired_ticket', '=', ticket) - .delete(); - } catch { - // fail silently - } - }; - - // First try to overwrite an existing lock, that has timed out - const stolen = await knex( - 'backstage_backend_common__task_locks', - ) - .where('id', '=', id) - .whereNotNull('acquired_ticket') - .where('expires_at', '<', knex.fn.now()) - .update({ - acquired_ticket: ticket, - acquired_at: knex.fn.now(), - expires_at: - knex.client.config.client === 'sqlite3' - ? knex.raw(`datetime('now', ?)`, [`${timeout} seconds`]) - : knex.raw(`now() + interval '${timeout} seconds'`), - }); - - if (stolen) { - return { acquired: true, release }; - } - - try { - await knex('backstage_backend_common__task_locks').insert( - { - id, - acquired_ticket: ticket, - acquired_at: knex.fn.now(), - expires_at: - knex.client.config.client === 'sqlite3' - ? knex.raw(`datetime('now', ?)`, [`${timeout} seconds`]) - : knex.raw(`now() + interval '${timeout} seconds'`), - }, - ); - return { acquired: true, release }; - } catch { - return { acquired: false }; - } - } - - async scheduleTask( - idWithoutPrefix: string, - options: { - timeout: Duration; - frequency: Duration; - initialDelay?: Duration; - }, - fn: () => Promise, - ): Promise<{ release: () => Promise }> { - validateId(idWithoutPrefix); - - const knex = await this.databaseFactory(); - const id = `plugin:${this.pluginId}:${idWithoutPrefix}`; - - return {}; - } -} /** * Deals with management and locking related to distributed tasks. @@ -122,23 +31,34 @@ export class PluginTaskManagerImpl implements PluginTaskManager { export class TaskManager { static fromConfig( config: Config, - options?: { databaseManager?: DatabaseManager }, + options?: { + databaseManager?: DatabaseManager; + logger?: Logger; + }, ): TaskManager { const databaseManager = options?.databaseManager ?? DatabaseManager.fromConfig(config); - return new TaskManager(databaseManager); + const logger = (options?.logger || getRootLogger()).child({ + type: 'taskManager', + }); + return new TaskManager(databaseManager, logger); } - constructor(private readonly databaseManager: DatabaseManager) {} + constructor( + private readonly databaseManager: DatabaseManager, + private readonly logger: Logger, + ) {} forPlugin(pluginId: string): PluginTaskManager { + const databaseFactory = memoize(async () => { + const knex = await this.databaseManager.forPlugin(pluginId).getClient(); + await migrateBackendCommon(knex); + return knex; + }); + return new PluginTaskManagerImpl( - pluginId, - memoize(async () => { - const knex = await this.databaseManager.forPlugin(pluginId).getClient(); - await migrateBackendCommon(knex); - return knex; - }), + databaseFactory, + this.logger.child({ plugin: pluginId }), ); } } diff --git a/packages/backend-common/src/tasks/TaskWorker.test.ts b/packages/backend-common/src/tasks/TaskWorker.test.ts new file mode 100644 index 0000000000..ca0d624560 --- /dev/null +++ b/packages/backend-common/src/tasks/TaskWorker.test.ts @@ -0,0 +1,257 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestDatabases } from '@backstage/backend-test-utils'; +import { Duration } from 'luxon'; +import waitForExpect from 'wait-for-expect'; +import { migrateBackendCommon } from '../database/migrateBackendCommon'; +import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; +import { getVoidLogger } from '../logging'; +import { TaskWorker } from './TaskWorker'; +import { TaskSettingsV1 } from './types'; + +describe('TaskWorker', () => { + const logger = getVoidLogger(); + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it.each(databases.eachSupportedId())( + 'can run a single task to completion, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendCommon(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + const settings: TaskSettingsV1 = { + version: 1, + }; + + const worker = new TaskWorker('task1', fn, knex, logger); + await worker.start(settings); + + waitForExpect(() => { + expect(fn).toBeCalledTimes(1); + }); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'goes through the expected states for a single run, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendCommon(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + const settings: TaskSettingsV1 = { + version: 1, + initialDelayDuration: Duration.fromObject({ seconds: 1 }).toISO(), + recurringAtMostEveryDuration: undefined, + timeoutAfterDuration: undefined, + }; + + const worker = new TaskWorker('task1', fn, knex, logger); + await worker.persistTask(settings); + + let row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }), + ); + expect(JSON.parse(row.settings_json)).toEqual({ + version: 1, + initialDelayDuration: 'PT1S', + }); + + await expect(worker.findReadyTask()).resolves.toEqual({ + result: 'not ready yet', + }); + + waitForExpect(async () => { + await expect(worker.findReadyTask()).resolves.toEqual({ + result: 'ready', + }); + }); + + row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }), + ); + + await expect(worker.tryClaimTask('ticket', settings)).resolves.toBe(true); + + row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: 'ticket', + current_run_started_at: expect.anything(), + current_run_expires_at: null, + }), + ); + + await expect(worker.tryReleaseTask('ticket', settings)).resolves.toBe( + true, + ); + + row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toBeUndefined(); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'runs tasks more than once even when the task throws, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendCommon(knex); + + const fn = jest.fn().mockRejectedValue(new Error('failed')); + const settings: TaskSettingsV1 = { + version: 1, + initialDelayDuration: undefined, + recurringAtMostEveryDuration: Duration.fromMillis(0).toISO(), + timeoutAfterDuration: undefined, + }; + + const worker = new TaskWorker('task1', fn, knex, logger); + worker.start(settings); + + waitForExpect(() => { + expect(fn).toBeCalledTimes(3); + }); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'does not clobber ticket lock when stolen, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendCommon(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + const settings: TaskSettingsV1 = { + version: 1, + recurringAtMostEveryDuration: Duration.fromMillis(0).toISO(), + }; + + const worker = new TaskWorker('task1', fn, knex, logger); + await worker.persistTask(settings); + await expect(worker.findReadyTask()).resolves.toEqual({ + result: 'ready', + settings, + }); + await expect(worker.tryClaimTask('ticket', settings)).resolves.toBe(true); + + let row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: 'ticket', + current_run_started_at: expect.anything(), + current_run_expires_at: null, + }), + ); + + await knex(DB_TASKS_TABLE) + .where('id', '=', 'task1') + .update({ current_run_ticket: 'stolen' }); + + await expect(worker.tryReleaseTask('ticket', settings)).resolves.toBe( + false, + ); + + row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: 'stolen', + current_run_started_at: expect.anything(), + current_run_expires_at: null, + }), + ); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'gracefully handles a disappeared task row, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendCommon(knex); + + const fn = jest.fn(async () => {}); + const settings: TaskSettingsV1 = { + version: 1, + recurringAtMostEveryDuration: Duration.fromMillis(0).toISO(), + }; + + const worker1 = new TaskWorker('task1', fn, knex, logger); + await worker1.persistTask(settings); + await knex(DB_TASKS_TABLE).where('id', '=', 'task1').delete(); + await expect(worker1.findReadyTask()).resolves.toEqual({ + result: 'abort', + }); + + const worker2 = new TaskWorker('task2', fn, knex, logger); + await worker2.persistTask(settings); + await expect(worker2.findReadyTask()).resolves.toEqual({ + result: 'ready', + settings, + }); + await knex(DB_TASKS_TABLE).where('id', '=', 'task2').delete(); + await expect(worker2.tryClaimTask('ticket', settings)).resolves.toBe( + false, + ); + + const worker3 = new TaskWorker('task3', fn, knex, logger); + await worker3.persistTask(settings); + await expect(worker3.findReadyTask()).resolves.toEqual({ + result: 'ready', + settings, + }); + await expect(worker3.tryClaimTask('ticket', settings)).resolves.toBe( + true, + ); + await knex(DB_TASKS_TABLE).where('id', '=', 'task3').delete(); + await expect(worker3.tryReleaseTask('ticket', settings)).resolves.toBe( + false, + ); + }, + 60_000, + ); +}); diff --git a/packages/backend-common/src/tasks/TaskWorker.ts b/packages/backend-common/src/tasks/TaskWorker.ts new file mode 100644 index 0000000000..3fd1f30716 --- /dev/null +++ b/packages/backend-common/src/tasks/TaskWorker.ts @@ -0,0 +1,277 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Knex } from 'knex'; +import { Duration } from 'luxon'; +import { v4 as uuid } from 'uuid'; +import { Logger } from 'winston'; +import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; +import { CancelToken } from './CancelToken'; +import { TaskSettingsV1, taskSettingsV1Schema } from './types'; +import { nowPlus } from './util'; + +const WORK_CHECK_FREQUENCY = Duration.fromObject({ seconds: 5 }); + +/** + * Performs the actual work of a task. + * + * @private + */ +export class TaskWorker { + private readonly taskId: string; + private readonly fn: () => void | Promise; + private readonly knex: Knex; + private readonly logger: Logger; + private readonly cancelToken: CancelToken; + + constructor( + taskId: string, + fn: () => void | Promise, + knex: Knex, + logger: Logger, + ) { + this.taskId = taskId; + this.fn = fn; + this.knex = knex; + this.logger = logger; + this.cancelToken = CancelToken.create(); + } + + async start(settings: TaskSettingsV1) { + try { + await this.persistTask(settings); + } catch (e) { + throw new Error(`Failed to persist task, ${e}`); + } + + this.logger.info( + `Task worker starting: ${this.taskId}, ${JSON.stringify(settings)}`, + ); + + (async () => { + try { + while (!this.cancelToken.isCancelled) { + const runResult = await this.runOnce(); + if (runResult.result === 'abort') { + break; + } + if (!settings.recurringAtMostEveryDuration) { + break; + } + + await this.sleep(WORK_CHECK_FREQUENCY); + } + this.logger.info(`Task worker finished: ${this.taskId}`); + } catch (e) { + this.logger.warn(`Task worker failed unexpectedly, ${e}`); + } + })(); + } + + stop() { + this.cancelToken.cancel(); + } + + /** + * Makes a single attempt at running the task to completion, if ready. + * + * @returns The outcome of the attempt + */ + async runOnce(): Promise< + | { result: 'not ready yet' } + | { result: 'abort' } + | { result: 'failed' } + | { result: 'completed' } + > { + const findResult = await this.findReadyTask(); + if ( + findResult.result === 'not ready yet' || + findResult.result === 'abort' + ) { + return findResult; + } + + const taskSettings = findResult.settings; + const ticket = uuid(); + + const claimed = await this.tryClaimTask(ticket, taskSettings); + if (!claimed) { + return { result: 'not ready yet' }; + } + + try { + await this.fn(); + } catch (e) { + await this.tryReleaseTask(ticket, taskSettings); + return { result: 'failed' }; + } + + await this.tryReleaseTask(ticket, taskSettings); + return { result: 'completed' }; + } + + /** + * Sleep for the given duration, but abort sooner if the cancel token + * triggers. + * + * @param duration - The amount of time to sleep, at most + */ + private async sleep(duration: Duration): Promise { + await Promise.race([ + new Promise(resolve => setTimeout(resolve, duration.as('milliseconds'))), + this.cancelToken.promise, + ]); + } + + /** + * Perform the initial store of the task info + */ + async persistTask(settings: TaskSettingsV1) { + // Perform an initial parse to ensure that we will definitely be able to + // read it back again. + taskSettingsV1Schema.parse(settings); + + const settingsJson = JSON.stringify(settings); + const startAt = settings.initialDelayDuration + ? nowPlus(Duration.fromISO(settings.initialDelayDuration), this.knex) + : this.knex.fn.now(); + + // It's OK if the task already exists; if it does, just replace its + // settings with the new value and start the loop as usual. + await this.knex(DB_TASKS_TABLE) + .insert({ + id: this.taskId, + settings_json: settingsJson, + next_run_start_at: startAt, + }) + .onConflict('id') + .merge(['settings_json']); + } + + /** + * Check if the task is ready to run + */ + async findReadyTask(): Promise< + | { result: 'not ready yet' } + | { result: 'abort' } + | { result: 'ready'; settings: TaskSettingsV1 } + > { + const [row] = await this.knex(DB_TASKS_TABLE) + .where('id', '=', this.taskId) + .select({ + settingsJson: 'settings_json', + ready: this.knex.raw( + ` + CASE + WHEN next_run_start_at <= ? AND current_run_ticket IS NULL THEN TRUE + ELSE FALSE + END`, + [this.knex.fn.now()], + ), + }); + + if (!row) { + this.logger.info( + 'No longer able to find task; aborting and assuming that it has been unregistered or expired', + ); + return { result: 'abort' }; + } else if (!row.ready) { + return { result: 'not ready yet' }; + } + + try { + const settings = taskSettingsV1Schema.parse(JSON.parse(row.settingsJson)); + return { result: 'ready', settings }; + } catch (e) { + this.logger.info( + 'No longer able to parse task settings; aborting and assuming that a ' + + 'newer version of the task has been issued and being handled by ' + + `other workers, ${e}`, + ); + return { result: 'abort' }; + } + } + + /** + * Attempts to claim a task that's ready for execution, on this worker's + * behalf. We should not attempt to perform the work unless the claim really + * goes through. + * + * @param ticket - A globally unique string that changes for each invocation + * @param settings - The settings of the task to claim + * @returns True if it was successfully claimed + */ + async tryClaimTask( + ticket: string, + settings: TaskSettingsV1, + ): Promise { + const startedAt = this.knex.fn.now(); + const expiresAt = settings.timeoutAfterDuration + ? nowPlus(Duration.fromISO(settings.timeoutAfterDuration), this.knex) + : this.knex.raw('null'); + + const rows = await this.knex(DB_TASKS_TABLE) + .where('id', '=', this.taskId) + .whereNull('current_run_ticket') + .update({ + current_run_ticket: ticket, + current_run_started_at: startedAt, + current_run_expires_at: expiresAt, + }); + + return rows === 1; + } + + async tryReleaseTask( + ticket: string, + settings: TaskSettingsV1, + ): Promise { + const { recurringAtMostEveryDuration } = settings; + + // If this is not a recurring task, and we still have the current run + // ticket, delete it from the table + if (recurringAtMostEveryDuration === undefined) { + const rows = await this.knex(DB_TASKS_TABLE) + .where('id', '=', this.taskId) + .where('current_run_ticket', '=', ticket) + .delete(); + + return rows === 1; + } + + // We make an effort to keep the datetime calculations in the database + // layer, making sure to not have to perform conversions back and forth and + // leaning on the database as a central clock source + const dbNull = this.knex.raw('null'); + const dt = Duration.fromISO(recurringAtMostEveryDuration).as('seconds'); + const nextRun = + this.knex.client.config.client === 'sqlite3' + ? this.knex.raw('datetime(next_run_start_at, ?)', [`+${dt} seconds`]) + : this.knex.raw(`next_run_start_at + interval '${dt} seconds'`); + + const rows = await this.knex(DB_TASKS_TABLE) + .where('id', '=', this.taskId) + .where('current_run_ticket', '=', ticket) + .update({ + next_run_start_at: nextRun, + current_run_ticket: dbNull, + current_run_started_at: dbNull, + current_run_expires_at: dbNull, + }); + + return rows === 1; + } +} diff --git a/packages/backend-common/src/tasks/types.ts b/packages/backend-common/src/tasks/types.ts index 4a4fcb474e..4b061f9069 100644 --- a/packages/backend-common/src/tasks/types.ts +++ b/packages/backend-common/src/tasks/types.ts @@ -15,6 +15,7 @@ */ import { Duration } from 'luxon'; +import { z } from 'zod'; /** * Deals with management and locking related to distributed tasks, for a given @@ -41,5 +42,34 @@ export interface PluginTaskManager { initialDelay?: Duration; }, fn: () => Promise, - ): Promise<{ release: () => Promise }>; + ): Promise<{ unschedule: () => Promise }>; } + +function isValidOptionalDurationString(d: string | undefined): boolean { + try { + return !d || Duration.fromISO(d).isValid === true; + } catch { + return false; + } +} + +export const taskSettingsV1Schema = z.object({ + version: z.literal(1), + initialDelayDuration: z + .string() + .optional() + .refine(isValidOptionalDurationString, { message: 'Invalid duration' }), + recurringAtMostEveryDuration: z + .string() + .optional() + .refine(isValidOptionalDurationString, { message: 'Invalid duration' }), + timeoutAfterDuration: z + .string() + .optional() + .refine(isValidOptionalDurationString, { message: 'Invalid duration' }), +}); + +/** + * The properties that control a scheduled task (version 1). + */ +export type TaskSettingsV1 = z.infer; diff --git a/packages/backend-common/src/tasks/util.ts b/packages/backend-common/src/tasks/util.ts index 7bd4ff61b5..dd5c2d4b7c 100644 --- a/packages/backend-common/src/tasks/util.ts +++ b/packages/backend-common/src/tasks/util.ts @@ -15,6 +15,8 @@ */ import { InputError } from '@backstage/errors'; +import { Knex } from 'knex'; +import { DateTime, Duration } from 'luxon'; // Keep the IDs compatible with e.g. Prometheus export function validateId(id: string) { @@ -24,3 +26,20 @@ export function validateId(id: string) { ); } } + +export function dbTime(t: Date | string): DateTime { + if (typeof t === 'string') { + return DateTime.fromSQL(t); + } + return DateTime.fromJSDate(t); +} + +export function nowPlus(duration: Duration | undefined, knex: Knex) { + const seconds = duration?.as('seconds') ?? 0; + if (!seconds) { + return knex.fn.now(); + } + return knex.client.config.client === 'sqlite3' + ? knex.raw(`datetime('now', ?)`, [`${seconds} seconds`]) + : knex.raw(`now() + interval '${seconds} seconds'`); +} diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 554c9a7d99..4e44729200 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -57,7 +57,7 @@ import techInsights from './plugins/techInsights'; import todo from './plugins/todo'; import { PluginEnvironment } from './types'; -async function makeCreateEnv(config: Config) { +function makeCreateEnv(config: Config) { const root = getRootLogger(); const reader = UrlReaders.default({ logger: root, config }); const discovery = SingleHostDiscovery.fromConfig(config); @@ -90,7 +90,7 @@ async function main() { argv: process.argv, logger, }); - const createEnv = await makeCreateEnv(config); + const createEnv = makeCreateEnv(config); const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck')); const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); diff --git a/yarn.lock b/yarn.lock index caba3db5fb..4c42941ad9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20895,11 +20895,6 @@ node-abi@^2.21.0: dependencies: semver "^5.4.1" -node-abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.0.tgz#e7b1f2b72f4c5a74b5594cc23fa8fe3b62010ab5" - integrity sha512-IqMCPbihDpbHV4bNws015hU0svIBGyzPjJearwXMGJyungWdblbBcboNojTz9bWOrrJD3zIwmcr5w5c+NH+2+A== - node-addon-api@^3.0.0: version "3.2.1" resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" From 751317dbf3faccb05188f480773a1702b8b9da37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 14 Oct 2021 17:04:24 +0200 Subject: [PATCH 095/181] finishing touches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-common/api-report.md | 3 + packages/backend-common/package.json | 1 - packages/backend-common/src/database/index.ts | 1 + .../src/database/migrateBackendCommon.ts | 3 +- .../backend-common/src/database/tables.ts | 1 + packages/backend-common/src/database/util.ts | 33 +++++ .../src/tasks/PluginTaskManagerImpl.test.ts | 89 ++++++++++++-- .../src/tasks/PluginTaskManagerImpl.ts | 44 +++---- .../src/tasks/PluginTaskManagerJanitor.ts | 114 ++++++++++++++++++ .../backend-common/src/tasks/TaskManager.ts | 11 ++ 10 files changed, 260 insertions(+), 40 deletions(-) create mode 100644 packages/backend-common/src/database/util.ts create mode 100644 packages/backend-common/src/tasks/PluginTaskManagerJanitor.ts diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 93c25bca24..480b6419fa 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -375,6 +375,9 @@ export class GitlabUrlReader implements UrlReader { export { isChildPath }; +// @public +export function isDatabaseConflictError(e: unknown): boolean; + // @public export function loadBackendConfig(options: { logger: Logger_2; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index a2255926e9..e0d4be1242 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -63,7 +63,6 @@ "minimatch": "^3.0.4", "minimist": "^1.2.5", "morgan": "^1.10.0", - "node-abort-controller": "^3.0.0", "raw-body": "^2.4.1", "selfsigned": "^1.10.7", "stoppable": "^1.1.0", diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index 7fcb8bf930..2a820f7367 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -28,3 +28,4 @@ export { } from './connection'; export type { PluginDatabaseManager } from './types'; +export { isDatabaseConflictError } from './util'; diff --git a/packages/backend-common/src/database/migrateBackendCommon.ts b/packages/backend-common/src/database/migrateBackendCommon.ts index 6283f3a96c..104e757e0b 100644 --- a/packages/backend-common/src/database/migrateBackendCommon.ts +++ b/packages/backend-common/src/database/migrateBackendCommon.ts @@ -16,6 +16,7 @@ import { Knex } from 'knex'; import { resolvePackagePath } from '../paths'; +import { DB_MIGRATIONS_TABLE } from './tables'; const migrationsDir = resolvePackagePath( '@backstage/backend-common', @@ -25,6 +26,6 @@ const migrationsDir = resolvePackagePath( export async function migrateBackendCommon(knex: Knex): Promise { await knex.migrate.latest({ directory: migrationsDir, - tableName: 'knex_migrations_backstage_backend_common', + tableName: DB_MIGRATIONS_TABLE, }); } diff --git a/packages/backend-common/src/database/tables.ts b/packages/backend-common/src/database/tables.ts index e566272ea6..90e8aeca40 100644 --- a/packages/backend-common/src/database/tables.ts +++ b/packages/backend-common/src/database/tables.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export const DB_MIGRATIONS_TABLE = 'backstage_backend_common__knex_migrations'; export const DB_MUTEXES_TABLE = 'backstage_backend_common__mutexes'; export const DB_TASKS_TABLE = 'backstage_backend_common__tasks'; diff --git a/packages/backend-common/src/database/util.ts b/packages/backend-common/src/database/util.ts new file mode 100644 index 0000000000..607554f294 --- /dev/null +++ b/packages/backend-common/src/database/util.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tries to deduce whether a thrown error is a database conflict. + * + * @public + * @param e - A thrown error + * @returns True if the error looks like it was a conflict error thrown by a + * known database engine + */ +export function isDatabaseConflictError(e: unknown) { + const message = (e as any)?.message; + + return ( + typeof message === 'string' && + (/SQLITE_CONSTRAINT: UNIQUE/.test(message) || + /unique constraint/.test(message)) + ); +} diff --git a/packages/backend-common/src/tasks/PluginTaskManagerImpl.test.ts b/packages/backend-common/src/tasks/PluginTaskManagerImpl.test.ts index ed5bf0911e..ef5f6db96f 100644 --- a/packages/backend-common/src/tasks/PluginTaskManagerImpl.test.ts +++ b/packages/backend-common/src/tasks/PluginTaskManagerImpl.test.ts @@ -14,8 +14,9 @@ * limitations under the License. */ -import { TestDatabases } from '@backstage/backend-test-utils'; +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Duration } from 'luxon'; +import waitForExpect from 'wait-for-expect'; import { migrateBackendCommon } from '../database/migrateBackendCommon'; import { getVoidLogger } from '../logging'; import { PluginTaskManagerImpl } from './PluginTaskManagerImpl'; @@ -25,17 +26,21 @@ describe('PluginTaskManagerImpl', () => { ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); - describe('locking', () => { + async function init(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + await migrateBackendCommon(knex); + const manager = new PluginTaskManagerImpl( + async () => knex, + getVoidLogger(), + ); + return { knex, manager }; + } + + describe('acquireLock', () => { it.each(databases.eachSupportedId())( 'can run the happy path, %p', async databaseId => { - const knex = await databases.init(databaseId); - await migrateBackendCommon(knex); - - const manager = new PluginTaskManagerImpl( - async () => knex, - getVoidLogger(), - ); + const { manager } = await init(databaseId); const lock1 = await manager.acquireLock('lock1', { timeout: Duration.fromMillis(5000), @@ -63,5 +68,71 @@ describe('PluginTaskManagerImpl', () => { await (lock1Again as any).release(); }, ); + + it.each(databases.eachSupportedId())( + 'rejects double lock attempts, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const lock1 = await manager.acquireLock('lock1', { + timeout: Duration.fromMillis(5000), + }); + const lock2 = await manager.acquireLock('lock1', { + timeout: Duration.fromMillis(5000), + }); + + expect(lock1.acquired).toBe(true); + expect(lock2.acquired).toBe(false); + + await (lock1 as any).release(); + + const lock1Again = await manager.acquireLock('lock1', { + timeout: Duration.fromMillis(5000), + }); + expect(lock1Again.acquired).toBe(true); + await (lock1Again as any).release(); + }, + ); + + it.each(databases.eachSupportedId())( + 'times out locks, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const lock1 = await manager.acquireLock('lock1', { + timeout: Duration.fromMillis(200), + }); + + expect(lock1.acquired).toBe(true); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + const lock2 = await manager.acquireLock('lock1', { + timeout: Duration.fromMillis(5000), + }); + expect(lock2.acquired).toBe(true); + await (lock2 as any).release(); + }, + ); + }); + + // This is just to test the wrapper code; most of the actual tests are in + // TaskWorker.test.ts + describe('scheduleTask', () => { + it.each(databases.eachSupportedId())( + 'can run the happy path, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const fn = jest.fn(); + const { unschedule } = await manager.scheduleTask('task1', {}, fn); + + await waitForExpect(() => { + expect(fn).toBeCalled(); + }); + + await unschedule(); + }, + ); }); }); diff --git a/packages/backend-common/src/tasks/PluginTaskManagerImpl.ts b/packages/backend-common/src/tasks/PluginTaskManagerImpl.ts index 8b9aad4388..2d1f192fe6 100644 --- a/packages/backend-common/src/tasks/PluginTaskManagerImpl.ts +++ b/packages/backend-common/src/tasks/PluginTaskManagerImpl.ts @@ -18,8 +18,8 @@ import { Knex } from 'knex'; import { Duration } from 'luxon'; import { v4 as uuid } from 'uuid'; import { Logger } from 'winston'; +import { isDatabaseConflictError } from '../database'; import { DbMutexesRow, DB_MUTEXES_TABLE } from '../database/tables'; -import { PluginTaskManagerJanitor } from './PluginTaskManagerJanitor'; import { TaskWorker } from './TaskWorker'; import { PluginTaskManager } from './types'; import { nowPlus, validateId } from './util'; @@ -28,8 +28,6 @@ import { nowPlus, validateId } from './util'; * Implements the actual task management. */ export class PluginTaskManagerImpl implements PluginTaskManager { - private janitor: PluginTaskManagerJanitor | undefined; - constructor( private readonly databaseFactory: () => Promise, private readonly logger: Logger, @@ -47,8 +45,6 @@ export class PluginTaskManagerImpl implements PluginTaskManager { validateId(id); const knex = await this.databaseFactory(); - await this.ensureJanitor(knex); - const ticket = uuid(); async function release() { @@ -62,30 +58,32 @@ export class PluginTaskManagerImpl implements PluginTaskManager { } } + const record: Knex.DbRecord = { + current_lock_ticket: ticket, + current_lock_acquired_at: knex.fn.now(), + current_lock_expires_at: options.timeout + ? nowPlus(options.timeout, knex) + : knex.raw('null'), + }; + // First try to overwrite an existing lock, that has timed out const stolen = await knex(DB_MUTEXES_TABLE) .where('id', '=', id) .whereNotNull('current_lock_ticket') .where('current_lock_expires_at', '<', knex.fn.now()) - .update({ - current_lock_ticket: ticket, - current_lock_acquired_at: knex.fn.now(), - current_lock_expires_at: nowPlus(options.timeout, knex), - }); + .update(record); if (stolen) { return { acquired: true, release }; } try { - await knex(DB_MUTEXES_TABLE).insert({ - id, - current_lock_ticket: ticket, - current_lock_acquired_at: knex.fn.now(), - current_lock_expires_at: nowPlus(options.timeout, knex), - }); + await knex(DB_MUTEXES_TABLE).insert({ id, ...record }); return { acquired: true, release }; - } catch { + } catch (e) { + if (!isDatabaseConflictError(e)) { + this.logger.warn(`Failed to acquire lock, ${e}`); + } return { acquired: false }; } } @@ -102,7 +100,6 @@ export class PluginTaskManagerImpl implements PluginTaskManager { validateId(id); const knex = await this.databaseFactory(); - await this.ensureJanitor(knex); const task = new TaskWorker(id, fn, knex, this.logger); await task.start({ @@ -118,15 +115,4 @@ export class PluginTaskManagerImpl implements PluginTaskManager { }, }; } - - private async ensureJanitor(knex: Knex) { - if (!this.janitor) { - this.janitor = new PluginTaskManagerJanitor({ - knex, - waitBetweenRuns: Duration.fromObject({ minutes: 1 }), - logger: this.logger, - }); - this.janitor.start(); - } - } } diff --git a/packages/backend-common/src/tasks/PluginTaskManagerJanitor.ts b/packages/backend-common/src/tasks/PluginTaskManagerJanitor.ts new file mode 100644 index 0000000000..26af986025 --- /dev/null +++ b/packages/backend-common/src/tasks/PluginTaskManagerJanitor.ts @@ -0,0 +1,114 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Knex } from 'knex'; +import { Duration } from 'luxon'; +import { Logger } from 'winston'; +import { + DbMutexesRow, + DbTasksRow, + DB_MUTEXES_TABLE, + DB_TASKS_TABLE, +} from '../database/tables'; +import { CancelToken } from './CancelToken'; + +/** + * Makes sure to auto-expire and clean up things that time out or for other + * reasons should not be left lingering. + */ +export class PluginTaskManagerJanitor { + private readonly knex: Knex; + private readonly waitBetweenRuns: Duration; + private readonly logger: Logger; + private readonly cancelToken: CancelToken; + + constructor(options: { + knex: Knex; + waitBetweenRuns: Duration; + logger: Logger; + }) { + this.knex = options.knex; + this.waitBetweenRuns = options.waitBetweenRuns; + this.logger = options.logger; + this.cancelToken = CancelToken.create(); + } + + async start() { + while (!this.cancelToken.isCancelled) { + try { + await this.runOnce(); + } catch (e) { + this.logger.warn(`Error while performing janitorial tasks, ${e}`); + } + + await this.sleep(this.waitBetweenRuns); + } + } + + async stop() { + this.cancelToken.cancel(); + } + + private async runOnce() { + // SQLite currently (Oct 1 2021) returns a number for returning() + // statements, effectively ignoring them and instead returning the outcome + // of the delete() - and knex also emits a warning about that fact, which + // is why we avoid that entirely for the sqlite3 driver. + // https://github.com/knex/knex/issues/4370 + // https://github.com/mapbox/node-sqlite3/issues/1453 + + const mutexesQuery = this.knex(DB_MUTEXES_TABLE) + .where('current_lock_expires_at', '<', this.knex.fn.now()) + .delete(); + + if (this.knex.client.config.client === 'sqlite3') { + const mutexes = await mutexesQuery; + if (mutexes > 0) { + this.logger.warn(`${mutexes} mutex locks timed out and were lost`); + } + } else { + const mutexes = await mutexesQuery.returning(['id']); + for (const { id } of mutexes) { + this.logger.warn(`Mutex lock timed out and was lost: ${id}`); + } + } + + const tasksQuery = this.knex(DB_TASKS_TABLE) + .where('current_run_expires_at', '<', this.knex.fn.now()) + .delete(); + + if (this.knex.client.config.client === 'sqlite3') { + const tasks = await tasksQuery; + this.logger.warn(`${tasks} tasks timed out and were lost`); + } else { + const tasks = await tasksQuery.returning(['id']); + for (const { id } of tasks) { + this.logger.warn(`Task timed out and was lost: ${id}`); + } + } + } + + /** + * Sleeps for the given duration, but aborts sooner if the cancel token + * triggers. + */ + private async sleep(duration: Duration) { + await Promise.race([ + new Promise(resolve => setTimeout(resolve, duration.as('milliseconds'))), + this.cancelToken.promise, + ]); + } +} diff --git a/packages/backend-common/src/tasks/TaskManager.ts b/packages/backend-common/src/tasks/TaskManager.ts index edb8e46609..ea49c2383e 100644 --- a/packages/backend-common/src/tasks/TaskManager.ts +++ b/packages/backend-common/src/tasks/TaskManager.ts @@ -16,11 +16,13 @@ import { Config } from '@backstage/config'; import { memoize } from 'lodash'; +import { Duration } from 'luxon'; import { Logger } from 'winston'; import { DatabaseManager } from '../database'; import { migrateBackendCommon } from '../database/migrateBackendCommon'; import { getRootLogger } from '../logging'; import { PluginTaskManagerImpl } from './PluginTaskManagerImpl'; +import { PluginTaskManagerJanitor } from './PluginTaskManagerJanitor'; import { PluginTaskManager } from './types'; /** @@ -52,7 +54,16 @@ export class TaskManager { forPlugin(pluginId: string): PluginTaskManager { const databaseFactory = memoize(async () => { const knex = await this.databaseManager.forPlugin(pluginId).getClient(); + await migrateBackendCommon(knex); + + const janitor = new PluginTaskManagerJanitor({ + knex, + waitBetweenRuns: Duration.fromObject({ minutes: 1 }), + logger: this.logger, + }); + janitor.start(); + return knex; }); From d7c1e0e34acf52172e879dce7a81c95ec8baf435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 15 Oct 2021 10:42:59 +0200 Subject: [PATCH 096/181] docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/happy-rice-tickle.md | 5 + .github/styles/vocab.txt | 3 + packages/backend-common/api-report.md | 29 ++--- packages/backend-common/package.json | 1 + .../src/tasks/PluginTaskManagerImpl.ts | 16 +-- .../backend-common/src/tasks/TaskManager.ts | 6 ++ packages/backend-common/src/tasks/index.ts | 2 +- packages/backend-common/src/tasks/types.ts | 101 ++++++++++++++++-- 8 files changed, 126 insertions(+), 37 deletions(-) create mode 100644 .changeset/happy-rice-tickle.md diff --git a/.changeset/happy-rice-tickle.md b/.changeset/happy-rice-tickle.md new file mode 100644 index 0000000000..475f03501c --- /dev/null +++ b/.changeset/happy-rice-tickle.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Add support for distributed mutexes and scheduled tasks, through the `TaskManager` class. This class can be particularly useful for coordinating things across many deployed instances of a given backend plugin. An example of this is catalog entity providers - with this facility you can register tasks similar to a cron job, and make sure that only one host at a time tries to execute the job, and that the timing (call frequency, timeouts etc) are retained as a global concern, letting you scale your workload safely without affecting the task behavior. diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 7e478953bc..c14047cd4d 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -52,6 +52,7 @@ configmaps configs const cookiecutter +cron css Datadog dataflow @@ -162,6 +163,8 @@ Monorepo monorepos msgraph msw +mutex +mutexes mysql namespace namespaced diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 480b6419fa..688c74159e 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -384,6 +384,11 @@ export function loadBackendConfig(options: { argv: string[]; }): Promise; +// @public +export interface LockOptions { + timeout: Duration; +} + // @public export function notFoundHandler(): RequestHandler; @@ -405,30 +410,22 @@ export type PluginEndpointDiscovery = { // @public export interface PluginTaskManager { - // (undocumented) acquireLock( id: string, - options: { - timeout: Duration; - }, + options: LockOptions, ): Promise< | { acquired: false; } | { acquired: true; - release: () => void | Promise; + release(): Promise; } >; - // (undocumented) scheduleTask( id: string, - options: { - timeout: Duration; - frequency: Duration; - initialDelay?: Duration; - }, - fn: () => Promise, + options: TaskOptions, + fn: () => void | Promise, ): Promise<{ unschedule: () => Promise; }>; @@ -614,7 +611,6 @@ export interface StatusCheckHandlerOptions { // @public export class TaskManager { constructor(databaseManager: DatabaseManager, logger: Logger_2); - // (undocumented) forPlugin(pluginId: string): PluginTaskManager; // (undocumented) static fromConfig( @@ -626,6 +622,13 @@ export class TaskManager { ): TaskManager; } +// @public +export interface TaskOptions { + frequency?: Duration; + initialDelay?: Duration; + timeout?: Duration; +} + // @public export type UrlReader = { read(url: string): Promise; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index e0d4be1242..555c2004ee 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -90,6 +90,7 @@ "@types/concat-stream": "^1.6.0", "@types/fs-extra": "^9.0.3", "@types/http-errors": "^1.6.3", + "@types/luxon": "^2.0.4", "@types/minimist": "^1.2.0", "@types/mock-fs": "^4.13.0", "@types/morgan": "^1.9.0", diff --git a/packages/backend-common/src/tasks/PluginTaskManagerImpl.ts b/packages/backend-common/src/tasks/PluginTaskManagerImpl.ts index 2d1f192fe6..540c7d501a 100644 --- a/packages/backend-common/src/tasks/PluginTaskManagerImpl.ts +++ b/packages/backend-common/src/tasks/PluginTaskManagerImpl.ts @@ -15,13 +15,12 @@ */ import { Knex } from 'knex'; -import { Duration } from 'luxon'; import { v4 as uuid } from 'uuid'; import { Logger } from 'winston'; import { isDatabaseConflictError } from '../database'; import { DbMutexesRow, DB_MUTEXES_TABLE } from '../database/tables'; import { TaskWorker } from './TaskWorker'; -import { PluginTaskManager } from './types'; +import { LockOptions, PluginTaskManager, TaskOptions } from './types'; import { nowPlus, validateId } from './util'; /** @@ -35,12 +34,9 @@ export class PluginTaskManagerImpl implements PluginTaskManager { async acquireLock( id: string, - options: { - timeout: Duration; - }, + options: LockOptions, ): Promise< - | { acquired: false } - | { acquired: true; release: () => void | Promise } + { acquired: false } | { acquired: true; release(): Promise } > { validateId(id); @@ -90,11 +86,7 @@ export class PluginTaskManagerImpl implements PluginTaskManager { async scheduleTask( id: string, - options: { - timeout?: Duration; - frequency?: Duration; - initialDelay?: Duration; - }, + options: TaskOptions, fn: () => void | Promise, ): Promise<{ unschedule: () => Promise }> { validateId(id); diff --git a/packages/backend-common/src/tasks/TaskManager.ts b/packages/backend-common/src/tasks/TaskManager.ts index ea49c2383e..db8228561f 100644 --- a/packages/backend-common/src/tasks/TaskManager.ts +++ b/packages/backend-common/src/tasks/TaskManager.ts @@ -51,6 +51,12 @@ export class TaskManager { private readonly logger: Logger, ) {} + /** + * Instantiates a task manager instance for the given plugin. + * + * @param pluginId - The unique ID of the plugin, for example "catalog" + * @returns A {@link PluginTaskManager} instance + */ forPlugin(pluginId: string): PluginTaskManager { const databaseFactory = memoize(async () => { const knex = await this.databaseManager.forPlugin(pluginId).getClient(); diff --git a/packages/backend-common/src/tasks/index.ts b/packages/backend-common/src/tasks/index.ts index 60329788b3..9f86f8fc64 100644 --- a/packages/backend-common/src/tasks/index.ts +++ b/packages/backend-common/src/tasks/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export type { PluginTaskManager } from './types'; export { TaskManager } from './TaskManager'; +export type { LockOptions, PluginTaskManager, TaskOptions } from './types'; diff --git a/packages/backend-common/src/tasks/types.ts b/packages/backend-common/src/tasks/types.ts index 4b061f9069..a8d356bed9 100644 --- a/packages/backend-common/src/tasks/types.ts +++ b/packages/backend-common/src/tasks/types.ts @@ -17,6 +17,64 @@ import { Duration } from 'luxon'; import { z } from 'zod'; +/** + * Options that apply to the acquiral of a given lock. + * + * @public + */ +export interface LockOptions { + /** + * The maximum amount of time that the lock can be held, before it's + * considered timed out and gets auto-released by the framework. + */ + timeout: Duration; +} + +/** + * Options that apply to the invocation of a given task. + * + * @public + */ +export interface TaskOptions { + /** + * The maximum amount of time that a single task invocation can take, before + * it's considered timed out and gets "released" such that a new invocation + * is permitted to take place (possibly, then, on a different worker). + * + * If no value is given for this field then there is no timeout. This is + * potentially dangerous. + */ + timeout?: Duration; + + /** + * The amount of time that should pass between task invocation starts. + * Essentially, this equals roughly how often you want the task to run. + * + * This is a best effort value; under some circumstances there can be + * deviations. For example, if the task runtime is longer than the frequency + * and the timeout has not been given or not been exceeded yet, the next + * invocation of this task will be delayed until after the previous one + * finishes. + * + * The system does its best to avoid overlapping invocations. + * + * If no value is given for this field then the task will only be invoked + * once (on any worker) and then unscheduled automatically. + */ + frequency?: Duration; + + /** + * The amount of time that should pass before the first invocation happens. + * + * This can be useful in cold start scenarios to stagger or delay some heavy + * compute jobs. + * + * If no value is given for this field then the first invocation will happen + * as soon as possible. + */ + initialDelay?: Duration; +} + /** * Deals with management and locking related to distributed tasks, for a given * plugin. @@ -24,24 +82,45 @@ import { z } from 'zod'; * @public */ export interface PluginTaskManager { + /** + * Attempts to acquire an exclusive lock. + * + * A lock can only be held by one party at a time. Any subsequent attempts to + * acquire the lock will fail, unless the timeout period has been exceeded or + * the lock was released by the previous holder. + * + * @param id - A unique ID (within the scope of the plugin) for a lock + * @param options - Options for the lock + * @returns The result of the lock attempt. If it was successfully acquired, + * you should remember to call its `release` method as soon as you + * are done with the lock. + */ acquireLock( id: string, - options: { - timeout: Duration; - }, + options: LockOptions, ): Promise< - | { acquired: false } - | { acquired: true; release: () => void | Promise } + { acquired: false } | { acquired: true; release(): Promise } >; + /** + * Schedules a task function for coordinated exclusive invocation across + * workers. + * + * If the task was already scheduled since before by us or by another party, + * its options are just overwritten with the given options, and things + * continue from there. + * + * @param id - A unique ID (within the scope of the plugin) for the task + * @param options - Options for the task + * @param fn - The actual task function to be invoked + * @returns An `unschedule` function that can be used to stop the task + * invocations later on. This removes the task entirely from storage + * and stops its invocations across all workers. + */ scheduleTask( id: string, - options: { - timeout: Duration; - frequency: Duration; - initialDelay?: Duration; - }, - fn: () => Promise, + options: TaskOptions, + fn: () => void | Promise, ): Promise<{ unschedule: () => Promise }>; } From 222793c849958e613f2fcae3a482e05368e20a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 15 Oct 2021 11:23:05 +0200 Subject: [PATCH 097/181] do not modify the backend yet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-common/package.json | 2 +- packages/backend/src/index.ts | 25 +++++++++++-------------- packages/backend/src/types.ts | 6 ++---- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 555c2004ee..47345c198d 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -41,6 +41,7 @@ "@types/cors": "^2.8.6", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", + "@types/luxon": "^2.0.4", "archiver": "^5.0.2", "aws-sdk": "^2.840.0", "compression": "^1.7.4", @@ -90,7 +91,6 @@ "@types/concat-stream": "^1.6.0", "@types/fs-extra": "^9.0.3", "@types/http-errors": "^1.6.3", - "@types/luxon": "^2.0.4", "@types/minimist": "^1.2.0", "@types/mock-fs": "^4.13.0", "@types/morgan": "^1.9.0", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 4e44729200..f978e84da9 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -22,39 +22,38 @@ * Happy hacking! */ +import Router from 'express-promise-router'; import { CacheManager, createServiceBuilder, - DatabaseManager, getRootLogger, loadBackendConfig, notFoundHandler, + DatabaseManager, SingleHostDiscovery, - TaskManager, UrlReaders, useHotMemoize, } from '@backstage/backend-common'; import { Config } from '@backstage/config'; -import Router from 'express-promise-router'; -import { metricsHandler, metricsInit } from './metrics'; -import app from './plugins/app'; +import healthcheck from './plugins/healthcheck'; +import { metricsInit, metricsHandler } from './metrics'; import auth from './plugins/auth'; import azureDevOps from './plugins/azure-devops'; -import badges from './plugins/badges'; import catalog from './plugins/catalog'; import codeCoverage from './plugins/codecoverage'; -import graphql from './plugins/graphql'; -import healthcheck from './plugins/healthcheck'; -import jenkins from './plugins/jenkins'; -import kafka from './plugins/kafka'; import kubernetes from './plugins/kubernetes'; -import proxy from './plugins/proxy'; +import kafka from './plugins/kafka'; import rollbar from './plugins/rollbar'; import scaffolder from './plugins/scaffolder'; +import proxy from './plugins/proxy'; import search from './plugins/search'; import techdocs from './plugins/techdocs'; import techInsights from './plugins/techInsights'; import todo from './plugins/todo'; +import graphql from './plugins/graphql'; +import app from './plugins/app'; +import badges from './plugins/badges'; +import jenkins from './plugins/jenkins'; import { PluginEnvironment } from './types'; function makeCreateEnv(config: Config) { @@ -65,15 +64,13 @@ function makeCreateEnv(config: Config) { root.info(`Created UrlReader ${reader}`); const databaseManager = DatabaseManager.fromConfig(config); - const taskManager = TaskManager.fromConfig(config); const cacheManager = CacheManager.fromConfig(config); return (plugin: string): PluginEnvironment => { const logger = root.child({ type: 'plugin', plugin }); const database = databaseManager.forPlugin(plugin); - const tasks = taskManager.forPlugin(plugin); const cache = cacheManager.forPlugin(plugin); - return { logger, cache, database, tasks, config, reader, discovery }; + return { logger, cache, database, config, reader, discovery }; }; } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index aafd25bc3f..8290e569ef 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -14,21 +14,19 @@ * limitations under the License. */ +import { Logger } from 'winston'; +import { Config } from '@backstage/config'; import { PluginCacheManager, PluginDatabaseManager, PluginEndpointDiscovery, - PluginTaskManager, UrlReader, } from '@backstage/backend-common'; -import { Config } from '@backstage/config'; -import { Logger } from 'winston'; export type PluginEnvironment = { logger: Logger; cache: PluginCacheManager; database: PluginDatabaseManager; - tasks: PluginTaskManager; config: Config; reader: UrlReader; discovery: PluginEndpointDiscovery; From e09bf604cd6989a463a326bbae228ccc736b3ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 24 Oct 2021 19:08:50 +0200 Subject: [PATCH 098/181] move to a separate package instead and address comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/happy-rice-tickle.md | 2 +- packages/backend-common/api-report.md | 50 ---------------- packages/backend-common/package.json | 11 +--- packages/backend-common/src/index.ts | 1 - packages/backend-tasks/.eslintrc.js | 3 + packages/backend-tasks/README.md | 36 +++++++++++ packages/backend-tasks/api-report.md | 59 +++++++++++++++++++ .../knexfile.js | 0 .../migrations/20210928160613_init.js | 27 ++++----- packages/backend-tasks/package.json | 54 +++++++++++++++++ .../src/database/migrateBackendTasks.ts} | 6 +- .../src/database/tables.ts | 8 +-- packages/backend-tasks/src/index.ts | 23 ++++++++ packages/backend-tasks/src/setupTests.ts | 17 ++++++ .../src/tasks/CancelToken.ts | 0 .../src/tasks/PluginTaskManagerImpl.test.ts | 6 +- .../src/tasks/PluginTaskManagerImpl.ts | 3 +- .../src/tasks/PluginTaskManagerJanitor.ts | 0 .../src/tasks/TaskManager.test.ts | 3 +- .../src/tasks/TaskManager.ts | 7 +-- .../src/tasks/TaskWorker.test.ts | 14 ++--- .../src/tasks/TaskWorker.ts | 0 .../src/tasks/index.ts | 0 .../src/tasks/types.ts | 0 .../src/tasks/util.ts | 0 25 files changed, 229 insertions(+), 101 deletions(-) create mode 100644 packages/backend-tasks/.eslintrc.js create mode 100644 packages/backend-tasks/README.md create mode 100644 packages/backend-tasks/api-report.md rename packages/{backend-common => backend-tasks}/knexfile.js (100%) rename packages/{backend-common => backend-tasks}/migrations/20210928160613_init.js (75%) create mode 100644 packages/backend-tasks/package.json rename packages/{backend-common/src/database/migrateBackendCommon.ts => backend-tasks/src/database/migrateBackendTasks.ts} (83%) rename packages/{backend-common => backend-tasks}/src/database/tables.ts (79%) create mode 100644 packages/backend-tasks/src/index.ts create mode 100644 packages/backend-tasks/src/setupTests.ts rename packages/{backend-common => backend-tasks}/src/tasks/CancelToken.ts (100%) rename packages/{backend-common => backend-tasks}/src/tasks/PluginTaskManagerImpl.test.ts (96%) rename packages/{backend-common => backend-tasks}/src/tasks/PluginTaskManagerImpl.ts (96%) rename packages/{backend-common => backend-tasks}/src/tasks/PluginTaskManagerJanitor.ts (100%) rename packages/{backend-common => backend-tasks}/src/tasks/TaskManager.test.ts (94%) rename packages/{backend-common => backend-tasks}/src/tasks/TaskManager.ts (91%) rename packages/{backend-common => backend-tasks}/src/tasks/TaskWorker.test.ts (95%) rename packages/{backend-common => backend-tasks}/src/tasks/TaskWorker.ts (100%) rename packages/{backend-common => backend-tasks}/src/tasks/index.ts (100%) rename packages/{backend-common => backend-tasks}/src/tasks/types.ts (100%) rename packages/{backend-common => backend-tasks}/src/tasks/util.ts (100%) diff --git a/.changeset/happy-rice-tickle.md b/.changeset/happy-rice-tickle.md index 475f03501c..1e39dc2e9c 100644 --- a/.changeset/happy-rice-tickle.md +++ b/.changeset/happy-rice-tickle.md @@ -2,4 +2,4 @@ '@backstage/backend-common': patch --- -Add support for distributed mutexes and scheduled tasks, through the `TaskManager` class. This class can be particularly useful for coordinating things across many deployed instances of a given backend plugin. An example of this is catalog entity providers - with this facility you can register tasks similar to a cron job, and make sure that only one host at a time tries to execute the job, and that the timing (call frequency, timeouts etc) are retained as a global concern, letting you scale your workload safely without affecting the task behavior. +Added the `isDatabaseConflictError` function. diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 688c74159e..7242c32a84 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -12,7 +12,6 @@ import { BitbucketIntegration } from '@backstage/integration'; import { Config } from '@backstage/config'; import cors from 'cors'; import Docker from 'dockerode'; -import { Duration } from 'luxon'; import { ErrorRequestHandler } from 'express'; import express from 'express'; import { GithubCredentialsProvider } from '@backstage/integration'; @@ -384,11 +383,6 @@ export function loadBackendConfig(options: { argv: string[]; }): Promise; -// @public -export interface LockOptions { - timeout: Duration; -} - // @public export function notFoundHandler(): RequestHandler; @@ -408,29 +402,6 @@ export type PluginEndpointDiscovery = { getExternalBaseUrl(pluginId: string): Promise; }; -// @public -export interface PluginTaskManager { - acquireLock( - id: string, - options: LockOptions, - ): Promise< - | { - acquired: false; - } - | { - acquired: true; - release(): Promise; - } - >; - scheduleTask( - id: string, - options: TaskOptions, - fn: () => void | Promise, - ): Promise<{ - unschedule: () => Promise; - }>; -} - // @public export type ReaderFactory = (options: { config: Config; @@ -608,27 +579,6 @@ export interface StatusCheckHandlerOptions { statusCheck?: StatusCheck; } -// @public -export class TaskManager { - constructor(databaseManager: DatabaseManager, logger: Logger_2); - forPlugin(pluginId: string): PluginTaskManager; - // (undocumented) - static fromConfig( - config: Config, - options?: { - databaseManager?: DatabaseManager; - logger?: Logger_2; - }, - ): TaskManager; -} - -// @public -export interface TaskOptions { - frequency?: Duration; - initialDelay?: Duration; - timeout?: Duration; -} - // @public export type UrlReader = { read(url: string): Promise; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 47345c198d..446b3b5afe 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -41,7 +41,6 @@ "@types/cors": "^2.8.6", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", - "@types/luxon": "^2.0.4", "archiver": "^5.0.2", "aws-sdk": "^2.840.0", "compression": "^1.7.4", @@ -60,7 +59,6 @@ "knex": "^0.95.1", "lodash": "^4.17.21", "logform": "^2.1.1", - "luxon": "^2.0.2", "minimatch": "^3.0.4", "minimist": "^1.2.5", "morgan": "^1.10.0", @@ -69,10 +67,8 @@ "stoppable": "^1.1.0", "tar": "^6.1.2", "unzipper": "^0.10.11", - "uuid": "^8.0.0", "winston": "^3.2.1", - "yn": "^4.0.0", - "zod": "^3.9.5" + "yn": "^4.0.0" }, "peerDependencies": { "pg-connection-string": "^2.3.0" @@ -85,7 +81,6 @@ "devDependencies": { "@backstage/backend-test-utils": "^0.1.8", "@backstage/cli": "^0.8.2", - "@backstage/test-utils": "^0.1.21", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", @@ -108,12 +103,10 @@ "msw": "^0.35.0", "mysql2": "^2.2.5", "recursive-readdir": "^2.2.2", - "supertest": "^6.1.3", - "wait-for-expect": "^3.0.2" + "supertest": "^6.1.3" }, "files": [ "dist", - "migrations/**/*.{js,d.ts}", "config.d.ts" ], "configSchema": "config.d.ts" diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index a906d7bc61..c214961a2e 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -31,5 +31,4 @@ export * from './paths'; export * from './reading'; export * from './scm'; export * from './service'; -export * from './tasks'; export * from './util'; diff --git a/packages/backend-tasks/.eslintrc.js b/packages/backend-tasks/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/packages/backend-tasks/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/packages/backend-tasks/README.md b/packages/backend-tasks/README.md new file mode 100644 index 0000000000..71c54b0a7f --- /dev/null +++ b/packages/backend-tasks/README.md @@ -0,0 +1,36 @@ +# @backstage/backend-tasks + +Common distributed task management / locking library for Backstage backends. + +## Usage + +Add the library to your backend package: + +```sh +# From your Backstage root directory +cd packages/backend +yarn add @backstage/backend-tasks +``` + +then make use of its facilities as necessary: + +```typescript +import { TaskManager } from '@backstage/backend-tasks'; + +const manager = TaskManager.fromConfig(rootConfig).forPlugin('my-plugin'); + +const { unschedule } = await manager.scheduleTask( + 'refresh-things', + { + frequency: Duration.fromObject({ minutes: 10 }), + }, + async () => { + await entityProvider.run(); + }, +); +``` + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md new file mode 100644 index 0000000000..4734e8b502 --- /dev/null +++ b/packages/backend-tasks/api-report.md @@ -0,0 +1,59 @@ +## API Report File for "@backstage/backend-tasks" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Config } from '@backstage/config'; +import { DatabaseManager } from '@backstage/backend-common'; +import { Duration } from 'luxon'; +import { Logger as Logger_2 } from 'winston'; + +// @public +export interface LockOptions { + timeout: Duration; +} + +// @public +export interface PluginTaskManager { + acquireLock( + id: string, + options: LockOptions, + ): Promise< + | { + acquired: false; + } + | { + acquired: true; + release(): Promise; + } + >; + scheduleTask( + id: string, + options: TaskOptions, + fn: () => void | Promise, + ): Promise<{ + unschedule: () => Promise; + }>; +} + +// @public +export class TaskManager { + constructor(databaseManager: DatabaseManager, logger: Logger_2); + forPlugin(pluginId: string): PluginTaskManager; + // (undocumented) + static fromConfig( + config: Config, + options?: { + databaseManager?: DatabaseManager; + logger?: Logger_2; + }, + ): TaskManager; +} + +// @public +export interface TaskOptions { + frequency?: Duration; + initialDelay?: Duration; + timeout?: Duration; +} +``` diff --git a/packages/backend-common/knexfile.js b/packages/backend-tasks/knexfile.js similarity index 100% rename from packages/backend-common/knexfile.js rename to packages/backend-tasks/knexfile.js diff --git a/packages/backend-common/migrations/20210928160613_init.js b/packages/backend-tasks/migrations/20210928160613_init.js similarity index 75% rename from packages/backend-common/migrations/20210928160613_init.js rename to packages/backend-tasks/migrations/20210928160613_init.js index 7a7590d903..77c8f2b945 100644 --- a/packages/backend-common/migrations/20210928160613_init.js +++ b/packages/backend-tasks/migrations/20210928160613_init.js @@ -23,7 +23,7 @@ exports.up = async function up(knex) { // // mutexes // - await knex.schema.createTable('backstage_backend_common__mutexes', table => { + await knex.schema.createTable('backstage_backend_tasks__mutexes', table => { table.comment('Locks used for mutual exclusion among multiple workers'); table .text('id') @@ -32,7 +32,7 @@ exports.up = async function up(knex) { .comment('The unique ID of this particular mutex'); table .text('current_lock_ticket') - .nullable() + .notNullable() .comment('A unique ticket for the current mutex lock'); table .dateTime('current_lock_acquired_at') @@ -42,12 +42,12 @@ exports.up = async function up(knex) { .dateTime('current_lock_expires_at') .nullable() .comment('The time when a locked mutex will time out and auto-release'); - table.index(['id'], 'backstage_backend_common__mutexes__id_idx'); + table.index(['id'], 'backstage_backend_tasks__mutexes__id_idx'); }); // // tasks // - await knex.schema.createTable('backstage_backend_common__tasks', table => { + await knex.schema.createTable('backstage_backend_tasks__tasks', table => { table.comment('Tasks used for scheduling work on multiple workers'); table .text('id') @@ -74,7 +74,7 @@ exports.up = async function up(knex) { .dateTime('current_run_expires_at') .nullable() .comment('The time that the current task run will time out'); - table.index(['id'], 'backstage_backend_common__tasks__id_idx'); + table.index(['id'], 'backstage_backend_tasks__tasks__id_idx'); }); }; @@ -85,18 +85,15 @@ exports.down = async function down(knex) { // // tasks // - await knex.schema.alterTable('backstage_backend_common__tasks', table => { - table.dropIndex([], 'backstage_backend_common__tasks__id_idx'); + await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { + table.dropIndex([], 'backstage_backend_tasks__tasks__id_idx'); }); - await knex.schema.dropTable('backstage_backend_common__tasks'); + await knex.schema.dropTable('backstage_backend_tasks__tasks'); // // locks // - await knex.schema.alterTable( - 'backstage_backend_common__task_locks', - table => { - table.dropIndex([], 'backstage_backend_common__task_locks__id_idx'); - }, - ); - await knex.schema.dropTable('backstage_backend_common__task_locks'); + await knex.schema.alterTable('backstage_backend_tasks__task_locks', table => { + table.dropIndex([], 'backstage_backend_tasks__task_locks__id_idx'); + }); + await knex.schema.dropTable('backstage_backend_tasks__task_locks'); }; diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json new file mode 100644 index 0000000000..a5374d7b6e --- /dev/null +++ b/packages/backend-tasks/package.json @@ -0,0 +1,54 @@ +{ + "name": "@backstage/backend-tasks", + "description": "Common distributed task management / locking library for Backstage backends", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/backend-tasks" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "scripts": { + "build": "backstage-cli build --outputs cjs,types", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.9.7", + "@backstage/config": "^0.1.10", + "@backstage/errors": "^0.1.3", + "@backstage/types": "^0.1.1", + "@types/luxon": "^2.0.4", + "knex": "^0.95.1", + "lodash": "^4.17.21", + "luxon": "^2.0.2", + "uuid": "^8.0.0", + "winston": "^3.2.1", + "zod": "^3.9.5" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^0.1.7", + "@backstage/cli": "^0.8.0", + "jest": "^26.0.1", + "wait-for-expect": "^3.0.2" + }, + "files": [ + "dist", + "migrations/**/*.{js,d.ts}" + ] +} diff --git a/packages/backend-common/src/database/migrateBackendCommon.ts b/packages/backend-tasks/src/database/migrateBackendTasks.ts similarity index 83% rename from packages/backend-common/src/database/migrateBackendCommon.ts rename to packages/backend-tasks/src/database/migrateBackendTasks.ts index 104e757e0b..cc0f4349ac 100644 --- a/packages/backend-common/src/database/migrateBackendCommon.ts +++ b/packages/backend-tasks/src/database/migrateBackendTasks.ts @@ -14,16 +14,16 @@ * limitations under the License. */ +import { resolvePackagePath } from '@backstage/backend-common'; import { Knex } from 'knex'; -import { resolvePackagePath } from '../paths'; import { DB_MIGRATIONS_TABLE } from './tables'; const migrationsDir = resolvePackagePath( - '@backstage/backend-common', + '@backstage/backend-tasks', 'migrations', ); -export async function migrateBackendCommon(knex: Knex): Promise { +export async function migrateBackendTasks(knex: Knex): Promise { await knex.migrate.latest({ directory: migrationsDir, tableName: DB_MIGRATIONS_TABLE, diff --git a/packages/backend-common/src/database/tables.ts b/packages/backend-tasks/src/database/tables.ts similarity index 79% rename from packages/backend-common/src/database/tables.ts rename to packages/backend-tasks/src/database/tables.ts index 90e8aeca40..b93dcf4faa 100644 --- a/packages/backend-common/src/database/tables.ts +++ b/packages/backend-tasks/src/database/tables.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -export const DB_MIGRATIONS_TABLE = 'backstage_backend_common__knex_migrations'; -export const DB_MUTEXES_TABLE = 'backstage_backend_common__mutexes'; -export const DB_TASKS_TABLE = 'backstage_backend_common__tasks'; +export const DB_MIGRATIONS_TABLE = 'backstage_backend_tasks__knex_migrations'; +export const DB_MUTEXES_TABLE = 'backstage_backend_tasks__mutexes'; +export const DB_TASKS_TABLE = 'backstage_backend_tasks__tasks'; export type DbMutexesRow = { id: string; - current_lock_ticket?: string; + current_lock_ticket: string; current_lock_acquired_at?: Date | string; current_lock_expires_at?: Date | string; }; diff --git a/packages/backend-tasks/src/index.ts b/packages/backend-tasks/src/index.ts new file mode 100644 index 0000000000..00a8aa2803 --- /dev/null +++ b/packages/backend-tasks/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Common distributed task management / locking library for Backstage backends + * + * @packageDocumentation + */ + +export * from './tasks'; diff --git a/packages/backend-tasks/src/setupTests.ts b/packages/backend-tasks/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/packages/backend-tasks/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/packages/backend-common/src/tasks/CancelToken.ts b/packages/backend-tasks/src/tasks/CancelToken.ts similarity index 100% rename from packages/backend-common/src/tasks/CancelToken.ts rename to packages/backend-tasks/src/tasks/CancelToken.ts diff --git a/packages/backend-common/src/tasks/PluginTaskManagerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts similarity index 96% rename from packages/backend-common/src/tasks/PluginTaskManagerImpl.test.ts rename to packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts index ef5f6db96f..f27a00cac6 100644 --- a/packages/backend-common/src/tasks/PluginTaskManagerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts @@ -14,11 +14,11 @@ * limitations under the License. */ +import { getVoidLogger } from '@backstage/backend-common'; import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Duration } from 'luxon'; import waitForExpect from 'wait-for-expect'; -import { migrateBackendCommon } from '../database/migrateBackendCommon'; -import { getVoidLogger } from '../logging'; +import { migrateBackendTasks } from '../database/migrateBackendTasks'; import { PluginTaskManagerImpl } from './PluginTaskManagerImpl'; describe('PluginTaskManagerImpl', () => { @@ -28,7 +28,7 @@ describe('PluginTaskManagerImpl', () => { async function init(databaseId: TestDatabaseId) { const knex = await databases.init(databaseId); - await migrateBackendCommon(knex); + await migrateBackendTasks(knex); const manager = new PluginTaskManagerImpl( async () => knex, getVoidLogger(), diff --git a/packages/backend-common/src/tasks/PluginTaskManagerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts similarity index 96% rename from packages/backend-common/src/tasks/PluginTaskManagerImpl.ts rename to packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts index 540c7d501a..c30c915353 100644 --- a/packages/backend-common/src/tasks/PluginTaskManagerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts @@ -14,10 +14,10 @@ * limitations under the License. */ +import { isDatabaseConflictError } from '@backstage/backend-common'; import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; import { Logger } from 'winston'; -import { isDatabaseConflictError } from '../database'; import { DbMutexesRow, DB_MUTEXES_TABLE } from '../database/tables'; import { TaskWorker } from './TaskWorker'; import { LockOptions, PluginTaskManager, TaskOptions } from './types'; @@ -65,7 +65,6 @@ export class PluginTaskManagerImpl implements PluginTaskManager { // First try to overwrite an existing lock, that has timed out const stolen = await knex(DB_MUTEXES_TABLE) .where('id', '=', id) - .whereNotNull('current_lock_ticket') .where('current_lock_expires_at', '<', knex.fn.now()) .update(record); diff --git a/packages/backend-common/src/tasks/PluginTaskManagerJanitor.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts similarity index 100% rename from packages/backend-common/src/tasks/PluginTaskManagerJanitor.ts rename to packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts diff --git a/packages/backend-common/src/tasks/TaskManager.test.ts b/packages/backend-tasks/src/tasks/TaskManager.test.ts similarity index 94% rename from packages/backend-common/src/tasks/TaskManager.test.ts rename to packages/backend-tasks/src/tasks/TaskManager.test.ts index e6a34e58c1..87efb5cfed 100644 --- a/packages/backend-common/src/tasks/TaskManager.test.ts +++ b/packages/backend-tasks/src/tasks/TaskManager.test.ts @@ -14,10 +14,9 @@ * limitations under the License. */ +import { DatabaseManager, getVoidLogger } from '@backstage/backend-common'; import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Duration } from 'luxon'; -import { DatabaseManager } from '../database'; -import { getVoidLogger } from '../logging'; import { TaskManager } from './TaskManager'; describe('TaskManager', () => { diff --git a/packages/backend-common/src/tasks/TaskManager.ts b/packages/backend-tasks/src/tasks/TaskManager.ts similarity index 91% rename from packages/backend-common/src/tasks/TaskManager.ts rename to packages/backend-tasks/src/tasks/TaskManager.ts index db8228561f..d065447fb2 100644 --- a/packages/backend-common/src/tasks/TaskManager.ts +++ b/packages/backend-tasks/src/tasks/TaskManager.ts @@ -14,13 +14,12 @@ * limitations under the License. */ +import { DatabaseManager, getRootLogger } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import { memoize } from 'lodash'; import { Duration } from 'luxon'; import { Logger } from 'winston'; -import { DatabaseManager } from '../database'; -import { migrateBackendCommon } from '../database/migrateBackendCommon'; -import { getRootLogger } from '../logging'; +import { migrateBackendTasks } from '../database/migrateBackendTasks'; import { PluginTaskManagerImpl } from './PluginTaskManagerImpl'; import { PluginTaskManagerJanitor } from './PluginTaskManagerJanitor'; import { PluginTaskManager } from './types'; @@ -61,7 +60,7 @@ export class TaskManager { const databaseFactory = memoize(async () => { const knex = await this.databaseManager.forPlugin(pluginId).getClient(); - await migrateBackendCommon(knex); + await migrateBackendTasks(knex); const janitor = new PluginTaskManagerJanitor({ knex, diff --git a/packages/backend-common/src/tasks/TaskWorker.test.ts b/packages/backend-tasks/src/tasks/TaskWorker.test.ts similarity index 95% rename from packages/backend-common/src/tasks/TaskWorker.test.ts rename to packages/backend-tasks/src/tasks/TaskWorker.test.ts index ca0d624560..2a7de2220a 100644 --- a/packages/backend-common/src/tasks/TaskWorker.test.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.test.ts @@ -14,12 +14,12 @@ * limitations under the License. */ +import { getVoidLogger } from '@backstage/backend-common'; import { TestDatabases } from '@backstage/backend-test-utils'; import { Duration } from 'luxon'; import waitForExpect from 'wait-for-expect'; -import { migrateBackendCommon } from '../database/migrateBackendCommon'; +import { migrateBackendTasks } from '../database/migrateBackendTasks'; import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; -import { getVoidLogger } from '../logging'; import { TaskWorker } from './TaskWorker'; import { TaskSettingsV1 } from './types'; @@ -37,7 +37,7 @@ describe('TaskWorker', () => { 'can run a single task to completion, %p', async databaseId => { const knex = await databases.init(databaseId); - await migrateBackendCommon(knex); + await migrateBackendTasks(knex); const fn = jest.fn( async () => new Promise(resolve => setTimeout(resolve, 50)), @@ -60,7 +60,7 @@ describe('TaskWorker', () => { 'goes through the expected states for a single run, %p', async databaseId => { const knex = await databases.init(databaseId); - await migrateBackendCommon(knex); + await migrateBackendTasks(knex); const fn = jest.fn( async () => new Promise(resolve => setTimeout(resolve, 50)), @@ -135,7 +135,7 @@ describe('TaskWorker', () => { 'runs tasks more than once even when the task throws, %p', async databaseId => { const knex = await databases.init(databaseId); - await migrateBackendCommon(knex); + await migrateBackendTasks(knex); const fn = jest.fn().mockRejectedValue(new Error('failed')); const settings: TaskSettingsV1 = { @@ -159,7 +159,7 @@ describe('TaskWorker', () => { 'does not clobber ticket lock when stolen, %p', async databaseId => { const knex = await databases.init(databaseId); - await migrateBackendCommon(knex); + await migrateBackendTasks(knex); const fn = jest.fn( async () => new Promise(resolve => setTimeout(resolve, 50)), @@ -212,7 +212,7 @@ describe('TaskWorker', () => { 'gracefully handles a disappeared task row, %p', async databaseId => { const knex = await databases.init(databaseId); - await migrateBackendCommon(knex); + await migrateBackendTasks(knex); const fn = jest.fn(async () => {}); const settings: TaskSettingsV1 = { diff --git a/packages/backend-common/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts similarity index 100% rename from packages/backend-common/src/tasks/TaskWorker.ts rename to packages/backend-tasks/src/tasks/TaskWorker.ts diff --git a/packages/backend-common/src/tasks/index.ts b/packages/backend-tasks/src/tasks/index.ts similarity index 100% rename from packages/backend-common/src/tasks/index.ts rename to packages/backend-tasks/src/tasks/index.ts diff --git a/packages/backend-common/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts similarity index 100% rename from packages/backend-common/src/tasks/types.ts rename to packages/backend-tasks/src/tasks/types.ts diff --git a/packages/backend-common/src/tasks/util.ts b/packages/backend-tasks/src/tasks/util.ts similarity index 100% rename from packages/backend-common/src/tasks/util.ts rename to packages/backend-tasks/src/tasks/util.ts From c1ac8dd174c25fb15245420813506f72a29d2c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 24 Oct 2021 19:22:41 +0200 Subject: [PATCH 099/181] log on failed release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts index c30c915353..f3d453e89b 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts @@ -43,16 +43,16 @@ export class PluginTaskManagerImpl implements PluginTaskManager { const knex = await this.databaseFactory(); const ticket = uuid(); - async function release() { + const release = async () => { try { await knex(DB_MUTEXES_TABLE) .where('id', '=', id) .where('current_lock_ticket', '=', ticket) .delete(); - } catch { - // fail silently + } catch (e) { + this.logger.warn(`Failed to release lock, ${e}`); } - } + }; const record: Knex.DbRecord = { current_lock_ticket: ticket, From 67456f7fc9994894b1ae231fa09d4d438d307bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 16:46:10 +0200 Subject: [PATCH 100/181] remove knexfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/knexfile.js | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 packages/backend-tasks/knexfile.js diff --git a/packages/backend-tasks/knexfile.js b/packages/backend-tasks/knexfile.js deleted file mode 100644 index 4c8be42673..0000000000 --- a/packages/backend-tasks/knexfile.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// This file makes it possible to run "yarn knex migrate:make some_file_name" -// to assist in making new migrations -module.exports = { - client: 'sqlite3', - connection: ':memory:', - useNullAsDefault: true, - migrations: { - directory: './migrations', - }, -}; From 3f1237147fbf5cfb33f931403ac657de8eeb37f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 16:54:36 +0200 Subject: [PATCH 101/181] remove the lock functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/api-report.md | 17 ---- .../migrations/20210928160613_init.js | 31 ------- packages/backend-tasks/src/database/tables.ts | 8 -- .../src/tasks/PluginTaskManagerImpl.test.ts | 81 ------------------- .../src/tasks/PluginTaskManagerImpl.ts | 58 +------------ .../src/tasks/PluginTaskManagerJanitor.ts | 23 +----- .../src/tasks/TaskManager.test.ts | 12 ++- packages/backend-tasks/src/tasks/index.ts | 2 +- packages/backend-tasks/src/tasks/types.ts | 33 -------- 9 files changed, 12 insertions(+), 253 deletions(-) diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 4734e8b502..e9d7884b43 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -8,25 +8,8 @@ import { DatabaseManager } from '@backstage/backend-common'; import { Duration } from 'luxon'; import { Logger as Logger_2 } from 'winston'; -// @public -export interface LockOptions { - timeout: Duration; -} - // @public export interface PluginTaskManager { - acquireLock( - id: string, - options: LockOptions, - ): Promise< - | { - acquired: false; - } - | { - acquired: true; - release(): Promise; - } - >; scheduleTask( id: string, options: TaskOptions, diff --git a/packages/backend-tasks/migrations/20210928160613_init.js b/packages/backend-tasks/migrations/20210928160613_init.js index 77c8f2b945..c5028e2e93 100644 --- a/packages/backend-tasks/migrations/20210928160613_init.js +++ b/packages/backend-tasks/migrations/20210928160613_init.js @@ -20,30 +20,6 @@ * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { - // - // mutexes - // - await knex.schema.createTable('backstage_backend_tasks__mutexes', table => { - table.comment('Locks used for mutual exclusion among multiple workers'); - table - .text('id') - .primary() - .notNullable() - .comment('The unique ID of this particular mutex'); - table - .text('current_lock_ticket') - .notNullable() - .comment('A unique ticket for the current mutex lock'); - table - .dateTime('current_lock_acquired_at') - .nullable() - .comment('The time when the mutex was locked'); - table - .dateTime('current_lock_expires_at') - .nullable() - .comment('The time when a locked mutex will time out and auto-release'); - table.index(['id'], 'backstage_backend_tasks__mutexes__id_idx'); - }); // // tasks // @@ -89,11 +65,4 @@ exports.down = async function down(knex) { table.dropIndex([], 'backstage_backend_tasks__tasks__id_idx'); }); await knex.schema.dropTable('backstage_backend_tasks__tasks'); - // - // locks - // - await knex.schema.alterTable('backstage_backend_tasks__task_locks', table => { - table.dropIndex([], 'backstage_backend_tasks__task_locks__id_idx'); - }); - await knex.schema.dropTable('backstage_backend_tasks__task_locks'); }; diff --git a/packages/backend-tasks/src/database/tables.ts b/packages/backend-tasks/src/database/tables.ts index b93dcf4faa..4777c4e60c 100644 --- a/packages/backend-tasks/src/database/tables.ts +++ b/packages/backend-tasks/src/database/tables.ts @@ -15,16 +15,8 @@ */ export const DB_MIGRATIONS_TABLE = 'backstage_backend_tasks__knex_migrations'; -export const DB_MUTEXES_TABLE = 'backstage_backend_tasks__mutexes'; export const DB_TASKS_TABLE = 'backstage_backend_tasks__tasks'; -export type DbMutexesRow = { - id: string; - current_lock_ticket: string; - current_lock_acquired_at?: Date | string; - current_lock_expires_at?: Date | string; -}; - export type DbTasksRow = { id: string; settings_json: string; diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts index f27a00cac6..1d1c89cb6b 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts @@ -16,7 +16,6 @@ import { getVoidLogger } from '@backstage/backend-common'; import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; -import { Duration } from 'luxon'; import waitForExpect from 'wait-for-expect'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; import { PluginTaskManagerImpl } from './PluginTaskManagerImpl'; @@ -36,86 +35,6 @@ describe('PluginTaskManagerImpl', () => { return { knex, manager }; } - describe('acquireLock', () => { - it.each(databases.eachSupportedId())( - 'can run the happy path, %p', - async databaseId => { - const { manager } = await init(databaseId); - - const lock1 = await manager.acquireLock('lock1', { - timeout: Duration.fromMillis(5000), - }); - const lock2 = await manager.acquireLock('lock2', { - timeout: Duration.fromMillis(5000), - }); - - expect(lock1.acquired).toBe(true); - expect(lock2.acquired).toBe(true); - - await expect( - manager.acquireLock('lock1', { - timeout: Duration.fromMillis(5000), - }), - ).resolves.toEqual({ acquired: false }); - - await (lock1 as any).release(); - await (lock2 as any).release(); - - const lock1Again = await manager.acquireLock('lock1', { - timeout: Duration.fromMillis(5000), - }); - expect(lock1Again.acquired).toBe(true); - await (lock1Again as any).release(); - }, - ); - - it.each(databases.eachSupportedId())( - 'rejects double lock attempts, %p', - async databaseId => { - const { manager } = await init(databaseId); - - const lock1 = await manager.acquireLock('lock1', { - timeout: Duration.fromMillis(5000), - }); - const lock2 = await manager.acquireLock('lock1', { - timeout: Duration.fromMillis(5000), - }); - - expect(lock1.acquired).toBe(true); - expect(lock2.acquired).toBe(false); - - await (lock1 as any).release(); - - const lock1Again = await manager.acquireLock('lock1', { - timeout: Duration.fromMillis(5000), - }); - expect(lock1Again.acquired).toBe(true); - await (lock1Again as any).release(); - }, - ); - - it.each(databases.eachSupportedId())( - 'times out locks, %p', - async databaseId => { - const { manager } = await init(databaseId); - - const lock1 = await manager.acquireLock('lock1', { - timeout: Duration.fromMillis(200), - }); - - expect(lock1.acquired).toBe(true); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - const lock2 = await manager.acquireLock('lock1', { - timeout: Duration.fromMillis(5000), - }); - expect(lock2.acquired).toBe(true); - await (lock2 as any).release(); - }, - ); - }); - // This is just to test the wrapper code; most of the actual tests are in // TaskWorker.test.ts describe('scheduleTask', () => { diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts index f3d453e89b..7859f4ee23 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts @@ -14,14 +14,11 @@ * limitations under the License. */ -import { isDatabaseConflictError } from '@backstage/backend-common'; import { Knex } from 'knex'; -import { v4 as uuid } from 'uuid'; import { Logger } from 'winston'; -import { DbMutexesRow, DB_MUTEXES_TABLE } from '../database/tables'; import { TaskWorker } from './TaskWorker'; -import { LockOptions, PluginTaskManager, TaskOptions } from './types'; -import { nowPlus, validateId } from './util'; +import { PluginTaskManager, TaskOptions } from './types'; +import { validateId } from './util'; /** * Implements the actual task management. @@ -32,57 +29,6 @@ export class PluginTaskManagerImpl implements PluginTaskManager { private readonly logger: Logger, ) {} - async acquireLock( - id: string, - options: LockOptions, - ): Promise< - { acquired: false } | { acquired: true; release(): Promise } - > { - validateId(id); - - const knex = await this.databaseFactory(); - const ticket = uuid(); - - const release = async () => { - try { - await knex(DB_MUTEXES_TABLE) - .where('id', '=', id) - .where('current_lock_ticket', '=', ticket) - .delete(); - } catch (e) { - this.logger.warn(`Failed to release lock, ${e}`); - } - }; - - const record: Knex.DbRecord = { - current_lock_ticket: ticket, - current_lock_acquired_at: knex.fn.now(), - current_lock_expires_at: options.timeout - ? nowPlus(options.timeout, knex) - : knex.raw('null'), - }; - - // First try to overwrite an existing lock, that has timed out - const stolen = await knex(DB_MUTEXES_TABLE) - .where('id', '=', id) - .where('current_lock_expires_at', '<', knex.fn.now()) - .update(record); - - if (stolen) { - return { acquired: true, release }; - } - - try { - await knex(DB_MUTEXES_TABLE).insert({ id, ...record }); - return { acquired: true, release }; - } catch (e) { - if (!isDatabaseConflictError(e)) { - this.logger.warn(`Failed to acquire lock, ${e}`); - } - return { acquired: false }; - } - } - async scheduleTask( id: string, options: TaskOptions, diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts index 26af986025..2912041b3f 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts @@ -17,12 +17,7 @@ import { Knex } from 'knex'; import { Duration } from 'luxon'; import { Logger } from 'winston'; -import { - DbMutexesRow, - DbTasksRow, - DB_MUTEXES_TABLE, - DB_TASKS_TABLE, -} from '../database/tables'; +import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; import { CancelToken } from './CancelToken'; /** @@ -70,22 +65,6 @@ export class PluginTaskManagerJanitor { // https://github.com/knex/knex/issues/4370 // https://github.com/mapbox/node-sqlite3/issues/1453 - const mutexesQuery = this.knex(DB_MUTEXES_TABLE) - .where('current_lock_expires_at', '<', this.knex.fn.now()) - .delete(); - - if (this.knex.client.config.client === 'sqlite3') { - const mutexes = await mutexesQuery; - if (mutexes > 0) { - this.logger.warn(`${mutexes} mutex locks timed out and were lost`); - } - } else { - const mutexes = await mutexesQuery.returning(['id']); - for (const { id } of mutexes) { - this.logger.warn(`Mutex lock timed out and was lost: ${id}`); - } - } - const tasksQuery = this.knex(DB_TASKS_TABLE) .where('current_run_expires_at', '<', this.knex.fn.now()) .delete(); diff --git a/packages/backend-tasks/src/tasks/TaskManager.test.ts b/packages/backend-tasks/src/tasks/TaskManager.test.ts index 87efb5cfed..cf6e808ee3 100644 --- a/packages/backend-tasks/src/tasks/TaskManager.test.ts +++ b/packages/backend-tasks/src/tasks/TaskManager.test.ts @@ -43,10 +43,14 @@ describe('TaskManager', () => { const database = await createDatabase(databaseId); const manager = new TaskManager(database, logger).forPlugin('test'); - const lock = await manager.acquireLock('lock1', { - timeout: Duration.fromMillis(5000), - }); - expect(lock.acquired).toBe(true); + const task = await manager.scheduleTask( + 'task1', + { + timeout: Duration.fromMillis(5000), + }, + () => {}, + ); + expect(task.unschedule).toBeDefined(); }, ); }); diff --git a/packages/backend-tasks/src/tasks/index.ts b/packages/backend-tasks/src/tasks/index.ts index 9f86f8fc64..d16a30b6c9 100644 --- a/packages/backend-tasks/src/tasks/index.ts +++ b/packages/backend-tasks/src/tasks/index.ts @@ -15,4 +15,4 @@ */ export { TaskManager } from './TaskManager'; -export type { LockOptions, PluginTaskManager, TaskOptions } from './types'; +export type { PluginTaskManager, TaskOptions } from './types'; diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index a8d356bed9..eed652093d 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -17,19 +17,6 @@ import { Duration } from 'luxon'; import { z } from 'zod'; -/** - * Options that apply to the acquiral of a given lock. - * - * @public - */ -export interface LockOptions { - /** - * The maximum amount of time that the lock can be held, before it's - * considered timed out and gets auto-released by the framework. - */ - timeout: Duration; -} - /** * Options that apply to the invocation of a given task. * @@ -82,26 +69,6 @@ export interface TaskOptions { * @public */ export interface PluginTaskManager { - /** - * Attempts to acquire an exclusive lock. - * - * A lock can only be held by one party at a time. Any subsequent attempts to - * acquire the lock will fail, unless the timeout period has been exceeded or - * the lock was released by the previous holder. - * - * @param id - A unique ID (within the scope of the plugin) for a lock - * @param options - Options for the lock - * @returns The result of the lock attempt. If it was successfully acquired, - * you should remember to call its `release` method as soon as you - * are done with the lock. - */ - acquireLock( - id: string, - options: LockOptions, - ): Promise< - { acquired: false } | { acquired: true; release(): Promise } - >; - /** * Schedules a task function for coordinated exclusive invocation across * workers. From 5d602f8e292e8717dafecfccefc2ea4377f72c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 16:56:26 +0200 Subject: [PATCH 102/181] get rid of the ts-ignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/src/tasks/CancelToken.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend-tasks/src/tasks/CancelToken.ts b/packages/backend-tasks/src/tasks/CancelToken.ts index 9ea9802e82..190a5d6562 100644 --- a/packages/backend-tasks/src/tasks/CancelToken.ts +++ b/packages/backend-tasks/src/tasks/CancelToken.ts @@ -15,7 +15,6 @@ */ export class CancelToken { - // @ts-ignore: is actually assigned by the Promise constructor #cancel: () => void; #isCancelled: boolean; #cancelPromise: Promise; @@ -26,6 +25,8 @@ export class CancelToken { private constructor() { this.#isCancelled = false; + + this.#cancel = () => {}; // Avoids a TS warning this.#cancelPromise = new Promise(resolve => { this.#cancel = () => { this.#isCancelled = true; From d252c983b575e7e8808b16ac2b39cfa9389dd2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 16:59:20 +0200 Subject: [PATCH 103/181] use dashes in not-ready-yet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/src/tasks/TaskWorker.test.ts | 2 +- packages/backend-tasks/src/tasks/TaskWorker.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend-tasks/src/tasks/TaskWorker.test.ts b/packages/backend-tasks/src/tasks/TaskWorker.test.ts index 2a7de2220a..5e0152f776 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.test.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.test.ts @@ -90,7 +90,7 @@ describe('TaskWorker', () => { }); await expect(worker.findReadyTask()).resolves.toEqual({ - result: 'not ready yet', + result: 'not-ready-yet', }); waitForExpect(async () => { diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index 3fd1f30716..2289975230 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -91,14 +91,14 @@ export class TaskWorker { * @returns The outcome of the attempt */ async runOnce(): Promise< - | { result: 'not ready yet' } + | { result: 'not-ready-yet' } | { result: 'abort' } | { result: 'failed' } | { result: 'completed' } > { const findResult = await this.findReadyTask(); if ( - findResult.result === 'not ready yet' || + findResult.result === 'not-ready-yet' || findResult.result === 'abort' ) { return findResult; @@ -109,7 +109,7 @@ export class TaskWorker { const claimed = await this.tryClaimTask(ticket, taskSettings); if (!claimed) { - return { result: 'not ready yet' }; + return { result: 'not-ready-yet' }; } try { @@ -165,7 +165,7 @@ export class TaskWorker { * Check if the task is ready to run */ async findReadyTask(): Promise< - | { result: 'not ready yet' } + | { result: 'not-ready-yet' } | { result: 'abort' } | { result: 'ready'; settings: TaskSettingsV1 } > { @@ -189,7 +189,7 @@ export class TaskWorker { ); return { result: 'abort' }; } else if (!row.ready) { - return { result: 'not ready yet' }; + return { result: 'not-ready-yet' }; } try { From aeaa2fe3e6fe7b29334c54d8a4e27c708b18a844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 17:01:24 +0200 Subject: [PATCH 104/181] log the task id as well when unable to parse settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/src/tasks/TaskWorker.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index 2289975230..dc48f843aa 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -197,9 +197,8 @@ export class TaskWorker { return { result: 'ready', settings }; } catch (e) { this.logger.info( - 'No longer able to parse task settings; aborting and assuming that a ' + - 'newer version of the task has been issued and being handled by ' + - `other workers, ${e}`, + `Task "${this.taskId}" is no longer able to parse task settings; aborting and assuming that a ` + + `newer version of the task has been issued and being handled by other workers, ${e}`, ); return { result: 'abort' }; } From cc3b6c25aa60e1ea6bee1cc20a456e581d944ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 19:08:59 +0200 Subject: [PATCH 105/181] make timeout and frequency mandatory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../src/tasks/PluginTaskManagerImpl.test.ts | 10 +++- .../src/tasks/PluginTaskManagerImpl.ts | 4 +- .../src/tasks/TaskManager.test.ts | 1 + .../src/tasks/TaskWorker.test.ts | 52 +++++++------------ .../backend-tasks/src/tasks/TaskWorker.ts | 14 ----- packages/backend-tasks/src/tasks/types.ts | 6 +-- 6 files changed, 34 insertions(+), 53 deletions(-) diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts index 1d1c89cb6b..193dd2a95d 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts @@ -16,6 +16,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { Duration } from 'luxon'; import waitForExpect from 'wait-for-expect'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; import { PluginTaskManagerImpl } from './PluginTaskManagerImpl'; @@ -44,7 +45,14 @@ describe('PluginTaskManagerImpl', () => { const { manager } = await init(databaseId); const fn = jest.fn(); - const { unschedule } = await manager.scheduleTask('task1', {}, fn); + const { unschedule } = await manager.scheduleTask( + 'task1', + { + timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), + }, + fn, + ); await waitForExpect(() => { expect(fn).toBeCalled(); diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts index 7859f4ee23..1b890a69c8 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts @@ -42,8 +42,8 @@ export class PluginTaskManagerImpl implements PluginTaskManager { await task.start({ version: 1, initialDelayDuration: options.initialDelay?.toISO(), - recurringAtMostEveryDuration: options.frequency?.toISO(), - timeoutAfterDuration: options.timeout?.toISO(), + recurringAtMostEveryDuration: options.frequency.toISO(), + timeoutAfterDuration: options.timeout.toISO(), }); return { diff --git a/packages/backend-tasks/src/tasks/TaskManager.test.ts b/packages/backend-tasks/src/tasks/TaskManager.test.ts index cf6e808ee3..002db0b3de 100644 --- a/packages/backend-tasks/src/tasks/TaskManager.test.ts +++ b/packages/backend-tasks/src/tasks/TaskManager.test.ts @@ -47,6 +47,7 @@ describe('TaskManager', () => { 'task1', { timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), }, () => {}, ); diff --git a/packages/backend-tasks/src/tasks/TaskWorker.test.ts b/packages/backend-tasks/src/tasks/TaskWorker.test.ts index 5e0152f776..2f977ca639 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.test.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.test.ts @@ -34,7 +34,7 @@ describe('TaskWorker', () => { }); it.each(databases.eachSupportedId())( - 'can run a single task to completion, %p', + 'goes through the expected states, %p', async databaseId => { const knex = await databases.init(databaseId); await migrateBackendTasks(knex); @@ -44,32 +44,9 @@ describe('TaskWorker', () => { ); const settings: TaskSettingsV1 = { version: 1, - }; - - const worker = new TaskWorker('task1', fn, knex, logger); - await worker.start(settings); - - waitForExpect(() => { - expect(fn).toBeCalledTimes(1); - }); - }, - 60_000, - ); - - it.each(databases.eachSupportedId())( - 'goes through the expected states for a single run, %p', - async databaseId => { - const knex = await databases.init(databaseId); - await migrateBackendTasks(knex); - - const fn = jest.fn( - async () => new Promise(resolve => setTimeout(resolve, 50)), - ); - const settings: TaskSettingsV1 = { - version: 1, - initialDelayDuration: Duration.fromObject({ seconds: 1 }).toISO(), - recurringAtMostEveryDuration: undefined, - timeoutAfterDuration: undefined, + initialDelayDuration: Duration.fromMillis(1000).toISO(), + recurringAtMostEveryDuration: Duration.fromMillis(2000).toISO(), + timeoutAfterDuration: Duration.fromMillis(60000).toISO(), }; const worker = new TaskWorker('task1', fn, knex, logger); @@ -87,6 +64,8 @@ describe('TaskWorker', () => { expect(JSON.parse(row.settings_json)).toEqual({ version: 1, initialDelayDuration: 'PT1S', + recurringAtMostEveryDuration: 'PT2S', + timeoutAfterDuration: 'PT60S', }); await expect(worker.findReadyTask()).resolves.toEqual({ @@ -117,7 +96,7 @@ describe('TaskWorker', () => { id: 'task1', current_run_ticket: 'ticket', current_run_started_at: expect.anything(), - current_run_expires_at: null, + current_run_expires_at: expect.anything(), }), ); @@ -126,7 +105,14 @@ describe('TaskWorker', () => { ); row = (await knex(DB_TASKS_TABLE))[0]; - expect(row).toBeUndefined(); + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }), + ); }, 60_000, ); @@ -142,7 +128,7 @@ describe('TaskWorker', () => { version: 1, initialDelayDuration: undefined, recurringAtMostEveryDuration: Duration.fromMillis(0).toISO(), - timeoutAfterDuration: undefined, + timeoutAfterDuration: Duration.fromMillis(60000).toISO(), }; const worker = new TaskWorker('task1', fn, knex, logger); @@ -167,6 +153,7 @@ describe('TaskWorker', () => { const settings: TaskSettingsV1 = { version: 1, recurringAtMostEveryDuration: Duration.fromMillis(0).toISO(), + timeoutAfterDuration: Duration.fromMillis(60000).toISO(), }; const worker = new TaskWorker('task1', fn, knex, logger); @@ -183,7 +170,7 @@ describe('TaskWorker', () => { id: 'task1', current_run_ticket: 'ticket', current_run_started_at: expect.anything(), - current_run_expires_at: null, + current_run_expires_at: expect.anything(), }), ); @@ -201,7 +188,7 @@ describe('TaskWorker', () => { id: 'task1', current_run_ticket: 'stolen', current_run_started_at: expect.anything(), - current_run_expires_at: null, + current_run_expires_at: expect.anything(), }), ); }, @@ -218,6 +205,7 @@ describe('TaskWorker', () => { const settings: TaskSettingsV1 = { version: 1, recurringAtMostEveryDuration: Duration.fromMillis(0).toISO(), + timeoutAfterDuration: Duration.fromMillis(60000).toISO(), }; const worker1 = new TaskWorker('task1', fn, knex, logger); diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index dc48f843aa..881a013360 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -68,9 +68,6 @@ export class TaskWorker { if (runResult.result === 'abort') { break; } - if (!settings.recurringAtMostEveryDuration) { - break; - } await this.sleep(WORK_CHECK_FREQUENCY); } @@ -240,17 +237,6 @@ export class TaskWorker { ): Promise { const { recurringAtMostEveryDuration } = settings; - // If this is not a recurring task, and we still have the current run - // ticket, delete it from the table - if (recurringAtMostEveryDuration === undefined) { - const rows = await this.knex(DB_TASKS_TABLE) - .where('id', '=', this.taskId) - .where('current_run_ticket', '=', ticket) - .delete(); - - return rows === 1; - } - // We make an effort to keep the datetime calculations in the database // layer, making sure to not have to perform conversions back and forth and // leaning on the database as a central clock source diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index eed652093d..989414ec12 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -31,7 +31,7 @@ export interface TaskOptions { * If no value is given for this field then there is no timeout. This is * potentially dangerous. */ - timeout?: Duration; + timeout: Duration; /** * The amount of time that should pass between task invocation starts. @@ -48,7 +48,7 @@ export interface TaskOptions { * If no value is given for this field then the task will only be invoked * once (on any worker) and then unscheduled automatically. */ - frequency?: Duration; + frequency: Duration; /** * The amount of time that should pass before the first invocation happens. @@ -107,11 +107,9 @@ export const taskSettingsV1Schema = z.object({ .refine(isValidOptionalDurationString, { message: 'Invalid duration' }), recurringAtMostEveryDuration: z .string() - .optional() .refine(isValidOptionalDurationString, { message: 'Invalid duration' }), timeoutAfterDuration: z .string() - .optional() .refine(isValidOptionalDurationString, { message: 'Invalid duration' }), }); From b91fe07309954fa6a18fa1934e3a5e9d9dffd233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 21:00:07 +0200 Subject: [PATCH 106/181] use a proper AbortController instead of the home baked one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/package.json | 1 + .../backend-tasks/src/tasks/CancelToken.ts | 49 ------------------- .../src/tasks/PluginTaskManagerJanitor.ts | 26 ++++------ .../backend-tasks/src/tasks/TaskWorker.ts | 29 ++++------- packages/backend-tasks/src/tasks/util.ts | 32 ++++++++++++ 5 files changed, 51 insertions(+), 86 deletions(-) delete mode 100644 packages/backend-tasks/src/tasks/CancelToken.ts diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index a5374d7b6e..dcb5922cf1 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -37,6 +37,7 @@ "knex": "^0.95.1", "lodash": "^4.17.21", "luxon": "^2.0.2", + "node-abort-controller": "^3.0.1", "uuid": "^8.0.0", "winston": "^3.2.1", "zod": "^3.9.5" diff --git a/packages/backend-tasks/src/tasks/CancelToken.ts b/packages/backend-tasks/src/tasks/CancelToken.ts deleted file mode 100644 index 190a5d6562..0000000000 --- a/packages/backend-tasks/src/tasks/CancelToken.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export class CancelToken { - #cancel: () => void; - #isCancelled: boolean; - #cancelPromise: Promise; - - static create(): CancelToken { - return new CancelToken(); - } - - private constructor() { - this.#isCancelled = false; - - this.#cancel = () => {}; // Avoids a TS warning - this.#cancelPromise = new Promise(resolve => { - this.#cancel = () => { - this.#isCancelled = true; - resolve(); - }; - }); - } - - cancel(): void { - this.#cancel(); - } - - get isCancelled(): boolean { - return this.#isCancelled; - } - - get promise(): Promise { - return this.#cancelPromise; - } -} diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts index 2912041b3f..ec65eadf1e 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts @@ -16,9 +16,10 @@ import { Knex } from 'knex'; import { Duration } from 'luxon'; +import { AbortController, AbortSignal } from 'node-abort-controller'; import { Logger } from 'winston'; import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; -import { CancelToken } from './CancelToken'; +import { sleep } from './util'; /** * Makes sure to auto-expire and clean up things that time out or for other @@ -28,7 +29,8 @@ export class PluginTaskManagerJanitor { private readonly knex: Knex; private readonly waitBetweenRuns: Duration; private readonly logger: Logger; - private readonly cancelToken: CancelToken; + private readonly abortController: AbortController; + private readonly abortSignal: AbortSignal; constructor(options: { knex: Knex; @@ -38,23 +40,24 @@ export class PluginTaskManagerJanitor { this.knex = options.knex; this.waitBetweenRuns = options.waitBetweenRuns; this.logger = options.logger; - this.cancelToken = CancelToken.create(); + this.abortController = new AbortController(); + this.abortSignal = this.abortController.signal; } async start() { - while (!this.cancelToken.isCancelled) { + while (!this.abortSignal.aborted) { try { await this.runOnce(); } catch (e) { this.logger.warn(`Error while performing janitorial tasks, ${e}`); } - await this.sleep(this.waitBetweenRuns); + await sleep(this.waitBetweenRuns, this.abortSignal); } } async stop() { - this.cancelToken.cancel(); + this.abortController.abort(); } private async runOnce() { @@ -79,15 +82,4 @@ export class PluginTaskManagerJanitor { } } } - - /** - * Sleeps for the given duration, but aborts sooner if the cancel token - * triggers. - */ - private async sleep(duration: Duration) { - await Promise.race([ - new Promise(resolve => setTimeout(resolve, duration.as('milliseconds'))), - this.cancelToken.promise, - ]); - } } diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index 881a013360..4b190c69c6 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -16,12 +16,12 @@ import { Knex } from 'knex'; import { Duration } from 'luxon'; +import { AbortController, AbortSignal } from 'node-abort-controller'; import { v4 as uuid } from 'uuid'; import { Logger } from 'winston'; import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; -import { CancelToken } from './CancelToken'; import { TaskSettingsV1, taskSettingsV1Schema } from './types'; -import { nowPlus } from './util'; +import { nowPlus, sleep } from './util'; const WORK_CHECK_FREQUENCY = Duration.fromObject({ seconds: 5 }); @@ -35,7 +35,8 @@ export class TaskWorker { private readonly fn: () => void | Promise; private readonly knex: Knex; private readonly logger: Logger; - private readonly cancelToken: CancelToken; + private readonly abortController: AbortController; + private readonly abortSignal: AbortSignal; constructor( taskId: string, @@ -47,7 +48,8 @@ export class TaskWorker { this.fn = fn; this.knex = knex; this.logger = logger; - this.cancelToken = CancelToken.create(); + this.abortController = new AbortController(); + this.abortSignal = this.abortController.signal; } async start(settings: TaskSettingsV1) { @@ -63,13 +65,13 @@ export class TaskWorker { (async () => { try { - while (!this.cancelToken.isCancelled) { + while (!this.abortSignal.aborted) { const runResult = await this.runOnce(); if (runResult.result === 'abort') { break; } - await this.sleep(WORK_CHECK_FREQUENCY); + await sleep(WORK_CHECK_FREQUENCY, this.abortSignal); } this.logger.info(`Task worker finished: ${this.taskId}`); } catch (e) { @@ -79,7 +81,7 @@ export class TaskWorker { } stop() { - this.cancelToken.cancel(); + this.abortController.abort(); } /** @@ -120,19 +122,6 @@ export class TaskWorker { return { result: 'completed' }; } - /** - * Sleep for the given duration, but abort sooner if the cancel token - * triggers. - * - * @param duration - The amount of time to sleep, at most - */ - private async sleep(duration: Duration): Promise { - await Promise.race([ - new Promise(resolve => setTimeout(resolve, duration.as('milliseconds'))), - this.cancelToken.promise, - ]); - } - /** * Perform the initial store of the task info */ diff --git a/packages/backend-tasks/src/tasks/util.ts b/packages/backend-tasks/src/tasks/util.ts index dd5c2d4b7c..72753a46b2 100644 --- a/packages/backend-tasks/src/tasks/util.ts +++ b/packages/backend-tasks/src/tasks/util.ts @@ -17,6 +17,7 @@ import { InputError } from '@backstage/errors'; import { Knex } from 'knex'; import { DateTime, Duration } from 'luxon'; +import { AbortSignal } from 'node-abort-controller'; // Keep the IDs compatible with e.g. Prometheus export function validateId(id: string) { @@ -43,3 +44,34 @@ export function nowPlus(duration: Duration | undefined, knex: Knex) { ? knex.raw(`datetime('now', ?)`, [`${seconds} seconds`]) : knex.raw(`now() + interval '${seconds} seconds'`); } + +/** + * Sleep for the given duration, but return sooner if the abort signal + * triggers. + * + * @param duration - The amount of time to sleep, at most + * @param abortSignal - An optional abort signal that short circuits the wait + */ +export async function sleep( + duration: Duration, + abortSignal?: AbortSignal, +): Promise { + if (abortSignal?.aborted) { + return; + } + + await new Promise(resolve => { + let timeoutHandle: NodeJS.Timeout | undefined = undefined; + + const done = () => { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + abortSignal?.removeEventListener('abort', done); + resolve(); + }; + + timeoutHandle = setTimeout(done, duration.as('milliseconds')); + abortSignal?.addEventListener('abort', done); + }); +} From d8c28d99cf597a45ad757b1bf40ba3077b514753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 21:11:19 +0200 Subject: [PATCH 107/181] make the original task definition into just one object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/README.md | 12 +++++----- packages/backend-tasks/api-report.md | 22 +++++++++---------- .../src/tasks/PluginTaskManagerImpl.test.ts | 12 +++++----- .../src/tasks/PluginTaskManagerImpl.ts | 20 ++++++++--------- .../src/tasks/TaskManager.test.ts | 14 +++++------- packages/backend-tasks/src/tasks/index.ts | 2 +- packages/backend-tasks/src/tasks/types.ts | 20 +++++++++++------ 7 files changed, 49 insertions(+), 53 deletions(-) diff --git a/packages/backend-tasks/README.md b/packages/backend-tasks/README.md index 71c54b0a7f..abb25079e3 100644 --- a/packages/backend-tasks/README.md +++ b/packages/backend-tasks/README.md @@ -19,15 +19,13 @@ import { TaskManager } from '@backstage/backend-tasks'; const manager = TaskManager.fromConfig(rootConfig).forPlugin('my-plugin'); -const { unschedule } = await manager.scheduleTask( - 'refresh-things', - { - frequency: Duration.fromObject({ minutes: 10 }), - }, - async () => { +const { unschedule } = await manager.scheduleTask({ + id: 'refresh-things', + frequency: Duration.fromObject({ minutes: 10 }), + fn: async () => { await entityProvider.run(); }, -); +}); ``` ## Documentation diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index e9d7884b43..f3aaf0e9bf 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -10,15 +10,20 @@ import { Logger as Logger_2 } from 'winston'; // @public export interface PluginTaskManager { - scheduleTask( - id: string, - options: TaskOptions, - fn: () => void | Promise, - ): Promise<{ + scheduleTask(task: TaskDefinition): Promise<{ unschedule: () => Promise; }>; } +// @public +export interface TaskDefinition { + fn: () => void | Promise; + frequency: Duration; + id: string; + initialDelay?: Duration; + timeout: Duration; +} + // @public export class TaskManager { constructor(databaseManager: DatabaseManager, logger: Logger_2); @@ -32,11 +37,4 @@ export class TaskManager { }, ): TaskManager; } - -// @public -export interface TaskOptions { - frequency?: Duration; - initialDelay?: Duration; - timeout?: Duration; -} ``` diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts index 193dd2a95d..bbb54a2aa0 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts @@ -45,14 +45,12 @@ describe('PluginTaskManagerImpl', () => { const { manager } = await init(databaseId); const fn = jest.fn(); - const { unschedule } = await manager.scheduleTask( - 'task1', - { - timeout: Duration.fromMillis(5000), - frequency: Duration.fromMillis(5000), - }, + const { unschedule } = await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), fn, - ); + }); await waitForExpect(() => { expect(fn).toBeCalled(); diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts index 1b890a69c8..fd543cf828 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts @@ -17,7 +17,7 @@ import { Knex } from 'knex'; import { Logger } from 'winston'; import { TaskWorker } from './TaskWorker'; -import { PluginTaskManager, TaskOptions } from './types'; +import { PluginTaskManager, TaskDefinition } from './types'; import { validateId } from './util'; /** @@ -30,25 +30,23 @@ export class PluginTaskManagerImpl implements PluginTaskManager { ) {} async scheduleTask( - id: string, - options: TaskOptions, - fn: () => void | Promise, + task: TaskDefinition, ): Promise<{ unschedule: () => Promise }> { - validateId(id); + validateId(task.id); const knex = await this.databaseFactory(); - const task = new TaskWorker(id, fn, knex, this.logger); - await task.start({ + const worker = new TaskWorker(task.id, task.fn, knex, this.logger); + await worker.start({ version: 1, - initialDelayDuration: options.initialDelay?.toISO(), - recurringAtMostEveryDuration: options.frequency.toISO(), - timeoutAfterDuration: options.timeout.toISO(), + initialDelayDuration: task.initialDelay?.toISO(), + recurringAtMostEveryDuration: task.frequency.toISO(), + timeoutAfterDuration: task.timeout.toISO(), }); return { async unschedule() { - await task.stop(); + await worker.stop(); }, }; } diff --git a/packages/backend-tasks/src/tasks/TaskManager.test.ts b/packages/backend-tasks/src/tasks/TaskManager.test.ts index 002db0b3de..c81cfc8ee9 100644 --- a/packages/backend-tasks/src/tasks/TaskManager.test.ts +++ b/packages/backend-tasks/src/tasks/TaskManager.test.ts @@ -43,14 +43,12 @@ describe('TaskManager', () => { const database = await createDatabase(databaseId); const manager = new TaskManager(database, logger).forPlugin('test'); - const task = await manager.scheduleTask( - 'task1', - { - timeout: Duration.fromMillis(5000), - frequency: Duration.fromMillis(5000), - }, - () => {}, - ); + const task = await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), + fn: () => {}, + }); expect(task.unschedule).toBeDefined(); }, ); diff --git a/packages/backend-tasks/src/tasks/index.ts b/packages/backend-tasks/src/tasks/index.ts index d16a30b6c9..e4fe1ad2a3 100644 --- a/packages/backend-tasks/src/tasks/index.ts +++ b/packages/backend-tasks/src/tasks/index.ts @@ -15,4 +15,4 @@ */ export { TaskManager } from './TaskManager'; -export type { PluginTaskManager, TaskOptions } from './types'; +export type { PluginTaskManager, TaskDefinition } from './types'; diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index 989414ec12..e2dfbea0a1 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -22,7 +22,17 @@ import { z } from 'zod'; * * @public */ -export interface TaskOptions { +export interface TaskDefinition { + /** + * A unique ID (within the scope of the plugin) for the task. + */ + id: string; + + /** + * The actual task function to be invoked regularly. + */ + fn: () => void | Promise; + /** * The maximum amount of time that a single task invocation can take, before * it's considered timed out and gets "released" such that a new invocation @@ -77,17 +87,13 @@ export interface PluginTaskManager { * its options are just overwritten with the given options, and things * continue from there. * - * @param id - A unique ID (within the scope of the plugin) for the task - * @param options - Options for the task - * @param fn - The actual task function to be invoked + * @param definition - The task definition * @returns An `unschedule` function that can be used to stop the task * invocations later on. This removes the task entirely from storage * and stops its invocations across all workers. */ scheduleTask( - id: string, - options: TaskOptions, - fn: () => void | Promise, + task: TaskDefinition, ): Promise<{ unschedule: () => Promise }>; } From 9bee0b9f719abb758cf58107aecbe1738edaa5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 21:55:55 +0200 Subject: [PATCH 108/181] make it possible to abort tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/api-report.md | 10 ++++- .../backend-tasks/src/tasks/TaskWorker.ts | 34 ++++++++------- packages/backend-tasks/src/tasks/index.ts | 2 +- packages/backend-tasks/src/tasks/types.ts | 7 +++- packages/backend-tasks/src/tasks/util.test.ts | 42 +++++++++++++++++++ packages/backend-tasks/src/tasks/util.ts | 30 ++++++++++++- 6 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 packages/backend-tasks/src/tasks/util.test.ts diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index f3aaf0e9bf..68c4358348 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AbortSignal as AbortSignal_2 } from 'node-abort-controller'; import { Config } from '@backstage/config'; import { DatabaseManager } from '@backstage/backend-common'; import { Duration } from 'luxon'; @@ -17,13 +18,20 @@ export interface PluginTaskManager { // @public export interface TaskDefinition { - fn: () => void | Promise; + fn: TaskFunction; frequency: Duration; id: string; initialDelay?: Duration; timeout: Duration; } +// Warning: (ae-missing-release-tag) "TaskFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type TaskFunction = + | ((abortSignal: AbortSignal_2) => void | Promise) + | (() => void | Promise); + // @public export class TaskManager { constructor(databaseManager: DatabaseManager, logger: Logger_2); diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index 4b190c69c6..9be1f2c1dc 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -16,12 +16,12 @@ import { Knex } from 'knex'; import { Duration } from 'luxon'; -import { AbortController, AbortSignal } from 'node-abort-controller'; +import { AbortController } from 'node-abort-controller'; import { v4 as uuid } from 'uuid'; import { Logger } from 'winston'; import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; -import { TaskSettingsV1, taskSettingsV1Schema } from './types'; -import { nowPlus, sleep } from './util'; +import { TaskFunction, TaskSettingsV1, taskSettingsV1Schema } from './types'; +import { delegateAbortController, nowPlus, sleep } from './util'; const WORK_CHECK_FREQUENCY = Duration.fromObject({ seconds: 5 }); @@ -32,24 +32,17 @@ const WORK_CHECK_FREQUENCY = Duration.fromObject({ seconds: 5 }); */ export class TaskWorker { private readonly taskId: string; - private readonly fn: () => void | Promise; + private readonly fn: TaskFunction; private readonly knex: Knex; private readonly logger: Logger; private readonly abortController: AbortController; - private readonly abortSignal: AbortSignal; - constructor( - taskId: string, - fn: () => void | Promise, - knex: Knex, - logger: Logger, - ) { + constructor(taskId: string, fn: TaskFunction, knex: Knex, logger: Logger) { this.taskId = taskId; this.fn = fn; this.knex = knex; this.logger = logger; this.abortController = new AbortController(); - this.abortSignal = this.abortController.signal; } async start(settings: TaskSettingsV1) { @@ -65,13 +58,13 @@ export class TaskWorker { (async () => { try { - while (!this.abortSignal.aborted) { + while (!this.abortController.signal.aborted) { const runResult = await this.runOnce(); if (runResult.result === 'abort') { break; } - await sleep(WORK_CHECK_FREQUENCY, this.abortSignal); + await sleep(WORK_CHECK_FREQUENCY, this.abortController.signal); } this.logger.info(`Task worker finished: ${this.taskId}`); } catch (e) { @@ -111,11 +104,22 @@ export class TaskWorker { return { result: 'not-ready-yet' }; } + // Abort the task execution either if the worker is stopped, or if the + // task timeout is hit + const taskAbortController = delegateAbortController( + this.abortController.signal, + ); + const timeoutHandle = setTimeout(() => { + taskAbortController.abort(); + }, Duration.fromISO(taskSettings.timeoutAfterDuration).as('milliseconds')); + try { - await this.fn(); + await this.fn(taskAbortController.signal); } catch (e) { await this.tryReleaseTask(ticket, taskSettings); return { result: 'failed' }; + } finally { + clearTimeout(timeoutHandle); } await this.tryReleaseTask(ticket, taskSettings); diff --git a/packages/backend-tasks/src/tasks/index.ts b/packages/backend-tasks/src/tasks/index.ts index e4fe1ad2a3..bb59b3a3e2 100644 --- a/packages/backend-tasks/src/tasks/index.ts +++ b/packages/backend-tasks/src/tasks/index.ts @@ -15,4 +15,4 @@ */ export { TaskManager } from './TaskManager'; -export type { PluginTaskManager, TaskDefinition } from './types'; +export type { PluginTaskManager, TaskDefinition, TaskFunction } from './types'; diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index e2dfbea0a1..3be6591887 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -15,8 +15,13 @@ */ import { Duration } from 'luxon'; +import { AbortSignal } from 'node-abort-controller'; import { z } from 'zod'; +export type TaskFunction = + | ((abortSignal: AbortSignal) => void | Promise) + | (() => void | Promise); + /** * Options that apply to the invocation of a given task. * @@ -31,7 +36,7 @@ export interface TaskDefinition { /** * The actual task function to be invoked regularly. */ - fn: () => void | Promise; + fn: TaskFunction; /** * The maximum amount of time that a single task invocation can take, before diff --git a/packages/backend-tasks/src/tasks/util.test.ts b/packages/backend-tasks/src/tasks/util.test.ts new file mode 100644 index 0000000000..75fd9f3673 --- /dev/null +++ b/packages/backend-tasks/src/tasks/util.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AbortController } from 'node-abort-controller'; +import { delegateAbortController } from './util'; + +describe('util', () => { + describe('delegateAbortController', () => { + it('inherits parent abort state', () => { + const parent = new AbortController(); + const child = delegateAbortController(parent.signal); + expect(parent.signal.aborted).toBe(false); + expect(child.signal.aborted).toBe(false); + parent.abort(); + expect(parent.signal.aborted).toBe(true); + expect(child.signal.aborted).toBe(true); + }); + + it('does not inherit from child to parent', () => { + const parent = new AbortController(); + const child = delegateAbortController(parent.signal); + expect(parent.signal.aborted).toBe(false); + expect(child.signal.aborted).toBe(false); + child.abort(); + expect(parent.signal.aborted).toBe(false); + expect(child.signal.aborted).toBe(true); + }); + }); +}); diff --git a/packages/backend-tasks/src/tasks/util.ts b/packages/backend-tasks/src/tasks/util.ts index 72753a46b2..378224ed57 100644 --- a/packages/backend-tasks/src/tasks/util.ts +++ b/packages/backend-tasks/src/tasks/util.ts @@ -17,7 +17,7 @@ import { InputError } from '@backstage/errors'; import { Knex } from 'knex'; import { DateTime, Duration } from 'luxon'; -import { AbortSignal } from 'node-abort-controller'; +import { AbortController, AbortSignal } from 'node-abort-controller'; // Keep the IDs compatible with e.g. Prometheus export function validateId(id: string) { @@ -75,3 +75,31 @@ export async function sleep( abortSignal?.addEventListener('abort', done); }); } + +/** + * Creates a new AbortController that, in addition to working as a regular + * standalone controller, also gets aborted if the given parent signal + * reaches aborted state. + * + * @param parent - The "parent" signal that can trigger the delegate + */ +export function delegateAbortController(parent: AbortSignal): AbortController { + const delegate = new AbortController(); + + if (parent.aborted) { + delegate.abort(); + } else { + const onParentAborted = () => { + delegate.abort(); + }; + + const onChildAborted = () => { + parent.removeEventListener('abort', onParentAborted); + }; + + parent.addEventListener('abort', onParentAborted, { once: true }); + delegate.signal.addEventListener('abort', onChildAborted, { once: true }); + } + + return delegate; +} From 26cf7f631d7a61004c76806b762b624555c80be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 22:40:52 +0200 Subject: [PATCH 109/181] final cleanup, removing unschedule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/README.md | 2 +- packages/backend-tasks/api-report.md | 5 ++-- .../migrations/20210928160613_init.js | 2 +- packages/backend-tasks/src/database/tables.ts | 2 +- .../src/tasks/PluginTaskManagerImpl.test.ts | 4 +-- .../src/tasks/PluginTaskManagerImpl.ts | 25 ++++++++----------- .../src/tasks/PluginTaskManagerJanitor.ts | 24 ++++++++---------- .../src/tasks/TaskManager.test.ts | 11 +++++--- .../backend-tasks/src/tasks/TaskWorker.ts | 24 +++++++----------- packages/backend-tasks/src/tasks/types.ts | 13 +++++----- packages/backend-tasks/src/tasks/util.ts | 24 ++++++++++-------- 11 files changed, 65 insertions(+), 71 deletions(-) diff --git a/packages/backend-tasks/README.md b/packages/backend-tasks/README.md index abb25079e3..e98c997af0 100644 --- a/packages/backend-tasks/README.md +++ b/packages/backend-tasks/README.md @@ -19,7 +19,7 @@ import { TaskManager } from '@backstage/backend-tasks'; const manager = TaskManager.fromConfig(rootConfig).forPlugin('my-plugin'); -const { unschedule } = await manager.scheduleTask({ +await manager.scheduleTask({ id: 'refresh-things', frequency: Duration.fromObject({ minutes: 10 }), fn: async () => { diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 68c4358348..8f7209b9ed 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -11,9 +11,7 @@ import { Logger as Logger_2 } from 'winston'; // @public export interface PluginTaskManager { - scheduleTask(task: TaskDefinition): Promise<{ - unschedule: () => Promise; - }>; + scheduleTask(task: TaskDefinition): Promise; } // @public @@ -22,6 +20,7 @@ export interface TaskDefinition { frequency: Duration; id: string; initialDelay?: Duration; + signal?: AbortSignal_2; timeout: Duration; } diff --git a/packages/backend-tasks/migrations/20210928160613_init.js b/packages/backend-tasks/migrations/20210928160613_init.js index c5028e2e93..cd812b59e8 100644 --- a/packages/backend-tasks/migrations/20210928160613_init.js +++ b/packages/backend-tasks/migrations/20210928160613_init.js @@ -36,7 +36,7 @@ exports.up = async function up(knex) { .comment('JSON serialized object with properties for this task'); table .dateTime('next_run_start_at') - .nullable() + .notNullable() .comment('The next time that the task should be started'); table .text('current_run_ticket') diff --git a/packages/backend-tasks/src/database/tables.ts b/packages/backend-tasks/src/database/tables.ts index 4777c4e60c..63aad6e42a 100644 --- a/packages/backend-tasks/src/database/tables.ts +++ b/packages/backend-tasks/src/database/tables.ts @@ -20,7 +20,7 @@ export const DB_TASKS_TABLE = 'backstage_backend_tasks__tasks'; export type DbTasksRow = { id: string; settings_json: string; - next_run_start_at?: Date | string; + next_run_start_at: Date; current_run_ticket?: string; current_run_started_at?: Date | string; current_run_expires_at?: Date | string; diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts index bbb54a2aa0..27fd1f9f3b 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts @@ -45,7 +45,7 @@ describe('PluginTaskManagerImpl', () => { const { manager } = await init(databaseId); const fn = jest.fn(); - const { unschedule } = await manager.scheduleTask({ + await manager.scheduleTask({ id: 'task1', timeout: Duration.fromMillis(5000), frequency: Duration.fromMillis(5000), @@ -55,8 +55,6 @@ describe('PluginTaskManagerImpl', () => { await waitForExpect(() => { expect(fn).toBeCalled(); }); - - await unschedule(); }, ); }); diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts index fd543cf828..497a58b348 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts @@ -29,25 +29,22 @@ export class PluginTaskManagerImpl implements PluginTaskManager { private readonly logger: Logger, ) {} - async scheduleTask( - task: TaskDefinition, - ): Promise<{ unschedule: () => Promise }> { + async scheduleTask(task: TaskDefinition): Promise { validateId(task.id); const knex = await this.databaseFactory(); const worker = new TaskWorker(task.id, task.fn, knex, this.logger); - await worker.start({ - version: 1, - initialDelayDuration: task.initialDelay?.toISO(), - recurringAtMostEveryDuration: task.frequency.toISO(), - timeoutAfterDuration: task.timeout.toISO(), - }); - - return { - async unschedule() { - await worker.stop(); + await worker.start( + { + version: 1, + initialDelayDuration: task.initialDelay?.toISO(), + recurringAtMostEveryDuration: task.frequency.toISO(), + timeoutAfterDuration: task.timeout.toISO(), }, - }; + { + signal: task.signal, + }, + ); } } diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts index ec65eadf1e..6594243243 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts @@ -16,7 +16,7 @@ import { Knex } from 'knex'; import { Duration } from 'luxon'; -import { AbortController, AbortSignal } from 'node-abort-controller'; +import { AbortSignal } from 'node-abort-controller'; import { Logger } from 'winston'; import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; import { sleep } from './util'; @@ -29,8 +29,6 @@ export class PluginTaskManagerJanitor { private readonly knex: Knex; private readonly waitBetweenRuns: Duration; private readonly logger: Logger; - private readonly abortController: AbortController; - private readonly abortSignal: AbortSignal; constructor(options: { knex: Knex; @@ -40,26 +38,20 @@ export class PluginTaskManagerJanitor { this.knex = options.knex; this.waitBetweenRuns = options.waitBetweenRuns; this.logger = options.logger; - this.abortController = new AbortController(); - this.abortSignal = this.abortController.signal; } - async start() { - while (!this.abortSignal.aborted) { + async start(abortSignal?: AbortSignal) { + while (!abortSignal?.aborted) { try { await this.runOnce(); } catch (e) { this.logger.warn(`Error while performing janitorial tasks, ${e}`); } - await sleep(this.waitBetweenRuns, this.abortSignal); + await sleep(this.waitBetweenRuns, abortSignal); } } - async stop() { - this.abortController.abort(); - } - private async runOnce() { // SQLite currently (Oct 1 2021) returns a number for returning() // statements, effectively ignoring them and instead returning the outcome @@ -68,9 +60,15 @@ export class PluginTaskManagerJanitor { // https://github.com/knex/knex/issues/4370 // https://github.com/mapbox/node-sqlite3/issues/1453 + const dbNull = this.knex.raw('null'); + const tasksQuery = this.knex(DB_TASKS_TABLE) .where('current_run_expires_at', '<', this.knex.fn.now()) - .delete(); + .update({ + current_run_ticket: dbNull, + current_run_started_at: dbNull, + current_run_expires_at: dbNull, + }); if (this.knex.client.config.client === 'sqlite3') { const tasks = await tasksQuery; diff --git a/packages/backend-tasks/src/tasks/TaskManager.test.ts b/packages/backend-tasks/src/tasks/TaskManager.test.ts index c81cfc8ee9..43fe79709c 100644 --- a/packages/backend-tasks/src/tasks/TaskManager.test.ts +++ b/packages/backend-tasks/src/tasks/TaskManager.test.ts @@ -18,6 +18,7 @@ import { DatabaseManager, getVoidLogger } from '@backstage/backend-common'; import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Duration } from 'luxon'; import { TaskManager } from './TaskManager'; +import waitForExpect from 'wait-for-expect'; describe('TaskManager', () => { const logger = getVoidLogger(); @@ -42,14 +43,18 @@ describe('TaskManager', () => { async databaseId => { const database = await createDatabase(databaseId); const manager = new TaskManager(database, logger).forPlugin('test'); + const fn = jest.fn(); - const task = await manager.scheduleTask({ + await manager.scheduleTask({ id: 'task1', timeout: Duration.fromMillis(5000), frequency: Duration.fromMillis(5000), - fn: () => {}, + fn, + }); + + await waitForExpect(() => { + expect(fn).toBeCalled(); }); - expect(task.unschedule).toBeDefined(); }, ); }); diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index 9be1f2c1dc..991d97b956 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -16,7 +16,7 @@ import { Knex } from 'knex'; import { Duration } from 'luxon'; -import { AbortController } from 'node-abort-controller'; +import { AbortSignal } from 'node-abort-controller'; import { v4 as uuid } from 'uuid'; import { Logger } from 'winston'; import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; @@ -35,17 +35,15 @@ export class TaskWorker { private readonly fn: TaskFunction; private readonly knex: Knex; private readonly logger: Logger; - private readonly abortController: AbortController; constructor(taskId: string, fn: TaskFunction, knex: Knex, logger: Logger) { this.taskId = taskId; this.fn = fn; this.knex = knex; this.logger = logger; - this.abortController = new AbortController(); } - async start(settings: TaskSettingsV1) { + async start(settings: TaskSettingsV1, options?: { signal?: AbortSignal }) { try { await this.persistTask(settings); } catch (e) { @@ -58,13 +56,13 @@ export class TaskWorker { (async () => { try { - while (!this.abortController.signal.aborted) { - const runResult = await this.runOnce(); + while (!options?.signal?.aborted) { + const runResult = await this.runOnce(options?.signal); if (runResult.result === 'abort') { break; } - await sleep(WORK_CHECK_FREQUENCY, this.abortController.signal); + await sleep(WORK_CHECK_FREQUENCY, options?.signal); } this.logger.info(`Task worker finished: ${this.taskId}`); } catch (e) { @@ -73,16 +71,14 @@ export class TaskWorker { })(); } - stop() { - this.abortController.abort(); - } - /** * Makes a single attempt at running the task to completion, if ready. * * @returns The outcome of the attempt */ - async runOnce(): Promise< + async runOnce( + signal?: AbortSignal, + ): Promise< | { result: 'not-ready-yet' } | { result: 'abort' } | { result: 'failed' } @@ -106,9 +102,7 @@ export class TaskWorker { // Abort the task execution either if the worker is stopped, or if the // task timeout is hit - const taskAbortController = delegateAbortController( - this.abortController.signal, - ); + const taskAbortController = delegateAbortController(signal); const timeoutHandle = setTimeout(() => { taskAbortController.abort(); }, Duration.fromISO(taskSettings.timeoutAfterDuration).as('milliseconds')); diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index 3be6591887..4cd33fd3e1 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -38,6 +38,12 @@ export interface TaskDefinition { */ fn: TaskFunction; + /** + * An abort signal that, when triggered, will stop the recurring execution of + * the task. + */ + signal?: AbortSignal; + /** * The maximum amount of time that a single task invocation can take, before * it's considered timed out and gets "released" such that a new invocation @@ -93,13 +99,8 @@ export interface PluginTaskManager { * continue from there. * * @param definition - The task definition - * @returns An `unschedule` function that can be used to stop the task - * invocations later on. This removes the task entirely from storage - * and stops its invocations across all workers. */ - scheduleTask( - task: TaskDefinition, - ): Promise<{ unschedule: () => Promise }>; + scheduleTask(task: TaskDefinition): Promise; } function isValidOptionalDurationString(d: string | undefined): boolean { diff --git a/packages/backend-tasks/src/tasks/util.ts b/packages/backend-tasks/src/tasks/util.ts index 378224ed57..0509f29363 100644 --- a/packages/backend-tasks/src/tasks/util.ts +++ b/packages/backend-tasks/src/tasks/util.ts @@ -83,22 +83,24 @@ export async function sleep( * * @param parent - The "parent" signal that can trigger the delegate */ -export function delegateAbortController(parent: AbortSignal): AbortController { +export function delegateAbortController(parent?: AbortSignal): AbortController { const delegate = new AbortController(); - if (parent.aborted) { - delegate.abort(); - } else { - const onParentAborted = () => { + if (parent) { + if (parent.aborted) { delegate.abort(); - }; + } else { + const onParentAborted = () => { + delegate.abort(); + }; - const onChildAborted = () => { - parent.removeEventListener('abort', onParentAborted); - }; + const onChildAborted = () => { + parent.removeEventListener('abort', onParentAborted); + }; - parent.addEventListener('abort', onParentAborted, { once: true }); - delegate.signal.addEventListener('abort', onChildAborted, { once: true }); + parent.addEventListener('abort', onParentAborted, { once: true }); + delegate.signal.addEventListener('abort', onChildAborted, { once: true }); + } } return delegate; From b5c106ac4cdbe141c25b7ba6782f5ea32a7f96e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 22:49:23 +0200 Subject: [PATCH 110/181] fix api export and test timeouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/api-report.md | 4 +--- .../backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts | 1 + packages/backend-tasks/src/tasks/TaskManager.test.ts | 1 + packages/backend-tasks/src/tasks/types.ts | 8 ++++++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 8f7209b9ed..2be69f8788 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -24,9 +24,7 @@ export interface TaskDefinition { timeout: Duration; } -// Warning: (ae-missing-release-tag) "TaskFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type TaskFunction = | ((abortSignal: AbortSignal_2) => void | Promise) | (() => void | Promise); diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts index 27fd1f9f3b..cbf754d5cf 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts @@ -56,6 +56,7 @@ describe('PluginTaskManagerImpl', () => { expect(fn).toBeCalled(); }); }, + 60_000, ); }); }); diff --git a/packages/backend-tasks/src/tasks/TaskManager.test.ts b/packages/backend-tasks/src/tasks/TaskManager.test.ts index 43fe79709c..3f2d9dbc54 100644 --- a/packages/backend-tasks/src/tasks/TaskManager.test.ts +++ b/packages/backend-tasks/src/tasks/TaskManager.test.ts @@ -56,5 +56,6 @@ describe('TaskManager', () => { expect(fn).toBeCalled(); }); }, + 60_000, ); }); diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index 4cd33fd3e1..c717a17c3b 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -18,6 +18,14 @@ import { Duration } from 'luxon'; import { AbortSignal } from 'node-abort-controller'; import { z } from 'zod'; +/** + * A function that can be called as a scheduled task. + * + * It may optionally accept an abort signal argument. When the signal triggers, + * processing should abort and return as quickly as possible. + * + * @public + */ export type TaskFunction = | ((abortSignal: AbortSignal) => void | Promise) | (() => void | Promise); From 92d85b80e92edaba95732aac8b64993f2f6290f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 Oct 2021 11:08:19 +0200 Subject: [PATCH 111/181] more tests and a review comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/README.md | 1 + packages/backend-tasks/src/tasks/util.test.ts | 36 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/backend-tasks/README.md b/packages/backend-tasks/README.md index e98c997af0..f8a2009af0 100644 --- a/packages/backend-tasks/README.md +++ b/packages/backend-tasks/README.md @@ -16,6 +16,7 @@ then make use of its facilities as necessary: ```typescript import { TaskManager } from '@backstage/backend-tasks'; +import { Duration } from 'luxon'; const manager = TaskManager.fromConfig(rootConfig).forPlugin('my-plugin'); diff --git a/packages/backend-tasks/src/tasks/util.test.ts b/packages/backend-tasks/src/tasks/util.test.ts index 75fd9f3673..f74669a7f4 100644 --- a/packages/backend-tasks/src/tasks/util.test.ts +++ b/packages/backend-tasks/src/tasks/util.test.ts @@ -14,10 +14,44 @@ * limitations under the License. */ +import { Duration } from 'luxon'; import { AbortController } from 'node-abort-controller'; -import { delegateAbortController } from './util'; +import { delegateAbortController, sleep, validateId } from './util'; describe('util', () => { + describe('validateId', () => { + it.each(['a', 'a_b', 'ab123c_2'])( + 'accepts valid inputs, %p', + async input => { + expect(validateId(input)).toBeUndefined(); + }, + ); + + it.each(['', 'a!', 'A', 'a-b', 'a.b', '_a', 'a_', null, Symbol('a')])( + 'rejects invalid inputs, %p', + async input => { + expect(() => validateId(input as any)).toThrow(); + }, + ); + }); + + describe('sleep', () => { + it('finishes the wait as expected with no signal', async () => { + const ac = new AbortController(); + const start = Date.now(); + await sleep(Duration.fromObject({ seconds: 1 }), ac.signal); + expect(Date.now() - start).toBeGreaterThan(800); + }, 5_000); + + it('aborts properly on the signal', async () => { + const ac = new AbortController(); + const promise = sleep(Duration.fromObject({ seconds: 10 }), ac.signal); + ac.abort(); + await promise; + expect(true).toBe(true); + }, 1_000); + }); + describe('delegateAbortController', () => { it('inherits parent abort state', () => { const parent = new AbortController(); From 85cc6bd6aaac7524e7d62d1463c80ecde7b9eaca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 Oct 2021 15:25:37 +0200 Subject: [PATCH 112/181] rename to TaskScheduler to reduce collision risk with other concepts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/README.md | 8 ++++---- packages/backend-tasks/api-report.md | 9 +++++---- packages/backend-tasks/package.json | 2 +- packages/backend-tasks/src/index.ts | 2 +- ...est.ts => PluginTaskSchedulerImpl.test.ts} | 4 ++-- ...agerImpl.ts => PluginTaskSchedulerImpl.ts} | 4 ++-- ...nitor.ts => PluginTaskSchedulerJanitor.ts} | 2 +- ...kManager.test.ts => TaskScheduler.test.ts} | 6 +++--- .../{TaskManager.ts => TaskScheduler.ts} | 20 +++++++++---------- packages/backend-tasks/src/tasks/index.ts | 8 ++++++-- packages/backend-tasks/src/tasks/types.ts | 5 ++--- 11 files changed, 37 insertions(+), 33 deletions(-) rename packages/backend-tasks/src/tasks/{PluginTaskManagerImpl.test.ts => PluginTaskSchedulerImpl.test.ts} (94%) rename packages/backend-tasks/src/tasks/{PluginTaskManagerImpl.ts => PluginTaskSchedulerImpl.ts} (91%) rename packages/backend-tasks/src/tasks/{PluginTaskManagerJanitor.ts => PluginTaskSchedulerJanitor.ts} (98%) rename packages/backend-tasks/src/tasks/{TaskManager.test.ts => TaskScheduler.test.ts} (91%) rename packages/backend-tasks/src/tasks/{TaskManager.ts => TaskScheduler.ts} (80%) diff --git a/packages/backend-tasks/README.md b/packages/backend-tasks/README.md index f8a2009af0..d3c361c02c 100644 --- a/packages/backend-tasks/README.md +++ b/packages/backend-tasks/README.md @@ -1,6 +1,6 @@ # @backstage/backend-tasks -Common distributed task management / locking library for Backstage backends. +Common distributed task management for Backstage backends. ## Usage @@ -15,12 +15,12 @@ yarn add @backstage/backend-tasks then make use of its facilities as necessary: ```typescript -import { TaskManager } from '@backstage/backend-tasks'; +import { TaskScheduler } from '@backstage/backend-tasks'; import { Duration } from 'luxon'; -const manager = TaskManager.fromConfig(rootConfig).forPlugin('my-plugin'); +const scheduler = TaskScheduler.fromConfig(rootConfig).forPlugin('my-plugin'); -await manager.scheduleTask({ +await scheduler.scheduleTask({ id: 'refresh-things', frequency: Duration.fromObject({ minutes: 10 }), fn: async () => { diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 2be69f8788..27259304c8 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -10,7 +10,7 @@ import { Duration } from 'luxon'; import { Logger as Logger_2 } from 'winston'; // @public -export interface PluginTaskManager { +export interface PluginTaskScheduler { scheduleTask(task: TaskDefinition): Promise; } @@ -30,9 +30,10 @@ export type TaskFunction = | (() => void | Promise); // @public -export class TaskManager { +export class TaskScheduler { constructor(databaseManager: DatabaseManager, logger: Logger_2); - forPlugin(pluginId: string): PluginTaskManager; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/backend-tasks" does not have an export "PluginTaskManager" + forPlugin(pluginId: string): PluginTaskScheduler; // (undocumented) static fromConfig( config: Config, @@ -40,6 +41,6 @@ export class TaskManager { databaseManager?: DatabaseManager; logger?: Logger_2; }, - ): TaskManager; + ): TaskScheduler; } ``` diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index dcb5922cf1..01671638e9 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-tasks", - "description": "Common distributed task management / locking library for Backstage backends", + "description": "Common distributed task management library for Backstage backends", "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/backend-tasks/src/index.ts b/packages/backend-tasks/src/index.ts index 00a8aa2803..dd75aca68c 100644 --- a/packages/backend-tasks/src/index.ts +++ b/packages/backend-tasks/src/index.ts @@ -15,7 +15,7 @@ */ /** - * Common distributed task management / locking library for Backstage backends + * Common distributed task management library for Backstage backends * * @packageDocumentation */ diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts similarity index 94% rename from packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts rename to packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts index cbf754d5cf..e387b85413 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts @@ -19,7 +19,7 @@ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Duration } from 'luxon'; import waitForExpect from 'wait-for-expect'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; -import { PluginTaskManagerImpl } from './PluginTaskManagerImpl'; +import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl'; describe('PluginTaskManagerImpl', () => { const databases = TestDatabases.create({ @@ -29,7 +29,7 @@ describe('PluginTaskManagerImpl', () => { async function init(databaseId: TestDatabaseId) { const knex = await databases.init(databaseId); await migrateBackendTasks(knex); - const manager = new PluginTaskManagerImpl( + const manager = new PluginTaskSchedulerImpl( async () => knex, getVoidLogger(), ); diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts similarity index 91% rename from packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts rename to packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts index 497a58b348..93975bc327 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts @@ -17,13 +17,13 @@ import { Knex } from 'knex'; import { Logger } from 'winston'; import { TaskWorker } from './TaskWorker'; -import { PluginTaskManager, TaskDefinition } from './types'; +import { PluginTaskScheduler, TaskDefinition } from './types'; import { validateId } from './util'; /** * Implements the actual task management. */ -export class PluginTaskManagerImpl implements PluginTaskManager { +export class PluginTaskSchedulerImpl implements PluginTaskScheduler { constructor( private readonly databaseFactory: () => Promise, private readonly logger: Logger, diff --git a/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.ts similarity index 98% rename from packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts rename to packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.ts index 6594243243..8b90afff42 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskManagerJanitor.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.ts @@ -25,7 +25,7 @@ import { sleep } from './util'; * Makes sure to auto-expire and clean up things that time out or for other * reasons should not be left lingering. */ -export class PluginTaskManagerJanitor { +export class PluginTaskSchedulerJanitor { private readonly knex: Knex; private readonly waitBetweenRuns: Duration; private readonly logger: Logger; diff --git a/packages/backend-tasks/src/tasks/TaskManager.test.ts b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts similarity index 91% rename from packages/backend-tasks/src/tasks/TaskManager.test.ts rename to packages/backend-tasks/src/tasks/TaskScheduler.test.ts index 3f2d9dbc54..ce8e797503 100644 --- a/packages/backend-tasks/src/tasks/TaskManager.test.ts +++ b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts @@ -17,10 +17,10 @@ import { DatabaseManager, getVoidLogger } from '@backstage/backend-common'; import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Duration } from 'luxon'; -import { TaskManager } from './TaskManager'; +import { TaskScheduler } from './TaskScheduler'; import waitForExpect from 'wait-for-expect'; -describe('TaskManager', () => { +describe('TaskScheduler', () => { const logger = getVoidLogger(); const databases = TestDatabases.create({ ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], @@ -42,7 +42,7 @@ describe('TaskManager', () => { 'can return a working plugin impl, %p', async databaseId => { const database = await createDatabase(databaseId); - const manager = new TaskManager(database, logger).forPlugin('test'); + const manager = new TaskScheduler(database, logger).forPlugin('test'); const fn = jest.fn(); await manager.scheduleTask({ diff --git a/packages/backend-tasks/src/tasks/TaskManager.ts b/packages/backend-tasks/src/tasks/TaskScheduler.ts similarity index 80% rename from packages/backend-tasks/src/tasks/TaskManager.ts rename to packages/backend-tasks/src/tasks/TaskScheduler.ts index d065447fb2..81d1fca479 100644 --- a/packages/backend-tasks/src/tasks/TaskManager.ts +++ b/packages/backend-tasks/src/tasks/TaskScheduler.ts @@ -20,29 +20,29 @@ import { memoize } from 'lodash'; import { Duration } from 'luxon'; import { Logger } from 'winston'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; -import { PluginTaskManagerImpl } from './PluginTaskManagerImpl'; -import { PluginTaskManagerJanitor } from './PluginTaskManagerJanitor'; -import { PluginTaskManager } from './types'; +import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl'; +import { PluginTaskSchedulerJanitor } from './PluginTaskSchedulerJanitor'; +import { PluginTaskScheduler } from './types'; /** - * Deals with management and locking related to distributed tasks. + * Deals with the scheduling of distributed tasks. * * @public */ -export class TaskManager { +export class TaskScheduler { static fromConfig( config: Config, options?: { databaseManager?: DatabaseManager; logger?: Logger; }, - ): TaskManager { + ): TaskScheduler { const databaseManager = options?.databaseManager ?? DatabaseManager.fromConfig(config); const logger = (options?.logger || getRootLogger()).child({ type: 'taskManager', }); - return new TaskManager(databaseManager, logger); + return new TaskScheduler(databaseManager, logger); } constructor( @@ -56,13 +56,13 @@ export class TaskManager { * @param pluginId - The unique ID of the plugin, for example "catalog" * @returns A {@link PluginTaskManager} instance */ - forPlugin(pluginId: string): PluginTaskManager { + forPlugin(pluginId: string): PluginTaskScheduler { const databaseFactory = memoize(async () => { const knex = await this.databaseManager.forPlugin(pluginId).getClient(); await migrateBackendTasks(knex); - const janitor = new PluginTaskManagerJanitor({ + const janitor = new PluginTaskSchedulerJanitor({ knex, waitBetweenRuns: Duration.fromObject({ minutes: 1 }), logger: this.logger, @@ -72,7 +72,7 @@ export class TaskManager { return knex; }); - return new PluginTaskManagerImpl( + return new PluginTaskSchedulerImpl( databaseFactory, this.logger.child({ plugin: pluginId }), ); diff --git a/packages/backend-tasks/src/tasks/index.ts b/packages/backend-tasks/src/tasks/index.ts index bb59b3a3e2..9e0a06f71c 100644 --- a/packages/backend-tasks/src/tasks/index.ts +++ b/packages/backend-tasks/src/tasks/index.ts @@ -14,5 +14,9 @@ * limitations under the License. */ -export { TaskManager } from './TaskManager'; -export type { PluginTaskManager, TaskDefinition, TaskFunction } from './types'; +export { TaskScheduler } from './TaskScheduler'; +export type { + PluginTaskScheduler, + TaskDefinition, + TaskFunction, +} from './types'; diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index c717a17c3b..4693af7ef3 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -92,12 +92,11 @@ export interface TaskDefinition { } /** - * Deals with management and locking related to distributed tasks, for a given - * plugin. + * Deals with the scheduling of distributed tasks, for a given plugin. * * @public */ -export interface PluginTaskManager { +export interface PluginTaskScheduler { /** * Schedules a task function for coordinated exclusive invocation across * workers. From 41009bb3a5cd17e14c88778d90d6f7d766252d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 Oct 2021 21:47:29 +0200 Subject: [PATCH 113/181] fix missing link in api report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/api-report.md | 1 - packages/backend-tasks/src/tasks/TaskScheduler.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 27259304c8..eeb6412840 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -32,7 +32,6 @@ export type TaskFunction = // @public export class TaskScheduler { constructor(databaseManager: DatabaseManager, logger: Logger_2); - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/backend-tasks" does not have an export "PluginTaskManager" forPlugin(pluginId: string): PluginTaskScheduler; // (undocumented) static fromConfig( diff --git a/packages/backend-tasks/src/tasks/TaskScheduler.ts b/packages/backend-tasks/src/tasks/TaskScheduler.ts index 81d1fca479..b24338e5c6 100644 --- a/packages/backend-tasks/src/tasks/TaskScheduler.ts +++ b/packages/backend-tasks/src/tasks/TaskScheduler.ts @@ -54,7 +54,7 @@ export class TaskScheduler { * Instantiates a task manager instance for the given plugin. * * @param pluginId - The unique ID of the plugin, for example "catalog" - * @returns A {@link PluginTaskManager} instance + * @returns A {@link PluginTaskScheduler} instance */ forPlugin(pluginId: string): PluginTaskScheduler { const databaseFactory = memoize(async () => { From 06b035d120d72d6b37887fa79c87fec44e2ec1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 29 Oct 2021 14:11:15 +0200 Subject: [PATCH 114/181] bump to the latest versions of backstage packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 01671638e9..10db5cfaac 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", + "@backstage/backend-common": "^0.9.8", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", "@backstage/types": "^0.1.1", "@types/luxon": "^2.0.4", "knex": "^0.95.1", @@ -43,8 +43,8 @@ "zod": "^3.9.5" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.7", - "@backstage/cli": "^0.8.0", + "@backstage/backend-test-utils": "^0.1.8", + "@backstage/cli": "^0.8.1", "jest": "^26.0.1", "wait-for-expect": "^3.0.2" }, From fdfd2f8a6285abb6692adff8f208c7a949cf5c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 29 Oct 2021 15:08:19 +0200 Subject: [PATCH 115/181] remove double config dep in cli MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/forty-ligers-protect.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/forty-ligers-protect.md diff --git a/.changeset/forty-ligers-protect.md b/.changeset/forty-ligers-protect.md new file mode 100644 index 0000000000..e3e46fb7f3 --- /dev/null +++ b/.changeset/forty-ligers-protect.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +remove double config dep From 3bf223818726c7d60da54dd6059940ca1c66e70e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 29 Oct 2021 15:25:17 +0200 Subject: [PATCH 116/181] update to the right version of the errors package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/early-cobras-explode.md | 5 +++++ packages/catalog-client/package.json | 2 +- packages/e2e-test/package.json | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/early-cobras-explode.md diff --git a/.changeset/early-cobras-explode.md b/.changeset/early-cobras-explode.md new file mode 100644 index 0000000000..34b4cb0e98 --- /dev/null +++ b/.changeset/early-cobras-explode.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': patch +--- + +Update to the right version of @backstage/errors diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index efdaacc1e0..9caac50c90 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/errors": "^0.1.3", + "@backstage/errors": "^0.1.4", "cross-fetch": "^3.0.6" }, "devDependencies": { diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 1b8e321edb..6c941cda58 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "@backstage/cli-common": "^0.1.1", - "@backstage/errors": "^0.1.2", + "@backstage/errors": "^0.1.4", "@types/fs-extra": "^9.0.1", "@types/node": "^14.14.32", "@types/puppeteer": "^5.4.4", From e72d9ba9f624ef5430ebe9807f726a8f710302b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 29 Oct 2021 16:23:08 +0200 Subject: [PATCH 117/181] remove unnecessary dep on backend-test-utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/yarn.lock b/yarn.lock index 4c42941ad9..6eb9e272d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29435,6 +29435,11 @@ zip-stream@^4.1.0: compress-commons "^4.1.0" readable-stream "^3.6.0" +zod@^3.9.5: + version "3.11.6" + resolved "https://registry.npmjs.org/zod/-/zod-3.11.6.tgz#e43a5e0c213ae2e02aefe7cb2b1a6fa3d7f1f483" + integrity sha512-daZ80A81I3/9lIydI44motWe6n59kRBfNzTuS2bfzVh1nAXi667TOTWWtatxyG+fwgNUiagSj/CWZwRRbevJIg== + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" From 3f447ec3e12dd10e3763d228597c2accb1cb2837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sat, 6 Nov 2021 09:58:15 +0100 Subject: [PATCH 118/181] use lodash.once instead of memoize, and skip unnecessary PK index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-tasks/migrations/20210928160613_init.js | 4 ---- packages/backend-tasks/src/tasks/TaskScheduler.ts | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/backend-tasks/migrations/20210928160613_init.js b/packages/backend-tasks/migrations/20210928160613_init.js index cd812b59e8..21d80ae24c 100644 --- a/packages/backend-tasks/migrations/20210928160613_init.js +++ b/packages/backend-tasks/migrations/20210928160613_init.js @@ -50,7 +50,6 @@ exports.up = async function up(knex) { .dateTime('current_run_expires_at') .nullable() .comment('The time that the current task run will time out'); - table.index(['id'], 'backstage_backend_tasks__tasks__id_idx'); }); }; @@ -61,8 +60,5 @@ exports.down = async function down(knex) { // // tasks // - await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { - table.dropIndex([], 'backstage_backend_tasks__tasks__id_idx'); - }); await knex.schema.dropTable('backstage_backend_tasks__tasks'); }; diff --git a/packages/backend-tasks/src/tasks/TaskScheduler.ts b/packages/backend-tasks/src/tasks/TaskScheduler.ts index b24338e5c6..f1668a6691 100644 --- a/packages/backend-tasks/src/tasks/TaskScheduler.ts +++ b/packages/backend-tasks/src/tasks/TaskScheduler.ts @@ -16,7 +16,7 @@ import { DatabaseManager, getRootLogger } from '@backstage/backend-common'; import { Config } from '@backstage/config'; -import { memoize } from 'lodash'; +import { once } from 'lodash'; import { Duration } from 'luxon'; import { Logger } from 'winston'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; @@ -57,7 +57,7 @@ export class TaskScheduler { * @returns A {@link PluginTaskScheduler} instance */ forPlugin(pluginId: string): PluginTaskScheduler { - const databaseFactory = memoize(async () => { + const databaseFactory = once(async () => { const knex = await this.databaseManager.forPlugin(pluginId).getClient(); await migrateBackendTasks(knex); From 5f606617dd73e60deeae9669c3afbeea985b2c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 11 Nov 2021 20:28:07 +0100 Subject: [PATCH 119/181] fixup yarn.lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-common/package.json | 2 +- yarn.lock | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 446b3b5afe..03ae1fbe62 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -79,8 +79,8 @@ } }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.8", "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", diff --git a/yarn.lock b/yarn.lock index 6eb9e272d1..c558014df3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20895,6 +20895,11 @@ node-abi@^2.21.0: dependencies: semver "^5.4.1" +node-abort-controller@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" + integrity sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw== + node-addon-api@^3.0.0: version "3.2.1" resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" From 1e33ce7407ce162bf706cc805aa3cd87173feded Mon Sep 17 00:00:00 2001 From: Jeremy Guarini Date: Thu, 11 Nov 2021 12:41:30 -0800 Subject: [PATCH 120/181] swapout material-table for backstage table Signed-off-by: Jeremy Guarini --- plugins/gcp-projects/package.json | 1 - .../ProjectListPage/ProjectListPage.tsx | 40 ++----------------- 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 48205e4fd8..65458ea88b 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -37,7 +37,6 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "material-table": "^1.69.3", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "^6.0.0-beta.0", diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx index 2797d17d0d..b37dc2fa50 100644 --- a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx @@ -16,16 +16,7 @@ // NEEDS WORK import { Button, LinearProgress, Tooltip, Typography } from '@material-ui/core'; -import MaterialTable from 'material-table'; -import ArrowDownward from '@material-ui/icons/ArrowDownward'; -import ChevronLeft from '@material-ui/icons/ChevronLeft'; -import ChevronRight from '@material-ui/icons/ChevronRight'; -import Clear from '@material-ui/icons/Clear'; -import FilterList from '@material-ui/icons/FilterList'; -import FirstPage from '@material-ui/icons/FirstPage'; -import LastPage from '@material-ui/icons/LastPage'; -import Search from '@material-ui/icons/Search'; -import React, { forwardRef } from 'react'; +import React from 'react'; import { useAsync } from 'react-use'; import { gcpApiRef, Project } from '../../api'; @@ -37,6 +28,7 @@ import { HeaderLabel, Link, Page, + Table, SupportButton, WarningPanel, } from '@backstage/core-components'; @@ -90,31 +82,7 @@ const PageContents = () => { return (
- ( - - )), - FirstPage: forwardRef((props, ref) => ( - - )), - LastPage: forwardRef((props, ref) => ( - - )), - NextPage: forwardRef((props, ref) => ( - - )), - PreviousPage: forwardRef((props, ref) => ( - - )), - ResetSearch: forwardRef((props, ref) => ( - - )), - Search: forwardRef((props, ref) => ), - SortArrow: forwardRef((props, ref) => ( - - )), - }} +
{ })) || [] } options={{ - filtering: true, pageSize: 5, pageSizeOptions: [5, 10, 25, 50, 100], - showTitle: false, }} /> From 4f3ba47bf362446f47ec29c05507d351724dee84 Mon Sep 17 00:00:00 2001 From: Jeremy Guarini Date: Thu, 11 Nov 2021 14:49:40 -0800 Subject: [PATCH 121/181] ran yarn install to clear out data-grid dependency Signed-off-by: Jeremy Guarini --- yarn.lock | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/yarn.lock b/yarn.lock index fc66a02031..517d66ba0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4416,16 +4416,6 @@ react-is "^16.8.0 || ^17.0.0" react-transition-group "^4.4.0" -"@material-ui/data-grid@^4.0.0-alpha.37": - version "4.0.0-alpha.37" - resolved "https://registry.npmjs.org/@material-ui/data-grid/-/data-grid-4.0.0-alpha.37.tgz#89d907c4e94e6a0db4e89e4f59160f7811546ca2" - integrity sha512-3T2AG31aad/lWLMLwn1XUP4mUf3H9YZES17dGuYByzkRLCXbBZHBTPEnCctWukajzwm+v0KGg3QpwitGoiDAjA== - dependencies: - "@material-ui/utils" "^5.0.0-alpha.14" - clsx "^1.0.4" - prop-types "^15.7.2" - reselect "^4.0.0" - "@material-ui/icons@^4.11.2", "@material-ui/icons@^4.9.1": version "4.11.2" resolved "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz#b3a7353266519cd743b6461ae9fdfcb1b25eb4c5" @@ -4513,17 +4503,6 @@ prop-types "^15.7.2" react-is "^16.8.0 || ^17.0.0" -"@material-ui/utils@^5.0.0-alpha.14": - version "5.0.0-beta.5" - resolved "https://registry.npmjs.org/@material-ui/utils/-/utils-5.0.0-beta.5.tgz#de492037e1f1f0910fda32e6f11b66dfcde2a1c2" - integrity sha512-wtJ3ovXWZdTAz5eLBqvMpYH/IBJb3qMQbGCyL1i00+sf7AUlAuv4QLx+QtX/siA6L7IpxUQVfqpoCpQH1eYRpQ== - dependencies: - "@babel/runtime" "^7.14.8" - "@types/prop-types" "^15.7.4" - "@types/react-is" "^16.7.1 || ^17.0.0" - prop-types "^15.7.2" - react-is "^17.0.2" - "@mattiasbuelens/web-streams-polyfill@^0.2.0": version "0.2.1" resolved "https://registry.npmjs.org/@mattiasbuelens/web-streams-polyfill/-/web-streams-polyfill-0.2.1.tgz#d7c4aa94f98084ec0787be084d47167d62ea5f67" @@ -7455,7 +7434,7 @@ resolved "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz#72a26101dc567b0d68fd956cf42314556e42d601" integrity sha512-VjID5MJb1eGKthz2qUerWT8+R4b9N+CHvGCzg9fn4kWZgaF9AhdYikQio3R7wV8YY1NsQKPaCwKz1Yff+aHNUQ== -"@types/prop-types@*", "@types/prop-types@^15.7.3", "@types/prop-types@^15.7.4": +"@types/prop-types@*", "@types/prop-types@^15.7.3": version "15.7.4" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== @@ -7496,13 +7475,6 @@ dependencies: "@types/react" "*" -"@types/react-is@^16.7.1 || ^17.0.0": - version "17.0.3" - resolved "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a" - integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw== - dependencies: - "@types/react" "*" - "@types/react-lazylog@^4.5.0": version "4.5.1" resolved "https://registry.npmjs.org/@types/react-lazylog/-/react-lazylog-4.5.1.tgz#babb5d814f7035b5434518769975e12f299356a8" From 0cc1fa949b2abda881d7796ec703d35680b05acf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Nov 2021 04:11:27 +0000 Subject: [PATCH 122/181] build(deps-dev): bump @types/jquery from 3.5.5 to 3.5.8 Bumps [@types/jquery](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jquery) from 3.5.5 to 3.5.8. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jquery) --- updated-dependencies: - dependency-name: "@types/jquery" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4c42941ad9..17847eefae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7288,9 +7288,9 @@ pretty-format "^26.0.0" "@types/jquery@^3.3.34": - version "3.5.5" - resolved "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.5.tgz#2c63f47c9c8d96693d272f5453602afd8338c903" - integrity sha512-6RXU9Xzpc6vxNrS6FPPapN1SxSHgQ336WC6Jj/N8q30OiaBZ00l1GBgeP7usjVZPivSkGUfL1z/WW6TX989M+w== + version "3.5.8" + resolved "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.8.tgz#83bfbcdf4e625c5471590f92703c06aadb052a09" + integrity sha512-cXk6NwqjDYg+UI9p2l3x0YmPa4m7RrXqmbK4IpVVpRJiYXU/QTo+UZrn54qfE1+9Gao4qpYqUnxm5ZCy2FTXAw== dependencies: "@types/sizzle" "*" From 72e0e17fb5b4065ccbf532d6d62ff1f7c27522fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Nov 2021 04:13:37 +0000 Subject: [PATCH 123/181] build(deps-dev): bump @types/nunjucks from 3.1.5 to 3.2.0 Bumps [@types/nunjucks](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/nunjucks) from 3.1.5 to 3.2.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/nunjucks) --- updated-dependencies: - dependency-name: "@types/nunjucks" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4c42941ad9..5e82f0d4ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7560,9 +7560,9 @@ integrity sha512-4QQmOF5KlwfxJ5IGXFIudkeLCdMABz03RcUXu+LCb24zmln8QW6aDjuGl4d4XPVLf2j+FnjelHTP7dvceAFbhA== "@types/nunjucks@^3.1.4": - version "3.1.5" - resolved "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.1.5.tgz#e0782333b75b4ff9ac9213aa160ca8c5132eaf57" - integrity sha512-0zEdmQNNvQ+xyV9kqQvAV93UVroTwhE78toVUDT0GBnGcW2jQBZnB4al9qq2LqI5qHOqROy/DvvAY/UwrbvV1A== + version "3.2.0" + resolved "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.2.0.tgz#2303629c91cdd39de103af979046d4a8590351e4" + integrity sha512-1FM36Hm3EdidJmWlZZafkg/kZME0UZ/0vQ46JE8R7R0JqQafah0r+d4i6d/MJg5DnKxEeAOAeifEVkzo7fEvGg== "@types/oauth@*": version "0.9.1" From f8ebdd571c1e148f983784b5e4e341c2aabad500 Mon Sep 17 00:00:00 2001 From: Marley Powell Date: Fri, 12 Nov 2021 08:33:39 +0000 Subject: [PATCH 124/181] refactor: Reworked how subset story paths are created. Signed-off-by: Marley Powell --- packages/storybook/.storybook/main.js | 28 +++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js index 8d60899e48..72ff42311d 100644 --- a/packages/storybook/.storybook/main.js +++ b/packages/storybook/.storybook/main.js @@ -1,22 +1,30 @@ const path = require('path'); const WebpackPluginFailBuildOnWarning = require('./webpack-plugin-fail-build-on-warning'); +/** + * This set of stories are the ones that we publish to backstage.io. + */ +const BACKSTAGE_CORE_STORIES = [ + 'packages/core-components', + 'plugins/org', + 'plugins/search', +]; + module.exports = ({ args }) => { // Calling storybook with no args causes our default list of stories to be used. // This set of stories are the ones that we publish to backstage.io // // If it's called with args, each arg should be the path to a package that we will // show the stories from, for example `yarn storybook plugins/catalog`. - let stories; - if (args.length === 0) { - stories = [ - '../../core-components/src/**/*.stories.tsx', - '../../../plugins/org/src/**/*.stories.tsx', - '../../../plugins/search/src/**/*.stories.tsx', - ]; - } else { - stories = args.map(arg => `../../../${arg}/src/**/*.stories.tsx`); - } + + const rootPath = '../../..'; + const storiesSrcGlob = 'src/**/*.stories.tsx'; + + const getStoriesPath = package => + path.posix.join(rootPath, package, storiesSrcGlob); + + const packages = args.length === 0 ? BACKSTAGE_CORE_STORIES : args; + const stories = packages.map(getStoriesPath); return { stories, From 8acb23b29f72e29913069386783985bab262d0bf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 17 Oct 2021 13:45:36 +0200 Subject: [PATCH 125/181] cli: initial create command Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 42 ++++++++++++++++++++++ packages/cli/src/commands/index.ts | 18 ++++++++++ 2 files changed, 60 insertions(+) create mode 100644 packages/cli/src/commands/create/create.ts diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts new file mode 100644 index 0000000000..ed4bcc9c20 --- /dev/null +++ b/packages/cli/src/commands/create/create.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Command } from 'commander'; + +function parseOptions(optionStrings: string[]): Record { + const options: Record = {}; + + for (const str of optionStrings) { + const [key] = str.split('=', 1); + const value = str.slice(key.length + 1); + if (!key || !value) { + throw new Error( + `Invalid option '${str}', must be of the format =`, + ); + } + options[key] = value; + } + + return options; +} + +export default async (cmd: Command) => { + const selected = cmd.opts().select; + console.log('DEBUG: selected =', selected); + + const options = parseOptions(cmd.opts().option); + console.log('DEBUG: options =', options); +}; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 4e01a8ca9f..06832c466b 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -75,6 +75,24 @@ export function registerCommands(program: CommanderStatic) { .option(...configOption) .action(lazy(() => import('./backend/dev').then(m => m.default))); + program + .command('create') + .storeOptionsAsProperties(false) + .description( + 'Open up an interactive guide to creating new things in your app', + ) + .option( + '--select ', + 'Select the thing you want to be creating upfront', + ) + .option( + '--option =', + 'Pre-fill options for the creation process', + (opt, arr: string[]) => [...arr, opt], + [], + ) + .action(lazy(() => import('./create/create').then(m => m.default))); + program .command('create-plugin') .option( From e607456dbcb77d779ee28af8c1980ca4f9e713be Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 17 Oct 2021 16:59:59 +0200 Subject: [PATCH 126/181] cli: create command factory foundations Signed-off-by: Patrik Oldsberg --- .../src/commands/create/FactoryRegistry.ts | 76 +++++++++++++++++++ packages/cli/src/commands/create/create.ts | 13 +++- .../create/factories/frontendPlugin.ts | 46 +++++++++++ .../src/commands/create/factories/index.ts | 17 +++++ packages/cli/src/commands/create/types.ts | 34 +++++++++ 5 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 packages/cli/src/commands/create/FactoryRegistry.ts create mode 100644 packages/cli/src/commands/create/factories/frontendPlugin.ts create mode 100644 packages/cli/src/commands/create/factories/index.ts create mode 100644 packages/cli/src/commands/create/types.ts diff --git a/packages/cli/src/commands/create/FactoryRegistry.ts b/packages/cli/src/commands/create/FactoryRegistry.ts new file mode 100644 index 0000000000..0f48acc337 --- /dev/null +++ b/packages/cli/src/commands/create/FactoryRegistry.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import inquirer from 'inquirer'; +import { AnyFactory } from './types'; +import * as factories from './factories'; +import partition from 'lodash/partition'; + +export class FactoryRegistry { + private static factoryMap = new Map( + Object.values(factories).map(factory => [factory.name, factory]), + ); + + static async interactiveSelect(preselected?: string): Promise { + let selected = preselected; + + if (!selected) { + const answers = await inquirer.prompt<{ name: string }>([ + { + type: 'list', + name: 'name', + message: 'What do you want to create?', + choices: Array.from(this.factoryMap.values()).map(factory => ({ + name: `${factory.name} - ${factory.description}`, + value: factory.name, + })), + }, + ]); + selected = answers.name; + } + + const factory = this.factoryMap.get(selected); + if (!factory) { + throw new Error(`Unknown selection '${selected}'`); + } + return factory; + } + + static async populateOptions( + factory: AnyFactory, + provided: Record, + ): Promise> { + const [hasAnswers, needsAnswers] = partition( + factory.options, + option => option.name in provided, + ); + + for (const option of hasAnswers) { + const value = provided[option.name]; + + if (option.validate) { + const result = option.validate(value); + if (result !== true) { + throw new Error(`Invalid option '${option.name}'. ${result}`); + } + } + } + + const answers = await inquirer.prompt(needsAnswers); + + return { ...provided, ...answers }; + } +} diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index ed4bcc9c20..2397e7ec58 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -15,6 +15,7 @@ */ import { Command } from 'commander'; +import { FactoryRegistry } from './FactoryRegistry'; function parseOptions(optionStrings: string[]): Record { const options: Record = {}; @@ -34,9 +35,13 @@ function parseOptions(optionStrings: string[]): Record { } export default async (cmd: Command) => { - const selected = cmd.opts().select; - console.log('DEBUG: selected =', selected); + const factory = await FactoryRegistry.interactiveSelect(cmd.opts().select); - const options = parseOptions(cmd.opts().option); - console.log('DEBUG: options =', options); + const providedOptions = parseOptions(cmd.opts().option); + const options = await FactoryRegistry.populateOptions( + factory, + providedOptions, + ); + + await factory.create(options); }; diff --git a/packages/cli/src/commands/create/factories/frontendPlugin.ts b/packages/cli/src/commands/create/factories/frontendPlugin.ts new file mode 100644 index 0000000000..195953e780 --- /dev/null +++ b/packages/cli/src/commands/create/factories/frontendPlugin.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createFactory } from '../types'; + +type Options = { + id: string; +}; + +export const frontendPlugin = createFactory({ + name: 'plugin', + description: 'A new frontend plugin', + options: [ + { + type: 'input', + name: 'id', + message: 'Enter an ID for the plugin', + validate: (value: string) => { + if (!value) { + return 'Please enter an ID for the plugin'; + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.'; + } + return true; + }, + }, + ], + async create(options: Options) { + console.log( + `Creating ${this.name} with options ${JSON.stringify(options)}`, + ); + }, +}); diff --git a/packages/cli/src/commands/create/factories/index.ts b/packages/cli/src/commands/create/factories/index.ts new file mode 100644 index 0000000000..4375ba2a45 --- /dev/null +++ b/packages/cli/src/commands/create/factories/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { frontendPlugin } from './frontendPlugin'; diff --git a/packages/cli/src/commands/create/types.ts b/packages/cli/src/commands/create/types.ts new file mode 100644 index 0000000000..e5252df6bc --- /dev/null +++ b/packages/cli/src/commands/create/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DistinctQuestion } from 'inquirer'; + +export type AnyOptions = Record; + +export interface Factory { + name: string; + description: string; + options: ReadonlyArray & { name: string }>; + create(options: Options): Promise; +} + +export type AnyFactory = Factory; + +export function createFactory( + config: Factory, +): AnyFactory { + return config as AnyFactory; +} From 58a4e67ded6ac591f8429f71bf49a6a4d934c71d Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 12 Nov 2021 11:10:27 +0100 Subject: [PATCH 127/181] Add theme switcher to sidebar of dev app Signed-off-by: Oliver Sand --- .changeset/big-cougars-glow.md | 5 + packages/dev-utils/package.json | 4 +- .../src/devApp/SidebarThemeSwitcher.test.tsx | 95 +++++++++++++ .../src/devApp/SidebarThemeSwitcher.tsx | 125 ++++++++++++++++++ packages/dev-utils/src/devApp/render.tsx | 53 ++++---- 5 files changed, 256 insertions(+), 26 deletions(-) create mode 100644 .changeset/big-cougars-glow.md create mode 100644 packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx create mode 100644 packages/dev-utils/src/devApp/SidebarThemeSwitcher.tsx diff --git a/.changeset/big-cougars-glow.md b/.changeset/big-cougars-glow.md new file mode 100644 index 0000000000..d1630905db --- /dev/null +++ b/.changeset/big-cougars-glow.md @@ -0,0 +1,5 @@ +--- +'@backstage/dev-utils': patch +--- + +Add theme switcher to sidebar of dev app. diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 0846ee8e4f..974329e8d4 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -45,10 +45,12 @@ "@testing-library/user-event": "^13.1.8", "@types/react": "*", "react": "^16.12.0", + "react-use": "^17.2.4", "react-dom": "^16.12.0", "react-hot-loader": "^4.12.21", "react-router": "6.0.0-beta.0", - "react-router-dom": "6.0.0-beta.0" + "react-router-dom": "6.0.0-beta.0", + "zen-observable": "^0.8.15" }, "devDependencies": { "@backstage/cli": "^0.8.0", diff --git a/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx new file mode 100644 index 0000000000..3907dc1007 --- /dev/null +++ b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx @@ -0,0 +1,95 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { AppThemeApi, appThemeApiRef } from '@backstage/core-plugin-api'; +import { renderInTestApp } from '@backstage/test-utils'; +import { BackstageTheme } from '@backstage/theme'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import ObservableImpl from 'zen-observable'; +import { SidebarThemeSwitcher } from './SidebarThemeSwitcher'; + +describe('SidebarThemeSwitcher', () => { + let appThemeApi: jest.Mocked; + let apiRegistry: ApiRegistry; + + beforeEach(() => { + appThemeApi = { + activeThemeId$: jest.fn(), + getActiveThemeId: jest.fn(), + getInstalledThemes: jest.fn(), + setActiveThemeId: jest.fn(), + }; + + appThemeApi.activeThemeId$.mockReturnValue( + ObservableImpl.of('dark'), + ); + appThemeApi.getInstalledThemes.mockReturnValue([ + { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + theme: {} as unknown as BackstageTheme, + }, + { + id: 'light', + title: 'Light Theme', + variant: 'light', + theme: {} as unknown as BackstageTheme, + }, + ]); + + apiRegistry = ApiRegistry.with(appThemeApiRef, appThemeApi); + }); + + it('should display current theme', async () => { + const { getByLabelText, getByRole, getByText } = await renderInTestApp( + + + , + ); + + const button = getByLabelText('Switch Theme'); + expect(button).toBeInTheDocument(); + + userEvent.click(button); + + expect(getByRole('listbox')).toBeInTheDocument(); + expect(getByText('Dark Theme')).toBeInTheDocument(); + expect( + getByText('Dark Theme').parentElement?.parentElement, + ).toHaveAttribute('aria-selected', 'true'); + }); + + it('should select different theme', async () => { + const { getByLabelText, getByRole, getByText } = await renderInTestApp( + + + , + ); + + const button = getByLabelText('Switch Theme'); + expect(button).toBeInTheDocument(); + + userEvent.click(button); + + expect(getByRole('listbox')).toBeInTheDocument(); + + userEvent.click(getByText('Light Theme')); + + expect(appThemeApi.setActiveThemeId).toHaveBeenCalledWith('light'); + }); +}); diff --git a/packages/dev-utils/src/devApp/SidebarThemeSwitcher.tsx b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.tsx new file mode 100644 index 0000000000..e13554bbf7 --- /dev/null +++ b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.tsx @@ -0,0 +1,125 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { SidebarItem } from '@backstage/core-components'; +import { appThemeApiRef, useApi } from '@backstage/core-plugin-api'; +import { ListItemIcon, ListItemText, Menu, MenuItem } from '@material-ui/core'; +import AutoIcon from '@material-ui/icons/BrightnessAuto'; +import React, { cloneElement, useCallback, useState } from 'react'; +import { useObservable } from 'react-use'; + +type ThemeIconProps = { + active?: boolean; + icon: JSX.Element | undefined; +}; + +const ThemeIcon = ({ active, icon }: ThemeIconProps) => + icon ? ( + cloneElement(icon, { + color: active ? 'primary' : undefined, + }) + ) : ( + + ); + +export const SidebarThemeSwitcher = () => { + const appThemeApi = useApi(appThemeApiRef); + const themeId = useObservable( + appThemeApi.activeThemeId$(), + appThemeApi.getActiveThemeId(), + ); + const themeIds = appThemeApi.getInstalledThemes(); + const activeTheme = themeIds.find(t => t.id === themeId); + + const [anchorEl, setAnchorEl] = useState(); + const open = Boolean(anchorEl); + + const handleOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleSelectTheme = (newThemeId: string | undefined) => { + if (themeIds.some(t => t.id === newThemeId)) { + appThemeApi.setActiveThemeId(newThemeId); + } else { + appThemeApi.setActiveThemeId(undefined); + } + + setAnchorEl(undefined); + }; + + const handleClose = () => { + setAnchorEl(undefined); + }; + + const ActiveIcon = useCallback( + () => , + [activeTheme], + ); + + return ( + <> + + + + Choose a theme + handleSelectTheme(undefined)} + > + + + + Auto + + + {themeIds.map(theme => { + const active = theme.id === themeId; + return ( + handleSelectTheme(theme.id)} + > + + + + {theme.title} + + ); + })} + + + ); +}; diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index d4b1c8db49..adea9e14f2 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -14,6 +14,30 @@ * limitations under the License. */ +import { createApp } from '@backstage/app-defaults'; +import { FlatRoutes } from '@backstage/core-app-api'; +import { + AlertDisplay, + OAuthRequestDialog, + Sidebar, + SidebarDivider, + SidebarItem, + SidebarPage, + SidebarSpace, + SidebarSpacer, +} from '@backstage/core-components'; +import { + AnyApiFactory, + ApiFactory, + AppTheme, + attachComponentData, + BackstagePlugin, + configApiRef, + createApiFactory, + createRouteRef, + IconComponent, + RouteRef, +} from '@backstage/core-plugin-api'; import { ScmIntegrationsApi, scmIntegrationsApiRef, @@ -24,31 +48,7 @@ import React, { ComponentType, ReactNode } from 'react'; import ReactDOM from 'react-dom'; import { hot } from 'react-hot-loader'; import { Route } from 'react-router'; - -import { - AlertDisplay, - OAuthRequestDialog, - Sidebar, - SidebarItem, - SidebarPage, - SidebarSpacer, -} from '@backstage/core-components'; - -import { - AnyApiFactory, - ApiFactory, - AppTheme, - attachComponentData, - configApiRef, - createApiFactory, - createRouteRef, - IconComponent, - RouteRef, - BackstagePlugin, -} from '@backstage/core-plugin-api'; - -import { createApp } from '@backstage/app-defaults'; -import { FlatRoutes } from '@backstage/core-app-api'; +import { SidebarThemeSwitcher } from './SidebarThemeSwitcher'; const GatheringRoute: (props: { path: string; @@ -203,6 +203,9 @@ export class DevAppBuilder { {this.sidebarItems} + + + {this.routes} From 2ca3f5ff4c3585505a72fdd285382c44451b51d6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 11:22:30 +0100 Subject: [PATCH 128/181] cli: added support for dynamically discovering create options Signed-off-by: Patrik Oldsberg --- .../src/commands/create/FactoryRegistry.ts | 42 ++++++++++++------- .../create/factories/frontendPlugin.ts | 29 ++++++++++++- packages/cli/src/commands/create/types.ts | 3 +- 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/commands/create/FactoryRegistry.ts b/packages/cli/src/commands/create/FactoryRegistry.ts index 0f48acc337..a959896bd0 100644 --- a/packages/cli/src/commands/create/FactoryRegistry.ts +++ b/packages/cli/src/commands/create/FactoryRegistry.ts @@ -53,24 +53,36 @@ export class FactoryRegistry { factory: AnyFactory, provided: Record, ): Promise> { - const [hasAnswers, needsAnswers] = partition( - factory.options, - option => option.name in provided, - ); + let currentOptions = provided; - for (const option of hasAnswers) { - const value = provided[option.name]; - - if (option.validate) { - const result = option.validate(value); - if (result !== true) { - throw new Error(`Invalid option '${option.name}'. ${result}`); - } - } + if (factory.optionsDiscovery) { + const discoveredOptions = await factory.optionsDiscovery(); + currentOptions = { + ...currentOptions, + ...(discoveredOptions as Record), + }; } - const answers = await inquirer.prompt(needsAnswers); + if (factory.optionsPrompts) { + const [hasAnswers, needsAnswers] = partition( + factory.optionsPrompts, + option => option.name in currentOptions, + ); - return { ...provided, ...answers }; + for (const option of hasAnswers) { + const value = provided[option.name]; + + if (option.validate) { + const result = option.validate(value); + if (result !== true) { + throw new Error(`Invalid option '${option.name}'. ${result}`); + } + } + } + + currentOptions = await inquirer.prompt(needsAnswers, currentOptions); + } + + return currentOptions; } } diff --git a/packages/cli/src/commands/create/factories/frontendPlugin.ts b/packages/cli/src/commands/create/factories/frontendPlugin.ts index 195953e780..35e18b9e09 100644 --- a/packages/cli/src/commands/create/factories/frontendPlugin.ts +++ b/packages/cli/src/commands/create/factories/frontendPlugin.ts @@ -14,20 +14,27 @@ * limitations under the License. */ +import { paths } from '../../../lib/paths'; +import { getCodeownersFilePath, parseOwnerIds } from '../../../lib/codeowners'; import { createFactory } from '../types'; type Options = { id: string; + owner?: string; + codeOwnersPath?: string; }; export const frontendPlugin = createFactory({ name: 'plugin', description: 'A new frontend plugin', - options: [ + optionsDiscovery: async () => ({ + codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), + }), + optionsPrompts: [ { type: 'input', name: 'id', - message: 'Enter an ID for the plugin', + message: 'Enter an ID for the plugin [required]', validate: (value: string) => { if (!value) { return 'Please enter an ID for the plugin'; @@ -37,6 +44,24 @@ export const frontendPlugin = createFactory({ return true; }, }, + { + type: 'input', + name: 'owner', + message: 'Enter an owner of the plugin to add to CODEOWNERS [optional]', + when: opts => Boolean(opts.codeOwnersPath), + validate: (value: string) => { + if (!value) { + return true; + } + + const ownerIds = parseOwnerIds(value); + if (!ownerIds) { + return 'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses (e.g. user@example.com).'; + } + + return true; + }, + }, ], async create(options: Options) { console.log( diff --git a/packages/cli/src/commands/create/types.ts b/packages/cli/src/commands/create/types.ts index e5252df6bc..a797664e37 100644 --- a/packages/cli/src/commands/create/types.ts +++ b/packages/cli/src/commands/create/types.ts @@ -21,7 +21,8 @@ export type AnyOptions = Record; export interface Factory { name: string; description: string; - options: ReadonlyArray & { name: string }>; + optionsDiscovery?(): Promise>; + optionsPrompts?: ReadonlyArray & { name: string }>; create(options: Options): Promise; } From 55e58c55684109303b8559db2ea578bf88e72c5d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 11:24:40 +0100 Subject: [PATCH 129/181] cli: moved bulk of create implementation over to lib Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 2 +- packages/cli/src/{commands => lib}/create/FactoryRegistry.ts | 0 .../src/{commands => lib}/create/factories/frontendPlugin.ts | 0 packages/cli/src/{commands => lib}/create/factories/index.ts | 0 packages/cli/src/{commands => lib}/create/types.ts | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename packages/cli/src/{commands => lib}/create/FactoryRegistry.ts (100%) rename packages/cli/src/{commands => lib}/create/factories/frontendPlugin.ts (100%) rename packages/cli/src/{commands => lib}/create/factories/index.ts (100%) rename packages/cli/src/{commands => lib}/create/types.ts (100%) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index 2397e7ec58..4c1ca68557 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -15,7 +15,7 @@ */ import { Command } from 'commander'; -import { FactoryRegistry } from './FactoryRegistry'; +import { FactoryRegistry } from '../../lib/create/FactoryRegistry'; function parseOptions(optionStrings: string[]): Record { const options: Record = {}; diff --git a/packages/cli/src/commands/create/FactoryRegistry.ts b/packages/cli/src/lib/create/FactoryRegistry.ts similarity index 100% rename from packages/cli/src/commands/create/FactoryRegistry.ts rename to packages/cli/src/lib/create/FactoryRegistry.ts diff --git a/packages/cli/src/commands/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts similarity index 100% rename from packages/cli/src/commands/create/factories/frontendPlugin.ts rename to packages/cli/src/lib/create/factories/frontendPlugin.ts diff --git a/packages/cli/src/commands/create/factories/index.ts b/packages/cli/src/lib/create/factories/index.ts similarity index 100% rename from packages/cli/src/commands/create/factories/index.ts rename to packages/cli/src/lib/create/factories/index.ts diff --git a/packages/cli/src/commands/create/types.ts b/packages/cli/src/lib/create/types.ts similarity index 100% rename from packages/cli/src/commands/create/types.ts rename to packages/cli/src/lib/create/types.ts From 3ca0d3368eb6ec86690941ef7300b312bdfcbd5e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 11:28:54 +0100 Subject: [PATCH 130/181] cli: display create prompt messages in blue and errors in red Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/create/FactoryRegistry.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/lib/create/FactoryRegistry.ts b/packages/cli/src/lib/create/FactoryRegistry.ts index a959896bd0..1b25f5950a 100644 --- a/packages/cli/src/lib/create/FactoryRegistry.ts +++ b/packages/cli/src/lib/create/FactoryRegistry.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import chalk from 'chalk'; import inquirer from 'inquirer'; import { AnyFactory } from './types'; import * as factories from './factories'; @@ -80,7 +81,22 @@ export class FactoryRegistry { } } - currentOptions = await inquirer.prompt(needsAnswers, currentOptions); + currentOptions = await inquirer.prompt( + needsAnswers.map(option => ({ + ...option, + message: option.message && chalk.blue(option.message), + validate: + option.validate && + (async (...args) => { + const result = await option.validate!(...args); + if (typeof result === 'string') { + return chalk.red(result); + } + return result; + }), + })), + currentOptions, + ); } return currentOptions; From 84936dbcbe9fac0244c71ca90da7404a5e5d2a78 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 12:03:23 +0100 Subject: [PATCH 131/181] cli: cmd options and discovery for additional creation context Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 37 +++++++++++++++++-- packages/cli/src/commands/index.ts | 6 +++ .../lib/create/factories/frontendPlugin.ts | 8 ++-- packages/cli/src/lib/create/types.ts | 15 +++++++- 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index 4c1ca68557..07378579d2 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -14,8 +14,11 @@ * limitations under the License. */ +import fs from 'fs-extra'; import { Command } from 'commander'; import { FactoryRegistry } from '../../lib/create/FactoryRegistry'; +import { paths } from '../../lib/paths'; +import { assertError } from '@backstage/errors'; function parseOptions(optionStrings: string[]): Record { const options: Record = {}; @@ -35,13 +38,41 @@ function parseOptions(optionStrings: string[]): Record { } export default async (cmd: Command) => { - const factory = await FactoryRegistry.interactiveSelect(cmd.opts().select); + const cmdOpts = cmd.opts(); - const providedOptions = parseOptions(cmd.opts().option); + const factory = await FactoryRegistry.interactiveSelect(cmdOpts.select); + + const providedOptions = parseOptions(cmdOpts.option); const options = await FactoryRegistry.populateOptions( factory, providedOptions, ); - await factory.create(options); + const rootPackageJson = await fs.readJson( + paths.resolveTargetRoot('package.json'), + ); + const isMonoRepo = Boolean(rootPackageJson.workspaces); + + let defaultVersion = '0.1.0'; + try { + const rootLernaJson = await fs.readJson( + paths.resolveTargetRoot('lerna.json'), + ); + if (rootLernaJson.version) { + defaultVersion = rootLernaJson.version; + } + } catch (error) { + assertError(error); + if (error.code !== 'ENOENT') { + throw error; + } + } + + await factory.create(options, { + isMonoRepo, + defaultVersion, + scope: cmdOpts.scope.replace(/^@/, ''), + npmRegistry: cmdOpts.npmRegistry, + private: Boolean(cmdOpts.private), + }); }; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 06832c466b..a37d5e9ffe 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -91,6 +91,12 @@ export function registerCommands(program: CommanderStatic) { (opt, arr: string[]) => [...arr, opt], [], ) + .option('--scope ', 'The scope to use for new packages') + .option( + '--npm-registry ', + 'The package registry to use for new packages', + ) + .option('--no-private', 'Do not mark new packages as private') .action(lazy(() => import('./create/create').then(m => m.default))); program diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index 35e18b9e09..1b13cff13d 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -16,7 +16,7 @@ import { paths } from '../../../lib/paths'; import { getCodeownersFilePath, parseOwnerIds } from '../../../lib/codeowners'; -import { createFactory } from '../types'; +import { createFactory, CreateContext } from '../types'; type Options = { id: string; @@ -63,9 +63,11 @@ export const frontendPlugin = createFactory({ }, }, ], - async create(options: Options) { + async create(options: Options, context: CreateContext) { console.log( - `Creating ${this.name} with options ${JSON.stringify(options)}`, + `Creating ${this.name} with options ${JSON.stringify( + options, + )} and context ${JSON.stringify(context)}`, ); }, }); diff --git a/packages/cli/src/lib/create/types.ts b/packages/cli/src/lib/create/types.ts index a797664e37..82bdd4978a 100644 --- a/packages/cli/src/lib/create/types.ts +++ b/packages/cli/src/lib/create/types.ts @@ -16,6 +16,19 @@ import { DistinctQuestion } from 'inquirer'; +export interface CreateContext { + /** The package scope to use for new packages */ + scope?: string; + /** The NPM registry to use for new packages */ + npmRegistry?: string; + /** Whether new packages should be marked as private */ + private: boolean; + /** Whether we are creating something in a monorepo or not */ + isMonoRepo: boolean; + /** The default version to use for new packages */ + defaultVersion: string; +} + export type AnyOptions = Record; export interface Factory { @@ -23,7 +36,7 @@ export interface Factory { description: string; optionsDiscovery?(): Promise>; optionsPrompts?: ReadonlyArray & { name: string }>; - create(options: Options): Promise; + create(options: Options, context?: CreateContext): Promise; } export type AnyFactory = Factory; From 14e8162d199cbf80dd1e4dfcce6006758e6c89ea Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 13:08:25 +0100 Subject: [PATCH 132/181] cli: added temp dir utility to create context Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 36 +++++++++++++++++----- packages/cli/src/lib/create/types.ts | 3 ++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index 07378579d2..d39fa2b9d0 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -14,7 +14,9 @@ * limitations under the License. */ +import os from 'os'; import fs from 'fs-extra'; +import { join as joinPath } from 'path'; import { Command } from 'commander'; import { FactoryRegistry } from '../../lib/create/FactoryRegistry'; import { paths } from '../../lib/paths'; @@ -68,11 +70,31 @@ export default async (cmd: Command) => { } } - await factory.create(options, { - isMonoRepo, - defaultVersion, - scope: cmdOpts.scope.replace(/^@/, ''), - npmRegistry: cmdOpts.npmRegistry, - private: Boolean(cmdOpts.private), - }); + const tempDirs = new Array(); + async function createTemporaryDirectory(name: string): Promise { + const dir = await fs.mkdtemp(joinPath(os.tmpdir(), name)); + tempDirs.push(dir); + return dir; + } + + try { + await factory.create(options, { + isMonoRepo, + defaultVersion, + scope: cmdOpts.scope.replace(/^@/, ''), + npmRegistry: cmdOpts.npmRegistry, + private: Boolean(cmdOpts.private), + createTemporaryDirectory, + }); + } finally { + for (const dir of tempDirs) { + try { + await fs.remove(dir); + } catch (error) { + console.error( + `Failed to remove temporary directory '${dir}', ${error}`, + ); + } + } + } }; diff --git a/packages/cli/src/lib/create/types.ts b/packages/cli/src/lib/create/types.ts index 82bdd4978a..629506b5f7 100644 --- a/packages/cli/src/lib/create/types.ts +++ b/packages/cli/src/lib/create/types.ts @@ -27,6 +27,9 @@ export interface CreateContext { isMonoRepo: boolean; /** The default version to use for new packages */ defaultVersion: string; + + /** Creates a temporary directory. This will always be deleted after creation is done. */ + createTemporaryDirectory(name: string): Promise; } export type AnyOptions = Record; From f3bd8f47578ef7a80bd2c28da0f8517aef200bf7 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Fri, 12 Nov 2021 11:56:31 +0000 Subject: [PATCH 133/181] build(deps): bump @microsoft/api-documenter to v7.13.68 Signed-off-by: Mike Lewis --- package.json | 2 +- yarn.lock | 53 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 3e6d376c46..ab0440fa67 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ }, "version": "1.0.0", "dependencies": { - "@microsoft/api-documenter": "^7.13.47", + "@microsoft/api-documenter": "^7.13.68", "@microsoft/api-extractor": "^7.18.7", "@microsoft/api-extractor-model": "^7.13.5", "@microsoft/tsdoc": "^0.13.2" diff --git a/yarn.lock b/yarn.lock index 20e268355d..5733c736b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4540,19 +4540,28 @@ resolved "https://registry.npmjs.org/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA== -"@microsoft/api-documenter@^7.13.47": - version "7.13.47" - resolved "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.13.47.tgz#0b7726634232b37f76c0e5e8353cdbb5b52d4ece" - integrity sha512-jk78Pf8cKL2WZf6CkKUUtwegdsTA1Jf0MfIzD50qpG7T257HLrqCi1t70ZA85VpRLR8oSeNHMayqNTWkdku9iA== +"@microsoft/api-documenter@^7.13.68": + version "7.13.68" + resolved "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.13.68.tgz#c1e144764cac0684adefe78fd848d78c3f374681" + integrity sha512-cRjwK1TDyGxFGgCsRG8G0Yi3Z4akvfWgw1pWAxKFbm7ajlQQGZcHPnb+n4lKlSeQ5g/cxc7hcdw54Mvisne9Bg== dependencies: - "@microsoft/api-extractor-model" "7.13.5" + "@microsoft/api-extractor-model" "7.13.16" "@microsoft/tsdoc" "0.13.2" - "@rushstack/node-core-library" "3.40.0" - "@rushstack/ts-command-line" "4.9.0" + "@rushstack/node-core-library" "3.43.2" + "@rushstack/ts-command-line" "4.10.4" colors "~1.2.1" js-yaml "~3.13.1" resolve "~1.17.0" +"@microsoft/api-extractor-model@7.13.16": + version "7.13.16" + resolved "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.13.16.tgz#1d67541ebbcea32672c5fdd9392dc1579b2fc23a" + integrity sha512-ttdxVXsTWL5dd26W1YNLe3LgDsE0EE273aZlcLe58W0opymBybCYU1Mn+OHQM8BuErrdvdN8LdpWAAbkiOEN/Q== + dependencies: + "@microsoft/tsdoc" "0.13.2" + "@microsoft/tsdoc-config" "~0.15.2" + "@rushstack/node-core-library" "3.43.2" + "@microsoft/api-extractor-model@7.13.5", "@microsoft/api-extractor-model@^7.13.5": version "7.13.5" resolved "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.13.5.tgz#7836a81ba47b9a654062ed0361e4eee69afae51e" @@ -5367,6 +5376,21 @@ timsort "~0.3.0" z-schema "~3.18.3" +"@rushstack/node-core-library@3.43.2": + version "3.43.2" + resolved "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.43.2.tgz#f067371a94fd92ed8f9d9aa8201c5e9e17a19f0f" + integrity sha512-b7AEhSf6CvZgvuDcWMFDeKx2mQSn9AVnMQVyxNxFeHCtLz3gJicqCOlw2GOXM8HKh6PInLdil/NVCDcstwSrIw== + dependencies: + "@types/node" "12.20.24" + colors "~1.2.1" + fs-extra "~7.0.1" + import-lazy "~4.0.0" + jju "~1.4.0" + resolve "~1.17.0" + semver "~7.3.0" + timsort "~0.3.0" + z-schema "~3.18.3" + "@rushstack/rig-package@0.3.0": version "0.3.0" resolved "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.0.tgz#334ad2846797861361b3445d4cc9ae9164b1885c" @@ -5375,6 +5399,16 @@ resolve "~1.17.0" strip-json-comments "~3.1.1" +"@rushstack/ts-command-line@4.10.4": + version "4.10.4" + resolved "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.4.tgz#05142b74e5cb207d3dd9b935c82f80d7fcb68042" + integrity sha512-4T5ao4UgDb6LmiRj4GumvG3VT/p6RSMgl7TN7S58ifaAGN2GeTNBajFCDdJs9QQP0d/4tA5p0SFzT7Ps5Byirg== + dependencies: + "@types/argparse" "1.0.38" + argparse "~1.0.9" + colors "~1.2.1" + string-argv "~0.3.1" + "@rushstack/ts-command-line@4.9.0": version "4.9.0" resolved "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.9.0.tgz#781ba42cff73cae097b6d5241b6441e7cc2fe6e0" @@ -7534,6 +7568,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== +"@types/node@12.20.24": + version "12.20.24" + resolved "https://registry.npmjs.org/@types/node/-/node-12.20.24.tgz#c37ac69cb2948afb4cef95f424fa0037971a9a5c" + integrity sha512-yxDeaQIAJlMav7fH5AQqPH1u8YIuhYJXYBzxaQ4PifsU0GDO38MSdmEDeRlIxrKbC6NbEaaEHDanWb+y30U8SQ== + "@types/node@^12.7.1": version "12.12.58" resolved "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz#46dae9b2b9ee5992818c8f7cee01ff4ce03ab44c" From 867ea81d15f7a6bb8b5e05b55e432eb3c932b7ec Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Fri, 12 Nov 2021 14:47:31 +0100 Subject: [PATCH 134/181] Add changeset Signed-off-by: Ben Lambert --- .changeset/tender-gorillas-peel.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tender-gorillas-peel.md diff --git a/.changeset/tender-gorillas-peel.md b/.changeset/tender-gorillas-peel.md new file mode 100644 index 0000000000..f7d7022740 --- /dev/null +++ b/.changeset/tender-gorillas-peel.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +bump `@rollup/plugin-commonjs` from 17.1.0 to 21.0.1 From 8b5bd7e0d5278e28779d3479bc619795b21b6bc2 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 12 Nov 2021 08:06:45 -0600 Subject: [PATCH 135/181] Updated changeset to minor for common Signed-off-by: Andre Wanlin --- .changeset/tame-buckets-move.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tame-buckets-move.md b/.changeset/tame-buckets-move.md index 13adb92b5e..f7ee3deb90 100644 --- a/.changeset/tame-buckets-move.md +++ b/.changeset/tame-buckets-move.md @@ -1,7 +1,7 @@ --- '@backstage/plugin-azure-devops': patch '@backstage/plugin-azure-devops-backend': patch -'@backstage/plugin-azure-devops-common': patch +'@backstage/plugin-azure-devops-common': minor --- Improved Date handling for the Azure DevOps set of plugins by using strings and letting the frontend handle the conversion to DateTime From e04cce9cdb5cf02e7c9748c5bb95d9b59391e421 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 15:22:11 +0100 Subject: [PATCH 136/181] cli: wrap create factories up in a bit more built-in logging Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 28 ++++++++++++++++++++++ packages/cli/src/lib/create/types.ts | 3 +++ 2 files changed, 31 insertions(+) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index d39fa2b9d0..7004341c3e 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -21,6 +21,7 @@ import { Command } from 'commander'; import { FactoryRegistry } from '../../lib/create/FactoryRegistry'; import { paths } from '../../lib/paths'; import { assertError } from '@backstage/errors'; +import { Task } from '../../lib/tasks'; function parseOptions(optionStrings: string[]): Record { const options: Record = {}; @@ -77,7 +78,11 @@ export default async (cmd: Command) => { return dir; } + let modified = false; try { + Task.log(); + Task.log(`Creating new ${factory.name}`); + await factory.create(options, { isMonoRepo, defaultVersion, @@ -85,7 +90,30 @@ export default async (cmd: Command) => { npmRegistry: cmdOpts.npmRegistry, private: Boolean(cmdOpts.private), createTemporaryDirectory, + markAsModified() { + modified = true; + }, }); + + Task.log(); + Task.log(`🎉 Successfully created ${factory.name}`); + Task.log(); + } catch (error) { + assertError(error); + Task.error(error.message); + + if (modified) { + Task.log('It seems that something went wrong in the creation process 🤔'); + Task.log(); + Task.log( + 'We have left the changes that were made intact in case you want to', + ); + Task.log( + 'continue manually, but you can also revert the changes and try again.', + ); + + Task.error(`🔥 Failed to create ${factory.name}!`); + } } finally { for (const dir of tempDirs) { try { diff --git a/packages/cli/src/lib/create/types.ts b/packages/cli/src/lib/create/types.ts index 629506b5f7..6a45067e9b 100644 --- a/packages/cli/src/lib/create/types.ts +++ b/packages/cli/src/lib/create/types.ts @@ -30,6 +30,9 @@ export interface CreateContext { /** Creates a temporary directory. This will always be deleted after creation is done. */ createTemporaryDirectory(name: string): Promise; + + /** Signal that the creation process got to a point where permanent modifications were made */ + markAsModified(): void; } export type AnyOptions = Record; From f8adebde77b0a6c299f1463f4a759942fab7be55 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 15:23:32 +0100 Subject: [PATCH 137/181] cli: build out task lib a bit with command execution, adding deps Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/tasks.ts | 81 +++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index e17015d6cf..691e77bd17 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -18,9 +18,14 @@ import chalk from 'chalk'; import fs from 'fs-extra'; import handlebars from 'handlebars'; import ora from 'ora'; +import { promisify } from 'util'; import { basename, dirname } from 'path'; import recursive from 'recursive-readdir'; +import { exec as execCb } from 'child_process'; import { paths } from './paths'; +import { assertError } from '@backstage/errors'; + +const exec = promisify(execCb); const TASK_NAME_MAX_LENGTH = 14; @@ -42,11 +47,11 @@ export class Task { process.exit(code); } - static async forItem( + static async forItem( task: string, item: string, - taskFunc: () => Promise, - ): Promise { + taskFunc: () => Promise, + ): Promise { const paddedTask = chalk.green(task.padEnd(TASK_NAME_MAX_LENGTH)); const spinner = ora({ @@ -56,13 +61,40 @@ export class Task { }).start(); try { - await taskFunc(); + const result = await taskFunc(); spinner.succeed(); + return result; } catch (error) { spinner.fail(); throw error; } } + + static async forCommand( + command: string, + options?: { cwd?: string; optional?: boolean }, + ) { + try { + await Task.forItem('executing', command, async () => { + await exec(command, { cwd: options?.cwd }); + }); + } catch (error) { + assertError(error); + if (error.stderr) { + process.stdout.write(error.stderr as Buffer); + } + if (error.stdout) { + process.stdout.write(error.stdout as Buffer); + } + if (options?.optional) { + Task.error(`Warning: Failed to execute command ${chalk.cyan(command)}`); + } else { + throw new Error( + `Failed to execute command '${chalk.cyan(command)}', ${error}`, + ); + } + } + } } export async function templatingTask( @@ -122,3 +154,44 @@ export async function templatingTask( } } } + +export async function addPackageDependency( + path: string, + options: { + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + }, +) { + try { + const pkgJson = await fs.readJson(path); + + const normalize = (obj: Record) => { + if (Object.keys(obj).length === 0) { + return undefined; + } + return Object.fromEntries( + Object.keys(obj) + .sort() + .map(key => [key, obj[key]]), + ); + }; + + pkgJson.dependencies = normalize({ + ...pkgJson.dependencies, + ...options.dependencies, + }); + pkgJson.devDependencies = normalize({ + ...pkgJson.devDependencies, + ...options.devDependencies, + }); + pkgJson.peerDependencies = normalize({ + ...pkgJson.peerDependencies, + ...options.peerDependencies, + }); + + await fs.writeJson(path, pkgJson, { spaces: 2 }); + } catch (error) { + throw new Error(`Failed to add package dependencies, ${error}`); + } +} From 602cf4d80dabe6a24e4585da37b9a0af09a3b320 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 15:24:08 +0100 Subject: [PATCH 138/181] cli: run templating tasks in strict mode Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/tasks.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index 691e77bd17..9e67f7f30c 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -117,7 +117,9 @@ export async function templatingTask( const destination = destinationFile.replace(/\.hbs$/, ''); const template = await fs.readFile(file); - const compiled = handlebars.compile(template.toString()); + const compiled = handlebars.compile(template.toString(), { + strict: true, + }); const contents = compiled( { name: basename(destination), ...context }, { From 248c8eaa25af0f4147fc383fe553765068530f44 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 15:33:45 +0100 Subject: [PATCH 139/181] cli: allow empty create option values Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index 7004341c3e..946918d77c 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -29,7 +29,7 @@ function parseOptions(optionStrings: string[]): Record { for (const str of optionStrings) { const [key] = str.split('=', 1); const value = str.slice(key.length + 1); - if (!key || !value) { + if (!key || str[key.length] !== '=') { throw new Error( `Invalid option '${str}', must be of the format =`, ); From 7b8d432de5842a420ca711e5895462118176ed12 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 15:39:17 +0100 Subject: [PATCH 140/181] cli: complete frontendPlugin create factory implementation Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/codeowners/codeowners.ts | 2 +- .../lib/create/factories/frontendPlugin.ts | 144 +++++++++++++++++- 2 files changed, 138 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/lib/codeowners/codeowners.ts b/packages/cli/src/lib/codeowners/codeowners.ts index 563bd1052d..b3fac12109 100644 --- a/packages/cli/src/lib/codeowners/codeowners.ts +++ b/packages/cli/src/lib/codeowners/codeowners.ts @@ -55,7 +55,7 @@ export function isValidSingleOwnerId(id: string): boolean { } export function parseOwnerIds( - spaceSeparatedOwnerIds: string, + spaceSeparatedOwnerIds: string | undefined, ): string[] | undefined { if (!spaceSeparatedOwnerIds || typeof spaceSeparatedOwnerIds !== 'string') { return undefined; diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index 1b13cff13d..a85403e8ed 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -14,9 +14,20 @@ * limitations under the License. */ -import { paths } from '../../../lib/paths'; -import { getCodeownersFilePath, parseOwnerIds } from '../../../lib/codeowners'; +import fs from 'fs-extra'; +import camelCase from 'lodash/camelCase'; +import upperFirst from 'lodash/upperFirst'; +import chalk from 'chalk'; +import { paths } from '../../paths'; +import { + addCodeownersEntry, + getCodeownersFilePath, + parseOwnerIds, +} from '../../codeowners'; import { createFactory, CreateContext } from '../types'; +import { Lockfile } from '../../versioning'; +import { addPackageDependency, Task, templatingTask } from '../../tasks'; +import { createPackageVersionProvider } from '../../version'; type Options = { id: string; @@ -63,11 +74,130 @@ export const frontendPlugin = createFactory({ }, }, ], - async create(options: Options, context: CreateContext) { - console.log( - `Creating ${this.name} with options ${JSON.stringify( - options, - )} and context ${JSON.stringify(context)}`, + async create(options: Options, ctx: CreateContext) { + const { id } = options; + + const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + const extensionName = `${upperFirst(camelCase(id))}Page`; + + const pluginDir = ctx.isMonoRepo + ? paths.resolveTargetRoot('plugins', id) + : paths.resolveTargetRoot(`backstage-plugin-${id}`); + + let lockfile: Lockfile | undefined; + try { + lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + } catch (error) { + console.warn(`No yarn.lock available, ${error}`); + } + + Task.section('Validating prerequisites'); + const shortPluginDir = pluginDir.replace(`${paths.targetRoot}/`, ''); + await Task.forItem('availability', shortPluginDir, async () => { + if (await fs.pathExists(pluginDir)) { + throw new Error( + `A plugin with the same ID already exists at ${chalk.cyan( + shortPluginDir, + )}. Please try again with a different ID.`, + ); + } + }); + + const tempDir = await Task.forItem('creating', 'temp dir', async () => { + return await ctx.createTemporaryDirectory(`backstage-plugin-${id}`); + }); + + Task.section('Executing plugin template'); + await templatingTask( + paths.resolveOwn('templates/default-plugin'), + tempDir, + { + id, + pluginVar: `${camelCase(id)}Plugin`, + pluginVersion: ctx.defaultVersion, + extensionName, + name, + privatePackage: ctx.private, + npmRegistry: ctx.npmRegistry, + }, + createPackageVersionProvider(lockfile), ); + + Task.section('Installing plugin'); + await Task.forItem('moving', shortPluginDir, async () => { + await fs.move(tempDir, pluginDir).catch(error => { + throw new Error( + `Failed to move plugin from ${tempDir} to ${pluginDir}, ${error.message}`, + ); + }); + }); + + ctx.markAsModified(); + + if (await fs.pathExists(paths.resolveTargetRoot('packages/app'))) { + await Task.forItem('app', 'adding dependency', async () => { + await addPackageDependency( + paths.resolveTargetRoot('packages/app/package.json'), + { + dependencies: { + [name]: `^${ctx.defaultVersion}`, + }, + }, + ); + }); + + await Task.forItem('app', 'adding import', async () => { + const pluginsFilePath = paths.resolveTargetRoot( + 'packages/app/src/App.tsx', + ); + if (!(await fs.pathExists(pluginsFilePath))) { + return; + } + + const content = await fs.readFile(pluginsFilePath, 'utf8'); + const revLines = content.split('\n').reverse(); + + const lastImportIndex = revLines.findIndex(line => + line.match(/ from ("|').*("|')/), + ); + const lastRouteIndex = revLines.findIndex(line => + line.match(/<\/FlatRoutes/), + ); + + if (lastImportIndex !== -1 && lastRouteIndex !== -1) { + const importLine = `import { ${extensionName} } from '${name}';`; + if (!content.includes(importLine)) { + revLines.splice(lastImportIndex, 0, importLine); + } + + const componentLine = `}/>`; + if (!content.includes(componentLine)) { + const [indentation] = + revLines[lastRouteIndex + 1].match(/^\s*/) ?? []; + revLines.splice(lastRouteIndex + 1, 0, indentation + componentLine); + } + + const newContent = revLines.reverse().join('\n'); + await fs.writeFile(pluginsFilePath, newContent, 'utf8'); + } + }); + } + + if (options.codeOwnersPath && options.owner) { + const ownerIds = parseOwnerIds(options.owner); + if (ownerIds && ownerIds.length > 0) { + await addCodeownersEntry( + options.codeOwnersPath, + `/plugins/${id}`, + ownerIds, + ); + } + } + + await Task.forCommand('yarn install', { cwd: pluginDir, optional: true }); + await Task.forCommand('yarn lint --fix', { + cwd: pluginDir, + optional: true, + }); }, }); From 1c0ac6291cdaa278aa58cf00592e6ab7b562e92d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 16:00:54 +0100 Subject: [PATCH 141/181] cli: added create backend plugin factory Signed-off-by: Patrik Oldsberg --- .../src/lib/create/factories/backendPlugin.ts | 164 ++++++++++++++++++ .../cli/src/lib/create/factories/index.ts | 1 + 2 files changed, 165 insertions(+) create mode 100644 packages/cli/src/lib/create/factories/backendPlugin.ts diff --git a/packages/cli/src/lib/create/factories/backendPlugin.ts b/packages/cli/src/lib/create/factories/backendPlugin.ts new file mode 100644 index 0000000000..9cf78900a7 --- /dev/null +++ b/packages/cli/src/lib/create/factories/backendPlugin.ts @@ -0,0 +1,164 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import camelCase from 'lodash/camelCase'; +import upperFirst from 'lodash/upperFirst'; +import chalk from 'chalk'; +import { paths } from '../../paths'; +import { + addCodeownersEntry, + getCodeownersFilePath, + parseOwnerIds, +} from '../../codeowners'; +import { createFactory, CreateContext } from '../types'; +import { Lockfile } from '../../versioning'; +import { addPackageDependency, Task, templatingTask } from '../../tasks'; +import { createPackageVersionProvider } from '../../version'; + +type Options = { + id: string; + owner?: string; + codeOwnersPath?: string; +}; + +export const backendPlugin = createFactory({ + name: 'backend-plugin', + description: 'A new backend plugin', + optionsDiscovery: async () => ({ + codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), + }), + optionsPrompts: [ + { + type: 'input', + name: 'id', + message: 'Enter an ID for the plugin [required]', + validate: (value: string) => { + if (!value) { + return 'Please enter an ID for the plugin'; + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.'; + } + return true; + }, + }, + { + type: 'input', + name: 'owner', + message: 'Enter an owner of the plugin to add to CODEOWNERS [optional]', + when: opts => Boolean(opts.codeOwnersPath), + validate: (value: string) => { + if (!value) { + return true; + } + + const ownerIds = parseOwnerIds(value); + if (!ownerIds) { + return 'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses (e.g. user@example.com).'; + } + + return true; + }, + }, + ], + async create(options: Options, ctx: CreateContext) { + const id = `${options.id}-backend`; + const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + + const pluginDir = ctx.isMonoRepo + ? paths.resolveTargetRoot('plugins', id) + : paths.resolveTargetRoot(`backstage-plugin-${id}`); + + let lockfile: Lockfile | undefined; + try { + lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + } catch (error) { + console.warn(`No yarn.lock available, ${error}`); + } + + Task.section('Validating prerequisites'); + const shortPluginDir = pluginDir.replace(`${paths.targetRoot}/`, ''); + await Task.forItem('availability', shortPluginDir, async () => { + if (await fs.pathExists(pluginDir)) { + throw new Error( + `A backend plugin with the same ID already exists at ${chalk.cyan( + shortPluginDir, + )}. Please try again with a different ID.`, + ); + } + }); + + const tempDir = await Task.forItem('creating', 'temp dir', async () => { + return await ctx.createTemporaryDirectory(`backstage-plugin-${id}`); + }); + + Task.section('Executing plugin template'); + await templatingTask( + paths.resolveOwn('templates/default-backend-plugin'), + tempDir, + { + id, + name, + pluginVar: `${camelCase(id)}Plugin`, + pluginVersion: ctx.defaultVersion, + privatePackage: ctx.private, + npmRegistry: ctx.npmRegistry, + }, + createPackageVersionProvider(lockfile), + ); + + Task.section('Installing plugin'); + await Task.forItem('moving', shortPluginDir, async () => { + await fs.move(tempDir, pluginDir).catch(error => { + throw new Error( + `Failed to move plugin from ${tempDir} to ${pluginDir}, ${error.message}`, + ); + }); + }); + + ctx.markAsModified(); + + if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) { + await Task.forItem('backend', 'adding dependency', async () => { + await addPackageDependency( + paths.resolveTargetRoot('packages/backend/package.json'), + { + dependencies: { + [name]: `^${ctx.defaultVersion}`, + }, + }, + ); + }); + } + + if (options.codeOwnersPath && options.owner) { + const ownerIds = parseOwnerIds(options.owner); + if (ownerIds && ownerIds.length > 0) { + await addCodeownersEntry( + options.codeOwnersPath, + `/plugins/${id}`, + ownerIds, + ); + } + } + + await Task.forCommand('yarn install', { cwd: pluginDir, optional: true }); + await Task.forCommand('yarn lint --fix', { + cwd: pluginDir, + optional: true, + }); + }, +}); diff --git a/packages/cli/src/lib/create/factories/index.ts b/packages/cli/src/lib/create/factories/index.ts index 4375ba2a45..2e16979e50 100644 --- a/packages/cli/src/lib/create/factories/index.ts +++ b/packages/cli/src/lib/create/factories/index.ts @@ -15,3 +15,4 @@ */ export { frontendPlugin } from './frontendPlugin'; +export { backendPlugin } from './backendPlugin'; From 142cd41b7e351c20f23a6a9f611586b28bcf81ce Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 16:08:32 +0100 Subject: [PATCH 142/181] cli: refactor create factories to use common prompts Signed-off-by: Patrik Oldsberg --- .../src/lib/create/factories/backendPlugin.ts | 35 +---------- .../lib/create/factories/common/prompts.ts | 58 +++++++++++++++++++ .../lib/create/factories/frontendPlugin.ts | 35 +---------- packages/cli/src/lib/create/types.ts | 14 +++-- 4 files changed, 70 insertions(+), 72 deletions(-) create mode 100644 packages/cli/src/lib/create/factories/common/prompts.ts diff --git a/packages/cli/src/lib/create/factories/backendPlugin.ts b/packages/cli/src/lib/create/factories/backendPlugin.ts index 9cf78900a7..d7855d5d68 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.ts @@ -28,6 +28,7 @@ import { createFactory, CreateContext } from '../types'; import { Lockfile } from '../../versioning'; import { addPackageDependency, Task, templatingTask } from '../../tasks'; import { createPackageVersionProvider } from '../../version'; +import { ownerPrompt, pluginIdPrompt } from './common/prompts'; type Options = { id: string; @@ -41,39 +42,7 @@ export const backendPlugin = createFactory({ optionsDiscovery: async () => ({ codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), }), - optionsPrompts: [ - { - type: 'input', - name: 'id', - message: 'Enter an ID for the plugin [required]', - validate: (value: string) => { - if (!value) { - return 'Please enter an ID for the plugin'; - } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { - return 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.'; - } - return true; - }, - }, - { - type: 'input', - name: 'owner', - message: 'Enter an owner of the plugin to add to CODEOWNERS [optional]', - when: opts => Boolean(opts.codeOwnersPath), - validate: (value: string) => { - if (!value) { - return true; - } - - const ownerIds = parseOwnerIds(value); - if (!ownerIds) { - return 'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses (e.g. user@example.com).'; - } - - return true; - }, - }, - ], + optionsPrompts: [pluginIdPrompt(), ownerPrompt()], async create(options: Options, ctx: CreateContext) { const id = `${options.id}-backend`; const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; diff --git a/packages/cli/src/lib/create/factories/common/prompts.ts b/packages/cli/src/lib/create/factories/common/prompts.ts new file mode 100644 index 0000000000..9c7672ddfb --- /dev/null +++ b/packages/cli/src/lib/create/factories/common/prompts.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Prompt } from '../../types'; +import { parseOwnerIds } from '../../../codeowners'; + +export function pluginIdPrompt(): Prompt<{ id: string }> { + return { + type: 'input', + name: 'id', + message: 'Enter the ID of the plugin [required]', + validate: (value: string) => { + if (!value) { + return 'Please enter the ID of the plugin'; + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.'; + } + return true; + }, + }; +} + +export function ownerPrompt(): Prompt<{ + owner?: string; + codeOwnersPath?: string; +}> { + return { + type: 'input', + name: 'owner', + message: 'Enter an owner to add to CODEOWNERS [optional]', + when: opts => Boolean(opts.codeOwnersPath), + validate: (value: string) => { + if (!value) { + return true; + } + + const ownerIds = parseOwnerIds(value); + if (!ownerIds) { + return 'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses (e.g. user@example.com).'; + } + + return true; + }, + }; +} diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index a85403e8ed..3fe01b7325 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -28,6 +28,7 @@ import { createFactory, CreateContext } from '../types'; import { Lockfile } from '../../versioning'; import { addPackageDependency, Task, templatingTask } from '../../tasks'; import { createPackageVersionProvider } from '../../version'; +import { ownerPrompt, pluginIdPrompt } from './common/prompts'; type Options = { id: string; @@ -41,39 +42,7 @@ export const frontendPlugin = createFactory({ optionsDiscovery: async () => ({ codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), }), - optionsPrompts: [ - { - type: 'input', - name: 'id', - message: 'Enter an ID for the plugin [required]', - validate: (value: string) => { - if (!value) { - return 'Please enter an ID for the plugin'; - } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { - return 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.'; - } - return true; - }, - }, - { - type: 'input', - name: 'owner', - message: 'Enter an owner of the plugin to add to CODEOWNERS [optional]', - when: opts => Boolean(opts.codeOwnersPath), - validate: (value: string) => { - if (!value) { - return true; - } - - const ownerIds = parseOwnerIds(value); - if (!ownerIds) { - return 'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses (e.g. user@example.com).'; - } - - return true; - }, - }, - ], + optionsPrompts: [pluginIdPrompt(), ownerPrompt()], async create(options: Options, ctx: CreateContext) { const { id } = options; diff --git a/packages/cli/src/lib/create/types.ts b/packages/cli/src/lib/create/types.ts index 6a45067e9b..fd2e684e11 100644 --- a/packages/cli/src/lib/create/types.ts +++ b/packages/cli/src/lib/create/types.ts @@ -37,18 +37,20 @@ export interface CreateContext { export type AnyOptions = Record; -export interface Factory { +export type Prompt = DistinctQuestion & { name: string }; + +export interface Factory { name: string; description: string; - optionsDiscovery?(): Promise>; - optionsPrompts?: ReadonlyArray & { name: string }>; - create(options: Options, context?: CreateContext): Promise; + optionsDiscovery?(): Promise>; + optionsPrompts?: ReadonlyArray>; + create(options: TOptions, context?: CreateContext): Promise; } export type AnyFactory = Factory; -export function createFactory( - config: Factory, +export function createFactory( + config: Factory, ): AnyFactory { return config as AnyFactory; } From 0ebcf168d25d04a7ba39b75929fbca273ae6446d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 16:36:57 +0100 Subject: [PATCH 143/181] cli: common helper for executing plugin package templates Signed-off-by: Patrik Oldsberg --- .../src/lib/create/factories/backendPlugin.ts | 57 +++----------- .../src/lib/create/factories/common/tasks.ts | 76 +++++++++++++++++++ .../lib/create/factories/frontendPlugin.ts | 60 +++------------ 3 files changed, 96 insertions(+), 97 deletions(-) create mode 100644 packages/cli/src/lib/create/factories/common/tasks.ts diff --git a/packages/cli/src/lib/create/factories/backendPlugin.ts b/packages/cli/src/lib/create/factories/backendPlugin.ts index d7855d5d68..68c684f6ab 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.ts @@ -16,8 +16,6 @@ import fs from 'fs-extra'; import camelCase from 'lodash/camelCase'; -import upperFirst from 'lodash/upperFirst'; -import chalk from 'chalk'; import { paths } from '../../paths'; import { addCodeownersEntry, @@ -25,10 +23,9 @@ import { parseOwnerIds, } from '../../codeowners'; import { createFactory, CreateContext } from '../types'; -import { Lockfile } from '../../versioning'; -import { addPackageDependency, Task, templatingTask } from '../../tasks'; -import { createPackageVersionProvider } from '../../version'; +import { addPackageDependency, Task } from '../../tasks'; import { ownerPrompt, pluginIdPrompt } from './common/prompts'; +import { executePluginPackageTemplate } from './common/tasks'; type Options = { id: string; @@ -47,38 +44,14 @@ export const backendPlugin = createFactory({ const id = `${options.id}-backend`; const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; - const pluginDir = ctx.isMonoRepo + const targetDir = ctx.isMonoRepo ? paths.resolveTargetRoot('plugins', id) : paths.resolveTargetRoot(`backstage-plugin-${id}`); - let lockfile: Lockfile | undefined; - try { - lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); - } catch (error) { - console.warn(`No yarn.lock available, ${error}`); - } - - Task.section('Validating prerequisites'); - const shortPluginDir = pluginDir.replace(`${paths.targetRoot}/`, ''); - await Task.forItem('availability', shortPluginDir, async () => { - if (await fs.pathExists(pluginDir)) { - throw new Error( - `A backend plugin with the same ID already exists at ${chalk.cyan( - shortPluginDir, - )}. Please try again with a different ID.`, - ); - } - }); - - const tempDir = await Task.forItem('creating', 'temp dir', async () => { - return await ctx.createTemporaryDirectory(`backstage-plugin-${id}`); - }); - - Task.section('Executing plugin template'); - await templatingTask( - paths.resolveOwn('templates/default-backend-plugin'), - tempDir, - { + await executePluginPackageTemplate(ctx, { + targetDir, + templateName: 'default-backend-plugin', + values: { id, name, pluginVar: `${camelCase(id)}Plugin`, @@ -86,20 +59,8 @@ export const backendPlugin = createFactory({ privatePackage: ctx.private, npmRegistry: ctx.npmRegistry, }, - createPackageVersionProvider(lockfile), - ); - - Task.section('Installing plugin'); - await Task.forItem('moving', shortPluginDir, async () => { - await fs.move(tempDir, pluginDir).catch(error => { - throw new Error( - `Failed to move plugin from ${tempDir} to ${pluginDir}, ${error.message}`, - ); - }); }); - ctx.markAsModified(); - if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) { await Task.forItem('backend', 'adding dependency', async () => { await addPackageDependency( @@ -124,9 +85,9 @@ export const backendPlugin = createFactory({ } } - await Task.forCommand('yarn install', { cwd: pluginDir, optional: true }); + await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); await Task.forCommand('yarn lint --fix', { - cwd: pluginDir, + cwd: targetDir, optional: true, }); }, diff --git a/packages/cli/src/lib/create/factories/common/tasks.ts b/packages/cli/src/lib/create/factories/common/tasks.ts new file mode 100644 index 0000000000..b644110a9f --- /dev/null +++ b/packages/cli/src/lib/create/factories/common/tasks.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import chalk from 'chalk'; +import { paths } from '../../../paths'; +import { Task, templatingTask } from '../../../tasks'; +import { Lockfile } from '../../../versioning'; +import { createPackageVersionProvider } from '../../../version'; +import { CreateContext } from '../../types'; + +export async function executePluginPackageTemplate( + ctx: CreateContext, + options: { + templateName: string; + targetDir: string; + values: Record; + }, +) { + const { targetDir } = options; + + let lockfile: Lockfile | undefined; + try { + lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + } catch { + /* ignored */ + } + + Task.section('Checking Prerequisites'); + const shortPluginDir = targetDir.replace(`${paths.targetRoot}/`, ''); + await Task.forItem('availability', shortPluginDir, async () => { + if (await fs.pathExists(targetDir)) { + throw new Error( + `A package with the same plugin ID already exists at ${chalk.cyan( + shortPluginDir, + )}. Please try again with a different ID.`, + ); + } + }); + + const tempDir = await Task.forItem('creating', 'temp dir', async () => { + return await ctx.createTemporaryDirectory('backstage-create'); + }); + + Task.section('Executing Template'); + await templatingTask( + paths.resolveOwn('templates', options.templateName), + tempDir, + options.values, + createPackageVersionProvider(lockfile), + ); + + Task.section('Installing'); + await Task.forItem('moving', shortPluginDir, async () => { + await fs.move(tempDir, targetDir).catch(error => { + throw new Error( + `Failed to move package from ${tempDir} to ${targetDir}, ${error.message}`, + ); + }); + }); + + ctx.markAsModified(); +} diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index 3fe01b7325..bb93bd311c 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -17,7 +17,6 @@ import fs from 'fs-extra'; import camelCase from 'lodash/camelCase'; import upperFirst from 'lodash/upperFirst'; -import chalk from 'chalk'; import { paths } from '../../paths'; import { addCodeownersEntry, @@ -25,10 +24,9 @@ import { parseOwnerIds, } from '../../codeowners'; import { createFactory, CreateContext } from '../types'; -import { Lockfile } from '../../versioning'; -import { addPackageDependency, Task, templatingTask } from '../../tasks'; -import { createPackageVersionProvider } from '../../version'; +import { addPackageDependency, Task } from '../../tasks'; import { ownerPrompt, pluginIdPrompt } from './common/prompts'; +import { executePluginPackageTemplate } from './common/tasks'; type Options = { id: string; @@ -49,60 +47,24 @@ export const frontendPlugin = createFactory({ const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; const extensionName = `${upperFirst(camelCase(id))}Page`; - const pluginDir = ctx.isMonoRepo + const targetDir = ctx.isMonoRepo ? paths.resolveTargetRoot('plugins', id) : paths.resolveTargetRoot(`backstage-plugin-${id}`); - let lockfile: Lockfile | undefined; - try { - lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); - } catch (error) { - console.warn(`No yarn.lock available, ${error}`); - } - - Task.section('Validating prerequisites'); - const shortPluginDir = pluginDir.replace(`${paths.targetRoot}/`, ''); - await Task.forItem('availability', shortPluginDir, async () => { - if (await fs.pathExists(pluginDir)) { - throw new Error( - `A plugin with the same ID already exists at ${chalk.cyan( - shortPluginDir, - )}. Please try again with a different ID.`, - ); - } - }); - - const tempDir = await Task.forItem('creating', 'temp dir', async () => { - return await ctx.createTemporaryDirectory(`backstage-plugin-${id}`); - }); - - Task.section('Executing plugin template'); - await templatingTask( - paths.resolveOwn('templates/default-plugin'), - tempDir, - { + await executePluginPackageTemplate(ctx, { + targetDir, + templateName: 'default-plugin', + values: { id, + name, + extensionName, pluginVar: `${camelCase(id)}Plugin`, pluginVersion: ctx.defaultVersion, - extensionName, - name, privatePackage: ctx.private, npmRegistry: ctx.npmRegistry, }, - createPackageVersionProvider(lockfile), - ); - - Task.section('Installing plugin'); - await Task.forItem('moving', shortPluginDir, async () => { - await fs.move(tempDir, pluginDir).catch(error => { - throw new Error( - `Failed to move plugin from ${tempDir} to ${pluginDir}, ${error.message}`, - ); - }); }); - ctx.markAsModified(); - if (await fs.pathExists(paths.resolveTargetRoot('packages/app'))) { await Task.forItem('app', 'adding dependency', async () => { await addPackageDependency( @@ -163,9 +125,9 @@ export const frontendPlugin = createFactory({ } } - await Task.forCommand('yarn install', { cwd: pluginDir, optional: true }); + await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); await Task.forCommand('yarn lint --fix', { - cwd: pluginDir, + cwd: targetDir, optional: true, }); }, From 367e09e31fd828561f94024bf90355576774a295 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 16:45:56 +0100 Subject: [PATCH 144/181] cli: refactor to simplify codeowners logic Signed-off-by: Patrik Oldsberg --- .../commands/create-plugin/createPlugin.ts | 9 ++---- .../remove-plugin/removePlugin.test.ts | 4 +-- packages/cli/src/lib/codeowners/codeowners.ts | 30 ++++++++++++++----- .../src/lib/create/factories/backendPlugin.ts | 17 ++--------- .../lib/create/factories/frontendPlugin.ts | 17 ++--------- 5 files changed, 33 insertions(+), 44 deletions(-) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index a5ea565cae..c80129662c 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -263,7 +263,6 @@ export default async (cmd: Command) => { const pluginDir = isMonoRepo ? paths.resolveTargetRoot('plugins', pluginId) : paths.resolveTargetRoot(pluginId); - const ownerIds = parseOwnerIds(answers.owner); const { version: pluginVersion } = isMonoRepo ? await fs.readJson(paths.resolveTargetRoot('lerna.json')) : { version: '0.1.0' }; @@ -318,12 +317,8 @@ export default async (cmd: Command) => { await addPluginExtensionToApp(pluginId, extensionName, name); } - if (ownerIds && ownerIds.length) { - await addCodeownersEntry( - codeownersPath!, - `/plugins/${pluginId}`, - ownerIds, - ); + if (answers.owner) { + await addCodeownersEntry(`/plugins/${pluginId}`, answers.owner); } Task.log(); diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts index 0390320128..8859b5d4cd 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts @@ -177,9 +177,9 @@ describe('removePlugin', () => { fse.readFileSync(mockedCodeownersPath, 'utf8'), ); await addCodeownersEntry( - testFilePath!, path.join('plugins', testPluginName), - ['@thisIsAtestTeam', 'test@gmail.com'], + '@thisIsAtestTeam test@gmail.com', + testFilePath, ); await removePluginFromCodeOwners(testFilePath, testPluginName); expect(testFileContent).toBe(codeOwnersFileContent); diff --git a/packages/cli/src/lib/codeowners/codeowners.ts b/packages/cli/src/lib/codeowners/codeowners.ts index b3fac12109..5734be95b8 100644 --- a/packages/cli/src/lib/codeowners/codeowners.ts +++ b/packages/cli/src/lib/codeowners/codeowners.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import path from 'path'; +import { paths } from '../paths'; const TEAM_ID_RE = /^@[-\w]+\/[-\w]+$/; const USER_ID_RE = /^@[-\w]+$/; @@ -30,14 +31,14 @@ type CodeownersEntry = { export async function getCodeownersFilePath( rootDir: string, ): Promise { - const paths = [ + const possiblePaths = [ path.join(rootDir, '.github', 'CODEOWNERS'), path.join(rootDir, '.gitlab', 'CODEOWNERS'), path.join(rootDir, 'docs', 'CODEOWNERS'), path.join(rootDir, 'CODEOWNERS'), ]; - for (const p of paths) { + for (const p of possiblePaths) { if (await fs.pathExists(p)) { return p; } @@ -70,11 +71,24 @@ export function parseOwnerIds( } export async function addCodeownersEntry( - codeownersFilePath: string, ownedPath: string, - ownerIds: string[], -): Promise { - const allLines = (await fs.readFile(codeownersFilePath, 'utf8')).split('\n'); + ownerStr: string, + codeownersFilePath?: string, +): Promise { + const ownerIds = parseOwnerIds(ownerStr); + if (!ownerIds || ownerIds.length === 0) { + return false; + } + + let filePath = codeownersFilePath; + if (!filePath) { + filePath = await getCodeownersFilePath(paths.targetRoot); + if (!filePath) { + return false; + } + } + + const allLines = (await fs.readFile(filePath, 'utf8')).split('\n'); // Only keep comments from the top of the file const commentLines = []; @@ -117,5 +131,7 @@ export async function addCodeownersEntry( const newLines = [...commentLines, '', ...newDeclarationLines, '']; - await fs.writeFile(codeownersFilePath, newLines.join('\n'), 'utf8'); + await fs.writeFile(filePath, newLines.join('\n'), 'utf8'); + + return true; } diff --git a/packages/cli/src/lib/create/factories/backendPlugin.ts b/packages/cli/src/lib/create/factories/backendPlugin.ts index 68c684f6ab..2fd461c2a9 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.ts @@ -17,11 +17,7 @@ import fs from 'fs-extra'; import camelCase from 'lodash/camelCase'; import { paths } from '../../paths'; -import { - addCodeownersEntry, - getCodeownersFilePath, - parseOwnerIds, -} from '../../codeowners'; +import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; import { createFactory, CreateContext } from '../types'; import { addPackageDependency, Task } from '../../tasks'; import { ownerPrompt, pluginIdPrompt } from './common/prompts'; @@ -74,15 +70,8 @@ export const backendPlugin = createFactory({ }); } - if (options.codeOwnersPath && options.owner) { - const ownerIds = parseOwnerIds(options.owner); - if (ownerIds && ownerIds.length > 0) { - await addCodeownersEntry( - options.codeOwnersPath, - `/plugins/${id}`, - ownerIds, - ); - } + if (options.owner) { + await addCodeownersEntry(`/plugins/${id}`, options.owner); } await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index bb93bd311c..1d30d0235d 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -18,11 +18,7 @@ import fs from 'fs-extra'; import camelCase from 'lodash/camelCase'; import upperFirst from 'lodash/upperFirst'; import { paths } from '../../paths'; -import { - addCodeownersEntry, - getCodeownersFilePath, - parseOwnerIds, -} from '../../codeowners'; +import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; import { createFactory, CreateContext } from '../types'; import { addPackageDependency, Task } from '../../tasks'; import { ownerPrompt, pluginIdPrompt } from './common/prompts'; @@ -114,15 +110,8 @@ export const frontendPlugin = createFactory({ }); } - if (options.codeOwnersPath && options.owner) { - const ownerIds = parseOwnerIds(options.owner); - if (ownerIds && ownerIds.length > 0) { - await addCodeownersEntry( - options.codeOwnersPath, - `/plugins/${id}`, - ownerIds, - ); - } + if (options.owner) { + await addCodeownersEntry(`/plugins/${id}`, options.owner); } await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); From 26eb174ce8f9ecd163738cc919aa8e27f51057c1 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Fri, 12 Nov 2021 15:33:22 +0000 Subject: [PATCH 145/181] Skip empty file names during scaffolder Signed-off-by: Tim Jacomb --- .changeset/ninety-spies-prove.md | 5 +++++ .../actions/builtin/fetch/template.test.ts | 17 +++++++++++++++++ .../actions/builtin/fetch/template.ts | 5 +++++ 3 files changed, 27 insertions(+) create mode 100644 .changeset/ninety-spies-prove.md diff --git a/.changeset/ninety-spies-prove.md b/.changeset/ninety-spies-prove.md new file mode 100644 index 0000000000..509aedd66f --- /dev/null +++ b/.changeset/ninety-spies-prove.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Skip empty file names when scaffolding with nunjucks diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts index 4befec5a81..755b435149 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts @@ -144,6 +144,7 @@ describe('fetch:template', () => { name: 'test-project', count: 1234, itemList: ['first', 'second', 'third'], + showDummyFile: false, }, }); @@ -163,6 +164,10 @@ describe('fetch:template', () => { }, '.${{ values.name }}': '${{ values.itemList | dump }}', 'a-binary-file.png': aBinaryFile, + '{% if values.showDummyFile %}dummy-file.txt{% else %}{% endif %}': + 'dummy file', + '${{ "dummy-file2.txt" if values.showDummyFile else "" }}': + 'some dummy file', }, }); @@ -181,6 +186,18 @@ describe('fetch:template', () => { ); }); + it('skips empty filename', async () => { + await expect( + fs.pathExists(`${workspacePath}/target/dummy-file.txt`), + ).resolves.toEqual(false); + }); + + it('skips empty filename syntax #2', async () => { + await expect( + fs.pathExists(`${workspacePath}/target/dummy-file2.txt`), + ).resolves.toEqual(false); + }); + it('copies files with no templating in names or content successfully', async () => { await expect( fs.readFile(`${workspacePath}/target/static.txt`, 'utf-8'), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts index bb568c25e6..47bfcccfdf 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts @@ -241,6 +241,11 @@ export function createFetchTemplateAction(options: { localOutputPath = templater.renderString(localOutputPath, context); } const outputPath = resolvePath(outputDir, localOutputPath); + // variables have been expanded to make an empty file name + // this is due to a conditional like if values.my_condition then file-name.txt else empty string so skip + if (outputDir === outputPath) { + continue; + } if (!renderContents && !extension) { ctx.logger.info( From 033f6abfcebc0b7ae1c276b412fcb306efe2443d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 18:15:03 +0100 Subject: [PATCH 146/181] cli: add tests for frontendPlugin create factory + minor fixes Signed-off-by: Patrik Oldsberg --- .../create/factories/frontendPlugin.test.ts | 232 ++++++++++++++++++ .../lib/create/factories/frontendPlugin.ts | 2 +- packages/cli/src/lib/tasks.ts | 8 +- 3 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 packages/cli/src/lib/create/factories/frontendPlugin.test.ts diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts new file mode 100644 index 0000000000..fe815ce80f --- /dev/null +++ b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts @@ -0,0 +1,232 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { resolve as resolvePath } from 'path'; +import { WriteStream } from 'tty'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { FactoryRegistry } from '../FactoryRegistry'; +import { frontendPlugin } from './frontendPlugin'; + +function createMockOutputStream() { + const output = new Array(); + return [ + output, + { + cursorTo: () => {}, + clearLine: () => {}, + moveCursor: () => {}, + write: (msg: string) => + // Clean up colors and whitespace + // eslint-disable-next-line no-control-regex + output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), + } as unknown as WriteStream & { fd: any }, + ] as const; +} + +const appTsxContent = ` +import { createApp } from '@backstage/app-defaults'; + +const router = ( + + } /> + +) +`; + +describe('frontendPlugin factory', () => { + beforeEach(() => { + jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...ps) => resolvePath('/root', ...ps)); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should create a frontend plugin', async () => { + mockFs({ + '/root': { + packages: { + app: { + 'package.json': JSON.stringify({}), + src: { + 'App.tsx': appTsxContent, + }, + }, + }, + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(frontendPlugin, { + id: 'test', + }); + + let modified = false; + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await frontendPlugin.create(options, { + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => { + modified = true; + }, + createTemporaryDirectory: () => fs.mkdtemp('test'), + }); + + expect(modified).toBe(true); + + expect(output).toEqual([ + 'Checking Prerequisites:', + 'availability plugins/test ✔', + 'creating temp dir ✔', + 'Executing Template:', + 'copying .eslintrc.js ✔', + 'templating README.md.hbs ✔', + 'templating package.json.hbs ✔', + 'copying tsconfig.json ✔', + 'templating index.tsx.hbs ✔', + 'templating index.ts.hbs ✔', + 'templating plugin.test.ts.hbs ✔', + 'templating plugin.ts.hbs ✔', + 'templating routes.ts.hbs ✔', + 'copying setupTests.ts ✔', + 'templating ExampleComponent.test.tsx.hbs ✔', + 'templating ExampleComponent.tsx.hbs ✔', + 'copying index.ts ✔', + 'templating ExampleFetchComponent.test.tsx.hbs ✔', + 'templating ExampleFetchComponent.tsx.hbs ✔', + 'copying index.ts ✔', + 'Installing:', + 'moving plugins/test ✔', + 'app adding dependency ✔', + 'app adding import ✔', + ]); + + await expect( + fs.readJson('/root/packages/app/package.json'), + ).resolves.toEqual({ + dependencies: { + 'plugin-test': '^1.0.0', + }, + }); + + await expect(fs.readFile('/root/packages/app/src/App.tsx', 'utf8')).resolves + .toBe(` +import { createApp } from '@backstage/app-defaults'; +import { TestPage } from 'plugin-test'; + +const router = ( + + } /> + } /> + +) +`); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: '/root/plugins/test', + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: '/root/plugins/test', + optional: true, + }); + }); + + it('should create a frontend plugin with more options and codeowners', async () => { + mockFs({ + '/root': { + CODEOWNERS: '', + packages: { + app: { + 'package.json': JSON.stringify({}), + src: { + 'App.tsx': appTsxContent, + }, + }, + }, + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(frontendPlugin, { + id: 'test', + owner: '@test-user', + }); + + const [, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await frontendPlugin.create(options, { + scope: 'internal', + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => {}, + createTemporaryDirectory: () => fs.mkdtemp('test'), + }); + + await expect( + fs.readJson('/root/packages/app/package.json'), + ).resolves.toEqual({ + dependencies: { + '@internal/plugin-test': '^1.0.0', + }, + }); + + await expect(fs.readFile('/root/packages/app/src/App.tsx', 'utf8')).resolves + .toBe(` +import { createApp } from '@backstage/app-defaults'; +import { TestPage } from '@internal/plugin-test'; + +const router = ( + + } /> + } /> + +) +`); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: '/root/plugins/test', + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: '/root/plugins/test', + optional: true, + }); + }); +}); diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index 1d30d0235d..50e8dd42a3 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -97,7 +97,7 @@ export const frontendPlugin = createFactory({ revLines.splice(lastImportIndex, 0, importLine); } - const componentLine = `}/>`; + const componentLine = `} />`; if (!content.includes(componentLine)) { const [indentation] = revLines[lastRouteIndex + 1].match(/^\s*/) ?? []; diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index 9e67f7f30c..c84eed435e 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -31,16 +31,16 @@ const TASK_NAME_MAX_LENGTH = 14; export class Task { static log(name: string = '') { - process.stdout.write(`${chalk.green(name)}\n`); + process.stderr.write(`${chalk.green(name)}\n`); } static error(message: string = '') { - process.stdout.write(`\n${chalk.red(message)}\n\n`); + process.stderr.write(`\n${chalk.red(message)}\n\n`); } static section(name: string) { const title = chalk.green(`${name}:`); - process.stdout.write(`\n ${title}\n`); + process.stderr.write(`\n ${title}\n`); } static exit(code: number = 0) { @@ -81,7 +81,7 @@ export class Task { } catch (error) { assertError(error); if (error.stderr) { - process.stdout.write(error.stderr as Buffer); + process.stderr.write(error.stderr as Buffer); } if (error.stdout) { process.stdout.write(error.stdout as Buffer); From 1cf00cbe334488b62f4c36f1f59170bea8ab0382 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 18:19:33 +0100 Subject: [PATCH 147/181] cli: added test for backendPlugin create factory Signed-off-by: Patrik Oldsberg --- .../create/factories/backendPlugin.test.ts | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 packages/cli/src/lib/create/factories/backendPlugin.test.ts diff --git a/packages/cli/src/lib/create/factories/backendPlugin.test.ts b/packages/cli/src/lib/create/factories/backendPlugin.test.ts new file mode 100644 index 0000000000..1100b3001e --- /dev/null +++ b/packages/cli/src/lib/create/factories/backendPlugin.test.ts @@ -0,0 +1,130 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { resolve as resolvePath } from 'path'; +import { WriteStream } from 'tty'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { FactoryRegistry } from '../FactoryRegistry'; +import { backendPlugin } from './backendPlugin'; + +function createMockOutputStream() { + const output = new Array(); + return [ + output, + { + cursorTo: () => {}, + clearLine: () => {}, + moveCursor: () => {}, + write: (msg: string) => + // Clean up colors and whitespace + // eslint-disable-next-line no-control-regex + output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), + } as unknown as WriteStream & { fd: any }, + ] as const; +} + +describe('backendPlugin factory', () => { + beforeEach(() => { + jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...ps) => resolvePath('/root', ...ps)); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should create a backend plugin', async () => { + mockFs({ + '/root': { + packages: { + backend: { + 'package.json': JSON.stringify({}), + }, + }, + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(backendPlugin, { + id: 'test', + }); + + let modified = false; + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await backendPlugin.create(options, { + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => { + modified = true; + }, + createTemporaryDirectory: () => fs.mkdtemp('test'), + }); + + expect(modified).toBe(true); + + expect(output).toEqual([ + 'Checking Prerequisites:', + 'availability plugins/test-backend ✔', + 'creating temp dir ✔', + 'Executing Template:', + 'copying .eslintrc.js ✔', + 'templating README.md.hbs ✔', + 'templating package.json.hbs ✔', + 'copying tsconfig.json ✔', + 'copying index.ts ✔', + 'templating run.ts.hbs ✔', + 'copying setupTests.ts ✔', + 'copying router.test.ts ✔', + 'copying router.ts ✔', + 'templating standaloneServer.ts.hbs ✔', + 'Installing:', + 'moving plugins/test-backend ✔', + 'backend adding dependency ✔', + ]); + + await expect( + fs.readJson('/root/packages/backend/package.json'), + ).resolves.toEqual({ + dependencies: { + 'plugin-test-backend': '^1.0.0', + }, + }); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: '/root/plugins/test-backend', + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: '/root/plugins/test-backend', + optional: true, + }); + }); +}); From 8a60033962c0e529c81f12c1695225377c59242b Mon Sep 17 00:00:00 2001 From: Zach Falen Date: Fri, 12 Nov 2021 12:53:13 -0700 Subject: [PATCH 148/181] hotfix for Backstage token generation, prefer .token over .idToken Signed-off-by: Zach Falen --- .../src/layout/SignInPage/commonProvider.tsx | 4 +++- plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/core-components/src/layout/SignInPage/commonProvider.tsx b/packages/core-components/src/layout/SignInPage/commonProvider.tsx index 103050a2d9..515ae01e4d 100644 --- a/packages/core-components/src/layout/SignInPage/commonProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/commonProvider.tsx @@ -49,7 +49,9 @@ const Component: ProviderComponent = ({ config, onResult }) => { userId: identity!.id, profile: profile!, getIdToken: () => { - return authApi.getBackstageIdentity().then(i => i!.idToken); + return authApi + .getBackstageIdentity() + .then(i => i!.token ?? i!.idToken); }, signOut: async () => { await authApi.signOut(); diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index a5128711bc..24b9a0f210 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -233,10 +233,12 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { return; } - if (!identity.idToken) { - identity.idToken = await this.options.tokenIssuer.issueToken({ + if (!(identity.token || identity.idToken)) { + identity.token = await this.options.tokenIssuer.issueToken({ claims: { sub: identity.id }, }); + } else if (!identity.token && identity.idToken) { + identity.token = identity.idToken; } } From 892c1d9202f42856e7585d670abb75f1d7059eaf Mon Sep 17 00:00:00 2001 From: Zach Falen Date: Fri, 12 Nov 2021 12:56:33 -0700 Subject: [PATCH 149/181] add changeset Signed-off-by: Zach Falen --- .changeset/ninety-grapes-love.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/ninety-grapes-love.md diff --git a/.changeset/ninety-grapes-love.md b/.changeset/ninety-grapes-love.md new file mode 100644 index 0000000000..9143bddd30 --- /dev/null +++ b/.changeset/ninety-grapes-love.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-auth-backend': patch +--- + +Update OAuthAdapter to create identity.token from identity.idToken if it does not exist, and prevent overwrites to identity.toke. Update login page commonProvider to prefer .token over .idToken From 7d6ab03ebbfc14ec4f1c8d0ebf9fa7957a200457 Mon Sep 17 00:00:00 2001 From: Zach Falen Date: Fri, 12 Nov 2021 13:32:48 -0700 Subject: [PATCH 150/181] update test Signed-off-by: Zach Falen --- plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index 27b629cc07..dfb3a0a79a 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -22,7 +22,7 @@ import { OAuthHandlers } from './types'; const mockResponseData = { providerInfo: { accessToken: 'ACCESS_TOKEN', - idToken: 'ID_TOKEN', + token: 'ID_TOKEN', expiresInSeconds: 10, scope: 'email', }, @@ -216,7 +216,7 @@ describe('OAuthAdapter', () => { ...mockResponseData, backstageIdentity: { id: mockResponseData.backstageIdentity.id, - idToken: 'my-id-token', + token: 'my-id-token', }, }); }); From 46a161a023fcca6d2130f12fc88cbd7befff0a57 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 21:35:40 +0100 Subject: [PATCH 151/181] scripts: make snyk issue script executable Signed-off-by: Patrik Oldsberg --- scripts/snyk-github-issue-sync.ts | 1 + 1 file changed, 1 insertion(+) mode change 100644 => 100755 scripts/snyk-github-issue-sync.ts diff --git a/scripts/snyk-github-issue-sync.ts b/scripts/snyk-github-issue-sync.ts old mode 100644 new mode 100755 index f1015f8139..9120089266 --- a/scripts/snyk-github-issue-sync.ts +++ b/scripts/snyk-github-issue-sync.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env yarn ts-node --transpile-only /* * Copyright 2021 The Backstage Authors * From dd751588657de1ed2b787fa86a5c4aed37db14f0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 21:39:26 +0100 Subject: [PATCH 152/181] scripts: tweak snyk script to match license issues Signed-off-by: Patrik Oldsberg --- scripts/snyk-github-issue-sync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/snyk-github-issue-sync.ts b/scripts/snyk-github-issue-sync.ts index 9120089266..da64e2ea82 100755 --- a/scripts/snyk-github-issue-sync.ts +++ b/scripts/snyk-github-issue-sync.ts @@ -34,7 +34,7 @@ const argv = minimist(process.argv.slice(2)); const GH_OWNER = 'backstage'; const GH_REPO = 'backstage'; const SNYK_GH_LABEL = 'snyk-vulnerability'; -const SNYK_ID_REGEX = /\[([A-Z0-9-:]+)]/i; +const SNYK_ID_REGEX = /\[([^\]]+)]/i; const isDryRun = 'dryrun' in argv; From 8f2a7184386e1fdbb43b3d35d1bf90bbf681fdf4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 18:38:47 +0100 Subject: [PATCH 153/181] cli: added create factory and template for plugin common package Signed-off-by: Patrik Oldsberg --- .../cli/src/lib/create/factories/index.ts | 1 + .../lib/create/factories/pluginCommon.test.ts | 123 ++++++++++++++++++ .../src/lib/create/factories/pluginCommon.ts | 67 ++++++++++ .../.eslintrc.js | 3 + .../README.md.hbs | 5 + .../package.json.hbs | 32 +++++ .../src/index.ts | 16 +++ .../src/setupTests.ts | 1 + .../tsconfig.json | 9 ++ 9 files changed, 257 insertions(+) create mode 100644 packages/cli/src/lib/create/factories/pluginCommon.test.ts create mode 100644 packages/cli/src/lib/create/factories/pluginCommon.ts create mode 100644 packages/cli/templates/default-common-plugin-package/.eslintrc.js create mode 100644 packages/cli/templates/default-common-plugin-package/README.md.hbs create mode 100644 packages/cli/templates/default-common-plugin-package/package.json.hbs create mode 100644 packages/cli/templates/default-common-plugin-package/src/index.ts create mode 100644 packages/cli/templates/default-common-plugin-package/src/setupTests.ts create mode 100644 packages/cli/templates/default-common-plugin-package/tsconfig.json diff --git a/packages/cli/src/lib/create/factories/index.ts b/packages/cli/src/lib/create/factories/index.ts index 2e16979e50..e4df9062f7 100644 --- a/packages/cli/src/lib/create/factories/index.ts +++ b/packages/cli/src/lib/create/factories/index.ts @@ -16,3 +16,4 @@ export { frontendPlugin } from './frontendPlugin'; export { backendPlugin } from './backendPlugin'; +export { pluginCommon } from './pluginCommon'; diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts new file mode 100644 index 0000000000..f7251dc443 --- /dev/null +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { resolve as resolvePath } from 'path'; +import { WriteStream } from 'tty'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { FactoryRegistry } from '../FactoryRegistry'; +import { pluginCommon } from './pluginCommon'; + +function createMockOutputStream() { + const output = new Array(); + return [ + output, + { + cursorTo: () => {}, + clearLine: () => {}, + moveCursor: () => {}, + write: (msg: string) => + // Clean up colors and whitespace + // eslint-disable-next-line no-control-regex + output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), + } as unknown as WriteStream & { fd: any }, + ] as const; +} + +describe('pluginCommon factory', () => { + beforeEach(() => { + jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...ps) => resolvePath('/root', ...ps)); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should create a common plugin package', async () => { + mockFs({ + '/root': { + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(pluginCommon, { + id: 'test', + }); + + let modified = false; + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await pluginCommon.create(options, { + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => { + modified = true; + }, + createTemporaryDirectory: () => fs.mkdtemp('test'), + }); + + expect(modified).toBe(true); + + expect(output).toEqual([ + 'Checking Prerequisites:', + 'availability plugins/test-common ✔', + 'creating temp dir ✔', + 'Executing Template:', + 'copying .eslintrc.js ✔', + 'templating README.md.hbs ✔', + 'templating package.json.hbs ✔', + 'copying tsconfig.json ✔', + 'copying index.ts ✔', + 'copying setupTests.ts ✔', + 'Installing:', + 'moving plugins/test-common ✔', + ]); + + await expect( + fs.readJson('/root/plugins/test-common/package.json'), + ).resolves.toEqual( + expect.objectContaining({ + name: 'plugin-test-common', + description: 'Common functionalities for the test-common plugin', + private: true, + version: '1.0.0', + }), + ); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: '/root/plugins/test-common', + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: '/root/plugins/test-common', + optional: true, + }); + }); +}); diff --git a/packages/cli/src/lib/create/factories/pluginCommon.ts b/packages/cli/src/lib/create/factories/pluginCommon.ts new file mode 100644 index 0000000000..5a6dd30046 --- /dev/null +++ b/packages/cli/src/lib/create/factories/pluginCommon.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { paths } from '../../paths'; +import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; +import { createFactory, CreateContext } from '../types'; +import { Task } from '../../tasks'; +import { ownerPrompt, pluginIdPrompt } from './common/prompts'; +import { executePluginPackageTemplate } from './common/tasks'; + +type Options = { + id: string; + owner?: string; + codeOwnersPath?: string; +}; + +export const pluginCommon = createFactory({ + name: 'plugin-common', + description: 'A new isomorphic common plugin package', + optionsDiscovery: async () => ({ + codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), + }), + optionsPrompts: [pluginIdPrompt(), ownerPrompt()], + async create(options: Options, ctx: CreateContext) { + const id = `${options.id}-common`; + const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + + const targetDir = ctx.isMonoRepo + ? paths.resolveTargetRoot('plugins', id) + : paths.resolveTargetRoot(`backstage-plugin-${id}`); + + await executePluginPackageTemplate(ctx, { + targetDir, + templateName: 'default-common-plugin-package', + values: { + id, + name, + privatePackage: ctx.private, + npmRegistry: ctx.npmRegistry, + pluginVersion: ctx.defaultVersion, + }, + }); + + if (options.owner) { + await addCodeownersEntry(`/plugins/${id}`, options.owner); + } + + await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); + await Task.forCommand('yarn lint --fix', { + cwd: targetDir, + optional: true, + }); + }, +}); diff --git a/packages/cli/templates/default-common-plugin-package/.eslintrc.js b/packages/cli/templates/default-common-plugin-package/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/packages/cli/templates/default-common-plugin-package/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/packages/cli/templates/default-common-plugin-package/README.md.hbs b/packages/cli/templates/default-common-plugin-package/README.md.hbs new file mode 100644 index 0000000000..917e18d4b9 --- /dev/null +++ b/packages/cli/templates/default-common-plugin-package/README.md.hbs @@ -0,0 +1,5 @@ +# {{name}} + +Welcome to the common package for the {{id}} plugin! + +_This plugin was created through the Backstage CLI_ diff --git a/packages/cli/templates/default-common-plugin-package/package.json.hbs b/packages/cli/templates/default-common-plugin-package/package.json.hbs new file mode 100644 index 0000000000..c7ba25ac49 --- /dev/null +++ b/packages/cli/templates/default-common-plugin-package/package.json.hbs @@ -0,0 +1,32 @@ +{ + "name": "{{name}}", + "description": "Common functionalities for the {{id}} plugin", + "version": "{{pluginVersion}}", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", +{{#if privatePackage}} "private": {{privatePackage}}, +{{/if}} + "publishConfig": { +{{#if npmRegistry}} "registry": "{{npmRegistry}}", +{{/if}} + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "devDependencies": { + "@backstage/cli": "{{versionQuery '@backstage/cli'}}" + }, + "files": [ + "dist" + ] +} diff --git a/packages/cli/templates/default-common-plugin-package/src/index.ts b/packages/cli/templates/default-common-plugin-package/src/index.ts new file mode 100644 index 0000000000..6ee452b5bb --- /dev/null +++ b/packages/cli/templates/default-common-plugin-package/src/index.ts @@ -0,0 +1,16 @@ +/** + * Common functionalities for the {{id}} plugin. + */ + +/** + * In this package you might for example declare types that are common + * between the frontend and backend plugin packages. + */ +export type CommonType = { + field: string +} + +/** + * Or you might declare some common constants. + */ +export const COMMON_CONSTANT = 1 diff --git a/packages/cli/templates/default-common-plugin-package/src/setupTests.ts b/packages/cli/templates/default-common-plugin-package/src/setupTests.ts new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/packages/cli/templates/default-common-plugin-package/src/setupTests.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/cli/templates/default-common-plugin-package/tsconfig.json b/packages/cli/templates/default-common-plugin-package/tsconfig.json new file mode 100644 index 0000000000..5ae9aeb62d --- /dev/null +++ b/packages/cli/templates/default-common-plugin-package/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "dist-types", + "rootDir": "." + } +} From 827fb840b46c59b4322d11e5400a2e6364aadcb8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 18:45:51 +0100 Subject: [PATCH 154/181] cli: fix for create crashing if no scope is specified Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index 946918d77c..5fff07b194 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -86,7 +86,7 @@ export default async (cmd: Command) => { await factory.create(options, { isMonoRepo, defaultVersion, - scope: cmdOpts.scope.replace(/^@/, ''), + scope: cmdOpts.scope?.replace(/^@/, ''), npmRegistry: cmdOpts.npmRegistry, private: Boolean(cmdOpts.private), createTemporaryDirectory, From a9f8363b5d66bc55830ad9f90e1367c74efb5f15 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Nov 2021 18:46:25 +0100 Subject: [PATCH 155/181] cli: format package.json after creation to simplify template Signed-off-by: Patrik Oldsberg --- .../src/lib/create/factories/common/tasks.ts | 8 ++ .../default-backend-plugin/package.json.hbs | 76 ++++++++++--------- .../package.json.hbs | 6 +- .../templates/default-plugin/package.json.hbs | 6 +- 4 files changed, 55 insertions(+), 41 deletions(-) diff --git a/packages/cli/src/lib/create/factories/common/tasks.ts b/packages/cli/src/lib/create/factories/common/tasks.ts index b644110a9f..6fe2e77fc7 100644 --- a/packages/cli/src/lib/create/factories/common/tasks.ts +++ b/packages/cli/src/lib/create/factories/common/tasks.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import chalk from 'chalk'; +import { resolve as resolvePath } from 'path'; import { paths } from '../../../paths'; import { Task, templatingTask } from '../../../tasks'; import { Lockfile } from '../../../versioning'; @@ -63,6 +64,13 @@ export async function executePluginPackageTemplate( createPackageVersionProvider(lockfile), ); + // Format package.json if it exists + const targetPkgJsonPath = resolvePath(targetDir, 'package.json'); + if (await fs.pathExists(targetPkgJsonPath)) { + const pkgJson = await fs.readJson(targetPkgJsonPath); + await fs.writeJson(targetPkgJsonPath, pkgJson, { spaces: 2 }); + } + Task.section('Installing'); await Task.forItem('moving', shortPluginDir, async () => { await fs.move(tempDir, targetDir).catch(error => { diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 5cca7dcb07..37bcd94186 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -4,41 +4,43 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - {{#if privatePackage}} "private": {{privatePackage}}, - {{/if}} +{{#if privatePackage}} + "private": {{privatePackage}}, +{{/if}} "publishConfig": { - {{#if npmRegistry}} "registry": "{{npmRegistry}}", - {{/if}} - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" - }, - "scripts": { - "start": "backstage-cli backend:dev", - "build": "backstage-cli backend:build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" - }, - "dependencies": { - "@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}", - "@backstage/config": "{{versionQuery '@backstage/config'}}", - "@types/express": "{{versionQuery '@types/express' '4.17.6'}}", - "express": "{{versionQuery 'express' '4.17.1'}}", - "express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}", - "winston": "{{versionQuery 'winston' '3.2.1'}}", - "cross-fetch": "{{versionQuery 'cross-fetch' '3.0.6'}}", - "yn": "{{versionQuery 'yn' '4.0.0'}}" - }, - "devDependencies": { - "@backstage/cli": "{{versionQuery '@backstage/cli'}}", - "@types/supertest": "{{versionQuery '@types/supertest' '2.0.8'}}", - "supertest": "{{versionQuery 'supertest' '4.0.2'}}", - "msw": "{{versionQuery 'msw' '0.35.0'}}" - }, - "files": [ - "dist" - ] - } +{{#if npmRegistry}} + "registry": "{{npmRegistry}}", +{{/if}} + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}", + "@backstage/config": "{{versionQuery '@backstage/config'}}", + "@types/express": "{{versionQuery '@types/express' '4.17.6'}}", + "express": "{{versionQuery 'express' '4.17.1'}}", + "express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}", + "winston": "{{versionQuery 'winston' '3.2.1'}}", + "cross-fetch": "{{versionQuery 'cross-fetch' '3.0.6'}}", + "yn": "{{versionQuery 'yn' '4.0.0'}}" + }, + "devDependencies": { + "@backstage/cli": "{{versionQuery '@backstage/cli'}}", + "@types/supertest": "{{versionQuery '@types/supertest' '2.0.8'}}", + "supertest": "{{versionQuery 'supertest' '4.0.2'}}", + "msw": "{{versionQuery 'msw' '0.35.0'}}" + }, + "files": [ + "dist" + ] +} diff --git a/packages/cli/templates/default-common-plugin-package/package.json.hbs b/packages/cli/templates/default-common-plugin-package/package.json.hbs index c7ba25ac49..efaee496e7 100644 --- a/packages/cli/templates/default-common-plugin-package/package.json.hbs +++ b/packages/cli/templates/default-common-plugin-package/package.json.hbs @@ -5,10 +5,12 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", -{{#if privatePackage}} "private": {{privatePackage}}, +{{#if privatePackage}} + "private": {{privatePackage}}, {{/if}} "publishConfig": { -{{#if npmRegistry}} "registry": "{{npmRegistry}}", +{{#if npmRegistry}} + "registry": "{{npmRegistry}}", {{/if}} "access": "public", "main": "dist/index.cjs.js", diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 41376821ba..624302da93 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -4,10 +4,12 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", -{{#if privatePackage}} "private": {{privatePackage}}, +{{#if privatePackage}} + "private": {{privatePackage}}, {{/if}} "publishConfig": { -{{#if npmRegistry}} "registry": "{{npmRegistry}}", +{{#if npmRegistry}} + "registry": "{{npmRegistry}}", {{/if}} "access": "public", "main": "dist/index.esm.js", From 1c291cb66f2d61467b7a0eeb1c967dc5675456ac Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 10:32:25 +0100 Subject: [PATCH 156/181] package.json: add backstage-create script Signed-off-by: Patrik Oldsberg --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e6d376c46..a7c5e2fbf7 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "lint:all": "lerna run lint --", "lint:type-deps": "node scripts/check-type-dependencies.js", "docker-build": "yarn tsc && yarn workspace example-backend build --build-dependencies && yarn workspace example-backend build-image", - "create-plugin": "backstage-cli create-plugin --scope backstage --no-private", + "backstage-create": "backstage-cli create --scope backstage --no-private", + "create-plugin": "yarn backstage-create --select plugin", "remove-plugin": "backstage-cli remove-plugin", "release": "changeset version && yarn diff --yes && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install", "prettier:check": "prettier --check .", From 8c83ce8084df178556d60752bf3f0f0c50db4473 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 10:38:49 +0100 Subject: [PATCH 157/181] cli: leave initial create logging to the factories Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 3 --- packages/cli/src/lib/create/factories/backendPlugin.test.ts | 2 ++ packages/cli/src/lib/create/factories/backendPlugin.ts | 4 ++++ packages/cli/src/lib/create/factories/frontendPlugin.test.ts | 2 ++ packages/cli/src/lib/create/factories/frontendPlugin.ts | 4 ++++ packages/cli/src/lib/create/factories/pluginCommon.test.ts | 2 ++ packages/cli/src/lib/create/factories/pluginCommon.ts | 4 ++++ 7 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index 5fff07b194..d80daa6ddf 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -80,9 +80,6 @@ export default async (cmd: Command) => { let modified = false; try { - Task.log(); - Task.log(`Creating new ${factory.name}`); - await factory.create(options, { isMonoRepo, defaultVersion, diff --git a/packages/cli/src/lib/create/factories/backendPlugin.test.ts b/packages/cli/src/lib/create/factories/backendPlugin.test.ts index 1100b3001e..cf597572ad 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.test.ts @@ -90,6 +90,8 @@ describe('backendPlugin factory', () => { expect(modified).toBe(true); expect(output).toEqual([ + '', + 'Creating backend plugin plugin-test-backend', 'Checking Prerequisites:', 'availability plugins/test-backend ✔', 'creating temp dir ✔', diff --git a/packages/cli/src/lib/create/factories/backendPlugin.ts b/packages/cli/src/lib/create/factories/backendPlugin.ts index 2fd461c2a9..2bdc62741d 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.ts @@ -15,6 +15,7 @@ */ import fs from 'fs-extra'; +import chalk from 'chalk'; import camelCase from 'lodash/camelCase'; import { paths } from '../../paths'; import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; @@ -40,6 +41,9 @@ export const backendPlugin = createFactory({ const id = `${options.id}-backend`; const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + Task.log(); + Task.log(`Creating backend plugin ${chalk.cyan(name)}`); + const targetDir = ctx.isMonoRepo ? paths.resolveTargetRoot('plugins', id) : paths.resolveTargetRoot(`backstage-plugin-${id}`); diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts index fe815ce80f..3b24f95e41 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts @@ -103,6 +103,8 @@ describe('frontendPlugin factory', () => { expect(modified).toBe(true); expect(output).toEqual([ + '', + 'Creating backend plugin plugin-test', 'Checking Prerequisites:', 'availability plugins/test ✔', 'creating temp dir ✔', diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index 50e8dd42a3..a6c0d0470d 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -15,6 +15,7 @@ */ import fs from 'fs-extra'; +import chalk from 'chalk'; import camelCase from 'lodash/camelCase'; import upperFirst from 'lodash/upperFirst'; import { paths } from '../../paths'; @@ -43,6 +44,9 @@ export const frontendPlugin = createFactory({ const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; const extensionName = `${upperFirst(camelCase(id))}Page`; + Task.log(); + Task.log(`Creating backend plugin ${chalk.cyan(name)}`); + const targetDir = ctx.isMonoRepo ? paths.resolveTargetRoot('plugins', id) : paths.resolveTargetRoot(`backstage-plugin-${id}`); diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts index f7251dc443..6e797f4cca 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -85,6 +85,8 @@ describe('pluginCommon factory', () => { expect(modified).toBe(true); expect(output).toEqual([ + '', + 'Creating backend plugin plugin-test-common', 'Checking Prerequisites:', 'availability plugins/test-common ✔', 'creating temp dir ✔', diff --git a/packages/cli/src/lib/create/factories/pluginCommon.ts b/packages/cli/src/lib/create/factories/pluginCommon.ts index 5a6dd30046..97b1eccec7 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import chalk from 'chalk'; import { paths } from '../../paths'; import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; import { createFactory, CreateContext } from '../types'; @@ -38,6 +39,9 @@ export const pluginCommon = createFactory({ const id = `${options.id}-common`; const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + Task.log(); + Task.log(`Creating backend plugin ${chalk.cyan(name)}`); + const targetDir = ctx.isMonoRepo ? paths.resolveTargetRoot('plugins', id) : paths.resolveTargetRoot(`backstage-plugin-${id}`); From 7b8f19492edfea021cc4681c392e3e43f79d5b74 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 12:30:04 +0100 Subject: [PATCH 158/181] cli: add test for plugin package template execution + fix Signed-off-by: Patrik Oldsberg --- .../lib/create/factories/common/tasks.test.ts | 117 ++++++++++++++++++ .../src/lib/create/factories/common/tasks.ts | 8 +- .../lib/create/factories/common/testUtils.ts | 68 ++++++++++ 3 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 packages/cli/src/lib/create/factories/common/tasks.test.ts create mode 100644 packages/cli/src/lib/create/factories/common/testUtils.ts diff --git a/packages/cli/src/lib/create/factories/common/tasks.test.ts b/packages/cli/src/lib/create/factories/common/tasks.test.ts new file mode 100644 index 0000000000..b3a751ba5b --- /dev/null +++ b/packages/cli/src/lib/create/factories/common/tasks.test.ts @@ -0,0 +1,117 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { createMockOutputStream, mockPaths } from './testUtils'; +import { CreateContext } from '../../types'; +import { executePluginPackageTemplate } from './tasks'; + +mockPaths({ + ownDir: '/own', + targetRoot: '/root', +}); + +describe('executePluginPackageTemplate', () => { + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should execute template', async () => { + mockFs({ + '/root': { + 'yarn.lock': ` +some-package@^1.1.0: + version "1.5.0" +`, + }, + '/own': { + templates: { + 'test-template': { + 'package.json.hbs': ` +{ + "name": "my-{{id}}-plugin", + {{#if makePrivate}} + "private": true, + {{/if}} + "description": "testing", + "dependencies": { + "some-package": "{{ versionQuery 'some-package' '1.3.0' }}", + "other-package": "{{ versionQuery 'other-package' '2.3.0' }}" + } +} +`, + subdir: { + 'templated.txt.hbs': 'Hello {{id}}!', + 'not-templated.txt': 'Hello {{id}}!', + }, + }, + }, + }, + }); + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + + let modified = false; + await executePluginPackageTemplate( + { + createTemporaryDirectory: (name: string) => fs.mkdtemp(name), + markAsModified: () => { + modified = true; + }, + } as CreateContext, + { + templateName: 'test-template', + targetDir: '/target', + values: { + id: 'testing', + makePrivate: true, + }, + }, + ); + + expect(modified).toBe(true); + expect(output).toEqual([ + 'Checking Prerequisites:', + 'availability /target ✔', + 'creating temp dir ✔', + 'Executing Template:', + 'templating package.json.hbs ✔', + 'copying not-templated.txt ✔', + 'templating templated.txt.hbs ✔', + 'Installing:', + 'moving /target ✔', + ]); + await expect(fs.readFile('/target/package.json', 'utf8')).resolves.toBe(`{ + "name": "my-testing-plugin", + "private": true, + "description": "testing", + "dependencies": { + "some-package": "^1.1.0", + "other-package": "^2.3.0" + } +} +`); + await expect( + fs.readFile('/target/subdir/templated.txt', 'utf8'), + ).resolves.toBe('Hello testing!'); + await expect( + fs.readFile('/target/subdir/not-templated.txt', 'utf8'), + ).resolves.toBe('Hello {{id}}!'); + }); +}); diff --git a/packages/cli/src/lib/create/factories/common/tasks.ts b/packages/cli/src/lib/create/factories/common/tasks.ts index 6fe2e77fc7..fd3bd4d930 100644 --- a/packages/cli/src/lib/create/factories/common/tasks.ts +++ b/packages/cli/src/lib/create/factories/common/tasks.ts @@ -65,10 +65,10 @@ export async function executePluginPackageTemplate( ); // Format package.json if it exists - const targetPkgJsonPath = resolvePath(targetDir, 'package.json'); - if (await fs.pathExists(targetPkgJsonPath)) { - const pkgJson = await fs.readJson(targetPkgJsonPath); - await fs.writeJson(targetPkgJsonPath, pkgJson, { spaces: 2 }); + const pkgJsonPath = resolvePath(tempDir, 'package.json'); + if (await fs.pathExists(pkgJsonPath)) { + const pkgJson = await fs.readJson(pkgJsonPath); + await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 }); } Task.section('Installing'); diff --git a/packages/cli/src/lib/create/factories/common/testUtils.ts b/packages/cli/src/lib/create/factories/common/testUtils.ts new file mode 100644 index 0000000000..651409451e --- /dev/null +++ b/packages/cli/src/lib/create/factories/common/testUtils.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { WriteStream } from 'tty'; +import { resolve as resolvePath } from 'path'; +import { paths } from '../../../paths'; + +export function mockPaths(options: { + ownDir?: string; + ownRoot?: string; + targetDir?: string; + targetRoot?: string; +}): void { + const { ownDir, ownRoot, targetDir, targetRoot } = options; + if (ownDir) { + paths.ownDir = ownDir; + jest + .spyOn(paths, 'resolveOwn') + .mockImplementation((...ps) => resolvePath(ownDir, ...ps)); + } + if (ownRoot) { + jest.spyOn(paths, 'ownRoot', 'get').mockReturnValue(ownRoot); + jest + .spyOn(paths, 'resolveOwnRoot') + .mockImplementation((...ps) => resolvePath(ownRoot, ...ps)); + } + if (targetDir) { + paths.targetDir = targetDir; + jest + .spyOn(paths, 'resolveTarget') + .mockImplementation((...ps) => resolvePath(targetDir, ...ps)); + } + if (targetRoot) { + jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue(targetRoot); + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...ps) => resolvePath(targetRoot, ...ps)); + } +} + +export function createMockOutputStream() { + const output = new Array(); + return [ + output, + { + cursorTo: () => {}, + clearLine: () => {}, + moveCursor: () => {}, + write: (msg: string) => + // Clean up colors and whitespace + // eslint-disable-next-line no-control-regex + output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), + } as unknown as WriteStream & { fd: any }, + ] as const; +} From 823a097b3b5a87205eefc8c086cf13e0ae0aa8c6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 12:36:47 +0100 Subject: [PATCH 159/181] cli: use common testing utils for create factory tests Signed-off-by: Patrik Oldsberg --- .../create/factories/backendPlugin.test.ts | 26 +++------------- .../create/factories/frontendPlugin.test.ts | 26 +++------------- .../lib/create/factories/pluginCommon.test.ts | 31 +++---------------- 3 files changed, 12 insertions(+), 71 deletions(-) diff --git a/packages/cli/src/lib/create/factories/backendPlugin.test.ts b/packages/cli/src/lib/create/factories/backendPlugin.test.ts index cf597572ad..409b7f476a 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.test.ts @@ -16,35 +16,17 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import { resolve as resolvePath } from 'path'; -import { WriteStream } from 'tty'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; +import { createMockOutputStream, mockPaths } from './common/testUtils'; import { backendPlugin } from './backendPlugin'; -function createMockOutputStream() { - const output = new Array(); - return [ - output, - { - cursorTo: () => {}, - clearLine: () => {}, - moveCursor: () => {}, - write: (msg: string) => - // Clean up colors and whitespace - // eslint-disable-next-line no-control-regex - output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), - } as unknown as WriteStream & { fd: any }, - ] as const; -} - describe('backendPlugin factory', () => { beforeEach(() => { - jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); - jest - .spyOn(paths, 'resolveTargetRoot') - .mockImplementation((...ps) => resolvePath('/root', ...ps)); + mockPaths({ + targetRoot: '/root', + }); }); afterEach(() => { diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts index 3b24f95e41..3e0f94f6ad 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts @@ -16,29 +16,12 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import { resolve as resolvePath } from 'path'; -import { WriteStream } from 'tty'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; +import { createMockOutputStream, mockPaths } from './common/testUtils'; import { frontendPlugin } from './frontendPlugin'; -function createMockOutputStream() { - const output = new Array(); - return [ - output, - { - cursorTo: () => {}, - clearLine: () => {}, - moveCursor: () => {}, - write: (msg: string) => - // Clean up colors and whitespace - // eslint-disable-next-line no-control-regex - output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), - } as unknown as WriteStream & { fd: any }, - ] as const; -} - const appTsxContent = ` import { createApp } from '@backstage/app-defaults'; @@ -51,10 +34,9 @@ const router = ( describe('frontendPlugin factory', () => { beforeEach(() => { - jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); - jest - .spyOn(paths, 'resolveTargetRoot') - .mockImplementation((...ps) => resolvePath('/root', ...ps)); + mockPaths({ + targetRoot: '/root', + }); }); afterEach(() => { diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts index 6e797f4cca..73edc5159b 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -16,40 +16,17 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import { resolve as resolvePath } from 'path'; -import { WriteStream } from 'tty'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; +import { createMockOutputStream, mockPaths } from './common/testUtils'; import { pluginCommon } from './pluginCommon'; -function createMockOutputStream() { - const output = new Array(); - return [ - output, - { - cursorTo: () => {}, - clearLine: () => {}, - moveCursor: () => {}, - write: (msg: string) => - // Clean up colors and whitespace - // eslint-disable-next-line no-control-regex - output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), - } as unknown as WriteStream & { fd: any }, - ] as const; -} - describe('pluginCommon factory', () => { beforeEach(() => { - jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); - jest - .spyOn(paths, 'resolveTargetRoot') - .mockImplementation((...ps) => resolvePath('/root', ...ps)); - }); - - afterEach(() => { - mockFs.restore(); - jest.resetAllMocks(); + mockPaths({ + targetRoot: '/root', + }); }); it('should create a common plugin package', async () => { From 9bbc2da04ee00711983284cf4955c05769623ca4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 13:18:07 +0100 Subject: [PATCH 160/181] docs/local-dev: add cli create command docs Signed-off-by: Patrik Oldsberg --- docs/local-dev/cli-commands.md | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/local-dev/cli-commands.md b/docs/local-dev/cli-commands.md index 0b14a2e136..18fdef892a 100644 --- a/docs/local-dev/cli-commands.md +++ b/docs/local-dev/cli-commands.md @@ -41,6 +41,7 @@ lint Lint a package test Run tests, forwarding args to Jest, defaulting to watch mode clean Delete cache directories +create Open up an interactive guide to creating new things in your app create-plugin Creates a new plugin in the current repository remove-plugin Removes plugin in the current repository @@ -277,6 +278,44 @@ Options: -h, --help display help for command ``` +## create + +Scope: `root` + +The `create` command opens up an interactive guide for you to create new things +in your app. If you do not pass in any options it is completely interactive, but +it is possible to pre-select what you want to create using the `--select` flag, +and provide options using `--options`, for example: + +```bash +backstage-cli create --select plugin --option id=foo +``` + +This command is typically added as script in the root `package.json` to be +executed with `yarn backstage-create`, using options that are appropriate for +the organization that owns the app repo. For example you may have it set up like +this: + +```json +{ + "scripts": { + "backstage-create": "backstage-cli create --scope internal --no-private --npm-registry https://acme.org/npm" + } +} +``` + +```text +Usage: backstage-cli create [options] + +Options: + --select Select the thing you want to be creating upfront + --option = Pre-fill options for the creation process (default: []) + --scope The scope to use for new packages + --npm-registry The package registry to use for new packages + --no-private Do not mark new packages as private + -h, --help display help for command +``` + ## create-plugin Scope: `root` From 16d06f6ac3ec8fd1553c4b847b96e692d9dc8125 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 13:20:48 +0100 Subject: [PATCH 161/181] changesets: add changeset for CLI create command Signed-off-by: Patrik Oldsberg --- .changeset/tidy-beans-reflect.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tidy-beans-reflect.md diff --git a/.changeset/tidy-beans-reflect.md b/.changeset/tidy-beans-reflect.md new file mode 100644 index 0000000000..20796075f0 --- /dev/null +++ b/.changeset/tidy-beans-reflect.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Introduces new `backstage-cli create` command to replace `create-plugin` and make space for creating a wider array of things. The create command also adds a new template for creating isomorphic common plugin packages. From 6a46eb2693223fce4f08bb3b707397a0bd7c2a82 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 15:01:50 +0100 Subject: [PATCH 162/181] cli: refactor inquirer prompt message transforms for clarity Signed-off-by: Patrik Oldsberg --- .../cli/src/lib/create/FactoryRegistry.ts | 50 +++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/lib/create/FactoryRegistry.ts b/packages/cli/src/lib/create/FactoryRegistry.ts index 1b25f5950a..9072d46c4c 100644 --- a/packages/cli/src/lib/create/FactoryRegistry.ts +++ b/packages/cli/src/lib/create/FactoryRegistry.ts @@ -16,10 +16,39 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; -import { AnyFactory } from './types'; +import { AnyFactory, Prompt } from './types'; import * as factories from './factories'; import partition from 'lodash/partition'; +function applyPromptMessageTransforms( + prompt: Prompt, + transforms: { + message: (msg: string) => string; + error: (msg: string) => string; + }, +): Prompt { + return { + ...prompt, + message: + prompt.message && + (async answers => { + if (typeof prompt.message === 'function') { + return transforms.message(await prompt.message(answers)); + } + return transforms.message(await prompt.message!); + }), + validate: + prompt.validate && + (async (...args) => { + const result = await prompt.validate!(...args); + if (typeof result === 'string') { + return transforms.error(result); + } + return result; + }), + }; +} + export class FactoryRegistry { private static factoryMap = new Map( Object.values(factories).map(factory => [factory.name, factory]), @@ -82,19 +111,12 @@ export class FactoryRegistry { } currentOptions = await inquirer.prompt( - needsAnswers.map(option => ({ - ...option, - message: option.message && chalk.blue(option.message), - validate: - option.validate && - (async (...args) => { - const result = await option.validate!(...args); - if (typeof result === 'string') { - return chalk.red(result); - } - return result; - }), - })), + needsAnswers.map(option => + applyPromptMessageTransforms(option, { + message: chalk.blue, + error: chalk.red, + }), + ), currentOptions, ); } From 069934c62760d3bc00c1713ba183ee084c5386fc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 15:02:08 +0100 Subject: [PATCH 163/181] cli: some more docs for create types Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/create/types.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/cli/src/lib/create/types.ts b/packages/cli/src/lib/create/types.ts index fd2e684e11..5a2460d0df 100644 --- a/packages/cli/src/lib/create/types.ts +++ b/packages/cli/src/lib/create/types.ts @@ -40,10 +40,31 @@ export type AnyOptions = Record; export type Prompt = DistinctQuestion & { name: string }; export interface Factory { + /** + * The name used for this factory. + */ name: string; + + /** + * A description that describes what this factory creates to the user. + */ description: string; + + /** + * An optional options discovery step that is run + * before the prompts to potentially fill in some of the options. + */ optionsDiscovery?(): Promise>; + + /** + * Inquirer prompts that will be filled in either interactively or + * through command line arguments. + */ optionsPrompts?: ReadonlyArray>; + + /** + * The main method of the factory that handles creation. + */ create(options: TOptions, context?: CreateContext): Promise; } From bd3ae04de9e990ed9bad2c8051cb491721f18d94 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 15:26:23 +0100 Subject: [PATCH 164/181] cli: fixes for common package factory Signed-off-by: Patrik Oldsberg --- .../src/lib/create/factories/pluginCommon.test.ts | 9 +++++++-- .../cli/src/lib/create/factories/pluginCommon.ts | 13 ++++++++----- .../src/{index.ts => index.ts.hbs} | 3 +++ 3 files changed, 18 insertions(+), 7 deletions(-) rename packages/cli/templates/default-common-plugin-package/src/{index.ts => index.ts.hbs} (90%) diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts index 73edc5159b..e329e2588f 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -29,6 +29,11 @@ describe('pluginCommon factory', () => { }); }); + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + it('should create a common plugin package', async () => { mockFs({ '/root': { @@ -72,7 +77,7 @@ describe('pluginCommon factory', () => { 'templating README.md.hbs ✔', 'templating package.json.hbs ✔', 'copying tsconfig.json ✔', - 'copying index.ts ✔', + 'templating index.ts.hbs ✔', 'copying setupTests.ts ✔', 'Installing:', 'moving plugins/test-common ✔', @@ -83,7 +88,7 @@ describe('pluginCommon factory', () => { ).resolves.toEqual( expect.objectContaining({ name: 'plugin-test-common', - description: 'Common functionalities for the test-common plugin', + description: 'Common functionalities for the test plugin', private: true, version: '1.0.0', }), diff --git a/packages/cli/src/lib/create/factories/pluginCommon.ts b/packages/cli/src/lib/create/factories/pluginCommon.ts index 97b1eccec7..af42d74b0b 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.ts @@ -36,15 +36,18 @@ export const pluginCommon = createFactory({ }), optionsPrompts: [pluginIdPrompt(), ownerPrompt()], async create(options: Options, ctx: CreateContext) { - const id = `${options.id}-common`; - const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + const { id } = options; + const suffix = `${id}-common`; + const name = ctx.scope + ? `@${ctx.scope}/plugin-${suffix}` + : `plugin-${suffix}`; Task.log(); Task.log(`Creating backend plugin ${chalk.cyan(name)}`); const targetDir = ctx.isMonoRepo - ? paths.resolveTargetRoot('plugins', id) - : paths.resolveTargetRoot(`backstage-plugin-${id}`); + ? paths.resolveTargetRoot('plugins', suffix) + : paths.resolveTargetRoot(`backstage-plugin-${suffix}`); await executePluginPackageTemplate(ctx, { targetDir, @@ -59,7 +62,7 @@ export const pluginCommon = createFactory({ }); if (options.owner) { - await addCodeownersEntry(`/plugins/${id}`, options.owner); + await addCodeownersEntry(`/plugins/${suffix}`, options.owner); } await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); diff --git a/packages/cli/templates/default-common-plugin-package/src/index.ts b/packages/cli/templates/default-common-plugin-package/src/index.ts.hbs similarity index 90% rename from packages/cli/templates/default-common-plugin-package/src/index.ts rename to packages/cli/templates/default-common-plugin-package/src/index.ts.hbs index 6ee452b5bb..2e1150d74e 100644 --- a/packages/cli/templates/default-common-plugin-package/src/index.ts +++ b/packages/cli/templates/default-common-plugin-package/src/index.ts.hbs @@ -1,5 +1,8 @@ +/***/ /** * Common functionalities for the {{id}} plugin. + * + * @packageDocumentation */ /** From 103d9e2f53e97f93ef742715a88be15f9941bb3d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 15:29:33 +0100 Subject: [PATCH 165/181] cli: fix create monorepo check Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/create/create.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index d80daa6ddf..64067e1b7e 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -51,10 +51,20 @@ export default async (cmd: Command) => { providedOptions, ); - const rootPackageJson = await fs.readJson( - paths.resolveTargetRoot('package.json'), - ); - const isMonoRepo = Boolean(rootPackageJson.workspaces); + let isMonoRepo = false; + try { + const rootPackageJson = await fs.readJson( + paths.resolveTargetRoot('package.json'), + ); + if (rootPackageJson.workspaces) { + isMonoRepo = true; + } + } catch (error) { + assertError(error); + if (error.code !== 'ENOENT') { + throw error; + } + } let defaultVersion = '0.1.0'; try { From edaaf2dccb8a20f26a26c20b912bc90234350f74 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 15:32:20 +0100 Subject: [PATCH 166/181] cli: fix non-monorepo package naming for create Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/create/factories/backendPlugin.test.ts | 4 ++-- packages/cli/src/lib/create/factories/backendPlugin.ts | 4 +++- .../cli/src/lib/create/factories/frontendPlugin.test.ts | 6 +++--- packages/cli/src/lib/create/factories/frontendPlugin.ts | 4 +++- packages/cli/src/lib/create/factories/pluginCommon.test.ts | 4 ++-- packages/cli/src/lib/create/factories/pluginCommon.ts | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/lib/create/factories/backendPlugin.test.ts b/packages/cli/src/lib/create/factories/backendPlugin.test.ts index 409b7f476a..5fe52c3732 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.test.ts @@ -73,7 +73,7 @@ describe('backendPlugin factory', () => { expect(output).toEqual([ '', - 'Creating backend plugin plugin-test-backend', + 'Creating backend plugin backstage-plugin-test-backend', 'Checking Prerequisites:', 'availability plugins/test-backend ✔', 'creating temp dir ✔', @@ -97,7 +97,7 @@ describe('backendPlugin factory', () => { fs.readJson('/root/packages/backend/package.json'), ).resolves.toEqual({ dependencies: { - 'plugin-test-backend': '^1.0.0', + 'backstage-plugin-test-backend': '^1.0.0', }, }); diff --git a/packages/cli/src/lib/create/factories/backendPlugin.ts b/packages/cli/src/lib/create/factories/backendPlugin.ts index 2bdc62741d..4d03a4da7b 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.ts @@ -39,7 +39,9 @@ export const backendPlugin = createFactory({ optionsPrompts: [pluginIdPrompt(), ownerPrompt()], async create(options: Options, ctx: CreateContext) { const id = `${options.id}-backend`; - const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + const name = ctx.scope + ? `@${ctx.scope}/plugin-${id}` + : `backstage-plugin-${id}`; Task.log(); Task.log(`Creating backend plugin ${chalk.cyan(name)}`); diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts index 3e0f94f6ad..a882a130ea 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts @@ -86,7 +86,7 @@ describe('frontendPlugin factory', () => { expect(output).toEqual([ '', - 'Creating backend plugin plugin-test', + 'Creating backend plugin backstage-plugin-test', 'Checking Prerequisites:', 'availability plugins/test ✔', 'creating temp dir ✔', @@ -117,14 +117,14 @@ describe('frontendPlugin factory', () => { fs.readJson('/root/packages/app/package.json'), ).resolves.toEqual({ dependencies: { - 'plugin-test': '^1.0.0', + 'backstage-plugin-test': '^1.0.0', }, }); await expect(fs.readFile('/root/packages/app/src/App.tsx', 'utf8')).resolves .toBe(` import { createApp } from '@backstage/app-defaults'; -import { TestPage } from 'plugin-test'; +import { TestPage } from 'backstage-plugin-test'; const router = ( diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.ts b/packages/cli/src/lib/create/factories/frontendPlugin.ts index a6c0d0470d..3862ae4883 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.ts @@ -41,7 +41,9 @@ export const frontendPlugin = createFactory({ async create(options: Options, ctx: CreateContext) { const { id } = options; - const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `plugin-${id}`; + const name = ctx.scope + ? `@${ctx.scope}/plugin-${id}` + : `backstage-plugin-${id}`; const extensionName = `${upperFirst(camelCase(id))}Page`; Task.log(); diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts index e329e2588f..a39bb3877d 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -68,7 +68,7 @@ describe('pluginCommon factory', () => { expect(output).toEqual([ '', - 'Creating backend plugin plugin-test-common', + 'Creating backend plugin backstage-plugin-test-common', 'Checking Prerequisites:', 'availability plugins/test-common ✔', 'creating temp dir ✔', @@ -87,7 +87,7 @@ describe('pluginCommon factory', () => { fs.readJson('/root/plugins/test-common/package.json'), ).resolves.toEqual( expect.objectContaining({ - name: 'plugin-test-common', + name: 'backstage-plugin-test-common', description: 'Common functionalities for the test plugin', private: true, version: '1.0.0', diff --git a/packages/cli/src/lib/create/factories/pluginCommon.ts b/packages/cli/src/lib/create/factories/pluginCommon.ts index af42d74b0b..1bcca2fb6a 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.ts @@ -40,7 +40,7 @@ export const pluginCommon = createFactory({ const suffix = `${id}-common`; const name = ctx.scope ? `@${ctx.scope}/plugin-${suffix}` - : `plugin-${suffix}`; + : `backstage-plugin-${suffix}`; Task.log(); Task.log(`Creating backend plugin ${chalk.cyan(name)}`); From 6dcfe227a29006a43763bf2c1d5e345a4bfc0cb3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 16:32:20 +0100 Subject: [PATCH 167/181] cli: add create factory for scaffolder modules Signed-off-by: Patrik Oldsberg --- .changeset/shiny-starfishes-float.md | 5 + .../cli/src/lib/create/factories/index.ts | 1 + .../create/factories/scaffolderModule.test.ts | 110 ++++++++++++++++++ .../lib/create/factories/scaffolderModule.ts | 96 +++++++++++++++ packages/cli/src/lib/version.ts | 2 + .../templates/scaffolder-module/.eslintrc.js | 3 + .../templates/scaffolder-module/README.md.hbs | 5 + .../scaffolder-module/package.json.hbs | 37 ++++++ .../src/actions/example/example.test.ts | 50 ++++++++ .../src/actions/example/example.ts | 57 +++++++++ .../src/actions/example/index.ts | 1 + .../scaffolder-module/src/actions/index.ts | 1 + .../scaffolder-module/src/index.ts.hbs | 8 ++ .../templates/scaffolder-module/tsconfig.json | 9 ++ 14 files changed, 385 insertions(+) create mode 100644 .changeset/shiny-starfishes-float.md create mode 100644 packages/cli/src/lib/create/factories/scaffolderModule.test.ts create mode 100644 packages/cli/src/lib/create/factories/scaffolderModule.ts create mode 100644 packages/cli/templates/scaffolder-module/.eslintrc.js create mode 100644 packages/cli/templates/scaffolder-module/README.md.hbs create mode 100644 packages/cli/templates/scaffolder-module/package.json.hbs create mode 100644 packages/cli/templates/scaffolder-module/src/actions/example/example.test.ts create mode 100644 packages/cli/templates/scaffolder-module/src/actions/example/example.ts create mode 100644 packages/cli/templates/scaffolder-module/src/actions/example/index.ts create mode 100644 packages/cli/templates/scaffolder-module/src/actions/index.ts create mode 100644 packages/cli/templates/scaffolder-module/src/index.ts.hbs create mode 100644 packages/cli/templates/scaffolder-module/tsconfig.json diff --git a/.changeset/shiny-starfishes-float.md b/.changeset/shiny-starfishes-float.md new file mode 100644 index 0000000000..0673791375 --- /dev/null +++ b/.changeset/shiny-starfishes-float.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added a scaffolder backend module template for the `create` command. diff --git a/packages/cli/src/lib/create/factories/index.ts b/packages/cli/src/lib/create/factories/index.ts index e4df9062f7..0764d33e2c 100644 --- a/packages/cli/src/lib/create/factories/index.ts +++ b/packages/cli/src/lib/create/factories/index.ts @@ -17,3 +17,4 @@ export { frontendPlugin } from './frontendPlugin'; export { backendPlugin } from './backendPlugin'; export { pluginCommon } from './pluginCommon'; +export { scaffolderModule } from './scaffolderModule'; diff --git a/packages/cli/src/lib/create/factories/scaffolderModule.test.ts b/packages/cli/src/lib/create/factories/scaffolderModule.test.ts new file mode 100644 index 0000000000..dde711b750 --- /dev/null +++ b/packages/cli/src/lib/create/factories/scaffolderModule.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { FactoryRegistry } from '../FactoryRegistry'; +import { createMockOutputStream, mockPaths } from './common/testUtils'; +import { scaffolderModule } from './scaffolderModule'; + +describe('scaffolderModule factory', () => { + beforeEach(() => { + mockPaths({ + targetRoot: '/root', + }); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should create a scaffolder backend module package', async () => { + mockFs({ + '/root': { + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(scaffolderModule, { + id: 'test', + }); + + let modified = false; + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await scaffolderModule.create(options, { + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => { + modified = true; + }, + createTemporaryDirectory: (name: string) => fs.mkdtemp(name), + }); + + expect(modified).toBe(true); + + expect(output).toEqual([ + '', + 'Creating module backstage-plugin-scaffolder-backend-module-test', + 'Checking Prerequisites:', + 'availability plugins/scaffolder-backend-module-test ✔', + 'creating temp dir ✔', + 'Executing Template:', + 'copying .eslintrc.js ✔', + 'templating README.md.hbs ✔', + 'templating package.json.hbs ✔', + 'copying tsconfig.json ✔', + 'templating index.ts.hbs ✔', + 'copying index.ts ✔', + 'copying example.test.ts ✔', + 'copying example.ts ✔', + 'copying index.ts ✔', + 'Installing:', + 'moving plugins/scaffolder-backend-module-test ✔', + ]); + + await expect( + fs.readJson('/root/plugins/scaffolder-backend-module-test/package.json'), + ).resolves.toEqual( + expect.objectContaining({ + name: 'backstage-plugin-scaffolder-backend-module-test', + description: 'The test module for @backstage/plugin-scaffolder-backend', + private: true, + version: '1.0.0', + }), + ); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: '/root/plugins/scaffolder-backend-module-test', + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: '/root/plugins/scaffolder-backend-module-test', + optional: true, + }); + }); +}); diff --git a/packages/cli/src/lib/create/factories/scaffolderModule.ts b/packages/cli/src/lib/create/factories/scaffolderModule.ts new file mode 100644 index 0000000000..b89f0fc691 --- /dev/null +++ b/packages/cli/src/lib/create/factories/scaffolderModule.ts @@ -0,0 +1,96 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import chalk from 'chalk'; +import { paths } from '../../paths'; +import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; +import { createFactory, CreateContext } from '../types'; +import { Task } from '../../tasks'; +import { ownerPrompt } from './common/prompts'; +import { executePluginPackageTemplate } from './common/tasks'; + +type Options = { + id: string; + owner?: string; + codeOwnersPath?: string; +}; + +export const scaffolderModule = createFactory({ + name: 'scaffolder-module', + description: + 'An module exporting custom actions for @backstage/plugin-scaffolder-backend', + optionsDiscovery: async () => ({ + codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), + }), + optionsPrompts: [ + { + type: 'input', + name: 'id', + message: 'Enter the name of the module [required]', + validate: (value: string) => { + if (!value) { + return 'Please enter the name of the module'; + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return 'Module names must be lowercase and contain only letters, digits, and dashes.'; + } + return true; + }, + }, + ownerPrompt(), + ], + async create(options: Options, ctx: CreateContext) { + const { id } = options; + const slug = `scaffolder-backend-module-${id}`; + + let name = `backstage-plugin-${slug}`; + if (ctx.scope) { + if (ctx.scope === 'backstage') { + name = `@backstage/plugin-${slug}`; + } else { + name = `@${ctx.scope}/backstage-plugin-${slug}`; + } + } + + Task.log(); + Task.log(`Creating module ${chalk.cyan(name)}`); + + const targetDir = ctx.isMonoRepo + ? paths.resolveTargetRoot('plugins', slug) + : paths.resolveTargetRoot(`backstage-plugin-${slug}`); + + await executePluginPackageTemplate(ctx, { + targetDir, + templateName: 'scaffolder-module', + values: { + id, + name, + privatePackage: ctx.private, + npmRegistry: ctx.npmRegistry, + pluginVersion: ctx.defaultVersion, + }, + }); + + if (options.owner) { + await addCodeownersEntry(`/plugins/${slug}`, options.owner); + } + + await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); + await Task.forCommand('yarn lint --fix', { + cwd: targetDir, + optional: true, + }); + }, +}); diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index c69072c64d..3b512b22c3 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -42,6 +42,7 @@ import { version as corePluginApi } from '@backstage/core-plugin-api/package.jso import { version as devUtils } from '@backstage/dev-utils/package.json'; import { version as testUtils } from '@backstage/test-utils/package.json'; import { version as theme } from '@backstage/theme/package.json'; +import { version as scaffolderBackend } from '@backstage/plugin-scaffolder-backend/package.json'; export const packageVersions: Record = { '@backstage/backend-common': backendCommon, @@ -53,6 +54,7 @@ export const packageVersions: Record = { '@backstage/dev-utils': devUtils, '@backstage/test-utils': testUtils, '@backstage/theme': theme, + '@backstage/plugin-scaffolder-backend': scaffolderBackend, }; export function findVersion() { diff --git a/packages/cli/templates/scaffolder-module/.eslintrc.js b/packages/cli/templates/scaffolder-module/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/packages/cli/templates/scaffolder-module/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/packages/cli/templates/scaffolder-module/README.md.hbs b/packages/cli/templates/scaffolder-module/README.md.hbs new file mode 100644 index 0000000000..8ee653a4ac --- /dev/null +++ b/packages/cli/templates/scaffolder-module/README.md.hbs @@ -0,0 +1,5 @@ +# {{name}} + +The {{id}} module for [@backstage/plugin-scaffolder-backend](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend). + +_This plugin was created through the Backstage CLI_ diff --git a/packages/cli/templates/scaffolder-module/package.json.hbs b/packages/cli/templates/scaffolder-module/package.json.hbs new file mode 100644 index 0000000000..de6ffd9e3a --- /dev/null +++ b/packages/cli/templates/scaffolder-module/package.json.hbs @@ -0,0 +1,37 @@ +{ + "name": "{{name}}", + "description": "The {{id}} module for @backstage/plugin-scaffolder-backend", + "version": "{{pluginVersion}}", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", +{{#if privatePackage}} + "private": {{privatePackage}}, +{{/if}} + "publishConfig": { +{{#if npmRegistry}} + "registry": "{{npmRegistry}}", +{{/if}} + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli build --output cjs,types", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/plugin-scaffolder-backend": "{{versionQuery '@backstage/plugin-scaffolder-backend'}}" + }, + "devDependencies": { + "@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}", + "@backstage/cli": "{{versionQuery '@backstage/cli'}}" + }, + "files": [ + "dist" + ] +} diff --git a/packages/cli/templates/scaffolder-module/src/actions/example/example.test.ts b/packages/cli/templates/scaffolder-module/src/actions/example/example.test.ts new file mode 100644 index 0000000000..e427b2c603 --- /dev/null +++ b/packages/cli/templates/scaffolder-module/src/actions/example/example.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PassThrough } from 'stream'; +import { createAcmeExampleAction } from './example'; +import { getVoidLogger } from '@backstage/backend-common'; + +describe('acme:example', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should call action', async () => { + const action = createAcmeExampleAction(); + + const logger = getVoidLogger(); + jest.spyOn(logger, 'info'); + + await action.handler({ + input: { + myParameter: 'test', + }, + workspacePath: '/tmp', + logger, + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory() { + // Usage of mock-fs is recommended for testing of filesystem operations + throw new Error('Not implemented'); + }, + }); + + expect(logger.info).toHaveBeenCalledWith( + 'Running example template with parameters: test', + ); + }); +}); diff --git a/packages/cli/templates/scaffolder-module/src/actions/example/example.ts b/packages/cli/templates/scaffolder-module/src/actions/example/example.ts new file mode 100644 index 0000000000..c20a4bdf25 --- /dev/null +++ b/packages/cli/templates/scaffolder-module/src/actions/example/example.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createTemplateAction } from '@backstage/plugin-scaffolder-backend'; + +/** + * Creates an `acme:example` Scaffolder action. + * + * @remarks + * + * See {@link https://example.com} for more information. + * + * @public + */ +export function createAcmeExampleAction() { + // For more information on how to define custom actions, see + // https://backstage.io/docs/features/software-templates/writing-custom-actions + return createTemplateAction<{ + myParameter: string; + }>({ + id: 'acme:example', + description: 'Runs Yeoman on an installed Yeoman generator', + schema: { + input: { + type: 'object', + required: ['myParameter'], + properties: { + myParameter: { + title: 'An example parameter', + description: 'This is the schema for our example parameter', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + ctx.logger.info( + `Running example template with parameters: ${ctx.input.myParameter}`, + ); + + await new Promise(resolve => setTimeout(resolve, 1000)); + }, + }); +} diff --git a/packages/cli/templates/scaffolder-module/src/actions/example/index.ts b/packages/cli/templates/scaffolder-module/src/actions/example/index.ts new file mode 100644 index 0000000000..e81099f333 --- /dev/null +++ b/packages/cli/templates/scaffolder-module/src/actions/example/index.ts @@ -0,0 +1 @@ +export { createAcmeExampleAction } from './example'; diff --git a/packages/cli/templates/scaffolder-module/src/actions/index.ts b/packages/cli/templates/scaffolder-module/src/actions/index.ts new file mode 100644 index 0000000000..ab6642ebb0 --- /dev/null +++ b/packages/cli/templates/scaffolder-module/src/actions/index.ts @@ -0,0 +1 @@ +export * from './example'; diff --git a/packages/cli/templates/scaffolder-module/src/index.ts.hbs b/packages/cli/templates/scaffolder-module/src/index.ts.hbs new file mode 100644 index 0000000000..3690e43b8e --- /dev/null +++ b/packages/cli/templates/scaffolder-module/src/index.ts.hbs @@ -0,0 +1,8 @@ +/***/ +/** + * The {{id}} module for @backstage/plugin-scaffolder-backend. + * + * @packageDocumentation + */ + +export * from './actions'; diff --git a/packages/cli/templates/scaffolder-module/tsconfig.json b/packages/cli/templates/scaffolder-module/tsconfig.json new file mode 100644 index 0000000000..5ae9aeb62d --- /dev/null +++ b/packages/cli/templates/scaffolder-module/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "dist-types", + "rootDir": "." + } +} From 4ca3542fdd0f14da90416ae19310757ce19bb8c3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Nov 2021 17:38:38 +0100 Subject: [PATCH 168/181] cli: fix for backend bundle building all packages Signed-off-by: Patrik Oldsberg --- .changeset/sour-cameras-hide.md | 5 +++++ packages/cli/src/lib/packager/index.ts | 21 +++++++++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 .changeset/sour-cameras-hide.md diff --git a/.changeset/sour-cameras-hide.md b/.changeset/sour-cameras-hide.md new file mode 100644 index 0000000000..a31e7a13c3 --- /dev/null +++ b/.changeset/sour-cameras-hide.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed a bug where calling `backstage-cli backend:bundle --build-dependencies` with no dependencies to be built would cause all monorepo packages to be built instead. diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index dba42e6013..4faf904177 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -106,18 +106,19 @@ export async function createDistWorkspace( if (options.buildDependencies) { const exclude = options.buildExcludes ?? []; - const scopeArgs = targets - .filter(target => !exclude.includes(target.name)) - .flatMap(target => ['--scope', target.name]); - const lernaArgs = - options.parallel && Number.isInteger(options.parallel) - ? ['--concurrency', options.parallel.toString()] - : []; + const toBuild = targets.filter(target => !exclude.includes(target.name)); + if (toBuild.length > 0) { + const scopeArgs = toBuild.flatMap(target => ['--scope', target.name]); + const lernaArgs = + options.parallel && Number.isInteger(options.parallel) + ? ['--concurrency', options.parallel.toString()] + : []; - await run('yarn', ['lerna', ...lernaArgs, 'run', ...scopeArgs, 'build'], { - cwd: paths.targetRoot, - }); + await run('yarn', ['lerna', ...lernaArgs, 'run', ...scopeArgs, 'build'], { + cwd: paths.targetRoot, + }); + } } await moveToDistWorkspace(targetDir, targets); From b5eec9e04410493ceb3548286ce1c1bc9230f532 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Nov 2021 04:13:26 +0000 Subject: [PATCH 169/181] build(deps-dev): bump eslint-plugin-cypress from 2.11.3 to 2.12.1 Bumps [eslint-plugin-cypress](https://github.com/cypress-io/eslint-plugin-cypress) from 2.11.3 to 2.12.1. - [Release notes](https://github.com/cypress-io/eslint-plugin-cypress/releases) - [Commits](https://github.com/cypress-io/eslint-plugin-cypress/compare/v2.11.3...v2.12.1) --- updated-dependencies: - dependency-name: eslint-plugin-cypress dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index e18f6fedb2..7f5e8b6d78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13688,9 +13688,9 @@ eslint-module-utils@^2.1.1, eslint-module-utils@^2.6.0: pkg-dir "^2.0.0" eslint-plugin-cypress@^2.10.3: - version "2.11.3" - resolved "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.3.tgz#54ee4067aa8192aa62810cd35080eb577e191ab7" - integrity sha512-hOoAid+XNFtpvOzZSNWP5LDrQBEJwbZwjib4XJ1KcRYKjeVj0mAmPmucG4Egli4j/aruv+Ow/acacoloWWCl9Q== + version "2.12.1" + resolved "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.12.1.tgz#9aeee700708ca8c058e00cdafe215199918c2632" + integrity sha512-c2W/uPADl5kospNDihgiLc7n87t5XhUbFDoTl6CfVkmG+kDAb5Ux10V9PoLPu9N+r7znpc+iQlcmAqT1A/89HA== dependencies: globals "^11.12.0" From 34a604b34ce70c3ac21fce7ec0174be3f9b061e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Nov 2021 04:15:32 +0000 Subject: [PATCH 170/181] build(deps): bump js-base64 from 3.6.1 to 3.7.2 Bumps [js-base64](https://github.com/dankogai/js-base64) from 3.6.1 to 3.7.2. - [Release notes](https://github.com/dankogai/js-base64/releases) - [Commits](https://github.com/dankogai/js-base64/compare/3.6.1...3.7.2) --- updated-dependencies: - dependency-name: js-base64 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index e18f6fedb2..b18057da72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18083,9 +18083,9 @@ joycon@^2.2.5: integrity sha512-YqvUxoOcVPnCp0VU1/56f+iKSdvIRJYPznH22BdXV3xMk75SFXhWeJkZ8C9XxUWt1b5x2X1SxuFygW1U0FmkEQ== js-base64@^3.6.0: - version "3.6.1" - resolved "https://registry.npmjs.org/js-base64/-/js-base64-3.6.1.tgz#555aae398b74694b4037af1f8a5a6209d170efbe" - integrity sha512-Frdq2+tRRGLQUIQOgsIGSCd1VePCS2fsddTG5dTCqR0JHgltXWfsxnY0gIXPoMeRmdom6Oyq+UMOFg5suduOjQ== + version "3.7.2" + resolved "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz#816d11d81a8aff241603d19ce5761e13e41d7745" + integrity sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ== js-cookie@^2.2.1: version "2.2.1" From 872a547dd6c3abeeb1eda41573fdee1023dc9e5f Mon Sep 17 00:00:00 2001 From: Terrence Benade Date: Mon, 15 Nov 2021 16:57:43 +1100 Subject: [PATCH 171/181] small grammatical error Signed-off-by: Terrence Benade --- docs/FAQ.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index c95e10ad4e..ea4ae352dd 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -117,7 +117,7 @@ through the proxy. Learn more about [the different components](overview/what-is-backstage.md) that make up Backstage. -### Why can't I dynamically install plugins without modifications the app? +### Why can't I dynamically install plugins without modifications to the app? This decision is part of the core architecture and development flow of Backstage. Plugins have a lot of freedom in what they provide and how they are From b565119196897ad86fcf67d1899b96ce87514399 Mon Sep 17 00:00:00 2001 From: Dede Hamzah Date: Mon, 15 Nov 2021 14:19:28 +0700 Subject: [PATCH 172/181] update api report docs Signed-off-by: Dede Hamzah --- plugins/user-settings/api-report.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/user-settings/api-report.md b/plugins/user-settings/api-report.md index 653e85a541..a801f2e807 100644 --- a/plugins/user-settings/api-report.md +++ b/plugins/user-settings/api-report.md @@ -37,10 +37,11 @@ export const ProviderSettingsItem: ({ // @public (undocumented) export const Router: ({ providerSettings }: Props) => JSX.Element; +// Warning: (ae-forgotten-export) The symbol "SettingsProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Settings" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Settings: () => JSX.Element; +export const Settings: (props: SettingsProps) => JSX.Element; // Warning: (ae-missing-release-tag) "UserSettingsAppearanceCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // From 8809b6c0ddeb0d4cde5fc571786e4876abded8be Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 15 Nov 2021 09:56:47 +0000 Subject: [PATCH 173/181] update versions of json-schema Signed-off-by: Brian Fletcher --- .changeset/empty-dots-attend.md | 8 ++++++++ packages/catalog-model/package.json | 2 +- packages/cli/package.json | 2 +- packages/config-loader/package.json | 2 +- plugins/scaffolder/package.json | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 .changeset/empty-dots-attend.md diff --git a/.changeset/empty-dots-attend.md b/.changeset/empty-dots-attend.md new file mode 100644 index 0000000000..3903dfa22b --- /dev/null +++ b/.changeset/empty-dots-attend.md @@ -0,0 +1,8 @@ +--- +'@backstage/catalog-model': patch +'@backstage/cli': patch +'@backstage/config-loader': patch +'@backstage/plugin-scaffolder': patch +--- + +Update the json-schema dependency version. diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index b64e536578..17c89ed824 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -36,7 +36,7 @@ "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.13", "ajv": "^7.0.3", - "json-schema": "^0.3.0", + "json-schema": "^0.4.0", "lodash": "^4.17.21", "uuid": "^8.0.0", "yup": "^0.32.9" diff --git a/packages/cli/package.json b/packages/cli/package.json index 546a1597e1..7253007b8f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -81,7 +81,7 @@ "inquirer": "^7.0.4", "jest": "^26.0.1", "jest-css-modules": "^2.1.0", - "json-schema": "^0.3.0", + "json-schema": "^0.4.0", "jest-transform-yaml": "^0.1.1", "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.4.2", diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 2e25d91856..76dcec35c6 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -38,7 +38,7 @@ "ajv": "^7.0.3", "chokidar": "^3.5.2", "fs-extra": "9.1.0", - "json-schema": "^0.3.0", + "json-schema": "^0.4.0", "json-schema-merge-allof": "^0.8.1", "json-schema-traverse": "^1.0.0", "typescript-json-schema": "^0.51.0", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 1d8bc4a0ea..594a8e6423 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -52,7 +52,7 @@ "git-url-parse": "^11.6.0", "humanize-duration": "^3.25.1", "immer": "^9.0.1", - "json-schema": "^0.3.0", + "json-schema": "^0.4.0", "lodash": "^4.17.21", "luxon": "^2.0.2", "qs": "^6.9.4", From a08022254b3ea835df565431ec4dc4d5d46b8353 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 15 Nov 2021 14:59:23 +0100 Subject: [PATCH 174/181] Add missing yarn.lock changes and reports Signed-off-by: Johan Haals --- plugins/catalog-backend/api-report.md | 1 + yarn.lock | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 143e31bc21..6660912dda 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -1290,6 +1290,7 @@ export class NextCatalogBuilder { locationService: LocationService; router: Router; }>; + getDefaultProcessors(): CatalogProcessor[]; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen replaceEntityPolicies(policies: EntityPolicy[]): NextCatalogBuilder; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen diff --git a/yarn.lock b/yarn.lock index 6898854595..37c1bd3329 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18305,10 +18305,10 @@ json-schema@0.2.3: resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= -json-schema@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.3.0.tgz#90a9c5054bd065422c00241851ce8d59475b701b" - integrity sha512-TYfxx36xfl52Rf1LU9HyWSLGPdYLL+SQ8/E/0yVyKG8wCCDaSrhPap0vEdlsZWRaS6tnKKLPGiEJGiREVC8kxQ== +json-schema@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" @@ -20635,6 +20635,11 @@ mock-fs@^5.1.0: resolved "https://registry.npmjs.org/mock-fs/-/mock-fs-5.1.0.tgz#a9aebd4e6d74a626f84b86eae8a372bd061754e8" integrity sha512-wXdQ2nIk81TYIGLphUnbXl8akQpjb9ItfZefMcTxZcoe+djMkd5POU8fQdSEErxVAeT4CgDHWveYquys4H6Cmw== +mock-fs@^5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7" + integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A== + modify-values@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" From b6a4bacdc4f35e1d5071f61224811213cb26a2af Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Nov 2021 16:14:48 +0100 Subject: [PATCH 175/181] core-plugin-api: added ErrorApi* prefix to Error and ErrorContext types Signed-off-by: Patrik Oldsberg --- .changeset/yellow-deers-act.md | 5 +++ packages/core-app-api/api-report.md | 7 ++-- packages/core-plugin-api/api-report.md | 32 +++++++++++-------- .../src/apis/definitions/ErrorApi.ts | 23 ++++++++++--- 4 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 .changeset/yellow-deers-act.md diff --git a/.changeset/yellow-deers-act.md b/.changeset/yellow-deers-act.md new file mode 100644 index 0000000000..a9008ea41e --- /dev/null +++ b/.changeset/yellow-deers-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': patch +--- + +Deprecated the `Error` and `ErrorContext` types, replacing them with identical `ErrorApiError` and `ErrorApiErrorContext` types. diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index d98b5ef5f8..26438ba798 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -28,8 +28,9 @@ import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { ConfigReader } from '@backstage/config'; import { DiscoveryApi } from '@backstage/core-plugin-api'; -import { Error as Error_2 } from '@backstage/core-plugin-api'; import { ErrorApi } from '@backstage/core-plugin-api'; +import { ErrorApiError } from '@backstage/core-plugin-api'; +import { ErrorApiErrorContext } from '@backstage/core-plugin-api'; import { ErrorContext } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FeatureFlag } from '@backstage/core-plugin-api'; @@ -327,8 +328,8 @@ export class ErrorAlerter implements ErrorApi { constructor(alertApi: AlertApi, errorApi: ErrorApi); // (undocumented) error$(): Observable<{ - error: Error_2; - context?: ErrorContext | undefined; + error: ErrorApiError; + context?: ErrorApiErrorContext | undefined; }>; // (undocumented) post(error: Error, context?: ErrorContext): void; diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 2765b02bca..87cbf1dd5f 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -404,23 +404,31 @@ export interface ElementCollection { }): ElementCollection; } -// @public -type Error_2 = { - name: string; - message: string; - stack?: string; -}; +// @public @deprecated (undocumented) +type Error_2 = ErrorApiError; export { Error_2 as Error }; // @public export type ErrorApi = { - post(error: Error_2, context?: ErrorContext): void; + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; error$(): Observable_2<{ - error: Error_2; - context?: ErrorContext; + error: ErrorApiError; + context?: ErrorApiErrorContext; }>; }; +// @public +export type ErrorApiError = { + name: string; + message: string; + stack?: string; +}; + +// @public +export type ErrorApiErrorContext = { + hidden?: boolean; +}; + // @public export const errorApiRef: ApiRef; @@ -431,10 +439,8 @@ export type ErrorBoundaryFallbackProps = { resetError: () => void; }; -// @public -export type ErrorContext = { - hidden?: boolean; -}; +// @public @deprecated (undocumented) +export type ErrorContext = ErrorApiErrorContext; // @public export type Extension = { diff --git a/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts b/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts index 820660ac52..45ee0901c8 100644 --- a/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts @@ -23,22 +23,34 @@ import { Observable } from '@backstage/types'; * * @public */ -export type Error = { +export type ErrorApiError = { name: string; message: string; stack?: string; }; +/** + * @public + * @deprecated Use ErrorApiError instead + */ +export type Error = ErrorApiError; + /** * Provides additional information about an error that was posted to the application. * * @public */ -export type ErrorContext = { +export type ErrorApiErrorContext = { // If set to true, this error should not be displayed to the user. Defaults to false. hidden?: boolean; }; +/** + * @public + * @deprecated Use ErrorApiErrorContext instead + */ +export type ErrorContext = ErrorApiErrorContext; + /** * The error API is used to report errors to the app, and display them to the user. * @@ -62,12 +74,15 @@ export type ErrorApi = { /** * Post an error for handling by the application. */ - post(error: Error, context?: ErrorContext): void; + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; /** * Observe errors posted by other parts of the application. */ - error$(): Observable<{ error: Error; context?: ErrorContext }>; + error$(): Observable<{ + error: ErrorApiError; + context?: ErrorApiErrorContext; + }>; }; /** From 0b1de527326325aedbf0488c454a922cc03e8b01 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Nov 2021 16:21:02 +0100 Subject: [PATCH 176/181] core-app-api,test-utils: migrated to using new ErrorApi* names Signed-off-by: Patrik Oldsberg --- .changeset/nine-bananas-mate.md | 6 ++++++ docs/api/utility-apis.md | 2 +- packages/core-app-api/api-report.md | 9 ++++----- .../implementations/ErrorApi/ErrorAlerter.ts | 9 +++++++-- .../ErrorApi/ErrorApiForwarder.ts | 12 ++++++++---- .../ErrorApi/UnhandledErrorForwarder.ts | 10 +++++++--- packages/test-utils/api-report.md | 13 +++++++------ .../testUtils/apis/ErrorApi/MockErrorApi.ts | 19 +++++++++++++------ 8 files changed, 53 insertions(+), 27 deletions(-) create mode 100644 .changeset/nine-bananas-mate.md diff --git a/.changeset/nine-bananas-mate.md b/.changeset/nine-bananas-mate.md new file mode 100644 index 0000000000..e119eb722f --- /dev/null +++ b/.changeset/nine-bananas-mate.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-app-api': patch +'@backstage/test-utils': patch +--- + +Migrated to using new `ErrorApiError` and `ErrorApiErrorContext` names. diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index 1b364bcdae..133653df44 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -199,7 +199,7 @@ export a class that `implements` the target API, for example: ```ts export class IgnoringErrorApi implements ErrorApi { - post(error: Error, context?: ErrorContext) { + post(error: ErrorApiError, context?: ErrorApiErrorContext) { // ignore error } } diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 26438ba798..2c390bf132 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -31,7 +31,6 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { ErrorApi } from '@backstage/core-plugin-api'; import { ErrorApiError } from '@backstage/core-plugin-api'; import { ErrorApiErrorContext } from '@backstage/core-plugin-api'; -import { ErrorContext } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FeatureFlag } from '@backstage/core-plugin-api'; import { FeatureFlagsApi } from '@backstage/core-plugin-api'; @@ -332,7 +331,7 @@ export class ErrorAlerter implements ErrorApi { context?: ErrorApiErrorContext | undefined; }>; // (undocumented) - post(error: Error, context?: ErrorContext): void; + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; } // @public @@ -340,10 +339,10 @@ export class ErrorApiForwarder implements ErrorApi { // (undocumented) error$(): Observable<{ error: Error; - context?: ErrorContext; + context?: ErrorApiErrorContext; }>; // (undocumented) - post(error: Error, context?: ErrorContext): void; + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; } // @public @@ -604,7 +603,7 @@ export type SignInResult = { // @public export class UnhandledErrorForwarder { - static forward(errorApi: ErrorApi, errorContext: ErrorContext): void; + static forward(errorApi: ErrorApi, errorContext: ErrorApiErrorContext): void; } // @public diff --git a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts index 2111798d6a..350213d938 100644 --- a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts +++ b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ErrorApi, ErrorContext, AlertApi } from '@backstage/core-plugin-api'; +import { + ErrorApi, + ErrorApiError, + ErrorApiErrorContext, + AlertApi, +} from '@backstage/core-plugin-api'; /** * Decorates an ErrorApi by also forwarding error messages @@ -27,7 +32,7 @@ export class ErrorAlerter implements ErrorApi { private readonly errorApi: ErrorApi, ) {} - post(error: Error, context?: ErrorContext) { + post(error: ErrorApiError, context?: ErrorApiErrorContext) { if (!context?.hidden) { this.alertApi.post({ message: error.message, severity: 'error' }); } diff --git a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts index cd4564a050..f67c00d991 100644 --- a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts +++ b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api'; +import { + ErrorApi, + ErrorApiError, + ErrorApiErrorContext, +} from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; import { PublishSubject } from '../../../lib/subjects'; @@ -26,14 +30,14 @@ import { PublishSubject } from '../../../lib/subjects'; export class ErrorApiForwarder implements ErrorApi { private readonly subject = new PublishSubject<{ error: Error; - context?: ErrorContext; + context?: ErrorApiErrorContext; }>(); - post(error: Error, context?: ErrorContext) { + post(error: ErrorApiError, context?: ErrorApiErrorContext) { this.subject.next({ error, context }); } - error$(): Observable<{ error: Error; context?: ErrorContext }> { + error$(): Observable<{ error: Error; context?: ErrorApiErrorContext }> { return this.subject; } } diff --git a/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts b/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts index 16e473fb3a..8e697f12b2 100644 --- a/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts +++ b/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts @@ -1,4 +1,8 @@ -import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api'; +import { + ErrorApi, + ErrorApiError, + ErrorApiErrorContext, +} from '@backstage/core-plugin-api'; /* * Copyright 2020 Spotify AB @@ -25,11 +29,11 @@ export class UnhandledErrorForwarder { /** * Add event listener, such that unhandled errors can be forwarded using an given `ErrorApi` instance */ - static forward(errorApi: ErrorApi, errorContext: ErrorContext) { + static forward(errorApi: ErrorApi, errorContext: ErrorApiErrorContext) { window.addEventListener( 'unhandledrejection', (e: PromiseRejectionEvent) => { - errorApi.post(e.reason as Error, errorContext); + errorApi.post(e.reason as ErrorApiError, errorContext); }, ); } diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index c4936f6d96..62a8d9f601 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -7,7 +7,8 @@ import { AnalyticsApi } from '@backstage/core-plugin-api'; import { AnalyticsEvent } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { ErrorApi } from '@backstage/core-plugin-api'; -import { ErrorContext } from '@backstage/core-plugin-api'; +import { ErrorApiError } from '@backstage/core-plugin-api'; +import { ErrorApiErrorContext } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; import { ReactElement } from 'react'; @@ -27,8 +28,8 @@ export type CollectedLogs = { // @public export type ErrorWithContext = { - error: Error; - context?: ErrorContext; + error: ErrorApiError; + context?: ErrorApiErrorContext; }; // @public @deprecated (undocumented) @@ -109,13 +110,13 @@ export class MockErrorApi implements ErrorApi { constructor(options?: MockErrorApiOptions); // (undocumented) error$(): Observable<{ - error: Error; - context?: ErrorContext; + error: ErrorApiError; + context?: ErrorApiErrorContext; }>; // (undocumented) getErrors(): ErrorWithContext[]; // (undocumented) - post(error: Error, context?: ErrorContext): void; + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; // (undocumented) waitForError(pattern: RegExp, timeoutMs?: number): Promise; } diff --git a/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts b/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts index 87601e29e2..96918b4e47 100644 --- a/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts +++ b/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api'; +import { + ErrorApi, + ErrorApiError, + ErrorApiErrorContext, +} from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; /** @@ -27,12 +31,12 @@ export type MockErrorApiOptions = { }; /** - * ErrorWithContext contains error and ErrorContext + * ErrorWithContext contains error and ErrorApiErrorContext * @public */ export type ErrorWithContext = { - error: Error; - context?: ErrorContext; + error: ErrorApiError; + context?: ErrorApiErrorContext; }; type Waiter = { @@ -59,7 +63,7 @@ export class MockErrorApi implements ErrorApi { constructor(private readonly options: MockErrorApiOptions = {}) {} - post(error: Error, context?: ErrorContext) { + post(error: ErrorApiError, context?: ErrorApiErrorContext) { if (this.options.collect) { this.errors.push({ error, context }); @@ -76,7 +80,10 @@ export class MockErrorApi implements ErrorApi { throw new Error(`MockErrorApi received unexpected error, ${error}`); } - error$(): Observable<{ error: Error; context?: ErrorContext }> { + error$(): Observable<{ + error: ErrorApiError; + context?: ErrorApiErrorContext; + }> { return nullObservable; } From dee1db5f8ac526a58fc66c56cdd4da2654389839 Mon Sep 17 00:00:00 2001 From: Soren Mathiasen Date: Mon, 15 Nov 2021 18:30:55 +0100 Subject: [PATCH 177/181] Adding Tradeshift as adopters Signed-off-by: Soren Mathiasen --- ADOPTERS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index 6736538fa0..f5f5e260c9 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -64,4 +64,5 @@ | [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc. | | [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. | | [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling | -| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem +| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem | +| [Tradeshift](https://www.tradeshift.com/) | [Soren Mathiasen](https://github.com/sorenmat) | Developer Portal: documentation, monitoring, service templates, service catalog for our micro services | From 2345b318fb49fb79c19e4ba087bffca279b3fd55 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Nov 2021 22:36:41 +0100 Subject: [PATCH 178/181] create-app: fix windows file copy and mocking Signed-off-by: Patrik Oldsberg --- packages/create-app/src/createApp.test.ts | 2 +- packages/create-app/src/lib/tasks.ts | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index 7b3b8f124c..09099e2d15 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -25,7 +25,7 @@ jest.mock('./lib/tasks'); beforeAll(() => { mockFs({ - 'package.json': '', // required by `findPaths(__dirname)` + [`${__dirname}/package.json`]: '', // required by `findPaths(__dirname)` 'templates/': mockFs.load(path.resolve(__dirname, '../templates/')), }); }); diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 0b3ca63728..3961e84753 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -19,7 +19,12 @@ import fs from 'fs-extra'; import handlebars from 'handlebars'; import ora from 'ora'; import recursive from 'recursive-readdir'; -import { basename, dirname, resolve as resolvePath } from 'path'; +import { + basename, + dirname, + resolve as resolvePath, + relative as relativePath, +} from 'path'; import { exec as execCb } from 'child_process'; import { packageVersions } from './versions'; import { promisify } from 'util'; @@ -85,7 +90,10 @@ export async function templatingTask( }); for (const file of files) { - const destinationFile = file.replace(templateDir, destinationDir); + const destinationFile = resolvePath( + destinationDir, + relativePath(templateDir, file), + ); await fs.ensureDir(dirname(destinationFile)); if (file.endsWith('.hbs')) { From ad3afc5ad63ced76ab81d2c07607023ad5a9d705 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Nov 2021 00:22:37 +0100 Subject: [PATCH 179/181] cli: trim away non-ascii in stream mock testing util Signed-off-by: Patrik Oldsberg --- .../create/factories/backendPlugin.test.ts | 28 ++++++------- .../lib/create/factories/common/tasks.test.ts | 12 +++--- .../lib/create/factories/common/testUtils.ts | 15 +++++-- .../create/factories/frontendPlugin.test.ts | 42 +++++++++---------- .../lib/create/factories/pluginCommon.test.ts | 18 ++++---- .../create/factories/scaffolderModule.test.ts | 24 +++++------ 6 files changed, 73 insertions(+), 66 deletions(-) diff --git a/packages/cli/src/lib/create/factories/backendPlugin.test.ts b/packages/cli/src/lib/create/factories/backendPlugin.test.ts index 5fe52c3732..916ba7187c 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.test.ts @@ -75,22 +75,22 @@ describe('backendPlugin factory', () => { '', 'Creating backend plugin backstage-plugin-test-backend', 'Checking Prerequisites:', - 'availability plugins/test-backend ✔', - 'creating temp dir ✔', + 'availability plugins/test-backend', + 'creating temp dir', 'Executing Template:', - 'copying .eslintrc.js ✔', - 'templating README.md.hbs ✔', - 'templating package.json.hbs ✔', - 'copying tsconfig.json ✔', - 'copying index.ts ✔', - 'templating run.ts.hbs ✔', - 'copying setupTests.ts ✔', - 'copying router.test.ts ✔', - 'copying router.ts ✔', - 'templating standaloneServer.ts.hbs ✔', + 'copying .eslintrc.js', + 'templating README.md.hbs', + 'templating package.json.hbs', + 'copying tsconfig.json', + 'copying index.ts', + 'templating run.ts.hbs', + 'copying setupTests.ts', + 'copying router.test.ts', + 'copying router.ts', + 'templating standaloneServer.ts.hbs', 'Installing:', - 'moving plugins/test-backend ✔', - 'backend adding dependency ✔', + 'moving plugins/test-backend', + 'backend adding dependency', ]); await expect( diff --git a/packages/cli/src/lib/create/factories/common/tasks.test.ts b/packages/cli/src/lib/create/factories/common/tasks.test.ts index b3a751ba5b..0892322e61 100644 --- a/packages/cli/src/lib/create/factories/common/tasks.test.ts +++ b/packages/cli/src/lib/create/factories/common/tasks.test.ts @@ -88,14 +88,14 @@ some-package@^1.1.0: expect(modified).toBe(true); expect(output).toEqual([ 'Checking Prerequisites:', - 'availability /target ✔', - 'creating temp dir ✔', + 'availability /target', + 'creating temp dir', 'Executing Template:', - 'templating package.json.hbs ✔', - 'copying not-templated.txt ✔', - 'templating templated.txt.hbs ✔', + 'templating package.json.hbs', + 'copying not-templated.txt', + 'templating templated.txt.hbs', 'Installing:', - 'moving /target ✔', + 'moving /target', ]); await expect(fs.readFile('/target/package.json', 'utf8')).resolves.toBe(`{ "name": "my-testing-plugin", diff --git a/packages/cli/src/lib/create/factories/common/testUtils.ts b/packages/cli/src/lib/create/factories/common/testUtils.ts index 651409451e..01081a7486 100644 --- a/packages/cli/src/lib/create/factories/common/testUtils.ts +++ b/packages/cli/src/lib/create/factories/common/testUtils.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +/* eslint-disable no-control-regex */ + import { WriteStream } from 'tty'; import { resolve as resolvePath } from 'path'; import { paths } from '../../../paths'; @@ -59,10 +61,15 @@ export function createMockOutputStream() { cursorTo: () => {}, clearLine: () => {}, moveCursor: () => {}, - write: (msg: string) => - // Clean up colors and whitespace - // eslint-disable-next-line no-control-regex - output.push(msg.replace(/\x1B\[\d\dm/g, '').trim()), + write: (msg: string) => { + let clean = msg; + // Remove terminal color escape sequences + clean = clean.replace(/\x1B\[\d\dm/g, ''); + // Remove any non-ascii + clean = clean.replace(/[^\x00-\x7F]+/g, ''); + clean = clean.trim(); + output.push(clean); + }, } as unknown as WriteStream & { fd: any }, ] as const; } diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts index a882a130ea..439b134478 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts @@ -88,29 +88,29 @@ describe('frontendPlugin factory', () => { '', 'Creating backend plugin backstage-plugin-test', 'Checking Prerequisites:', - 'availability plugins/test ✔', - 'creating temp dir ✔', + 'availability plugins/test', + 'creating temp dir', 'Executing Template:', - 'copying .eslintrc.js ✔', - 'templating README.md.hbs ✔', - 'templating package.json.hbs ✔', - 'copying tsconfig.json ✔', - 'templating index.tsx.hbs ✔', - 'templating index.ts.hbs ✔', - 'templating plugin.test.ts.hbs ✔', - 'templating plugin.ts.hbs ✔', - 'templating routes.ts.hbs ✔', - 'copying setupTests.ts ✔', - 'templating ExampleComponent.test.tsx.hbs ✔', - 'templating ExampleComponent.tsx.hbs ✔', - 'copying index.ts ✔', - 'templating ExampleFetchComponent.test.tsx.hbs ✔', - 'templating ExampleFetchComponent.tsx.hbs ✔', - 'copying index.ts ✔', + 'copying .eslintrc.js', + 'templating README.md.hbs', + 'templating package.json.hbs', + 'copying tsconfig.json', + 'templating index.tsx.hbs', + 'templating index.ts.hbs', + 'templating plugin.test.ts.hbs', + 'templating plugin.ts.hbs', + 'templating routes.ts.hbs', + 'copying setupTests.ts', + 'templating ExampleComponent.test.tsx.hbs', + 'templating ExampleComponent.tsx.hbs', + 'copying index.ts', + 'templating ExampleFetchComponent.test.tsx.hbs', + 'templating ExampleFetchComponent.tsx.hbs', + 'copying index.ts', 'Installing:', - 'moving plugins/test ✔', - 'app adding dependency ✔', - 'app adding import ✔', + 'moving plugins/test', + 'app adding dependency', + 'app adding import', ]); await expect( diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts index a39bb3877d..3124df6b1d 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -70,17 +70,17 @@ describe('pluginCommon factory', () => { '', 'Creating backend plugin backstage-plugin-test-common', 'Checking Prerequisites:', - 'availability plugins/test-common ✔', - 'creating temp dir ✔', + 'availability plugins/test-common', + 'creating temp dir', 'Executing Template:', - 'copying .eslintrc.js ✔', - 'templating README.md.hbs ✔', - 'templating package.json.hbs ✔', - 'copying tsconfig.json ✔', - 'templating index.ts.hbs ✔', - 'copying setupTests.ts ✔', + 'copying .eslintrc.js', + 'templating README.md.hbs', + 'templating package.json.hbs', + 'copying tsconfig.json', + 'templating index.ts.hbs', + 'copying setupTests.ts', 'Installing:', - 'moving plugins/test-common ✔', + 'moving plugins/test-common', ]); await expect( diff --git a/packages/cli/src/lib/create/factories/scaffolderModule.test.ts b/packages/cli/src/lib/create/factories/scaffolderModule.test.ts index dde711b750..a5f1f8e63a 100644 --- a/packages/cli/src/lib/create/factories/scaffolderModule.test.ts +++ b/packages/cli/src/lib/create/factories/scaffolderModule.test.ts @@ -70,20 +70,20 @@ describe('scaffolderModule factory', () => { '', 'Creating module backstage-plugin-scaffolder-backend-module-test', 'Checking Prerequisites:', - 'availability plugins/scaffolder-backend-module-test ✔', - 'creating temp dir ✔', + 'availability plugins/scaffolder-backend-module-test', + 'creating temp dir', 'Executing Template:', - 'copying .eslintrc.js ✔', - 'templating README.md.hbs ✔', - 'templating package.json.hbs ✔', - 'copying tsconfig.json ✔', - 'templating index.ts.hbs ✔', - 'copying index.ts ✔', - 'copying example.test.ts ✔', - 'copying example.ts ✔', - 'copying index.ts ✔', + 'copying .eslintrc.js', + 'templating README.md.hbs', + 'templating package.json.hbs', + 'copying tsconfig.json', + 'templating index.ts.hbs', + 'copying index.ts', + 'copying example.test.ts', + 'copying example.ts', + 'copying index.ts', 'Installing:', - 'moving plugins/scaffolder-backend-module-test ✔', + 'moving plugins/scaffolder-backend-module-test', ]); await expect( From 6f279f9b8eebf4cea506baea2c6bca733de290c6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Nov 2021 01:52:56 +0100 Subject: [PATCH 180/181] cli: fix failing create tests on windows Signed-off-by: Patrik Oldsberg --- .../src/lib/create/factories/backendPlugin.test.ts | 9 +++++---- .../src/lib/create/factories/common/tasks.test.ts | 5 +++-- .../cli/src/lib/create/factories/common/tasks.ts | 4 ++-- .../src/lib/create/factories/frontendPlugin.test.ts | 13 +++++++------ .../src/lib/create/factories/pluginCommon.test.ts | 9 +++++---- .../lib/create/factories/scaffolderModule.test.ts | 9 +++++---- 6 files changed, 27 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/lib/create/factories/backendPlugin.test.ts b/packages/cli/src/lib/create/factories/backendPlugin.test.ts index 916ba7187c..6f4a73c02d 100644 --- a/packages/cli/src/lib/create/factories/backendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/backendPlugin.test.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; +import { sep, resolve as resolvePath } from 'path'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; @@ -75,7 +76,7 @@ describe('backendPlugin factory', () => { '', 'Creating backend plugin backstage-plugin-test-backend', 'Checking Prerequisites:', - 'availability plugins/test-backend', + `availability plugins${sep}test-backend`, 'creating temp dir', 'Executing Template:', 'copying .eslintrc.js', @@ -89,7 +90,7 @@ describe('backendPlugin factory', () => { 'copying router.ts', 'templating standaloneServer.ts.hbs', 'Installing:', - 'moving plugins/test-backend', + `moving plugins${sep}test-backend`, 'backend adding dependency', ]); @@ -103,11 +104,11 @@ describe('backendPlugin factory', () => { expect(Task.forCommand).toHaveBeenCalledTimes(2); expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: '/root/plugins/test-backend', + cwd: resolvePath('/root/plugins/test-backend'), optional: true, }); expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: '/root/plugins/test-backend', + cwd: resolvePath('/root/plugins/test-backend'), optional: true, }); }); diff --git a/packages/cli/src/lib/create/factories/common/tasks.test.ts b/packages/cli/src/lib/create/factories/common/tasks.test.ts index 0892322e61..49381676e7 100644 --- a/packages/cli/src/lib/create/factories/common/tasks.test.ts +++ b/packages/cli/src/lib/create/factories/common/tasks.test.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; +import { sep } from 'path'; import { createMockOutputStream, mockPaths } from './testUtils'; import { CreateContext } from '../../types'; import { executePluginPackageTemplate } from './tasks'; @@ -88,14 +89,14 @@ some-package@^1.1.0: expect(modified).toBe(true); expect(output).toEqual([ 'Checking Prerequisites:', - 'availability /target', + `availability ..${sep}target`, 'creating temp dir', 'Executing Template:', 'templating package.json.hbs', 'copying not-templated.txt', 'templating templated.txt.hbs', 'Installing:', - 'moving /target', + `moving ..${sep}target`, ]); await expect(fs.readFile('/target/package.json', 'utf8')).resolves.toBe(`{ "name": "my-testing-plugin", diff --git a/packages/cli/src/lib/create/factories/common/tasks.ts b/packages/cli/src/lib/create/factories/common/tasks.ts index fd3bd4d930..93af1e2b00 100644 --- a/packages/cli/src/lib/create/factories/common/tasks.ts +++ b/packages/cli/src/lib/create/factories/common/tasks.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import chalk from 'chalk'; -import { resolve as resolvePath } from 'path'; +import { resolve as resolvePath, relative as relativePath } from 'path'; import { paths } from '../../../paths'; import { Task, templatingTask } from '../../../tasks'; import { Lockfile } from '../../../versioning'; @@ -41,7 +41,7 @@ export async function executePluginPackageTemplate( } Task.section('Checking Prerequisites'); - const shortPluginDir = targetDir.replace(`${paths.targetRoot}/`, ''); + const shortPluginDir = relativePath(paths.targetRoot, targetDir); await Task.forItem('availability', shortPluginDir, async () => { if (await fs.pathExists(targetDir)) { throw new Error( diff --git a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts index 439b134478..27ee14ea2c 100644 --- a/packages/cli/src/lib/create/factories/frontendPlugin.test.ts +++ b/packages/cli/src/lib/create/factories/frontendPlugin.test.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; +import { sep, resolve as resolvePath } from 'path'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; @@ -88,7 +89,7 @@ describe('frontendPlugin factory', () => { '', 'Creating backend plugin backstage-plugin-test', 'Checking Prerequisites:', - 'availability plugins/test', + `availability plugins${sep}test`, 'creating temp dir', 'Executing Template:', 'copying .eslintrc.js', @@ -108,7 +109,7 @@ describe('frontendPlugin factory', () => { 'templating ExampleFetchComponent.tsx.hbs', 'copying index.ts', 'Installing:', - 'moving plugins/test', + `moving plugins${sep}test`, 'app adding dependency', 'app adding import', ]); @@ -136,11 +137,11 @@ const router = ( expect(Task.forCommand).toHaveBeenCalledTimes(2); expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: '/root/plugins/test', + cwd: resolvePath('/root/plugins/test'), optional: true, }); expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: '/root/plugins/test', + cwd: resolvePath('/root/plugins/test'), optional: true, }); }); @@ -205,11 +206,11 @@ const router = ( expect(Task.forCommand).toHaveBeenCalledTimes(2); expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: '/root/plugins/test', + cwd: resolvePath('/root/plugins/test'), optional: true, }); expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: '/root/plugins/test', + cwd: resolvePath('/root/plugins/test'), optional: true, }); }); diff --git a/packages/cli/src/lib/create/factories/pluginCommon.test.ts b/packages/cli/src/lib/create/factories/pluginCommon.test.ts index 3124df6b1d..602fce7bbd 100644 --- a/packages/cli/src/lib/create/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/create/factories/pluginCommon.test.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; +import { sep, resolve as resolvePath } from 'path'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; @@ -70,7 +71,7 @@ describe('pluginCommon factory', () => { '', 'Creating backend plugin backstage-plugin-test-common', 'Checking Prerequisites:', - 'availability plugins/test-common', + `availability plugins${sep}test-common`, 'creating temp dir', 'Executing Template:', 'copying .eslintrc.js', @@ -80,7 +81,7 @@ describe('pluginCommon factory', () => { 'templating index.ts.hbs', 'copying setupTests.ts', 'Installing:', - 'moving plugins/test-common', + `moving plugins${sep}test-common`, ]); await expect( @@ -96,11 +97,11 @@ describe('pluginCommon factory', () => { expect(Task.forCommand).toHaveBeenCalledTimes(2); expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: '/root/plugins/test-common', + cwd: resolvePath('/root/plugins/test-common'), optional: true, }); expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: '/root/plugins/test-common', + cwd: resolvePath('/root/plugins/test-common'), optional: true, }); }); diff --git a/packages/cli/src/lib/create/factories/scaffolderModule.test.ts b/packages/cli/src/lib/create/factories/scaffolderModule.test.ts index a5f1f8e63a..c60c58e619 100644 --- a/packages/cli/src/lib/create/factories/scaffolderModule.test.ts +++ b/packages/cli/src/lib/create/factories/scaffolderModule.test.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; +import { sep, resolve as resolvePath } from 'path'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; @@ -70,7 +71,7 @@ describe('scaffolderModule factory', () => { '', 'Creating module backstage-plugin-scaffolder-backend-module-test', 'Checking Prerequisites:', - 'availability plugins/scaffolder-backend-module-test', + `availability plugins${sep}scaffolder-backend-module-test`, 'creating temp dir', 'Executing Template:', 'copying .eslintrc.js', @@ -83,7 +84,7 @@ describe('scaffolderModule factory', () => { 'copying example.ts', 'copying index.ts', 'Installing:', - 'moving plugins/scaffolder-backend-module-test', + `moving plugins${sep}scaffolder-backend-module-test`, ]); await expect( @@ -99,11 +100,11 @@ describe('scaffolderModule factory', () => { expect(Task.forCommand).toHaveBeenCalledTimes(2); expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { - cwd: '/root/plugins/scaffolder-backend-module-test', + cwd: resolvePath('/root/plugins/scaffolder-backend-module-test'), optional: true, }); expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { - cwd: '/root/plugins/scaffolder-backend-module-test', + cwd: resolvePath('/root/plugins/scaffolder-backend-module-test'), optional: true, }); }); From 6c1348591e513137442f6d539fae3d905f72e442 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Nov 2021 04:22:02 +0000 Subject: [PATCH 181/181] build(deps-dev): bump prettier from 2.4.0 to 2.4.1 Bumps [prettier](https://github.com/prettier/prettier) from 2.4.0 to 2.4.1. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/2.4.0...2.4.1) --- updated-dependencies: - dependency-name: prettier dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 37c1bd3329..9bff375969 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23142,9 +23142,9 @@ prettier@^1.19.1: integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== prettier@^2.2.1: - version "2.4.0" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.4.0.tgz#85bdfe0f70c3e777cf13a4ffff39713ca6f64cba" - integrity sha512-DsEPLY1dE5HF3BxCRBmD4uYZ+5DCbvatnolqTqcxEgKVZnL2kUfyu7b8pPQ5+hTBkdhU9SLUmK0/pHb07RE4WQ== + version "2.4.1" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c" + integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA== prettier@~2.2.1: version "2.2.1"