From 0287b186135431c67ddb01834b12e65bd599aaf3 Mon Sep 17 00:00:00 2001 From: Jeremy Guarini Date: Fri, 29 Oct 2021 10:20:14 -0700 Subject: [PATCH 001/110] 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/110] 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/110] 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 1b8efd52f0b1aab5c7860c0ef71675bdd49ce95e Mon Sep 17 00:00:00 2001 From: Jeremy Guarini Date: Mon, 8 Nov 2021 10:33:40 -0800 Subject: [PATCH 004/110] 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 005/110] 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 006/110] 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 007/110] 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 008/110] 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 009/110] 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 010/110] 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 011/110] 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 012/110] 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 013/110] 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 014/110] 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 015/110] 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 016/110] 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 017/110] 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 018/110] 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 019/110] 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 020/110] 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 021/110] 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 022/110] 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 79dc3f9a29603dc7866a2907acb8f01bc0e55564 Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Thu, 11 Nov 2021 08:09:01 +0000 Subject: [PATCH 023/110] 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 024/110] 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 025/110] 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 026/110] 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 027/110] 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 028/110] 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 029/110] 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 030/110] 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 031/110] 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 032/110] 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 033/110] 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 034/110] 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 035/110] 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 036/110] 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 037/110] 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 0a7a2eba76febb07a613fb76ccf4ef6a8208ed02 Mon Sep 17 00:00:00 2001 From: Marley Powell Date: Thu, 11 Nov 2021 16:22:25 +0000 Subject: [PATCH 038/110] 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 039/110] 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 040/110] 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 041/110] 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 042/110] 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 043/110] 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 044/110] 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 045/110] 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 046/110] 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 047/110] 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 048/110] 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 049/110] 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 050/110] 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 051/110] 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 052/110] 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 053/110] 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 054/110] 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 055/110] 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 056/110] 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 057/110] 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 058/110] 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 059/110] 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 060/110] 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 061/110] 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 062/110] 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 063/110] 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 064/110] 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 065/110] 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 066/110] 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 067/110] 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 068/110] 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 069/110] 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 070/110] 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 071/110] 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 072/110] 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 073/110] 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 074/110] 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 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 075/110] 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 076/110] 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 077/110] 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 078/110] 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 079/110] 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 080/110] 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 081/110] 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 082/110] 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 083/110] 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 084/110] 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 085/110] 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 086/110] 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 087/110] 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 088/110] 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 089/110] 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 090/110] 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 091/110] 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 092/110] 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 093/110] 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 094/110] 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 095/110] 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 096/110] 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 097/110] 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 098/110] 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 099/110] 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 100/110] 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 101/110] 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 102/110] 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 103/110] 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 104/110] 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 105/110] 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 106/110] 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 107/110] 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 108/110] 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 109/110] 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 58a4e67ded6ac591f8429f71bf49a6a4d934c71d Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 12 Nov 2021 11:10:27 +0100 Subject: [PATCH 110/110] 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}