diff --git a/.changeset/chatty-seals-tap.md b/.changeset/chatty-seals-tap.md new file mode 100644 index 0000000000..01d69e1562 --- /dev/null +++ b/.changeset/chatty-seals-tap.md @@ -0,0 +1,6 @@ +--- +'@backstage/create-app': patch +'@backstage/plugin-catalog': patch +--- + +Add link from Template entity to the scaffolder launch page for the template in the AboutCard. diff --git a/.changeset/clean-meals-pay.md b/.changeset/clean-meals-pay.md new file mode 100644 index 0000000000..629549112c --- /dev/null +++ b/.changeset/clean-meals-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins-backend': patch +--- + +Don't expose username and authentication header if configured. diff --git a/.changeset/clean-years-whisper.md b/.changeset/clean-years-whisper.md new file mode 100644 index 0000000000..fd77430cd7 --- /dev/null +++ b/.changeset/clean-years-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Update set up documentation to export homepage as element instead of React component. diff --git a/.changeset/giant-maps-compete.md b/.changeset/giant-maps-compete.md new file mode 100644 index 0000000000..776332b308 --- /dev/null +++ b/.changeset/giant-maps-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-pagerduty': patch +--- + +Add a homepage widget for the `Pagerduty` component. diff --git a/.changeset/nice-readers-heal.md b/.changeset/nice-readers-heal.md new file mode 100644 index 0000000000..54534f8289 --- /dev/null +++ b/.changeset/nice-readers-heal.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Fixed typo in HostDiscovery's JSDoc diff --git a/.changeset/selfish-carrots-sneeze.md b/.changeset/selfish-carrots-sneeze.md new file mode 100644 index 0000000000..0920f78f86 --- /dev/null +++ b/.changeset/selfish-carrots-sneeze.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-proxy-backend': patch +--- + +Marked headers `Authorization` and `X-Api-Key` as secret in order to not show up in frontend configuration. diff --git a/.changeset/weak-rats-serve.md b/.changeset/weak-rats-serve.md new file mode 100644 index 0000000000..fe7cd78510 --- /dev/null +++ b/.changeset/weak-rats-serve.md @@ -0,0 +1,9 @@ +--- +'@backstage/core-app-api': patch +'@backstage/core-plugin-api': patch +'@backstage/dev-utils': patch +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder': patch +--- + +Add `PropsWithChildren` to usages of `ComponentType`, in preparation for React 18 where the children are no longer implicit. diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index 63a97ec54f..9c6f6380a1 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -40,10 +40,10 @@ Inside your `packages/app` directory, create a new file where our new homepage c ```tsx import React from 'react'; -export const HomePage = () => { +export const homePage = ( /* We will shortly compose a pretty homepage here. */ - return

Welcome to Backstage!

; -}; +

Welcome to Backstage!

+); ``` #### 3. Update router for the root `/` route @@ -161,13 +161,11 @@ import React from 'react'; import Grid from '@material-ui/core/Grid'; import { HomePageCompanyLogo } from '@backstage/plugin-home'; -export const HomePage = () => { - return ( - - - - +export const homePage = ( + + + - ); -}; + +); ``` diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index 39532e806f..39b60c76c5 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -261,8 +261,8 @@ returned identity. async (user, ctx): Promise => { const entity = await defaultUserTransformer(user, ctx); - if (entity && user.organizationVerifiedDomainEmails) { - entity.spec.profile!.email = user.organizationVerifiedDomainEmails[0] || ''; + if (entity && user.organizationVerifiedDomainEmails?.length) { + entity.spec.profile!.email = user.organizationVerifiedDomainEmails[0]; } return entity; diff --git a/microsite/data/plugins/analytics-module-ga.yaml b/microsite/data/plugins/analytics-module-ga.yaml index d58ca322eb..54dea6454a 100644 --- a/microsite/data/plugins/analytics-module-ga.yaml +++ b/microsite/data/plugins/analytics-module-ga.yaml @@ -2,7 +2,7 @@ title: 'Analytics Module: Google Analytics' author: Spotify authorUrl: https://github.com/spotify -category: Analytics +category: Monitoring description: Track usage of your Backstage instance using Google Analytics. documentation: https://github.com/backstage/backstage/blob/master/plugins/analytics-module-ga/README.md iconUrl: /img/ga-icon.png diff --git a/microsite/data/plugins/firehydrant.yaml b/microsite/data/plugins/firehydrant.yaml index b52357e808..8a1f122051 100644 --- a/microsite/data/plugins/firehydrant.yaml +++ b/microsite/data/plugins/firehydrant.yaml @@ -2,7 +2,7 @@ title: FireHydrant author: FireHydrant authorUrl: https://firehydrant.io/ -category: Incident Management +category: Monitoring description: View service incidents information from FireHydrant, such as active incidents and incident metrics, directly within Backstage. documentation: https://github.com/backstage/backstage/blob/master/plugins/firehydrant/README.md iconUrl: https://raw.githubusercontent.com/backstage/backstage/master/plugins/firehydrant/doc/firehydrant_logo.png diff --git a/microsite/data/plugins/git-release-manager.yaml b/microsite/data/plugins/git-release-manager.yaml index 6d357a23f6..947209e6d5 100644 --- a/microsite/data/plugins/git-release-manager.yaml +++ b/microsite/data/plugins/git-release-manager.yaml @@ -2,7 +2,7 @@ title: GitHub Release Manager author: Spotify authorUrl: https://github.com/spotify -category: Release management +category: Source Control Mgmt description: Manage releases without having to juggle git commands. documentation: https://github.com/backstage/backstage/tree/master/plugins/git-release-manager iconUrl: /img/git-release-manager-logo.svg diff --git a/microsite/data/plugins/keycloak.yaml b/microsite/data/plugins/keycloak.yaml index 882d43f515..ea657c74f2 100644 --- a/microsite/data/plugins/keycloak.yaml +++ b/microsite/data/plugins/keycloak.yaml @@ -2,7 +2,7 @@ title: Authentication and Authorization with Keycloak author: Red Hat authorUrl: https://redhat.com -category: Authentication +category: Authentication/Authorization description: Load users and groups from Keycloak, enabling use of multiple authentication providers to be applied to Backstage entities. documentation: https://janus-idp.io/plugins/keycloak iconUrl: http://janus-idp.io/images/plugins/keycloak.svg diff --git a/microsite/data/plugins/kpt-config-as-data.yaml b/microsite/data/plugins/kpt-config-as-data.yaml index 2ed67262a8..af157a70c3 100644 --- a/microsite/data/plugins/kpt-config-as-data.yaml +++ b/microsite/data/plugins/kpt-config-as-data.yaml @@ -2,7 +2,7 @@ title: Configuration as Data author: kpt authorUrl: https://kpt.dev -category: Configuration +category: Configuration Management description: Configuration GUI over GitOps using kpt, with WYSIWYG editing, review and approval, versioning and undo, and package cloning and upgrades. documentation: https://github.com/GoogleContainerTools/kpt-backstage-plugins/tree/main/plugins/cad iconUrl: https://github.com/GoogleContainerTools/kpt/blob/main/logo/KptLogoSmall.png?raw=true diff --git a/microsite/data/plugins/ldap-auth.yml b/microsite/data/plugins/ldap-auth.yml index 1800f7370c..7446f28f6f 100644 --- a/microsite/data/plugins/ldap-auth.yml +++ b/microsite/data/plugins/ldap-auth.yml @@ -2,7 +2,7 @@ title: LDAP Auth author: ImmobiliareLabs authorUrl: https://github.com/immobiliare -category: Authentication +category: Authentication/Authorization description: Authenticate users to an external LDAP server documentation: https://github.com/immobiliare/backstage-plugin-ldap-auth/blob/main/README.md iconUrl: https://avatars.githubusercontent.com/u/10090828 diff --git a/microsite/data/plugins/ocm.yaml b/microsite/data/plugins/ocm.yaml index 15c23a0fb7..9e9b8ff1b7 100644 --- a/microsite/data/plugins/ocm.yaml +++ b/microsite/data/plugins/ocm.yaml @@ -2,7 +2,7 @@ title: Multi Cluster View with OCM author: Red Hat authorUrl: https://redhat.com -category: OCM +category: Infrastructure description: View clusters from OCM's MultiClusterHub and MultiCluster Engine in Backstage. documentation: https://janus-idp.io/plugins/ocm iconUrl: http://janus-idp.io/images/plugins/ocm.svg diff --git a/microsite/data/plugins/okta-entity-providers.yaml b/microsite/data/plugins/okta-entity-providers.yaml index 047992e785..4544c9eae7 100644 --- a/microsite/data/plugins/okta-entity-providers.yaml +++ b/microsite/data/plugins/okta-entity-providers.yaml @@ -2,7 +2,7 @@ title: Okta Organization Entity Provider author: roadie.io authorUrl: https://roadie.io/?utm_source=backstage.io&utm_medium=marketplace&utm_campaign=okta-entity-provider -category: Identity +category: Authentication/Authorization description: Load users and groups from Okta into the Backstage catalog. documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/tree/main/plugins/backend/catalog-backend-module-okta iconUrl: https://roadie.io/images/logos/okta.png diff --git a/microsite/data/plugins/rootly.yaml b/microsite/data/plugins/rootly.yaml index 4aaf10ee7b..fa65e2d6ef 100644 --- a/microsite/data/plugins/rootly.yaml +++ b/microsite/data/plugins/rootly.yaml @@ -2,7 +2,7 @@ title: Rootly author: Rootly authorUrl: https://rootly.com/ -category: Incident Management +category: Monitoring description: Import your Backstage entities into Rootly services and view incidents & analytics. documentation: https://github.com/rootlyhq/backstage-plugin/blob/master/README.md iconUrl: https://raw.githubusercontent.com/rootlyhq/backstage-plugin/master/docs/logo.png diff --git a/microsite/data/plugins/s3-viewer.yaml b/microsite/data/plugins/s3-viewer.yaml index 2eeeb23415..57d5b22ba1 100644 --- a/microsite/data/plugins/s3-viewer.yaml +++ b/microsite/data/plugins/s3-viewer.yaml @@ -2,7 +2,7 @@ title: S3 Viewer author: Spreadgroup authorUrl: https://github.com/spreadshirt -category: AWS +category: Infrastructure description: Visualization of S3 buckets and their contents in file explorer style. documentation: https://github.com/spreadshirt/backstage-plugin-s3/ iconUrl: /img/s3-bucket.svg diff --git a/microsite/data/plugins/solo-io-dev-portal.yaml b/microsite/data/plugins/solo-io-dev-portal.yaml new file mode 100644 index 0000000000..dcc5722092 --- /dev/null +++ b/microsite/data/plugins/solo-io-dev-portal.yaml @@ -0,0 +1,10 @@ +--- +title: Solo.io Gloo Platform Portal +author: Solo.io +authorUrl: https://solo.io +category: Infrastructure +description: View, test, and manage your Gloo Platform Portal APIs through our Backstage plugin. +documentation: https://github.com/solo-io/dev-portal-backstage-public#readme +iconUrl: img/solo-io-gloo-outline.svg +npmPackageName: '@solo.io/dev-portal-backstage-plugin' +addedDate: '2023-05-19' diff --git a/microsite/data/plugins/statusneo-github-plugin.yaml b/microsite/data/plugins/statusneo-github-plugin.yaml index 9c56dd4ae4..2497a99c1b 100644 --- a/microsite/data/plugins/statusneo-github-plugin.yaml +++ b/microsite/data/plugins/statusneo-github-plugin.yaml @@ -1,7 +1,7 @@ title: Statusneo GitHub Plugin author: Statusneo authorUrl: https://statusneo.com/ -category: GitHub +category: Source Control Mgmt description: Provides status cards GitHub pull requests and GitHub actions. documentation: https://github.com/StatusNeo/backstage-plugin-github iconUrl: /img/github-statusneo.png diff --git a/microsite/data/plugins/tekton-pipelines.yaml b/microsite/data/plugins/tekton-pipelines.yaml index 2da80d34bf..fe19ab2af2 100644 --- a/microsite/data/plugins/tekton-pipelines.yaml +++ b/microsite/data/plugins/tekton-pipelines.yaml @@ -2,7 +2,7 @@ title: Tekton Pipelines author: JQuad Group authorUrl: https://github.com/jquad-group -category: CI +category: CI/CD description: View the status of the Tekton PipelineRun resources. documentation: https://github.com/jquad-group/backstage-jquad#readme iconUrl: https://raw.githubusercontent.com/jquad-group/backstage-jquad/main/img/tekton-horizontal-color.png diff --git a/microsite/data/plugins/tekton.yaml b/microsite/data/plugins/tekton.yaml index b1ebfb1349..fa94b89c35 100644 --- a/microsite/data/plugins/tekton.yaml +++ b/microsite/data/plugins/tekton.yaml @@ -2,7 +2,7 @@ title: Pipelines with Tekton author: Red Hat authorUrl: https://redhat.com -category: CI +category: CI/CD description: Easily view Tekton PipelineRun status for your services in Backstage. documentation: https://janus-idp.io/plugins/tekton iconUrl: http://janus-idp.io/images/plugins/tekton.svg diff --git a/microsite/package.json b/microsite/package.json index 98301ce2d0..f1732c1142 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -20,6 +20,7 @@ "@docusaurus/module-type-aliases": "0.0.0-5580", "@spotify/prettier-config": "^14.0.0", "@tsconfig/docusaurus": "^1.0.6", + "@types/luxon": "^3.0.0", "@types/webpack-env": "^1.18.0", "js-yaml": "^4.1.0", "prettier": "^2.6.2", @@ -34,6 +35,7 @@ "@swc/core": "^1.3.46", "clsx": "^1.1.1", "docusaurus-plugin-sass": "^0.2.3", + "luxon": "^3.0.0", "prism-react-renderer": "^1.3.5", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/microsite/src/components/contentBlock/contentBlock.tsx b/microsite/src/components/contentBlock/contentBlock.tsx index aae065db44..983f684927 100644 --- a/microsite/src/components/contentBlock/contentBlock.tsx +++ b/microsite/src/components/contentBlock/contentBlock.tsx @@ -1,6 +1,6 @@ import Link from '@docusaurus/Link'; import clsx from 'clsx'; -import React, { FC, PropsWithChildren, ReactNode } from 'react'; +import React, { PropsWithChildren, ReactNode } from 'react'; import contentBlockStyles from './contentBlock.module.scss'; diff --git a/microsite/src/pages/plugins/_pluginCard.tsx b/microsite/src/pages/plugins/_pluginCard.tsx index d6b4017db5..ba8e8069d2 100644 --- a/microsite/src/pages/plugins/_pluginCard.tsx +++ b/microsite/src/pages/plugins/_pluginCard.tsx @@ -1,5 +1,6 @@ import Link from '@docusaurus/Link'; import { SimpleCard } from '@site/src/components/simpleCard/simpleCard'; +import clsx from 'clsx'; import React from 'react'; export interface IPluginData { @@ -10,6 +11,8 @@ export interface IPluginData { documentation: string; iconUrl: string; title: string; + addedDate: string; + isNew: boolean; order?: number; } @@ -22,14 +25,17 @@ export const PluginCard = ({ description, documentation, iconUrl, + isNew = false, title, }: IPluginData) => ( + {isNew &&
NEW
} + {title} -

{title}

+

{title}

by {author} diff --git a/microsite/src/pages/plugins/index.tsx b/microsite/src/pages/plugins/index.tsx index b1c9601110..94e9df0e8d 100644 --- a/microsite/src/pages/plugins/index.tsx +++ b/microsite/src/pages/plugins/index.tsx @@ -1,12 +1,19 @@ import Link from '@docusaurus/Link'; +import PluginsFilter from '@site/src/components/pluginsFilter/pluginsFilter'; +import { calcIsNewPlugin } from '@site/src/util/calcIsNewPlugin'; +import { truncateDescription } from '@site/src/util/truncateDescription'; +import { ChipCategory } from '@site/src/util/types'; import Layout from '@theme/Layout'; import clsx from 'clsx'; import React, { useState } from 'react'; + import { IPluginData, PluginCard } from './_pluginCard'; import pluginsStyles from './plugins.module.scss'; -import { ChipCategory } from '@site/src/util/types'; -import { truncateDescription } from '@site/src/util/truncateDescription'; -import PluginsFilter from '@site/src/components/pluginsFilter/pluginsFilter'; + +interface IPluginsList { + corePlugins: IPluginData[]; + otherPlugins: IPluginData[]; +} const pluginsContext = require.context( '../../../data/plugins', @@ -14,17 +21,20 @@ const pluginsContext = require.context( /\.ya?ml/, ); -const plugins = pluginsContext.keys().reduce( +const plugins: IPluginsList = pluginsContext.keys().reduce( (acum, id) => { - const pluginData: IPluginData = pluginsContext(id).default; + let pluginData: IPluginData = pluginsContext(id).default; + const category: keyof IPluginsList = + pluginData.category === 'Core Feature' ? 'corePlugins' : 'otherPlugins'; - acum[ - pluginData.category === 'Core Feature' ? 'corePlugins' : 'otherPlugins' - ].push(truncateDescription(pluginData)); + pluginData = calcIsNewPlugin(pluginData); + pluginData = truncateDescription(pluginData); + + acum[category].push(pluginData); return acum; }, - { corePlugins: [] as IPluginData[], otherPlugins: [] as IPluginData[] }, + { corePlugins: [], otherPlugins: [] } as IPluginsList, ); plugins.corePlugins.sort((a, b) => a.order - b.order); diff --git a/microsite/src/pages/plugins/plugins.module.scss b/microsite/src/pages/plugins/plugins.module.scss index d5ea5de6e7..9db634c3ed 100644 --- a/microsite/src/pages/plugins/plugins.module.scss +++ b/microsite/src/pages/plugins/plugins.module.scss @@ -11,6 +11,7 @@ :global(.card .card__header) { display: grid; row-gap: 0.25rem; + position: relative; column-gap: 1rem; align-items: center; grid-template-columns: auto 1fr; @@ -19,6 +20,28 @@ margin: 0; } + :global(.newRibbon) { + --paddingX: 1.2lh; + --rotation: 45deg; + --translateX: 30%; + --translateY: -100%; + + top: 0; + right: 0; + padding: 0 var(--paddingX); + position: absolute; + text-align: center; + + transform: translateY(var(--translateY)) translateX(var(--translateX)) + rotate(var(--rotation)); + transform-origin: bottom left; + background-color: var(--ifm-color-primary); + } + + :global(.newRibbonPadding) { + padding-right: 1.2lh; + } + :global(img) { width: 80px; grid-row: 1/4; diff --git a/microsite/src/util/calcIsNewPlugin.tsx b/microsite/src/util/calcIsNewPlugin.tsx new file mode 100644 index 0000000000..4b23e1d47d --- /dev/null +++ b/microsite/src/util/calcIsNewPlugin.tsx @@ -0,0 +1,18 @@ +import { DateTime } from 'luxon'; + +export const newDaysLimit = 100; + +export const calcIsNewPlugin = < + T extends { addedDate: string; isNew: boolean }, +>( + itemData: T, +) => { + const addedDate = DateTime.fromISO(itemData.addedDate); + const daysOld = Math.abs(addedDate.diffNow('days').days); + const isNew = !isNaN(daysOld) && daysOld < newDaysLimit; + + return { + ...itemData, + isNew, + }; +}; diff --git a/microsite/static/img/solo-io-gloo-outline.svg b/microsite/static/img/solo-io-gloo-outline.svg new file mode 100644 index 0000000000..debb47038d --- /dev/null +++ b/microsite/static/img/solo-io-gloo-outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 7097fb337e..1d116ad6da 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -2941,6 +2941,13 @@ __metadata: languageName: node linkType: hard +"@types/luxon@npm:^3.0.0": + version: 3.3.0 + resolution: "@types/luxon@npm:3.3.0" + checksum: f7e3a89fc3ca404fbc3ea538653ed6860bc28f570a8c4d6d24449b89b9b553b7d6ad6cc94a9e129c5b8c9a2b97f0c365b3017f811e59c4a859a9c219a1c918e0 + languageName: node + linkType: hard + "@types/mdast@npm:^3.0.0": version: 3.0.10 resolution: "@types/mdast@npm:3.0.10" @@ -3752,10 +3759,12 @@ __metadata: "@spotify/prettier-config": ^14.0.0 "@swc/core": ^1.3.46 "@tsconfig/docusaurus": ^1.0.6 + "@types/luxon": ^3.0.0 "@types/webpack-env": ^1.18.0 clsx: ^1.1.1 docusaurus-plugin-sass: ^0.2.3 js-yaml: ^4.1.0 + luxon: ^3.0.0 prettier: ^2.6.2 prism-react-renderer: ^1.3.5 react: ^17.0.2 @@ -7348,6 +7357,13 @@ __metadata: languageName: node linkType: hard +"luxon@npm:^3.0.0": + version: 3.3.0 + resolution: "luxon@npm:3.3.0" + checksum: 50cf17a0dc155c3dcacbeae8c0b7e80db425e0ba97b9cbdf12a7fc142d841ff1ab1560919f033af46240ed44e2f70c49f76e3422524c7fc8bb8d81ca47c66187 + languageName: node + linkType: hard + "make-dir@npm:^3.0.2": version: 3.1.0 resolution: "make-dir@npm:3.1.0" diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index bc9eac8c64..d46774a757 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -142,6 +142,7 @@ const app = createApp({ bind(catalogPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, viewTechDoc: techdocsPlugin.routes.docRoot, + createFromTemplate: scaffolderPlugin.routes.selectedTemplate, }); bind(apiDocsPlugin.externalRoutes, { registerApi: catalogImportPlugin.routes.importPage, diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx index da1d0d425c..aa3739728e 100644 --- a/packages/app/src/components/home/HomePage.tsx +++ b/packages/app/src/components/home/HomePage.tsx @@ -30,6 +30,7 @@ import { HomePageCalendar } from '@backstage/plugin-gcalendar'; import { MicrosoftCalendarCard } from '@backstage/plugin-microsoft-calendar'; import React from 'react'; import HomeIcon from '@material-ui/icons/Home'; +import { HomePagePagerDutyCard } from '@backstage/plugin-pagerduty'; const clockConfigs: ClockConfig[] = [ { @@ -93,6 +94,7 @@ export const homePage = ( + diff --git a/packages/app/src/components/home/templates/DefaultTemplate.stories.tsx b/packages/app/src/components/home/templates/DefaultTemplate.stories.tsx index 52fab3c55f..05d9a4eecd 100644 --- a/packages/app/src/components/home/templates/DefaultTemplate.stories.tsx +++ b/packages/app/src/components/home/templates/DefaultTemplate.stories.tsx @@ -38,7 +38,7 @@ import { } from '@backstage/plugin-search-react'; import { stackOverflowApiRef, HomePageStackOverflowQuestions } from '@backstage/plugin-stack-overflow'; import { Grid, makeStyles } from '@material-ui/core'; -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; const entities = [ { @@ -107,7 +107,7 @@ starredEntitiesApi.toggleStarred('component:default/example-starred-entity-4'); export default { title: 'Plugins/Home/Templates', decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp( <> ; + NotFoundErrorPage: ComponentType>; BootErrorPage: ComponentType; - Progress: ComponentType<{}>; - Router: ComponentType<{ - basename?: string; - }>; + Progress: ComponentType>; + Router: ComponentType< + PropsWithChildren<{ + basename?: string; + }> + >; ErrorBoundaryFallback: ComponentType; - ThemeProvider?: ComponentType<{}>; + ThemeProvider?: ComponentType>; SignInPage?: ComponentType; }; @@ -274,9 +276,9 @@ export type AuthApiCreateOptions = { export type BackstageApp = { getPlugins(): BackstagePlugin[]; getSystemIcon(key: string): IconComponent | undefined; - createRoot(element: JSX.Element): ComponentType<{}>; - getProvider(): ComponentType<{}>; - getRouter(): ComponentType<{}>; + createRoot(element: JSX.Element): ComponentType>; + getProvider(): ComponentType>; + getRouter(): ComponentType>; }; // @public @@ -316,10 +318,10 @@ export type BitbucketSession = { }; // @public -export type BootErrorPageProps = { +export type BootErrorPageProps = PropsWithChildren<{ step: 'load-config' | 'load-chunk'; error: Error; -}; +}>; export { ConfigReader }; @@ -359,11 +361,11 @@ export class ErrorApiForwarder implements ErrorApi { } // @public -export type ErrorBoundaryFallbackProps = { +export type ErrorBoundaryFallbackProps = PropsWithChildren<{ plugin?: BackstagePlugin; error: Error; resetError: () => void; -}; +}>; // @public export const FeatureFlagged: (props: FeatureFlaggedProps) => JSX.Element; @@ -596,9 +598,9 @@ export class SamlAuth } // @public -export type SignInPageProps = { +export type SignInPageProps = PropsWithChildren<{ onSignInSuccess(identityApi: IdentityApi): void; -}; +}>; // @public export class UnhandledErrorForwarder { diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 33d43cbca6..470f7f8d75 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -242,7 +242,7 @@ export class AppManager implements BackstageApp { return this.components; } - createRoot(element: JSX.Element): ComponentType<{}> { + createRoot(element: JSX.Element): ComponentType> { const AppProvider = this.getProvider(); const AppRoot = () => { return {element}; @@ -251,7 +251,7 @@ export class AppManager implements BackstageApp { } #getProviderCalled = false; - getProvider(): ComponentType<{}> { + getProvider(): ComponentType> { if (this.#getProviderCalled) { throw new Error( 'app.getProvider() or app.createRoot() has already been called, and can only be called once', @@ -404,7 +404,7 @@ export class AppManager implements BackstageApp { return Provider; } - getRouter(): ComponentType<{}> { + getRouter(): ComponentType> { return AppRouter; } diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index c34407ca3a..7f38e2d006 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ComponentType } from 'react'; +import { ComponentType, PropsWithChildren } from 'react'; import { AnyApiFactory, AppTheme, @@ -33,33 +33,33 @@ import { AppConfig } from '@backstage/config'; * * @public */ -export type BootErrorPageProps = { +export type BootErrorPageProps = PropsWithChildren<{ step: 'load-config' | 'load-chunk'; error: Error; -}; +}>; /** * Props for the `SignInPage` component of {@link AppComponents}. * * @public */ -export type SignInPageProps = { +export type SignInPageProps = PropsWithChildren<{ /** * Set the IdentityApi on successful sign-in. This should only be called once. */ onSignInSuccess(identityApi: IdentityApi): void; -}; +}>; /** * Props for the fallback error boundary. * * @public */ -export type ErrorBoundaryFallbackProps = { +export type ErrorBoundaryFallbackProps = PropsWithChildren<{ plugin?: BackstagePlugin; error: Error; resetError: () => void; -}; +}>; /** * A set of replaceable core components that are part of every Backstage app. @@ -67,12 +67,12 @@ export type ErrorBoundaryFallbackProps = { * @public */ export type AppComponents = { - NotFoundErrorPage: ComponentType<{}>; + NotFoundErrorPage: ComponentType>; BootErrorPage: ComponentType; - Progress: ComponentType<{}>; - Router: ComponentType<{ basename?: string }>; + 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. @@ -319,7 +319,7 @@ export type BackstageApp = { * ); * ``` */ - createRoot(element: JSX.Element): ComponentType<{}>; + createRoot(element: JSX.Element): ComponentType>; /** * Provider component that should wrap the Router created with getRouter() @@ -327,7 +327,7 @@ export type BackstageApp = { * * @deprecated Use {@link BackstageApp.createRoot} instead. */ - getProvider(): ComponentType<{}>; + getProvider(): ComponentType>; /** * Router component that should wrap the App Routes create with getRoutes() @@ -335,7 +335,7 @@ export type BackstageApp = { * * @deprecated Import and use the {@link AppRouter} component from `@backstage/core-app-api` instead */ - getRouter(): ComponentType<{}>; + getRouter(): ComponentType>; }; /** diff --git a/packages/core-components/src/components/Link/Link.stories.tsx b/packages/core-components/src/components/Link/Link.stories.tsx index 6049ec68b1..e069459e76 100644 --- a/packages/core-components/src/components/Link/Link.stories.tsx +++ b/packages/core-components/src/components/Link/Link.stories.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { Link } from './Link'; import { Route, @@ -37,7 +37,7 @@ export default { title: 'Navigation/Link', component: Link, decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp(

diff --git a/packages/core-components/src/components/LinkButton/LinkButton.stories.tsx b/packages/core-components/src/components/LinkButton/LinkButton.stories.tsx index 3dc49abb7f..ec9398e47d 100644 --- a/packages/core-components/src/components/LinkButton/LinkButton.stories.tsx +++ b/packages/core-components/src/components/LinkButton/LinkButton.stories.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { LinkButton } from './LinkButton'; import { useLocation } from 'react-router-dom'; import { createRouteRef, useRouteRef } from '@backstage/core-plugin-api'; @@ -39,7 +39,7 @@ export default { title: 'Inputs/Button', component: LinkButton, decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp( <> diff --git a/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx index d244ef7715..fca963eb74 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx @@ -14,14 +14,16 @@ * limitations under the License. */ -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { wrapInTestApp } from '@backstage/test-utils'; import { LogViewer } from './LogViewer'; export default { title: 'Data Display/LogViewer', component: LogViewer, - decorators: [(Story: ComponentType<{}>) => wrapInTestApp()], + decorators: [ + (Story: ComponentType>) => wrapInTestApp(), + ], }; const exampleLog = `Starting up task with 3 steps diff --git a/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx b/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx index 8daf7b69b6..f647166250 100644 --- a/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx +++ b/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx @@ -23,7 +23,7 @@ import MenuBookIcon from '@material-ui/icons/MenuBook'; import CloudQueueIcon from '@material-ui/icons/CloudQueue'; import AcUnitIcon from '@material-ui/icons/AcUnit'; import AppsIcon from '@material-ui/icons/Apps'; -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { SidebarPage } from './Page'; import { Sidebar } from './Bar'; import { SidebarGroup } from './SidebarGroup'; @@ -46,7 +46,7 @@ export default { title: 'Layout/Sidebar', component: Sidebar, decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp(, { mountedRoutes: { '/': routeRef } }), ], }; diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 52ee47f6bb..227b9b9816 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -12,6 +12,7 @@ import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api'; import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api'; import { JsonValue } from '@backstage/types'; import { Observable } from '@backstage/types'; +import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; @@ -137,14 +138,16 @@ export type ApiRefConfig = { // @public export type AppComponents = { - NotFoundErrorPage: ComponentType<{}>; + NotFoundErrorPage: ComponentType>; BootErrorPage: ComponentType; - Progress: ComponentType<{}>; - Router: ComponentType<{ - basename?: string; - }>; + Progress: ComponentType>; + Router: ComponentType< + PropsWithChildren<{ + basename?: string; + }> + >; ErrorBoundaryFallback: ComponentType; - ThemeProvider?: ComponentType<{}>; + ThemeProvider?: ComponentType>; SignInPage?: ComponentType; }; @@ -247,10 +250,10 @@ export const bitbucketServerAuthApiRef: ApiRef< >; // @public -export type BootErrorPageProps = { +export type BootErrorPageProps = PropsWithChildren<{ step: 'load-config' | 'load-chunk'; error: Error; -}; +}>; // @public export type CommonAnalyticsContext = { @@ -405,11 +408,11 @@ export type ErrorApiErrorContext = { export const errorApiRef: ApiRef; // @public -export type ErrorBoundaryFallbackProps = { +export type ErrorBoundaryFallbackProps = PropsWithChildren<{ plugin?: BackstagePlugin_2; error: Error; resetError: () => void; -}; +}>; // @public export type Extension = { @@ -690,9 +693,9 @@ export enum SessionState { } // @public -export type SignInPageProps = { +export type SignInPageProps = PropsWithChildren<{ onSignInSuccess(identityApi: IdentityApi_2): void; -}; +}>; // @public export interface StorageApi { 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 056402f2a8..8d62f29c52 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 @@ -40,12 +40,14 @@ const app = createApp({ bind(catalogPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, viewTechDoc: techdocsPlugin.routes.docRoot, + createFromTemplate: scaffolderPlugin.routes.selectedTemplate, }); bind(apiDocsPlugin.externalRoutes, { registerApi: catalogImportPlugin.routes.importPage, }); bind(scaffolderPlugin.externalRoutes, { registerComponent: catalogImportPlugin.routes.importPage, + viewTechDoc: techdocsPlugin.routes.docRoot, }); bind(orgPlugin.externalRoutes, { catalogIndex: catalogPlugin.routes.catalogIndex, diff --git a/packages/dev-utils/api-report.md b/packages/dev-utils/api-report.md index e1cb93313f..7ff48db175 100644 --- a/packages/dev-utils/api-report.md +++ b/packages/dev-utils/api-report.md @@ -12,6 +12,7 @@ import { ComponentType } from 'react'; import { Entity } from '@backstage/catalog-model'; import { GridProps } from '@material-ui/core'; import { IconComponent } from '@backstage/core-plugin-api'; +import { PropsWithChildren } from 'react'; import { ReactNode } from 'react'; // @public @@ -22,7 +23,7 @@ export class DevAppBuilder { addPage(opts: DevAppPageOptions): DevAppBuilder; addRootChild(node: ReactNode): DevAppBuilder; addThemes(themes: AppTheme[]): this; - build(): ComponentType<{}>; + build(): ComponentType>; registerApi< Api, Impl extends Api, diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index d2c91c9394..36e87855ed 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -44,7 +44,7 @@ import { } from '@backstage/integration-react'; import { Box } from '@material-ui/core'; import BookmarkIcon from '@material-ui/icons/Bookmark'; -import React, { ComponentType, ReactNode } from 'react'; +import React, { ComponentType, ReactNode, PropsWithChildren } from 'react'; import ReactDOM from 'react-dom'; import { createRoutesFromChildren, Route } from 'react-router-dom'; import { SidebarThemeSwitcher } from './SidebarThemeSwitcher'; @@ -164,7 +164,7 @@ export class DevAppBuilder { /** * Build a DevApp component using the resources registered so far */ - build(): ComponentType<{}> { + build(): ComponentType> { const fakeRouteRef = createRouteRef({ id: 'fake' }); const FakePage = () => Page belonging to another plugin.; attachComponentData(FakePage, 'core.mountPoint', fakeRouteRef); diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx index e2938dc9fc..eda3597225 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx @@ -30,7 +30,7 @@ describe('', () => { const apiDocsConfig: jest.Mocked = { getApiDefinitionWidget: jest.fn(), } as any; - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiTypeTitle.test.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiTypeTitle.test.tsx index 5476857a06..d249e59aeb 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiTypeTitle.test.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiTypeTitle.test.tsx @@ -24,7 +24,7 @@ describe('', () => { const apiDocsConfig: jest.Mocked = { getApiDefinitionWidget: jest.fn(), } as any; - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx index afb836ecc9..ea51d3989d 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx @@ -39,7 +39,7 @@ describe('', () => { getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx index 85b353f04e..6ab55c5c66 100644 --- a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx @@ -39,7 +39,7 @@ describe('', () => { getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx index 40ffeff330..5f69e90f9f 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx @@ -39,7 +39,7 @@ describe('', () => { getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx index abc91d8793..f7a18cf671 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx @@ -35,7 +35,7 @@ describe('', () => { getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx index bdd45c3d58..8b003bf2e2 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx @@ -35,7 +35,7 @@ describe('', () => { getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx index bee5587ea1..d50fdc964b 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { EntityPeekAheadPopover, EntityPeekAheadPopoverProps, @@ -80,7 +80,7 @@ const defaultArgs = { export default { title: 'Catalog /PeekAheadPopover', decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp( <> ) => + (Story: ComponentType>) => wrapInTestApp(, { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.stories.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.stories.tsx index 176dd24530..7b94bd14df 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.stories.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { EntityRefLinks, EntityRefLinksProps } from './EntityRefLinks'; import { wrapInTestApp } from '@backstage/test-utils'; import { CompoundEntityRef } from '@backstage/catalog-model'; @@ -27,7 +27,7 @@ const defaultArgs = { export default { title: 'Catalog /EntityRefLinks', decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp(, { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, diff --git a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx index eccad8e17f..f21b3cb710 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx @@ -27,7 +27,7 @@ import { useStarredEntities } from './useStarredEntities'; describe('useStarredEntities', () => { let mockApi: StarredEntitiesApi; - let wrapper: React.ComponentType; + let wrapper: React.ComponentType>; const mockEntity: Entity = { apiVersion: '1', diff --git a/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx index ad168e5f9d..d24b27f903 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx @@ -27,7 +27,7 @@ describe('useStarredEntity', () => { toggleStarred: jest.fn(), starredEntitie$: jest.fn(), }; - let wrapper: React.ComponentType; + let wrapper: React.ComponentType>; beforeEach(() => { wrapper = (props: PropsWithChildren<{}>) => ( diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 970d8d3251..6bb4fcc4a1 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -105,6 +105,13 @@ export const catalogPlugin: BackstagePlugin< }, true >; + createFromTemplate: ExternalRouteRef< + { + namespace: string; + templateName: string; + }, + true + >; }, CatalogInputPluginOptions >; diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index e76be49515..74f72e3631 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -40,6 +40,7 @@ "@backstage/integration-react": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "@backstage/plugin-search-react": "workspace:^", "@backstage/theme": "workspace:^", diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index 869d6f1757..e8d2657e9f 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -30,7 +30,7 @@ import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { screen } from '@testing-library/react'; import React from 'react'; -import { viewTechDocRouteRef } from '../../routes'; +import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; import { AboutCard } from './AboutCard'; describe('', () => { @@ -505,4 +505,165 @@ describe('', () => { expect(screen.getByText('View TechDocs')).toBeVisible(); expect(screen.getByText('View TechDocs').closest('a')).toBeNull(); }); + + it('renders launch template link', async () => { + const entity = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'create-react-app-template', + namespace: 'default', + }, + }; + + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + '/create/templates/:namespace/:templateName': + createFromTemplateRouteRef, + }, + }, + ); + + expect(screen.getByText('Launch Template')).toBeVisible(); + expect(screen.getByText('Launch Template').closest('a')).toHaveAttribute( + 'href', + '/create/templates/default/create-react-app-template', + ); + }); + + it.each([ + { + testName: 'entity is not a template', + entity: { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Component', + metadata: { + name: 'create-react-app-template', + namespace: 'default', + }, + }, + }, + { + testName: 'apiVersion is not scaffolder.backstage.io/v1beta3', + entity: { + apiVersion: 'v1', + kind: 'Template', + metadata: { + name: 'create-react-app-template', + namespace: 'default', + }, + }, + }, + ])( + 'should not render launch template link when $testName', + async ({ entity }) => { + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + '/create/templates/:namespace/:templateName': + createFromTemplateRouteRef, + }, + }, + ); + + expect(screen.queryByText('Launch Template')).toBeNull(); + }, + ); + + it('renders disabled launch template link when route is not bound', async () => { + const entity = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'create-react-app-template', + namespace: 'default', + }, + }; + + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(screen.getByText('Launch Template')).toBeVisible(); + expect(screen.getByText('Launch Template').closest('a')).toBeNull(); + }); }); diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index cb48be2b26..a08a9be5a4 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { ANNOTATION_EDIT_URL, ANNOTATION_LOCATION, @@ -30,6 +29,7 @@ import { alertApiRef, errorApiRef, useApi, + useApp, useRouteRef, } from '@backstage/core-plugin-api'; import { @@ -41,6 +41,7 @@ import { getEntitySourceLocation, useEntity, } from '@backstage/plugin-catalog-react'; +import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { Card, CardContent, @@ -49,11 +50,13 @@ import { IconButton, makeStyles, } from '@material-ui/core'; +import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import CachedIcon from '@material-ui/icons/Cached'; import DocsIcon from '@material-ui/icons/Description'; import EditIcon from '@material-ui/icons/Edit'; import React, { useCallback } from 'react'; -import { viewTechDocRouteRef } from '../../routes'; + +import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; import { AboutContent } from './AboutContent'; const useStyles = makeStyles({ @@ -90,6 +93,7 @@ export interface AboutCardProps { */ export function AboutCard(props: AboutCardProps) { const { variant } = props; + const app = useApp(); const classes = useStyles(); const { entity } = useEntity(); const scmIntegrationsApi = useApi(scmIntegrationsApiRef); @@ -97,6 +101,7 @@ export function AboutCard(props: AboutCardProps) { const alertApi = useApi(alertApiRef); const errorApi = useApi(errorApiRef); const viewTechdocLink = useRouteRef(viewTechDocRouteRef); + const templateRoute = useRouteRef(createFromTemplateRouteRef); const entitySourceLocation = getEntitySourceLocation( entity, @@ -126,6 +131,26 @@ export function AboutCard(props: AboutCardProps) { }), }; + const subHeaderLinks = [viewInSource, viewInTechDocs]; + + if (isTemplateEntityV1beta3(entity)) { + const Icon = app.getSystemIcon('scaffolder') ?? CreateComponentIcon; + + const launchTemplate: IconLinkVerticalProps = { + label: 'Launch Template', + icon: , + disabled: !templateRoute, + href: + templateRoute && + templateRoute({ + templateName: entity.metadata.name, + namespace: entity.metadata.namespace || DEFAULT_NAMESPACE, + }), + }; + + subHeaderLinks.push(launchTemplate); + } + let cardClass = ''; if (variant === 'gridItem') { cardClass = classes.gridItemCard; @@ -179,7 +204,7 @@ export function AboutCard(props: AboutCardProps) { } - subheader={} + subheader={} /> diff --git a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx index 94a9a86b70..e5900d75e0 100644 --- a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx +++ b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx @@ -28,7 +28,7 @@ import { DependencyOfComponentsCard } from './DependencyOfComponentsCard'; describe('', () => { const getEntities: jest.MockedFunction = jest.fn(); - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx index 657f95c60f..9255ee4f36 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx +++ b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx @@ -28,7 +28,7 @@ import { DependsOnComponentsCard } from './DependsOnComponentsCard'; describe('', () => { const getEntities: jest.MockedFunction = jest.fn(); - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx index 5a77cd6ab4..3b0d7dcff3 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx +++ b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx @@ -28,7 +28,7 @@ import { DependsOnResourcesCard } from './DependsOnResourcesCard'; describe('', () => { const getEntities: jest.MockedFunction = jest.fn(); - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx index 9d46f2ed42..3651421797 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx @@ -28,7 +28,7 @@ import { HasComponentsCard } from './HasComponentsCard'; describe('', () => { const getEntities: jest.MockedFunction = jest.fn(); - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx index 364d503a96..4bf6524a9c 100644 --- a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx +++ b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx @@ -28,7 +28,7 @@ import { HasResourcesCard } from './HasResourcesCard'; describe('', () => { const getEntities: jest.MockedFunction = jest.fn(); - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx index b76a0a68ec..97e69e93d1 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx @@ -28,7 +28,7 @@ import { HasSubcomponentsCard } from './HasSubcomponentsCard'; describe('', () => { const getEntities: jest.MockedFunction = jest.fn(); - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx index 634fe57e82..224e0c6703 100644 --- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx @@ -28,7 +28,7 @@ import { HasSystemsCard } from './HasSystemsCard'; describe('', () => { const getEntities: jest.MockedFunction = jest.fn(); - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index ea6ddcaff2..711f52767a 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -21,7 +21,11 @@ import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { createComponentRouteRef, viewTechDocRouteRef } from './routes'; +import { + createComponentRouteRef, + createFromTemplateRouteRef, + viewTechDocRouteRef, +} from './routes'; import { createApiFactory, createComponentExtension, @@ -77,6 +81,7 @@ export const catalogPlugin = createPlugin({ externalRoutes: { createComponent: createComponentRouteRef, viewTechDoc: viewTechDocRouteRef, + createFromTemplate: createFromTemplateRouteRef, }, __experimentalConfigure( options?: CatalogInputPluginOptions, diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 5c0d195d74..ab70a3c551 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -30,6 +30,12 @@ export const viewTechDocRouteRef = createExternalRouteRef({ params: ['namespace', 'kind', 'name'], }); +export const createFromTemplateRouteRef = createExternalRouteRef({ + id: 'create-from-template', + optional: true, + params: ['namespace', 'templateName'], +}); + export const rootRouteRef = createRouteRef({ id: 'catalog', }); diff --git a/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx b/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx index 046e3e9860..52ddaa0f4e 100644 --- a/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx +++ b/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx @@ -26,7 +26,7 @@ describe('', () => { getFindingSummary: jest.fn(), getFindingSummaries: jest.fn(), }; - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx index be07b4707f..ec78ac906b 100644 --- a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx +++ b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx @@ -38,7 +38,7 @@ describe('', () => { getFindingSummary: jest.fn(), getFindingSummaries: jest.fn(), }; - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/home/README.md b/plugins/home/README.md index 42fbf73d18..8bb474aa4b 100644 --- a/plugins/home/README.md +++ b/plugins/home/README.md @@ -22,11 +22,9 @@ yarn add --cwd packages/app @backstage/plugin-home ```tsx import React from 'react'; -export const HomePage = () => { - return { - /* TODO: Compose a Home Page here */ - }; -}; +export const homePage = ( + /* TODO: Compose a Home Page here */ +); ``` 2. Add a route where the homepage will live, presumably `/`. @@ -35,11 +33,11 @@ export const HomePage = () => { ```tsx import { HomepageCompositionRoot } from '@backstage/plugin-home'; -import { HomePage } from './components/home/HomePage'; +import { homePage } from './components/home/HomePage'; // ... }> - + {homePage} ; // ... ``` @@ -72,15 +70,13 @@ import React from 'react'; import Grid from '@material-ui/core/Grid'; import { RandomJokeHomePageComponent } from '@backstage/plugin-home'; -export const HomePage = () => { - return ( - - - - +export const homePage = ( + + + - ); -}; + +); ``` Additionally, the App Integrator is provided an escape hatch in case the way the card is rendered does not fit their requirements. They may optionally pass the `Renderer`-prop, which will receive the `title`, `content` and optionally `actions`, `settings` and `contextProvider`, if they exist for the component. This allows the App Integrator to render the content in any way they want. @@ -102,18 +98,16 @@ import { HomePageSearchBar } from '@backstage/plugin-search'; import { HomePageCalendar } from '@backstage/plugin-gcalendar'; import { MicrosoftCalendarCard } from '@backstage/plugin-microsoft-calendar'; -export const HomePage = () => { - return ( - - // Insert the allowed widgets inside the grid - - - - - - - ); -}; +export const homePage = ( + + // Insert the allowed widgets inside the grid + + + + + + +); ``` ### Creating Customizable Components diff --git a/plugins/home/src/homePageComponents/CompanyLogo/CompanyLogo.stories.tsx b/plugins/home/src/homePageComponents/CompanyLogo/CompanyLogo.stories.tsx index 741ad2e977..da5c318af2 100644 --- a/plugins/home/src/homePageComponents/CompanyLogo/CompanyLogo.stories.tsx +++ b/plugins/home/src/homePageComponents/CompanyLogo/CompanyLogo.stories.tsx @@ -21,12 +21,12 @@ import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; import { configApiRef } from '@backstage/core-plugin-api'; import { ConfigReader } from '@backstage/core-app-api'; import { Grid, makeStyles } from '@material-ui/core'; -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; export default { title: 'Plugins/Home/Components/CompanyLogo', decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp( ) => wrapInTestApp()], + decorators: [ + (Story: ComponentType>) => wrapInTestApp(), + ], }; export const Default = () => { diff --git a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx index a5e05e56c1..8172041f94 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx @@ -23,7 +23,7 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { Grid } from '@material-ui/core'; -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; const starredEntitiesApi = new MockStarredEntitiesApi(); starredEntitiesApi.toggleStarred('component:default/example-starred-entity'); @@ -73,7 +73,7 @@ const mockCatalogApi = { export default { title: 'Plugins/Home/Components/StarredEntities', decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp( ) => wrapInTestApp()], + decorators: [ + (Story: ComponentType>) => wrapInTestApp(), + ], }; export const Default = () => { diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.stories.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.stories.tsx index ddcce2fe41..d7d12f02c6 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.stories.tsx +++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.stories.tsx @@ -16,12 +16,14 @@ import { Header } from '@backstage/core-components'; import { wrapInTestApp } from '@backstage/test-utils'; -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { WelcomeTitle } from './WelcomeTitle'; export default { title: 'Plugins/Home/Components/WelcomeTitle', - decorators: [(Story: ComponentType<{}>) => wrapInTestApp()], + decorators: [ + (Story: ComponentType>) => wrapInTestApp(), + ], }; export const Default = () => { diff --git a/plugins/jenkins-backend/config.d.ts b/plugins/jenkins-backend/config.d.ts index 3dda0a73c5..5b725dd801 100644 --- a/plugins/jenkins-backend/config.d.ts +++ b/plugins/jenkins-backend/config.d.ts @@ -38,11 +38,17 @@ export interface Config { * instance. */ name: string; - baseUrl: string; username: string; /** @visibility secret */ apiKey: string; + extraRequestHeaders?: Partial<{ + /** @visibility secret */ + Authorization: string; + /** @visibility secret */ + authorization: string; + [key: string]: string; + }>; }[]; }; } diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx index 526d7c9c4c..e10d6322e0 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx @@ -23,7 +23,7 @@ import { } from '@backstage/plugin-catalog-react'; import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; import { Grid } from '@material-ui/core'; -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { GroupProfileCard } from './GroupProfileCard'; const dummyDepartment = { @@ -74,7 +74,7 @@ export default { title: 'Plugins/Org/Group Profile Card', component: GroupProfileCard, decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp( diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx index a27be7857b..02598691e0 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx @@ -21,7 +21,7 @@ import { } from '@backstage/plugin-catalog-react'; import { wrapInTestApp } from '@backstage/test-utils'; import { Grid } from '@material-ui/core'; -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { UserProfileCard } from './UserProfileCard'; const dummyGroup = { @@ -94,7 +94,7 @@ export default { title: 'Plugins/Org/User Profile Card', component: UserProfileCard, decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp(, { mountedRoutes: { '/a': entityRouteRef, diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md index 2affe1c423..c0ba5c8fe0 100644 --- a/plugins/pagerduty/README.md +++ b/plugins/pagerduty/README.md @@ -95,6 +95,30 @@ annotations: pagerduty.com/integration-key: [INTEGRATION_KEY] ``` +### The homepage component + +You may also add the component to the homepage of Backstage. Edit the `HomePage.tsx` file, import `PagerDutyHomepageCard` and add it to the home page. e.g. + +```typescript jsx +... +export const homePage = ( + + ... + + + ... + + + + +); +``` + Next, provide the [API token](https://support.pagerduty.com/docs/generating-api-keys#generating-a-general-access-rest-api-key) that the client will use to make requests to the [PagerDuty API](https://developer.pagerduty.com/docs/rest-api-v2/rest-api/). Add the proxy configuration in `app-config.yaml`: diff --git a/plugins/pagerduty/api-report.md b/plugins/pagerduty/api-report.md index b460086976..dd24aa9bec 100644 --- a/plugins/pagerduty/api-report.md +++ b/plugins/pagerduty/api-report.md @@ -7,6 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { CardExtensionProps } from '@backstage/plugin-home-react'; import { ConfigApi } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; @@ -14,7 +15,24 @@ import { FetchApi } from '@backstage/core-plugin-api'; import { ReactNode } from 'react'; // @public (undocumented) -export const EntityPagerDutyCard: (props: PagerDutyCardProps) => JSX.Element; +export const EntityPagerDutyCard: ( + props: EntityPagerDutyCardProps, +) => JSX.Element; + +// @public (undocumented) +export type EntityPagerDutyCardProps = { + readOnly?: boolean; +}; + +// @public (undocumented) +export const HomePagePagerDutyCard: ( + props: CardExtensionProps, +) => JSX.Element; + +// @public (undocumented) +export type HomePagePagerDutyCardProps = PagerDutyEntity & { + readOnly?: boolean; +}; // @public (undocumented) const isPluginApplicableToEntity: (entity: Entity) => boolean; @@ -31,6 +49,9 @@ export interface PagerDutyApi { ): Promise; getOnCallByPolicyId(policyId: string): Promise; getServiceByEntity(entity: Entity): Promise; + getServiceByPagerDutyEntity( + pagerDutyEntity: PagerDutyEntity, + ): Promise; triggerAlarm(request: PagerDutyTriggerAlarmRequest): Promise; } @@ -44,13 +65,11 @@ export type PagerDutyAssignee = { html_url: string; }; -// @public (undocumented) -export const PagerDutyCard: (props: PagerDutyCardProps) => JSX.Element; +// @public @deprecated (undocumented) +export const PagerDutyCard: (props: EntityPagerDutyCardProps) => JSX.Element; -// @public (undocumented) -export type PagerDutyCardProps = { - readOnly?: boolean; -}; +// @public @deprecated (undocumented) +export type PagerDutyCardProps = EntityPagerDutyCardProps; // @public (undocumented) export type PagerDutyChangeEvent = { @@ -98,6 +117,10 @@ export class PagerDutyClient implements PagerDutyApi { // (undocumented) getServiceByEntity(entity: Entity): Promise; // (undocumented) + getServiceByPagerDutyEntity( + pagerDutyEntity: PagerDutyEntity, + ): Promise; + // (undocumented) triggerAlarm(request: PagerDutyTriggerAlarmRequest): Promise; } diff --git a/plugins/pagerduty/dev/index.tsx b/plugins/pagerduty/dev/index.tsx index e45e0ffd8f..4ed8e07263 100644 --- a/plugins/pagerduty/dev/index.tsx +++ b/plugins/pagerduty/dev/index.tsx @@ -15,136 +15,12 @@ */ import React from 'react'; -import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { createDevApp } from '@backstage/dev-utils'; import { pagerDutyPlugin, EntityPagerDutyCard } from '../src/plugin'; import { pagerDutyApiRef } from '../src/api'; -import { PagerDutyApi, PagerDutyTriggerAlarmRequest } from '../src/api/types'; -import { - PagerDutyIncident, - PagerDutyChangeEvent, -} from '../src/components/types'; - -const mockEntity: Entity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'backstage', - description: 'backstage.io', - annotations: { - 'github.com/project-slug': 'backstage/backstage', - 'pagerduty.com/service-id': 'foo', - 'pagerduty.com/integration-key': 'foo', - }, - }, - spec: { - lifecycle: 'production', - type: 'website', - owner: 'user:guest', - }, -}; - -const mockPagerDutyApi: PagerDutyApi = { - async getServiceByEntity(entity: Entity) { - return { - service: { - name: entity.metadata.name, - integrationKey: 'key', - id: '123', - html_url: 'http://service', - escalation_policy: { - id: '123', - html_url: 'http://escalationpolicy', - user: { - id: '123', - summary: 'summary', - email: 'email@email.com', - html_url: 'http://user', - name: 'some-user', - }, - }, - }, - }; - }, - - async getIncidentsByServiceId(serviceId: string) { - const incident = (title: string) => { - return { - id: '123', - title: title, - status: 'acknowledged', - html_url: 'http://incident', - assignments: [ - { - assignee: { - id: '123', - summary: 'Jane Doe', - html_url: 'http://assignee', - }, - }, - ], - serviceId: serviceId, - created_at: '2015-10-06T21:30:42Z', - } as PagerDutyIncident; - }; - - return { - incidents: [ - incident('Some Alerting Incident'), - incident('Another Alerting Incident'), - ], - }; - }, - - async getChangeEventsByServiceId(serviceId: string) { - const changeEvent = (description: string) => { - return { - id: serviceId, - source: 'some-source', - html_url: 'http://changeevent', - links: [ - { - href: 'http://link', - text: 'link text', - }, - ], - summary: description, - timestamp: '2018-10-06T21:30:42Z', - } as PagerDutyChangeEvent; - }; - - return { - change_events: [ - changeEvent('us-east-1 deployment'), - changeEvent('us-west-2 deployment'), - ], - }; - }, - - async getOnCallByPolicyId() { - const oncall = (name: string, escalation: number) => { - return { - user: { - id: '123', - name: name, - html_url: 'http://assignee', - summary: 'summary', - email: 'email@email.com', - }, - escalation_level: escalation, - }; - }; - - return { - oncalls: [oncall('Jane Doe', 1), oncall('John Doe', 2)], - }; - }, - - async triggerAlarm(request: PagerDutyTriggerAlarmRequest) { - return new Response(request.description); - }, -}; +import { mockPagerDutyApi } from './mockPagerDutyApi'; +import { mockEntity } from './mockEntity'; createDevApp() .registerApi({ diff --git a/plugins/pagerduty/dev/mockEntity.ts b/plugins/pagerduty/dev/mockEntity.ts new file mode 100644 index 0000000000..251ba80706 --- /dev/null +++ b/plugins/pagerduty/dev/mockEntity.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Entity } from '@backstage/catalog-model'; + +export const mockEntity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'backstage', + description: 'backstage.io', + annotations: { + 'github.com/project-slug': 'backstage/backstage', + 'pagerduty.com/service-id': 'foo', + 'pagerduty.com/integration-key': 'foo', + }, + }, + spec: { + lifecycle: 'production', + type: 'website', + owner: 'user:guest', + }, +}; diff --git a/plugins/pagerduty/dev/mockPagerDutyApi.ts b/plugins/pagerduty/dev/mockPagerDutyApi.ts new file mode 100644 index 0000000000..f8836d7043 --- /dev/null +++ b/plugins/pagerduty/dev/mockPagerDutyApi.ts @@ -0,0 +1,146 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + PagerDutyApi, + PagerDutyChangeEvent, + PagerDutyEntity, + PagerDutyIncident, + PagerDutyTriggerAlarmRequest, +} from '../src'; +import { Entity } from '@backstage/catalog-model'; + +export const mockPagerDutyApi: PagerDutyApi = { + async getServiceByPagerDutyEntity(pagerDutyEntity: PagerDutyEntity) { + return { + service: { + name: pagerDutyEntity.name, + integrationKey: 'key', + id: '123', + html_url: 'http://service', + escalation_policy: { + id: '123', + html_url: 'http://escalationpolicy', + user: { + id: '123', + summary: 'summary', + email: 'email@email.com', + html_url: 'http://user', + name: 'some-user', + }, + }, + }, + }; + }, + + async getServiceByEntity(entity: Entity) { + return { + service: { + name: entity.metadata.name, + integrationKey: 'key', + id: '123', + html_url: 'http://service', + escalation_policy: { + id: '123', + html_url: 'http://escalationpolicy', + user: { + id: '123', + summary: 'summary', + email: 'email@email.com', + html_url: 'http://user', + name: 'some-user', + }, + }, + }, + }; + }, + + async getIncidentsByServiceId(serviceId: string) { + const incident = (title: string) => { + return { + id: '123', + title: title, + status: 'acknowledged', + html_url: 'http://incident', + assignments: [ + { + assignee: { + id: '123', + summary: 'Jane Doe', + html_url: 'http://assignee', + }, + }, + ], + serviceId: serviceId, + created_at: '2015-10-06T21:30:42Z', + } as PagerDutyIncident; + }; + + return { + incidents: [ + incident('Some Alerting Incident'), + incident('Another Alerting Incident'), + ], + }; + }, + + async getChangeEventsByServiceId(serviceId: string) { + const changeEvent = (description: string) => { + return { + id: serviceId, + source: 'some-source', + html_url: 'http://changeevent', + links: [ + { + href: 'http://link', + text: 'link text', + }, + ], + summary: description, + timestamp: '2018-10-06T21:30:42Z', + } as PagerDutyChangeEvent; + }; + + return { + change_events: [ + changeEvent('us-east-1 deployment'), + changeEvent('us-west-2 deployment'), + ], + }; + }, + + async getOnCallByPolicyId() { + const oncall = (name: string, escalation: number) => { + return { + user: { + id: '123', + name: name, + html_url: 'http://assignee', + summary: 'summary', + email: 'email@email.com', + }, + escalation_level: escalation, + }; + }; + + return { + oncalls: [oncall('Jane Doe', 1), oncall('John Doe', 2)], + }; + }, + + async triggerAlarm(request: PagerDutyTriggerAlarmRequest) { + return new Response(request.description); + }, +}; diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 5437764465..dc40d566ce 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -38,6 +38,7 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-home-react": "workspace:^", "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 7d99be219d..350b838951 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -30,6 +30,7 @@ import { createApiRef, ConfigApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; import { Entity } from '@backstage/catalog-model'; import { getPagerDutyEntity } from '../components/pagerDutyEntity'; +import { PagerDutyEntity } from '../types'; /** @public */ export class UnauthorizedError extends Error {} @@ -63,8 +64,10 @@ export class PagerDutyClient implements PagerDutyApi { constructor(private readonly config: PagerDutyClientApiConfig) {} - async getServiceByEntity(entity: Entity): Promise { - const { integrationKey, serviceId } = getPagerDutyEntity(entity); + async getServiceByPagerDutyEntity( + pagerDutyEntity: PagerDutyEntity, + ): Promise { + const { integrationKey, serviceId } = pagerDutyEntity; let response: PagerDutyServiceResponse; let url: string; @@ -92,6 +95,9 @@ export class PagerDutyClient implements PagerDutyApi { return response; } + async getServiceByEntity(entity: Entity): Promise { + return await this.getServiceByPagerDutyEntity(getPagerDutyEntity(entity)); + } async getIncidentsByServiceId( serviceId: string, ): Promise { diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index f1971d53d5..a878afc447 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -22,6 +22,7 @@ import { } from '../components/types'; import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { PagerDutyEntity } from '../types'; export type PagerDutyServicesResponse = { services: PagerDutyService[]; @@ -57,6 +58,14 @@ export type PagerDutyTriggerAlarmRequest = { /** @public */ export interface PagerDutyApi { + /** + * Fetches the service for the provided pager duty Entity. + * + */ + getServiceByPagerDutyEntity( + pagerDutyEntity: PagerDutyEntity, + ): Promise; + /** * Fetches the service for the provided Entity. * diff --git a/plugins/pagerduty/src/components/EntityPagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/EntityPagerDutyCard/index.test.tsx new file mode 100644 index 0000000000..3f3a4ca1a8 --- /dev/null +++ b/plugins/pagerduty/src/components/EntityPagerDutyCard/index.test.tsx @@ -0,0 +1,386 @@ +/* + * 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 from 'react'; +import { render, waitFor, fireEvent, act } from '@testing-library/react'; +import { + EntityPagerDutyCard, + isPluginApplicableToEntity, +} from '../EntityPagerDutyCard'; +import { Entity } from '@backstage/catalog-model'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { NotFoundError } from '@backstage/errors'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; +import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../../api'; +import { PagerDutyService, PagerDutyUser } from '../types'; + +import { alertApiRef } from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; + +const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: { + 'pagerduty.com/integration-key': 'abc123', + }, + }, +}; + +const entityWithoutAnnotations: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: {}, + }, +}; + +const entityWithServiceId: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: { + 'pagerduty.com/service-id': 'def456', + }, + }, +}; + +const entityWithAllAnnotations: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: { + 'pagerduty.com/integration-key': 'abc123', + 'pagerduty.com/service-id': 'def456', + }, + }, +}; + +const user: PagerDutyUser = { + name: 'person1', + id: 'p1', + summary: 'person1', + email: 'person1@example.com', + html_url: 'http://a.com/id1', +}; + +const service: PagerDutyService = { + id: 'def456', + name: 'pagerduty-name', + html_url: 'www.example.com', + escalation_policy: { + id: 'def', + user: user, + html_url: 'http://a.com/id1', + }, + integrationKey: 'abc123', +}; + +const mockPagerDutyApi: Partial = { + getServiceByEntity: async () => ({ service }), + getServiceByPagerDutyEntity: async () => ({ service }), + getOnCallByPolicyId: async () => ({ oncalls: [] }), + getIncidentsByServiceId: async () => ({ incidents: [] }), +}; + +const apis = TestApiRegistry.from( + [pagerDutyApiRef, mockPagerDutyApi], + [alertApiRef, {}], +); + +describe('isPluginApplicableToEntity', () => { + describe('when entity has no annotations', () => { + it('returns false', () => { + expect(isPluginApplicableToEntity(entityWithoutAnnotations)).toBe(false); + }); + }); + + describe('when entity has the pagerduty.com/integration-key annotation', () => { + it('returns true', () => { + expect(isPluginApplicableToEntity(entity)).toBe(true); + }); + }); + + describe('when entity has the pagerduty.com/service-id annotation', () => { + it('returns true', () => { + expect(isPluginApplicableToEntity(entityWithServiceId)).toBe(true); + }); + }); + + describe('when entity has all annotations', () => { + it('returns true', () => { + expect(isPluginApplicableToEntity(entityWithAllAnnotations)).toBe(true); + }); + }); +}); + +describe('EntityPagerDutyCard', () => { + it('Render pagerduty', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Service Directory')).toBeInTheDocument(); + expect(getByText('Create Incident')).toBeInTheDocument(); + expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + expect(getByText('Empty escalation policy')).toBeInTheDocument(); + }); + + it('Handles custom error for missing token', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new UnauthorizedError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Missing or invalid PagerDuty Token')).toBeInTheDocument(); + }); + + it('Handles custom NotFoundError', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new NotFoundError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('PagerDuty Service Not Found')).toBeInTheDocument(); + }); + + it('handles general error', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new Error('An error occurred')); + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + + expect( + getByText( + 'Error encountered while fetching information. An error occurred', + ), + ).toBeInTheDocument(); + }); + + it('opens the dialog when trigger button is clicked', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId, getByRole } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Service Directory')).toBeInTheDocument(); + + const triggerLink = getByText('Create Incident'); + await act(async () => { + fireEvent.click(triggerLink); + }); + expect(getByRole('dialog')).toBeInTheDocument(); + }); + + describe('when entity has the pagerduty.com/service-id annotation', () => { + it('Renders PagerDuty service information', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Service Directory')).toBeInTheDocument(); + expect(getByText('Create Incident')).toBeInTheDocument(); + expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + expect(getByText('Empty escalation policy')).toBeInTheDocument(); + }); + + it('Handles custom error for missing token', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new UnauthorizedError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect( + getByText('Missing or invalid PagerDuty Token'), + ).toBeInTheDocument(); + }); + + it('Handles custom NotFoundError', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new NotFoundError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('PagerDuty Service Not Found')).toBeInTheDocument(); + }); + + it('handles general error', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new Error('An error occurred')); + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + + expect( + getByText( + 'Error encountered while fetching information. An error occurred', + ), + ).toBeInTheDocument(); + }); + + it('disables the Create Incident button', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { queryByTestId, getByTitle } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect( + getByTitle('Must provide an integration-key to create incidents') + .className, + ).toMatch('disabled'); + }); + }); + + describe('when entity has all annotations', () => { + it('queries by integration key', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Service Directory')).toBeInTheDocument(); + expect(getByText('Create Incident')).toBeInTheDocument(); + expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + expect(getByText('Empty escalation policy')).toBeInTheDocument(); + }); + }); + + describe('when entity has all annotations but the plugin has been configured to be "read only"', () => { + it('queries by integration key but does not render the "Create Incident" button', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Service Directory')).toBeInTheDocument(); + expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + expect(getByText('Empty escalation policy')).toBeInTheDocument(); + expect(() => getByText('Create Incident')).toThrow(); + }); + }); +}); diff --git a/plugins/pagerduty/src/components/EntityPagerDutyCard/index.tsx b/plugins/pagerduty/src/components/EntityPagerDutyCard/index.tsx new file mode 100644 index 0000000000..50e6f3a096 --- /dev/null +++ b/plugins/pagerduty/src/components/EntityPagerDutyCard/index.tsx @@ -0,0 +1,41 @@ +/* + * 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 from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID } from '../constants'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { getPagerDutyEntity } from '../pagerDutyEntity'; +import { PagerDutyCard } from '../PagerDutyCard'; + +/** @public */ +export const isPluginApplicableToEntity = (entity: Entity) => + Boolean( + entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY] || + entity.metadata.annotations?.[PAGERDUTY_SERVICE_ID], + ); + +/** @public */ +export type EntityPagerDutyCardProps = { + readOnly?: boolean; +}; + +/** @public */ +export const EntityPagerDutyCard = (props: EntityPagerDutyCardProps) => { + const { readOnly } = props; + const { entity } = useEntity(); + const pagerDutyEntity = getPagerDutyEntity(entity); + return ; +}; diff --git a/plugins/pagerduty/src/components/Errors/ServiceNotFoundError.tsx b/plugins/pagerduty/src/components/Errors/ServiceNotFoundError.tsx index d8b61ef7cd..236585735e 100644 --- a/plugins/pagerduty/src/components/Errors/ServiceNotFoundError.tsx +++ b/plugins/pagerduty/src/components/Errors/ServiceNotFoundError.tsx @@ -21,7 +21,7 @@ export const ServiceNotFoundError = () => ( { + return ; +}; diff --git a/plugins/pagerduty/src/components/HomePagePagerDutyCard/index.ts b/plugins/pagerduty/src/components/HomePagePagerDutyCard/index.ts new file mode 100644 index 0000000000..e9038852a6 --- /dev/null +++ b/plugins/pagerduty/src/components/HomePagePagerDutyCard/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { Content } from './Content'; +export type { HomePagePagerDutyCardProps } from './Content'; diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index 34c8914fde..6f4501450c 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -15,9 +15,7 @@ */ import React from 'react'; import { render, waitFor, fireEvent, act } from '@testing-library/react'; -import { PagerDutyCard, isPluginApplicableToEntity } from '../PagerDutyCard'; -import { Entity } from '@backstage/catalog-model'; -import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { PagerDutyCard } from '../PagerDutyCard'; import { NotFoundError } from '@backstage/errors'; import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../../api'; @@ -26,49 +24,6 @@ import { PagerDutyService, PagerDutyUser } from '../types'; import { alertApiRef } from '@backstage/core-plugin-api'; import { ApiProvider } from '@backstage/core-app-api'; -const entity: Entity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'pagerduty-test', - annotations: { - 'pagerduty.com/integration-key': 'abc123', - }, - }, -}; - -const entityWithoutAnnotations: Entity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'pagerduty-test', - annotations: {}, - }, -}; - -const entityWithServiceId: Entity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'pagerduty-test', - annotations: { - 'pagerduty.com/service-id': 'def456', - }, - }, -}; - -const entityWithAllAnnotations: Entity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'pagerduty-test', - annotations: { - 'pagerduty.com/integration-key': 'abc123', - 'pagerduty.com/service-id': 'def456', - }, - }, -}; - const user: PagerDutyUser = { name: 'person1', id: 'p1', @@ -100,44 +55,16 @@ const apis = TestApiRegistry.from( [alertApiRef, {}], ); -describe('isPluginApplicableToEntity', () => { - describe('when entity has no annotations', () => { - it('returns false', () => { - expect(isPluginApplicableToEntity(entityWithoutAnnotations)).toBe(false); - }); - }); - - describe('when entity has the pagerduty.com/integration-key annotation', () => { - it('returns true', () => { - expect(isPluginApplicableToEntity(entity)).toBe(true); - }); - }); - - describe('when entity has the pagerduty.com/service-id annotation', () => { - it('returns true', () => { - expect(isPluginApplicableToEntity(entityWithServiceId)).toBe(true); - }); - }); - - describe('when entity has all annotations', () => { - it('returns true', () => { - expect(isPluginApplicableToEntity(entityWithAllAnnotations)).toBe(true); - }); - }); -}); - -describe('PageDutyCard', () => { +describe('PagerDutyCard', () => { it('Render pagerduty', async () => { - mockPagerDutyApi.getServiceByEntity = jest + mockPagerDutyApi.getServiceByPagerDutyEntity = jest .fn() .mockImplementationOnce(async () => ({ service })); const { getByText, queryByTestId } = render( wrapInTestApp( - - - + , ), ); @@ -149,16 +76,14 @@ describe('PageDutyCard', () => { }); it('Handles custom error for missing token', async () => { - mockPagerDutyApi.getServiceByEntity = jest + mockPagerDutyApi.getServiceByPagerDutyEntity = jest .fn() .mockRejectedValueOnce(new UnauthorizedError()); const { getByText, queryByTestId } = render( wrapInTestApp( - - - + , ), ); @@ -167,16 +92,14 @@ describe('PageDutyCard', () => { }); it('Handles custom NotFoundError', async () => { - mockPagerDutyApi.getServiceByEntity = jest + mockPagerDutyApi.getServiceByPagerDutyEntity = jest .fn() .mockRejectedValueOnce(new NotFoundError()); const { getByText, queryByTestId } = render( wrapInTestApp( - - - + , ), ); @@ -185,15 +108,13 @@ describe('PageDutyCard', () => { }); it('handles general error', async () => { - mockPagerDutyApi.getServiceByEntity = jest + mockPagerDutyApi.getServiceByPagerDutyEntity = jest .fn() .mockRejectedValueOnce(new Error('An error occurred')); const { getByText, queryByTestId } = render( wrapInTestApp( - - - + , ), ); @@ -207,16 +128,14 @@ describe('PageDutyCard', () => { }); it('opens the dialog when trigger button is clicked', async () => { - mockPagerDutyApi.getServiceByEntity = jest + mockPagerDutyApi.getServiceByPagerDutyEntity = jest .fn() .mockImplementationOnce(async () => ({ service })); const { getByText, queryByTestId, getByRole } = render( wrapInTestApp( - - - + , ), ); @@ -232,16 +151,14 @@ describe('PageDutyCard', () => { describe('when entity has the pagerduty.com/service-id annotation', () => { it('Renders PagerDuty service information', async () => { - mockPagerDutyApi.getServiceByEntity = jest + mockPagerDutyApi.getServiceByPagerDutyEntity = jest .fn() .mockImplementationOnce(async () => ({ service })); const { getByText, queryByTestId } = render( wrapInTestApp( - - - + , ), ); @@ -253,16 +170,18 @@ describe('PageDutyCard', () => { }); it('Handles custom error for missing token', async () => { - mockPagerDutyApi.getServiceByEntity = jest + mockPagerDutyApi.getServiceByPagerDutyEntity = jest .fn() .mockRejectedValueOnce(new UnauthorizedError()); const { getByText, queryByTestId } = render( wrapInTestApp( - - - + , ), ); @@ -273,16 +192,18 @@ describe('PageDutyCard', () => { }); it('Handles custom NotFoundError', async () => { - mockPagerDutyApi.getServiceByEntity = jest + mockPagerDutyApi.getServiceByPagerDutyEntity = jest .fn() .mockRejectedValueOnce(new NotFoundError()); const { getByText, queryByTestId } = render( wrapInTestApp( - - - + , ), ); @@ -291,15 +212,17 @@ describe('PageDutyCard', () => { }); it('handles general error', async () => { - mockPagerDutyApi.getServiceByEntity = jest + mockPagerDutyApi.getServiceByPagerDutyEntity = jest .fn() .mockRejectedValueOnce(new Error('An error occurred')); const { getByText, queryByTestId } = render( wrapInTestApp( - - - + , ), ); @@ -313,16 +236,14 @@ describe('PageDutyCard', () => { }); it('disables the Create Incident button', async () => { - mockPagerDutyApi.getServiceByEntity = jest + mockPagerDutyApi.getServiceByPagerDutyEntity = jest .fn() .mockImplementationOnce(async () => ({ service })); const { queryByTestId, getByTitle } = render( wrapInTestApp( - - - + , ), ); @@ -336,16 +257,18 @@ describe('PageDutyCard', () => { describe('when entity has all annotations', () => { it('queries by integration key', async () => { - mockPagerDutyApi.getServiceByEntity = jest + mockPagerDutyApi.getServiceByPagerDutyEntity = jest .fn() .mockImplementationOnce(async () => ({ service })); const { getByText, queryByTestId } = render( wrapInTestApp( - - - + , ), ); @@ -359,16 +282,19 @@ describe('PageDutyCard', () => { describe('when entity has all annotations but the plugin has been configured to be "read only"', () => { it('queries by integration key but does not render the "Create Incident" button', async () => { - mockPagerDutyApi.getServiceByEntity = jest + mockPagerDutyApi.getServiceByPagerDutyEntity = jest .fn() .mockImplementationOnce(async () => ({ service })); const { getByText, queryByTestId } = render( wrapInTestApp( - - - + , ), ); diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx index fd23166d88..bb89865c5f 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ import React, { ReactNode, useState, useCallback } from 'react'; -import { Entity } from '@backstage/catalog-model'; import { Card, CardHeader, Divider, CardContent } from '@material-ui/core'; import { Incidents } from '../Incident'; import { EscalationPolicy } from '../Escalation'; @@ -25,7 +24,6 @@ import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; import { MissingTokenError, ServiceNotFoundError } from '../Errors'; import WebIcon from '@material-ui/icons/Web'; import DateRangeIcon from '@material-ui/icons/DateRange'; -import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID } from '../constants'; import { TriggerDialog } from '../TriggerDialog'; import { ChangeEvents } from '../ChangeEvents'; @@ -39,30 +37,20 @@ import { CardTab, InfoCard, } from '@backstage/core-components'; -import { useEntity } from '@backstage/plugin-catalog-react'; -import { getPagerDutyEntity } from '../pagerDutyEntity'; +import { PagerDutyEntity } from '../../types'; const BasicCard = ({ children }: { children: ReactNode }) => ( {children} ); /** @public */ -export const isPluginApplicableToEntity = (entity: Entity) => - Boolean( - entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY] || - entity.metadata.annotations?.[PAGERDUTY_SERVICE_ID], - ); - -/** @public */ -export type PagerDutyCardProps = { +export type PagerDutyCardProps = PagerDutyEntity & { readOnly?: boolean; }; /** @public */ export const PagerDutyCard = (props: PagerDutyCardProps) => { - const { readOnly } = props; - const { entity } = useEntity(); - const pagerDutyEntity = getPagerDutyEntity(entity); + const { readOnly, integrationKey, name } = props; const api = useApi(pagerDutyApiRef); const [refreshIncidents, setRefreshIncidents] = useState(false); const [refreshChangeEvents, setRefreshChangeEvents] = @@ -86,7 +74,9 @@ export const PagerDutyCard = (props: PagerDutyCardProps) => { loading, error, } = useAsync(async () => { - const { service: foundService } = await api.getServiceByEntity(entity); + const { service: foundService } = await api.getServiceByPagerDutyEntity( + props, + ); return { id: foundService.id, @@ -137,7 +127,7 @@ export const PagerDutyCard = (props: PagerDutyCardProps) => { * There is no guarantee the current user entity has a valid email association, so instead just * only allow triggering incidents when an integration key is present. */ - const createIncidentDisabled = !pagerDutyEntity.integrationKey; + const createIncidentDisabled = !integrationKey; const triggerLink: IconLinkVerticalProps = { label: 'Create Incident', onClick: showDialog, @@ -195,6 +185,8 @@ export const PagerDutyCard = (props: PagerDutyCardProps) => { showDialog={dialogShown} handleDialog={hideDialog} onIncidentCreated={handleRefresh} + name={name} + integrationKey={integrationKey} /> )} diff --git a/plugins/pagerduty/src/components/TriggerButton/index.tsx b/plugins/pagerduty/src/components/TriggerButton/index.tsx index 732298c6d8..1c12103d73 100644 --- a/plugins/pagerduty/src/components/TriggerButton/index.tsx +++ b/plugins/pagerduty/src/components/TriggerButton/index.tsx @@ -33,7 +33,7 @@ const useStyles = makeStyles(theme => ({ /** @public */ export function TriggerButton(props: { children?: ReactNode }) { const { buttonStyle } = useStyles(); - const { integrationKey } = usePagerdutyEntity(); + const { integrationKey, name } = usePagerdutyEntity(); const [dialogShown, setDialogShown] = useState(false); const showDialog = useCallback(() => { @@ -57,7 +57,12 @@ export function TriggerButton(props: { children?: ReactNode }) { : 'Missing integration key'} {integrationKey && ( - + )} ); diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx index c6874cc7f3..74cd5100fa 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -17,8 +17,6 @@ import React from 'react'; import { fireEvent, act } from '@testing-library/react'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { pagerDutyApiRef } from '../../api'; -import { Entity } from '@backstage/catalog-model'; -import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TriggerDialog } from './TriggerDialog'; import { ApiProvider } from '@backstage/core-app-api'; @@ -49,26 +47,15 @@ describe('TriggerDialog', () => { ); it('open the dialog and trigger an alarm', async () => { - const entity: Entity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'pagerduty-test', - annotations: { - 'pagerduty.com/integration-key': 'abc123', - }, - }, - }; - const { getByText, getByRole, getByTestId } = await renderInTestApp( - - {}} - onIncidentCreated={() => {}} - /> - + {}} + onIncidentCreated={() => {}} + /> , ); @@ -89,8 +76,7 @@ describe('TriggerDialog', () => { }); expect(mockTriggerAlarmFn).toHaveBeenCalled(); expect(mockTriggerAlarmFn).toHaveBeenCalledWith({ - integrationKey: - entity!.metadata!.annotations!['pagerduty.com/integration-key'], + integrationKey: 'abc123', source: window.location.toString(), description, userName: 'guest', diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx index 4dc4f04699..a1e88b8fa6 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx @@ -28,7 +28,6 @@ import { import useAsyncFn from 'react-use/lib/useAsyncFn'; import { pagerDutyApiRef } from '../../api'; import { Alert } from '@material-ui/lab'; -import { usePagerdutyEntity } from '../../hooks'; import { useApi, alertApiRef, @@ -40,14 +39,17 @@ type Props = { showDialog: boolean; handleDialog: () => void; onIncidentCreated?: () => void; + name: string; + integrationKey: string; }; export const TriggerDialog = ({ showDialog, handleDialog, onIncidentCreated: onIncidentCreated, + name, + integrationKey, }: Props) => { - const { name, integrationKey } = usePagerdutyEntity(); const alertApi = useApi(alertApiRef); const identityApi = useApi(identityApiRef); const api = useApi(pagerDutyApiRef); diff --git a/plugins/pagerduty/src/components/index.ts b/plugins/pagerduty/src/components/index.ts index e7c1ff4e72..264d7195e9 100644 --- a/plugins/pagerduty/src/components/index.ts +++ b/plugins/pagerduty/src/components/index.ts @@ -23,12 +23,13 @@ export type { PagerDutyUser, } from './types'; -export type { PagerDutyCardProps } from './PagerDutyCard'; +export type { EntityPagerDutyCardProps } from './EntityPagerDutyCard'; +export type { HomePagePagerDutyCardProps } from './HomePagePagerDutyCard'; export { isPluginApplicableToEntity, isPluginApplicableToEntity as isPagerDutyAvailable, - PagerDutyCard, -} from './PagerDutyCard'; + EntityPagerDutyCard, +} from './EntityPagerDutyCard'; export { TriggerButton } from './TriggerButton'; diff --git a/plugins/pagerduty/src/deprecated.ts b/plugins/pagerduty/src/deprecated.ts new file mode 100644 index 0000000000..e9f60f6894 --- /dev/null +++ b/plugins/pagerduty/src/deprecated.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityPagerDutyCardProps, EntityPagerDutyCard } from './components'; + +/** + * @public + * @deprecated Please use EntityPagerDutyCard + */ +export const PagerDutyCard = EntityPagerDutyCard; + +/** + * @public + * @deprecated Please use EntityPagerDutyCardProps + */ +export type PagerDutyCardProps = EntityPagerDutyCardProps; diff --git a/plugins/pagerduty/src/index.ts b/plugins/pagerduty/src/index.ts index 2e0eddc9a8..c6f7aef89a 100644 --- a/plugins/pagerduty/src/index.ts +++ b/plugins/pagerduty/src/index.ts @@ -24,9 +24,12 @@ export { pagerDutyPlugin, pagerDutyPlugin as plugin, EntityPagerDutyCard, + HomePagePagerDutyCard, } from './plugin'; export * from './components'; + export * from './api'; +export * from './deprecated'; export type { PagerDutyEntity } from './types'; diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts index 73e7e0b397..59cc5cd8cc 100644 --- a/plugins/pagerduty/src/plugin.ts +++ b/plugins/pagerduty/src/plugin.ts @@ -23,6 +23,8 @@ import { configApiRef, createComponentExtension, } from '@backstage/core-plugin-api'; +import { createCardExtension } from '@backstage/plugin-home-react'; +import { HomePagePagerDutyCardProps } from './components/HomePagePagerDutyCard/Content'; export const rootRouteRef = createRouteRef({ id: 'pagerduty', @@ -51,7 +53,38 @@ export const EntityPagerDutyCard = pagerDutyPlugin.provide( name: 'EntityPagerDutyCard', component: { lazy: () => - import('./components/PagerDutyCard').then(m => m.PagerDutyCard), + import('./components/EntityPagerDutyCard').then( + m => m.EntityPagerDutyCard, + ), + }, + }), +); + +/** @public */ +export const HomePagePagerDutyCard = pagerDutyPlugin.provide( + createCardExtension({ + name: 'HomePagePagerDutyCard', + title: 'PagerDuty Homepage Card', + components: () => import('./components/HomePagePagerDutyCard'), + settings: { + schema: { + title: 'PagerDuty', + type: 'object', + properties: { + integrationKey: { + title: 'PagerDuty integration key', + type: 'string', + }, + serviceId: { + title: 'PagerDuty service id', + type: 'string', + }, + name: { + title: 'PagerDuty service name', + type: 'string', + }, + }, + }, }, }), ); diff --git a/plugins/proxy-backend/config.d.ts b/plugins/proxy-backend/config.d.ts index 59fc0dc5a6..410ff86a2e 100644 --- a/plugins/proxy-backend/config.d.ts +++ b/plugins/proxy-backend/config.d.ts @@ -31,7 +31,17 @@ export interface Config { /** * Object with extra headers to be added to target requests. */ - headers?: { [key: string]: string }; + headers?: Partial<{ + /** @visibility secret */ + Authorization: string; + /** @visibility secret */ + authorization: string; + /** @visibility secret */ + 'X-Api-Key': string; + /** @visibility secret */ + 'x-api-key': string; + [key: string]: string; + }>; /** * Changes the origin of the host header to the target URL. Default: true. */ diff --git a/plugins/scaffolder-backend-module-gitlab/api-report.md b/plugins/scaffolder-backend-module-gitlab/api-report.md index f7cf6024ec..eccada5d51 100644 --- a/plugins/scaffolder-backend-module-gitlab/api-report.md +++ b/plugins/scaffolder-backend-module-gitlab/api-report.md @@ -26,8 +26,8 @@ export const createGitlabProjectAccessTokenAction: (options: { integrations: ScmIntegrationRegistry; }) => TemplateAction< { - projectId: string | number; repoUrl: string; + projectId: string | number; token?: string | undefined; name?: string | undefined; accessLevel?: number | undefined; @@ -44,8 +44,8 @@ export const createGitlabProjectDeployTokenAction: (options: { }) => TemplateAction< { name: string; - projectId: string | number; repoUrl: string; + projectId: string | number; token?: string | undefined; username?: string | undefined; scopes?: string[] | undefined; @@ -63,8 +63,8 @@ export const createGitlabProjectVariableAction: (options: { { key: string; value: string; - projectId: string | number; repoUrl: string; + projectId: string | number; variableType: string; token?: string | undefined; variableProtected?: boolean | undefined; diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index 1761c88edf..82610a2fc3 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -207,7 +207,9 @@ export interface ScaffolderGetIntegrationsListResponse { } // @public -export const ScaffolderLayouts: React.ComponentType; +export const ScaffolderLayouts: React_2.ComponentType< + React_2.PropsWithChildren<{}> +>; // @public (undocumented) export type ScaffolderOutputLink = { diff --git a/plugins/scaffolder-react/src/layouts/createScaffolderLayout.ts b/plugins/scaffolder-react/src/layouts/createScaffolderLayout.ts index 328f366007..6b2ae1b33b 100644 --- a/plugins/scaffolder-react/src/layouts/createScaffolderLayout.ts +++ b/plugins/scaffolder-react/src/layouts/createScaffolderLayout.ts @@ -17,6 +17,7 @@ import { LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from './keys'; import { attachComponentData, Extension } from '@backstage/core-plugin-api'; import type { FormProps as SchemaFormProps } from '@rjsf/core-v5'; +import React from 'react'; /** * The field template from `@rjsf/core` which is a react component that gets passed `@rjsf/core` field related props. @@ -67,7 +68,8 @@ export function createScaffolderLayout( * * @public */ -export const ScaffolderLayouts: React.ComponentType = (): JSX.Element | null => - null; +export const ScaffolderLayouts: React.ComponentType< + React.PropsWithChildren<{}> +> = (): JSX.Element | null => null; attachComponentData(ScaffolderLayouts, LAYOUTS_WRAPPER_KEY, true); diff --git a/plugins/scaffolder/alpha-api-report.md b/plugins/scaffolder/alpha-api-report.md index 6ceacdc008..de657c2138 100644 --- a/plugins/scaffolder/alpha-api-report.md +++ b/plugins/scaffolder/alpha-api-report.md @@ -27,7 +27,7 @@ export type NextRouterProps = { TemplateCardComponent?: React_2.ComponentType<{ template: TemplateEntityV1beta3; }>; - TaskPageComponent?: React_2.ComponentType<{}>; + TaskPageComponent?: React_2.ComponentType>; TemplateOutputsComponent?: React_2.ComponentType<{ output?: ScaffolderTaskOutput; }>; diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 3495522310..e50395be05 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -29,6 +29,7 @@ import { ListActionsResponse as ListActionsResponse_2 } from '@backstage/plugin- import { LogEvent as LogEvent_2 } from '@backstage/plugin-scaffolder-react'; import { Observable } from '@backstage/types'; import { PathParams } from '@backstage/core-plugin-api'; +import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -382,7 +383,7 @@ export type RouterProps = { template: TemplateEntityV1beta3; }> | undefined; - TaskPageComponent?: ComponentType<{}>; + TaskPageComponent?: ComponentType>; }; groups?: Array<{ title?: React_2.ReactNode; @@ -466,7 +467,9 @@ export type ScaffolderGetIntegrationsListResponse = ScaffolderGetIntegrationsListResponse_2; // @public @deprecated (undocumented) -export const ScaffolderLayouts: ComponentType<{}>; +export const ScaffolderLayouts: ComponentType<{ + children?: ReactNode; +}>; // @public @deprecated (undocumented) export type ScaffolderOutputlink = ScaffolderOutputLink; diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 05d5cc9f0a..3ceda359cc 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ComponentType, useEffect } from 'react'; +import React, { ComponentType, useEffect, PropsWithChildren } from 'react'; import { Navigate, Route, Routes, useOutlet } from 'react-router-dom'; import { Entity } from '@backstage/catalog-model'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; @@ -52,7 +52,7 @@ export type RouterProps = { TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta3 }> | undefined; - TaskPageComponent?: ComponentType<{}>; + TaskPageComponent?: ComponentType>; }; groups?: Array<{ title?: React.ReactNode; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx index 598616aaa3..cbc587a732 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx @@ -49,7 +49,7 @@ describe('', () => { getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { entities = [ diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx index 00508593f5..1d843ea8a2 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx @@ -55,7 +55,7 @@ describe('', () => { getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; beforeEach(() => { entities = [ diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index 9c50d0d979..2061cd1243 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -58,7 +58,7 @@ export type NextRouterProps = { TemplateCardComponent?: React.ComponentType<{ template: TemplateEntityV1beta3; }>; - TaskPageComponent?: React.ComponentType<{}>; + TaskPageComponent?: React.ComponentType>; TemplateOutputsComponent?: React.ComponentType<{ output?: ScaffolderTaskOutput; }>; diff --git a/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx index 6db7955ec8..0eaece87bb 100644 --- a/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx +++ b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { Grid } from '@material-ui/core'; import LabelIcon from '@material-ui/icons/Label'; @@ -31,7 +31,7 @@ export default { title: 'Plugins/Search/SearchAutocomplete', component: SearchAutocomplete, decorators: [ - (Story: ComponentType<{}>) => ( + (Story: ComponentType>) => ( diff --git a/plugins/search-react/src/components/SearchAutocomplete/SearchAutocompleteDefaultOption.stories.tsx b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocompleteDefaultOption.stories.tsx index 40a3c5cdca..f671546efa 100644 --- a/plugins/search-react/src/components/SearchAutocomplete/SearchAutocompleteDefaultOption.stories.tsx +++ b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocompleteDefaultOption.stories.tsx @@ -30,7 +30,7 @@ export default { title: 'Plugins/Search/SearchAutocompleteDefaultOption', component: SearchAutocompleteDefaultOption, decorators: [ - (Story: ComponentType<{}>) => ( + (Story: ComponentType>) => ( diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx index 05a46d11e1..f342eed211 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { Grid, makeStyles } from '@material-ui/core'; import { TestApiProvider } from '@backstage/test-utils'; @@ -28,7 +28,7 @@ export default { title: 'Plugins/Search/SearchBar', component: SearchBar, decorators: [ - (Story: ComponentType<{}>) => ( + (Story: ComponentType>) => ( diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.stories.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.stories.tsx index 2c8a629f01..505c1e13c3 100644 --- a/plugins/search-react/src/components/SearchFilter/SearchFilter.stories.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { Grid, Paper } from '@material-ui/core'; import { TestApiProvider } from '@backstage/test-utils'; @@ -27,7 +27,7 @@ export default { title: 'Plugins/Search/SearchFilter', component: SearchFilter, decorators: [ - (Story: ComponentType<{}>) => ( + (Story: ComponentType>) => ( diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx index 4e72c0fa25..dfb4e7ea91 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { Grid } from '@material-ui/core'; import { TestApiProvider } from '@backstage/test-utils'; @@ -28,7 +28,7 @@ export default { title: 'Plugins/Search/SearchPagination', component: SearchPagination, decorators: [ - (Story: ComponentType<{}>) => ( + (Story: ComponentType>) => ( diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx index f3086fc40a..a64d053dae 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { List, ListItem } from '@material-ui/core'; import DefaultIcon from '@material-ui/icons/InsertDriveFile'; @@ -70,7 +70,7 @@ export default { title: 'Plugins/Search/SearchResult', component: SearchResult, decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp( diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx index 40f7d30ff4..2f497d4568 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx @@ -14,7 +14,12 @@ * limitations under the License. */ -import React, { ComponentType, useCallback, useState } from 'react'; +import React, { + ComponentType, + useCallback, + useState, + PropsWithChildren, +} from 'react'; import { Grid, @@ -72,7 +77,7 @@ export default { title: 'Plugins/Search/SearchResultGroup', component: SearchResultGroup, decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp( diff --git a/plugins/search-react/src/components/SearchResultList/SearchResultList.stories.tsx b/plugins/search-react/src/components/SearchResultList/SearchResultList.stories.tsx index 4369b6636e..d822d0f52d 100644 --- a/plugins/search-react/src/components/SearchResultList/SearchResultList.stories.tsx +++ b/plugins/search-react/src/components/SearchResultList/SearchResultList.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ComponentType, useState } from 'react'; +import React, { ComponentType, useState, PropsWithChildren } from 'react'; import { Grid, ListItem, ListItemIcon, ListItemText } from '@material-ui/core'; @@ -59,7 +59,7 @@ export default { title: 'Plugins/Search/SearchResultList', component: SearchResultList, decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp( diff --git a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.stories.tsx b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.stories.tsx index 9a10aaa41b..c1b1f0df87 100644 --- a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.stories.tsx +++ b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.stories.tsx @@ -18,12 +18,12 @@ import { rootRouteRef, HomePageSearchBar } from '../../plugin'; import { searchApiRef } from '@backstage/plugin-search-react'; import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; import { Grid, makeStyles } from '@material-ui/core'; -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; export default { title: 'Plugins/Home/Components/SearchBar', decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp( <> ) => + (Story: ComponentType>) => wrapInTestApp( ) => ( + (Story: ComponentType>) => ( diff --git a/plugins/stack-overflow/src/home/StackOverflowQuestions/StackOverflowQuestions.stories.tsx b/plugins/stack-overflow/src/home/StackOverflowQuestions/StackOverflowQuestions.stories.tsx index aed1ed25aa..9f0fd75341 100644 --- a/plugins/stack-overflow/src/home/StackOverflowQuestions/StackOverflowQuestions.stories.tsx +++ b/plugins/stack-overflow/src/home/StackOverflowQuestions/StackOverflowQuestions.stories.tsx @@ -19,7 +19,7 @@ import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; import { configApiRef } from '@backstage/core-plugin-api'; import { ConfigReader } from '@backstage/config'; import { Grid } from '@material-ui/core'; -import React, { ComponentType } from 'react'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { StackOverflowIcon } from '../../icons'; import { stackOverflowApiRef } from '../../api'; @@ -46,7 +46,7 @@ export default { title: 'Plugins/Home/Components/StackOverflow', component: HomePageStackOverflowQuestions, decorators: [ - (Story: ComponentType<{}>) => + (Story: ComponentType>) => wrapInTestApp( <> ) => wrapInTestApp()], + decorators: [ + (Story: ComponentType>) => wrapInTestApp(), + ], }; export const Default = () => { diff --git a/plugins/techdocs/src/reader/components/useReaderState.test.tsx b/plugins/techdocs/src/reader/components/useReaderState.test.tsx index 08bfa8f9ad..5f3e2d9662 100644 --- a/plugins/techdocs/src/reader/components/useReaderState.test.tsx +++ b/plugins/techdocs/src/reader/components/useReaderState.test.tsx @@ -26,7 +26,7 @@ import { } from './useReaderState'; describe('useReaderState', () => { - let Wrapper: React.ComponentType; + let Wrapper: React.ComponentType>; const techdocsStorageApi: jest.Mocked = { getApiOrigin: jest.fn(), diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx b/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx index dc4eeb85be..2b1f427be3 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx +++ b/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React, { FC, PropsWithChildren } from 'react'; import { renderHook } from '@testing-library/react-hooks'; import { ConfigReader } from '@backstage/core-app-api'; @@ -31,7 +31,7 @@ const configApiMock: ConfigApi = new ConfigReader({ }, }); -const wrapper: FC = ({ children }) => ( +const wrapper: FC> = ({ children }) => ( {children} diff --git a/yarn.lock b/yarn.lock index 07c79358be..712fd10e6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5868,6 +5868,7 @@ __metadata: "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" + "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^" "@backstage/test-utils": "workspace:^" @@ -7889,6 +7890,7 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-home-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@material-ui/core": ^4.12.2 @@ -11079,8 +11081,8 @@ __metadata: linkType: hard "@google-cloud/storage@npm:^6.0.0": - version: 6.9.5 - resolution: "@google-cloud/storage@npm:6.9.5" + version: 6.10.1 + resolution: "@google-cloud/storage@npm:6.10.1" dependencies: "@google-cloud/paginator": ^3.0.7 "@google-cloud/projectify": ^3.0.0 @@ -11099,7 +11101,7 @@ __metadata: retry-request: ^5.0.0 teeny-request: ^8.0.0 uuid: ^8.0.0 - checksum: eff663cc44ee553fbd2f62b67ef422783f29210c67f55c86c13e63a40c36f3c9b78e076b2a08bb6a74a9fdc452de9240aac91e0fa11c3a4f782416a230d88b74 + checksum: 8908500e3057456ccac5cca17224807898a5c51c0423dec336e802ed0bc0785be7658b0050f848102a2f9207dbf3fb2649f6cc8e515c49fce3a134a8f158ecea languageName: node linkType: hard @@ -12488,17 +12490,17 @@ __metadata: linkType: hard "@kubernetes-models/apimachinery@npm:^1.1.0": - version: 1.1.1 - resolution: "@kubernetes-models/apimachinery@npm:1.1.1" + version: 1.2.0 + resolution: "@kubernetes-models/apimachinery@npm:1.2.0" dependencies: - "@kubernetes-models/base": ^4.0.1 - "@kubernetes-models/validate": ^3.0.1 + "@kubernetes-models/base": ^4.0.2 + "@kubernetes-models/validate": ^3.1.0 tslib: ^2.4.0 - checksum: 915d6a55b485335622d43c6e1f1031ea949bfb3e985deba6389462b4c0a3b3030e71e6eebfc1e7b894afee543de2e5a1af0ff6694225a372e220f7dccb5b6eed + checksum: ac93980b732239517af47e0cb512e28fc320ed8348eaa8a68f65b16adc98cf90b6b36d9a9752d825e6e1ac84ae0c0d37e2dc8c8de140011506753a058c89bcf2 languageName: node linkType: hard -"@kubernetes-models/base@npm:^4.0.0, @kubernetes-models/base@npm:^4.0.1": +"@kubernetes-models/base@npm:^4.0.0, @kubernetes-models/base@npm:^4.0.1, @kubernetes-models/base@npm:^4.0.2": version: 4.0.2 resolution: "@kubernetes-models/base@npm:4.0.2" dependencies: @@ -12509,7 +12511,7 @@ __metadata: languageName: node linkType: hard -"@kubernetes-models/validate@npm:^3.0.0, @kubernetes-models/validate@npm:^3.0.1, @kubernetes-models/validate@npm:^3.1.0": +"@kubernetes-models/validate@npm:^3.0.0, @kubernetes-models/validate@npm:^3.1.0": version: 3.1.0 resolution: "@kubernetes-models/validate@npm:3.1.0" dependencies: @@ -12999,9 +13001,9 @@ __metadata: linkType: hard "@microsoft/microsoft-graph-types@npm:^2.25.0, @microsoft/microsoft-graph-types@npm:^2.6.0": - version: 2.25.0 - resolution: "@microsoft/microsoft-graph-types@npm:2.25.0" - checksum: b311385e0a51f827082af5a016f35b1d6cd4d861121e8767b3279ab1a17475977cddbbdcc208abdddf0f953fd6ce4634d3c302e317cf50af4cb6111385347b4f + version: 2.33.0 + resolution: "@microsoft/microsoft-graph-types@npm:2.33.0" + checksum: 9a07e258750cfdedebdc05670c9951fe507ba0868839a411ff36592015660a547d1e4fd9abec1533da979238ef674ce2bf4c363ab0ae2082818313fba0978ecb languageName: node linkType: hard @@ -14221,18 +14223,18 @@ __metadata: linkType: hard "@react-hookz/web@npm:^23.0.0": - version: 23.0.0 - resolution: "@react-hookz/web@npm:23.0.0" + version: 23.0.1 + resolution: "@react-hookz/web@npm:23.0.1" dependencies: "@react-hookz/deep-equal": ^1.0.4 peerDependencies: - js-cookie: ^3.0.1 + js-cookie: ^3.0.5 react: ^16.8 || ^17 || ^18 react-dom: ^16.8 || ^17 || ^18 peerDependenciesMeta: js-cookie: optional: true - checksum: 230bff62291bcafa65e0b9adcc3f0769fd63fabc226c77149e7797d1ea67362e9ad581ceff3cdfe2949698798d2deefb223c05bdfed1e465b5d92298d48cf3e5 + checksum: 321770570c947321ba44de6ecee17337e4bb0c2a0c9d0dff5cf29e999faa2b7c1132c72a15aface46da9a709b715042ab48a30085d2a8feefba91aac96e82037 languageName: node linkType: hard @@ -21473,11 +21475,11 @@ __metadata: linkType: hard "cron@npm:^2.0.0": - version: 2.3.0 - resolution: "cron@npm:2.3.0" + version: 2.3.1 + resolution: "cron@npm:2.3.1" dependencies: luxon: ^3.2.1 - checksum: 70b220f3fc4b6bcd594f7f2c690bbf7a3a03b0e081eb0119f8e3ca21635f2da07d89e038d561d4c0742c8b0dd04977f25e307d0608455ef9f527a556b2da92fc + checksum: ac27c257cb8dba4b51978b4767119fc79bfc0dfa35ef0be2533bc4142ae006130515236e2102d04ee3ce44bffc2a4e37a3bee18a2152bbb303bf5ff944e25204 languageName: node linkType: hard @@ -32820,9 +32822,9 @@ __metadata: linkType: hard "openapi-types@npm:^12.0.0": - version: 12.1.1 - resolution: "openapi-types@npm:12.1.1" - checksum: 881dbc514e22046eb790c81370e912ca2daf5beec13cde77e8a15b5ec2635140487abdebf681ec969777067bcbb56b3c0880a0a22aa236d075d8fca5da9535ad + version: 12.1.3 + resolution: "openapi-types@npm:12.1.3" + checksum: 7fa5547f87a58d2aa0eba6e91d396f42d7d31bc3ae140e61b5d60b47d2fd068b48776f42407d5a8da7280cf31195aa128c2fc285e8bb871d1105edee5647a0bb languageName: node linkType: hard