From 1083a2c47e7da4dfaf87e471783a12882dcb4147 Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Mon, 31 Oct 2022 12:53:15 +0100 Subject: [PATCH 001/155] Used Layout pattern in SettingsPage, analogous to Explore Plugin Signed-off-by: Nikita Karpukhin --- packages/app/src/App.tsx | 26 ++++-- .../DefaultSettingsPage.test.tsx} | 11 ++- .../DefaultSettingsPage.tsx | 47 +++++++++++ .../components/DefaultSettingsPage/index.ts | 17 ++++ .../SettingsLayout/SettingsLayout.tsx | 79 +++++++++++++++++++ .../src/components/SettingsLayout/index.ts | 18 +++++ .../src/components/SettingsPage.tsx | 58 ++------------ plugins/user-settings/src/components/index.ts | 1 + 8 files changed, 190 insertions(+), 67 deletions(-) rename plugins/user-settings/src/components/{SettingsPage.test.tsx => DefaultSettingsPage/DefaultSettingsPage.test.tsx} (89%) create mode 100644 plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.tsx create mode 100644 plugins/user-settings/src/components/DefaultSettingsPage/index.ts create mode 100644 plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx create mode 100644 plugins/user-settings/src/components/SettingsLayout/index.ts diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 2526f6740f..a21fe2ce9d 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -60,18 +60,18 @@ import { HomepageCompositionRoot } from '@backstage/plugin-home'; import { LighthousePage } from '@backstage/plugin-lighthouse'; import { NewRelicPage } from '@backstage/plugin-newrelic'; import { - ScaffolderFieldExtensions, - ScaffolderPage, NextScaffolderPage, - scaffolderPlugin, + ScaffolderFieldExtensions, ScaffolderLayouts, + ScaffolderPage, + scaffolderPlugin, } from '@backstage/plugin-scaffolder'; import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; import { TechDocsIndexPage, - TechDocsReaderPage, techdocsPlugin, + TechDocsReaderPage, } from '@backstage/plugin-techdocs'; import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { @@ -80,8 +80,10 @@ import { TextSize, } from '@backstage/plugin-techdocs-module-addons-contrib'; import { + SettingsLayout, + UserSettingsFeatureFlags, + UserSettingsGeneral, UserSettingsPage, - UserSettingsTab, } from '@backstage/plugin-user-settings'; import { AdvancedSettings } from './components/advancedSettings'; import AlarmIcon from '@material-ui/icons/Alarm'; @@ -267,9 +269,17 @@ const routes = ( element={} /> }> - - - + + + + + + + + + + + } /> } /> diff --git a/plugins/user-settings/src/components/SettingsPage.test.tsx b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx similarity index 89% rename from plugins/user-settings/src/components/SettingsPage.test.tsx rename to plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx index 01810bcc8e..24810a149e 100644 --- a/plugins/user-settings/src/components/SettingsPage.test.tsx +++ b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx @@ -16,16 +16,15 @@ import React from 'react'; import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; -import { SettingsPage } from './SettingsPage'; -import { UserSettingsTab } from './UserSettingsTab'; +import { DefaultSettingsPage } from './DefaultSettingsPage'; +import { UserSettingsTab } from '../UserSettingsTab'; +import { useOutlet } from 'react-router'; jest.mock('react-router', () => ({ ...jest.requireActual('react-router'), useOutlet: jest.fn().mockReturnValue(undefined), })); -import { useOutlet } from 'react-router'; - describe('', () => { beforeEach(() => { (useOutlet as jest.Mock).mockReset(); @@ -33,7 +32,7 @@ describe('', () => { it('should render the settings page with 3 tabs', async () => { const { container } = await renderWithEffects( - wrapInTestApp(), + wrapInTestApp(), ); const tabs = container.querySelectorAll('[class*=MuiTabs-root] button'); @@ -48,7 +47,7 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); const { container } = await renderWithEffects( - wrapInTestApp(), + wrapInTestApp(), ); const tabs = container.querySelectorAll('[class*=MuiTabs-root] button'); diff --git a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.tsx b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.tsx new file mode 100644 index 0000000000..4b65a26e5e --- /dev/null +++ b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { UserSettingsAuthProviders } from '../AuthProviders'; +import { UserSettingsFeatureFlags } from '../FeatureFlags'; +import { UserSettingsGeneral } from '../General'; +import { SettingsLayout } from '../SettingsLayout'; + +/** + * @public + */ +export const DefaultSettingsPage = (props: { + providerSettings?: JSX.Element; +}) => { + const { providerSettings } = props; + + return ( + + + + + + + + + + + + ); +}; diff --git a/plugins/user-settings/src/components/DefaultSettingsPage/index.ts b/plugins/user-settings/src/components/DefaultSettingsPage/index.ts new file mode 100644 index 0000000000..34dcb9340d --- /dev/null +++ b/plugins/user-settings/src/components/DefaultSettingsPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { DefaultSettingsPage } from './DefaultSettingsPage'; diff --git a/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx b/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx new file mode 100644 index 0000000000..cba2b2bf1f --- /dev/null +++ b/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { TabProps } from '@material-ui/core'; +import { + Header, + Page, + RoutedTabs, + useSidebarPinState, +} from '@backstage/core-components'; +import { + attachComponentData, + useElementFilter, +} from '@backstage/core-plugin-api'; + +/** @public */ +export type SubRoute = { + path: string; + title: string; + children: JSX.Element; + tabProps?: TabProps; +}; + +const dataKey = 'plugin.explore.settingsLayoutRoute'; + +const Route: (props: SubRoute) => null = () => null; +attachComponentData(Route, dataKey, true); + +// This causes all mount points that are discovered within this route to use the path of the route itself +attachComponentData(Route, 'core.gatherMountPoints', true); + +/** @public */ +export type SettingsLayoutProps = { + title?: string; + subtitle?: string; + children?: React.ReactNode; +}; + +/** + * @public + */ +export const SettingsLayout = (props: SettingsLayoutProps) => { + const { title, children } = props; + const { isMobile } = useSidebarPinState(); + + const routes = useElementFilter(children, elements => + elements + .selectByComponentData({ + key: dataKey, + withStrictError: + 'Child of SettingsLayout must be an SettingsLayout.Route', + }) + .getElements() + .map(child => child.props), + ); + + return ( + + {!isMobile &&
} + + + ); +}; + +SettingsLayout.Route = Route; diff --git a/plugins/user-settings/src/components/SettingsLayout/index.ts b/plugins/user-settings/src/components/SettingsLayout/index.ts new file mode 100644 index 0000000000..36a5633c94 --- /dev/null +++ b/plugins/user-settings/src/components/SettingsLayout/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { SettingsLayoutProps, SubRoute } from './SettingsLayout'; +export { SettingsLayout } from './SettingsLayout'; diff --git a/plugins/user-settings/src/components/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage.tsx index 9803bf4761..3fa1057881 100644 --- a/plugins/user-settings/src/components/SettingsPage.tsx +++ b/plugins/user-settings/src/components/SettingsPage.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2022 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,60 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { - Header, - Page, - TabbedLayout, - useSidebarPinState, -} from '@backstage/core-components'; -import React from 'react'; import { useOutlet } from 'react-router'; -import { useElementFilter } from '@backstage/core-plugin-api'; -import { UserSettingsAuthProviders } from './AuthProviders'; -import { UserSettingsFeatureFlags } from './FeatureFlags'; -import { UserSettingsGeneral } from './General'; -import { USER_SETTINGS_TAB_KEY, UserSettingsTabProps } from './UserSettingsTab'; +import React from 'react'; +import { DefaultSettingsPage } from './DefaultSettingsPage'; -/** - * @public - */ -export const SettingsPage = (props: { providerSettings?: JSX.Element }) => { - const { providerSettings } = props; - const { isMobile } = useSidebarPinState(); +export const SettingsPage = () => { const outlet = useOutlet(); - const tabs = useElementFilter(outlet, elements => - elements - .selectByComponentData({ - key: USER_SETTINGS_TAB_KEY, - }) - .getElements(), - ); - - return ( - - {!isMobile &&
} - - - - - - - - - - - - {tabs.map((child, i) => ( - - {child} - - ))} - - - ); + return <>{outlet || }; }; diff --git a/plugins/user-settings/src/components/index.ts b/plugins/user-settings/src/components/index.ts index 8dc76ee7f1..8eb43a7a10 100644 --- a/plugins/user-settings/src/components/index.ts +++ b/plugins/user-settings/src/components/index.ts @@ -21,3 +21,4 @@ export * from './General'; export * from './FeatureFlags'; export { useUserProfile } from './useUserProfileInfo'; export * from './UserSettingsTab'; +export * from './SettingsLayout'; From 408027734d9d3f65a3e88238960731774fe45df6 Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Mon, 31 Oct 2022 15:15:45 +0100 Subject: [PATCH 002/155] Added check for UserSettingsTab, so that the existing code doesn't break Signed-off-by: Nikita Karpukhin --- packages/app/src/App.tsx | 18 ++------ .../DefaultSettingsPage.tsx | 9 +++- .../SettingsLayout/SettingsLayout.tsx | 2 +- .../components/SettingsPage/SettingsPage.tsx | 43 +++++++++++++++++++ .../index.ts} | 10 +---- 5 files changed, 57 insertions(+), 25 deletions(-) create mode 100644 plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx rename plugins/user-settings/src/components/{SettingsPage.tsx => SettingsPage/index.ts} (70%) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index a21fe2ce9d..0f60012759 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -80,10 +80,8 @@ import { TextSize, } from '@backstage/plugin-techdocs-module-addons-contrib'; import { - SettingsLayout, - UserSettingsFeatureFlags, - UserSettingsGeneral, UserSettingsPage, + UserSettingsTab, } from '@backstage/plugin-user-settings'; import { AdvancedSettings } from './components/advancedSettings'; import AlarmIcon from '@material-ui/icons/Alarm'; @@ -269,17 +267,9 @@ const routes = ( element={} /> }> - - - - - - - - - - - + + + } /> } /> diff --git a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.tsx b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.tsx index 4b65a26e5e..6348845763 100644 --- a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.tsx +++ b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.tsx @@ -19,14 +19,16 @@ import { UserSettingsAuthProviders } from '../AuthProviders'; import { UserSettingsFeatureFlags } from '../FeatureFlags'; import { UserSettingsGeneral } from '../General'; import { SettingsLayout } from '../SettingsLayout'; +import { UserSettingsTabProps } from '../UserSettingsTab'; /** * @public */ export const DefaultSettingsPage = (props: { + tabs?: React.ReactElement[]; providerSettings?: JSX.Element; }) => { - const { providerSettings } = props; + const { providerSettings, tabs } = props; return ( @@ -42,6 +44,11 @@ export const DefaultSettingsPage = (props: { + {tabs?.map((child, i) => ( + + {child} + + ))} ); }; diff --git a/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx b/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx index cba2b2bf1f..23394819d6 100644 --- a/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx +++ b/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx @@ -35,7 +35,7 @@ export type SubRoute = { tabProps?: TabProps; }; -const dataKey = 'plugin.explore.settingsLayoutRoute'; +const dataKey = 'plugin.user-settings.settingsLayoutRoute'; const Route: (props: SubRoute) => null = () => null; attachComponentData(Route, dataKey, true); diff --git a/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx new file mode 100644 index 0000000000..3d1f93aaef --- /dev/null +++ b/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx @@ -0,0 +1,43 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useOutlet } from 'react-router'; +import React from 'react'; +import { DefaultSettingsPage } from '../DefaultSettingsPage'; +import { useElementFilter } from '@backstage/core-plugin-api'; +import { + USER_SETTINGS_TAB_KEY, + UserSettingsTabProps, +} from '../UserSettingsTab'; + +export const SettingsPage = (props: { providerSettings?: JSX.Element }) => { + const { providerSettings } = props; + const outlet = useOutlet(); + const tabs = useElementFilter(outlet, elements => + elements + .selectByComponentData({ + key: USER_SETTINGS_TAB_KEY, + }) + .getElements(), + ); + + return ( + <> + {(tabs.length === 0 && outlet) || ( + + )} + + ); +}; diff --git a/plugins/user-settings/src/components/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage/index.ts similarity index 70% rename from plugins/user-settings/src/components/SettingsPage.tsx rename to plugins/user-settings/src/components/SettingsPage/index.ts index 3fa1057881..e957a1bf09 100644 --- a/plugins/user-settings/src/components/SettingsPage.tsx +++ b/plugins/user-settings/src/components/SettingsPage/index.ts @@ -13,12 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useOutlet } from 'react-router'; -import React from 'react'; -import { DefaultSettingsPage } from './DefaultSettingsPage'; - -export const SettingsPage = () => { - const outlet = useOutlet(); - - return <>{outlet || }; -}; +export { SettingsPage } from './SettingsPage'; From a27892953bf20070fe3474adaf0a20a54fa7c323 Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Mon, 31 Oct 2022 15:15:51 +0100 Subject: [PATCH 003/155] Added tests for both for UserSettingsTab and SettingsLayout setups Signed-off-by: Nikita Karpukhin --- .../DefaultSettingsPage.test.tsx | 5 +- .../SettingsPage/SettingsPage.test.tsx | 80 +++++++++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx diff --git a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx index 24810a149e..d7bedbd5ca 100644 --- a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx +++ b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx @@ -25,7 +25,7 @@ jest.mock('react-router', () => ({ useOutlet: jest.fn().mockReturnValue(undefined), })); -describe('', () => { +describe('', () => { beforeEach(() => { (useOutlet as jest.Mock).mockReset(); }); @@ -45,9 +45,8 @@ describe('', () => {
Advanced settings
); - (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); const { container } = await renderWithEffects( - wrapInTestApp(), + wrapInTestApp(), ); const tabs = container.querySelectorAll('[class*=MuiTabs-root] button'); diff --git a/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx new file mode 100644 index 0000000000..883358de5e --- /dev/null +++ b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { SettingsPage } from './SettingsPage'; +import { UserSettingsTab } from '../UserSettingsTab'; +import { useOutlet } from 'react-router'; +import { SettingsLayout } from '../SettingsLayout'; + +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), + useOutlet: jest.fn().mockReturnValue(undefined), +})); + +describe('', () => { + beforeEach(() => { + (useOutlet as jest.Mock).mockReset(); + }); + + it('should render the default settings page with 3 tabs', async () => { + const { container } = await renderWithEffects( + wrapInTestApp(), + ); + + const tabs = container.querySelectorAll('[class*=MuiTabs-root] button'); + expect(tabs).toHaveLength(3); + }); + + it('should render the default settings page with 4 tabs when extra tabs are provided', async () => { + const advancedTabRoute = ( + +
Advanced settings
+
+ ); + (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); + const { container } = await renderWithEffects( + wrapInTestApp(), + ); + + const tabs = container.querySelectorAll('[class*=MuiTabs-root] button'); + expect(tabs).toHaveLength(4); + expect(tabs[3].textContent).toEqual('Advanced'); + }); + + it('should render the custom settings page when custom layout is provided', async () => { + const customLayout = ( + + +
User settings
+
+ +
Advanced settings
+
+
+ ); + (useOutlet as jest.Mock).mockReturnValue(customLayout); + const { container } = await renderWithEffects( + wrapInTestApp(), + ); + + const tabs = container.querySelectorAll('[class*=MuiTabs-root] button'); + expect(tabs).toHaveLength(2); + expect(tabs[0].textContent).toEqual('General'); + expect(tabs[1].textContent).toEqual('Advanced'); + }); +}); From a350001484316370f2fbef97d10251d29046c90d Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Mon, 31 Oct 2022 15:29:37 +0100 Subject: [PATCH 004/155] Updated README.md Signed-off-by: Nikita Karpukhin --- plugins/user-settings/README.md | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md index ce07d76257..0912e34b0a 100644 --- a/plugins/user-settings/README.md +++ b/plugins/user-settings/README.md @@ -93,3 +93,43 @@ make sure you use a similar component structure as the other tabs. You can take a look at [the example extra tab](https://github.com/backstage/backstage/blob/master/packages/app/src/components/advancedSettings/AdvancedSettings.tsx) we have created in Backstage's demo app. + +To change the layout altogether, create a custom page in `packages/app/src/components/user-settings/SettingsPage.tsx`: +```typescript jsx +import React from 'react'; +import { + SettingsLayout, + UserSettingsGeneral, +} from '@backstage/plugin-user-settings'; +import { AdvancedSettings } from './advancedSettings'; + +export const SettingsPage = () => { + return ( + + + + + + + + + ); +}; + +export const settingsPage = ; +``` + +Now register the new settings page in `packages/app/src/App.tsx`: + +```typescript jsx ++ import { settingsPage } from './components/settings/settingsPage'; + +const routes = ( + +- } /> ++ }> ++ {settingsPage} ++ + +); +``` From 38064a945bc82de1db5184e5a918ca18918f9a31 Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Mon, 31 Oct 2022 16:41:14 +0100 Subject: [PATCH 005/155] Added changeset Signed-off-by: Nikita Karpukhin --- .changeset/nice-phones-sin.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nice-phones-sin.md diff --git a/.changeset/nice-phones-sin.md b/.changeset/nice-phones-sin.md new file mode 100644 index 0000000000..ade5c2de8b --- /dev/null +++ b/.changeset/nice-phones-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +Added the ability to fully customize settings page From 1695ff8c82a10772c1de41363f489671e9bd3918 Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Mon, 31 Oct 2022 16:45:05 +0100 Subject: [PATCH 006/155] Revert App.tsx changes Signed-off-by: Nikita Karpukhin --- packages/app/src/App.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 0f60012759..2526f6740f 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -60,18 +60,18 @@ import { HomepageCompositionRoot } from '@backstage/plugin-home'; import { LighthousePage } from '@backstage/plugin-lighthouse'; import { NewRelicPage } from '@backstage/plugin-newrelic'; import { - NextScaffolderPage, ScaffolderFieldExtensions, - ScaffolderLayouts, ScaffolderPage, + NextScaffolderPage, scaffolderPlugin, + ScaffolderLayouts, } from '@backstage/plugin-scaffolder'; import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; import { TechDocsIndexPage, - techdocsPlugin, TechDocsReaderPage, + techdocsPlugin, } from '@backstage/plugin-techdocs'; import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { From bec063868f4eacb0c565f2d430aa9b24c0e41604 Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Mon, 31 Oct 2022 17:12:30 +0100 Subject: [PATCH 007/155] Ran Prettier on the README.md Signed-off-by: Nikita Karpukhin --- plugins/user-settings/README.md | 92 ++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 41 deletions(-) diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md index 0912e34b0a..3d500fbcc3 100644 --- a/plugins/user-settings/README.md +++ b/plugins/user-settings/README.md @@ -4,12 +4,14 @@ Welcome to the user-settings plugin! ## About the plugin -This plugin provides two components, `` is intended to be used within the [``](https://backstage.io/storybook/?path=/story/sidebar--sample-sidebar) and displays the signed-in users profile picture and name. The second component is a settings page where the user can control different settings across the App. +This plugin provides two components, `` is intended to be used within +the [``](https://backstage.io/storybook/?path=/story/sidebar--sample-sidebar) and displays the signed-in users +profile picture and name. The second component is a settings page where the user can control different settings across +the App. -It also provides a `UserSettingsStorage` implementation of the `StorageApi`, to -be used in the frontend as a persistent alternative to the builtin `WebStorage`. -Please see [the backend -README](https://github.com/backstage/backstage/tree/master/plugins/user-settings-backend) +It also provides a `UserSettingsStorage` implementation of the `StorageApi`, to be used in the frontend as a persistent +alternative to the builtin `WebStorage`. Please +see [the backend README](https://github.com/backstage/backstage/tree/master/plugins/user-settings-backend) for installation instructions. ## Components Usage @@ -17,51 +19,60 @@ for installation instructions. Add the item to the Sidebar: ```ts -import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; +import {Settings as SidebarSettings} from '@backstage/plugin-user-settings'; - - -; + + +< /SidebarPage>; ``` Add the page to the App routing: ```ts -import { UserSettingsPage } from '@backstage/plugin-user-settings'; +import {UserSettingsPage} from '@backstage/plugin-user-settings'; const AppRoutes = () => ( - } /> - -); + +} +/> +< /Routes> +) +; ``` ### Props **Auth Providers** -By default, the plugin provides a list of configured authentication providers fetched from `app-config.yaml` and displayed in the "Authentication Providers" tab. +By default, the plugin provides a list of configured authentication providers fetched from `app-config.yaml` and +displayed in the "Authentication Providers" tab. If you want to supply your own custom list of Authentication Providers, use the `providerSettings` prop: ```ts const MyAuthProviders = () => ( - - {someAction} - + + {someAction} < /ListItemSecondaryAction> + < /ListItem> ); const AppRoutes = () => ( } />} - /> - -); + path = "/settings" +element = { < SettingsRouter +providerSettings = { < MyAuthProviders / > +} +/>} +/> +< /Routes> +) +; ``` > **Note that the list of providers expects to be rendered within a MUI [``](https://material-ui.com/components/lists/)** @@ -70,10 +81,9 @@ const AppRoutes = () => ( By default, the plugin renders 3 tabs of settings; GENERAL, AUTHENTICATION PROVIDERS, and FEATURE FLAGS. -If you want to add more options for your users, -just pass the extra tabs using `UserSettingsTab` components as children of the `UserSettingsPage` route. -The path is in this case a child of the settings path, -in the example below it would be `/settings/advanced` so that you can easily link to it. +If you want to add more options for your users, just pass the extra tabs using `UserSettingsTab` components as children +of the `UserSettingsPage` route. The path is in this case a child of the settings path, in the example below it would +be `/settings/advanced` so that you can easily link to it. ```tsx import { @@ -81,55 +91,55 @@ import { UserSettingsTab, } from '@backstage/plugin-user-settings'; -}> +}> - + ; ``` -To standardize the UI of all setting tabs, -make sure you use a similar component structure as the other tabs. -You can take a look at +To standardize the UI of all setting tabs, make sure you use a similar component structure as the other tabs. You can +take a look at [the example extra tab](https://github.com/backstage/backstage/blob/master/packages/app/src/components/advancedSettings/AdvancedSettings.tsx) we have created in Backstage's demo app. To change the layout altogether, create a custom page in `packages/app/src/components/user-settings/SettingsPage.tsx`: + ```typescript jsx import React from 'react'; import { SettingsLayout, UserSettingsGeneral, } from '@backstage/plugin-user-settings'; -import { AdvancedSettings } from './advancedSettings'; +import {AdvancedSettings} from './advancedSettings'; export const SettingsPage = () => { return ( - + - + ); }; -export const settingsPage = ; +export const settingsPage = ; ``` Now register the new settings page in `packages/app/src/App.tsx`: -```typescript jsx -+ import { settingsPage } from './components/settings/settingsPage'; +```diff ++ import {settingsPage} from './components/settings/settingsPage'; const routes = ( -- } /> -+ }> -+ {settingsPage} -+ + - }/> + + }> + + {settingsPage} + + ); ``` From 8bc63687340e99352ed82ef86a0edcf008df8b6f Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Mon, 31 Oct 2022 17:36:59 +0100 Subject: [PATCH 008/155] Ran Prettier on the README.md externally Signed-off-by: Nikita Karpukhin --- plugins/user-settings/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md index 3d500fbcc3..ce435ee3ce 100644 --- a/plugins/user-settings/README.md +++ b/plugins/user-settings/README.md @@ -91,9 +91,9 @@ import { UserSettingsTab, } from '@backstage/plugin-user-settings'; -}> +}> - + ; ``` @@ -111,22 +111,22 @@ import { SettingsLayout, UserSettingsGeneral, } from '@backstage/plugin-user-settings'; -import {AdvancedSettings} from './advancedSettings'; +import { AdvancedSettings } from './advancedSettings'; export const SettingsPage = () => { return ( - + - + ); }; -export const settingsPage = ; +export const settingsPage = ; ``` Now register the new settings page in `packages/app/src/App.tsx`: From 5d7b14027fad0b241eaaa6eafa48283a1854e675 Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Mon, 31 Oct 2022 18:13:22 +0100 Subject: [PATCH 009/155] Updated api-report.md Signed-off-by: Nikita Karpukhin --- plugins/user-settings/api-report.md | 28 +++++++++++++++++++ .../components/SettingsPage/SettingsPage.tsx | 1 + 2 files changed, 29 insertions(+) diff --git a/plugins/user-settings/api-report.md b/plugins/user-settings/api-report.md index c51d4f992a..1323bb8d7f 100644 --- a/plugins/user-settings/api-report.md +++ b/plugins/user-settings/api-report.md @@ -18,10 +18,12 @@ import { Observable } from '@backstage/types'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SessionApi } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; +import { TabProps } from '@material-ui/core'; // @public (undocumented) export const DefaultProviderSettings: (props: { @@ -42,6 +44,32 @@ export const Router: (props: { providerSettings?: JSX.Element }) => JSX.Element; // @public (undocumented) export const Settings: (props: { icon?: IconComponent }) => JSX.Element; +// @public (undocumented) +export const SettingsLayout: { + (props: SettingsLayoutProps): JSX.Element; + Route: (props: SubRoute) => null; +}; + +// @public (undocumented) +export type SettingsLayoutProps = { + title?: string; + subtitle?: string; + children?: React_2.ReactNode; +}; + +// @public (undocumented) +export type SubRoute = { + path: string; + title: string; + children: JSX.Element; + tabProps?: TabProps< + React_2.ElementType, + { + component?: React_2.ElementType; + } + >; +}; + // @public (undocumented) export const USER_SETTINGS_TAB_KEY = 'user-settings.tab'; diff --git a/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx index 3d1f93aaef..8559bcd8c8 100644 --- a/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx +++ b/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx @@ -22,6 +22,7 @@ import { UserSettingsTabProps, } from '../UserSettingsTab'; +/** @public */ export const SettingsPage = (props: { providerSettings?: JSX.Element }) => { const { providerSettings } = props; const outlet = useOutlet(); From 3207eee5bfcd49b073ddfe5d0331c23b80a8bd7e Mon Sep 17 00:00:00 2001 From: Maximilian Vorbrodt Date: Wed, 2 Nov 2022 11:36:48 +0100 Subject: [PATCH 010/155] prevent scrolling when hovering over graph Signed-off-by: Maximilian Vorbrodt --- .../src/components/DependencyGraph/DependencyGraph.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index 5159f67d9e..4ff68443f4 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -259,6 +259,8 @@ export function DependencyGraph( ); } + container.on('wheel', e => e.preventDefault()); + if (zoom === 'enabled') { enableZoom(); } else if (zoom === 'enable-on-click') { From 11c1c4dc239d3eea677d070a987e7fc598199856 Mon Sep 17 00:00:00 2001 From: Maximilian Vorbrodt Date: Wed, 2 Nov 2022 11:38:57 +0100 Subject: [PATCH 011/155] add background to group diagram Signed-off-by: Maximilian Vorbrodt --- .../GroupsExplorerContent/GroupsDiagram.tsx | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx index 23553b8b76..c366d1c6c1 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx @@ -35,7 +35,7 @@ import { getEntityRelations, } from '@backstage/plugin-catalog-react'; import { BackstageTheme } from '@backstage/theme'; -import { makeStyles, Typography, useTheme } from '@material-ui/core'; +import { makeStyles, Typography, Paper, useTheme } from '@material-ui/core'; import ZoomOutMap from '@material-ui/icons/ZoomOutMap'; import classNames from 'classnames'; import React from 'react'; @@ -46,6 +46,12 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({ flex: 1, minHeight: 0, }, + graphWrapper: { + position: 'relative', + flex: 1, + minHeight: 0, + display: 'flex', + }, organizationNode: { fill: theme.palette.secondary.light, stroke: theme.palette.secondary.light, @@ -62,6 +68,15 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({ justifyContent: 'center', color: 'black', }, + legend: { + position: 'absolute', + bottom: 0, + right: 0, + padding: theme.spacing(1), + '& .icon': { + verticalAlign: 'bottom', + }, + }, textOrganization: { color: theme.palette.secondary.contrastText, }, @@ -222,21 +237,26 @@ export function GroupsDiagram() { return ( <> - - - Use pinch & zoom - to move around the diagram. - + + + + + Use pinch & zoom to move around + the diagram. + + ); } From e169c225a43d05befcde7b1d62a1e3b06ab44dfc Mon Sep 17 00:00:00 2001 From: Maximilian Vorbrodt Date: Wed, 2 Nov 2022 11:40:17 +0100 Subject: [PATCH 012/155] remove max height from graph Signed-off-by: Maximilian Vorbrodt --- .../src/components/DependencyGraph/DependencyGraph.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index 4ff68443f4..3ecdbedddf 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -396,7 +396,6 @@ export function DependencyGraph( ref={containerRef} {...svgProps} width="100%" - height={maxHeight} viewBox={`0 0 ${maxWidth} ${maxHeight}`} > From 9140cb69c32905710f97dcde343ed445d783057d Mon Sep 17 00:00:00 2001 From: Maximilian Vorbrodt Date: Wed, 2 Nov 2022 11:43:20 +0100 Subject: [PATCH 013/155] add changeset Signed-off-by: Maximilian Vorbrodt --- .changeset/cuddly-jeans-laugh.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/cuddly-jeans-laugh.md diff --git a/.changeset/cuddly-jeans-laugh.md b/.changeset/cuddly-jeans-laugh.md new file mode 100644 index 0000000000..6dadb85022 --- /dev/null +++ b/.changeset/cuddly-jeans-laugh.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-explore': patch +--- + +Adds a background to the group diagram and prevents weird scroll/zoom behavior when hovering the graph. With this fix the user can only zoom in and out while hovering the graph. If the cursor is not hovering the graph the user can scroll up and down on the page. In total this creates more consistent scrolling or zooming behavior. From 92a0cbb2f07be3faf9ff39c3fcb3a1ae686ea45d Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Fri, 4 Nov 2022 18:08:45 +0100 Subject: [PATCH 014/155] Fix style in README.md Signed-off-by: Nikita Karpukhin --- plugins/user-settings/README.md | 42 +++++++++++++-------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md index ce435ee3ce..c1b50710f4 100644 --- a/plugins/user-settings/README.md +++ b/plugins/user-settings/README.md @@ -18,30 +18,26 @@ for installation instructions. Add the item to the Sidebar: -```ts +```tsx import {Settings as SidebarSettings} from '@backstage/plugin-user-settings'; - - + + < /SidebarPage>; ``` Add the page to the App routing: -```ts -import {UserSettingsPage} from '@backstage/plugin-user-settings'; +```tsx +import { UserSettingsPage } from '@backstage/plugin-user-settings'; const AppRoutes = () => ( - -} -/> -< /Routes> -) -; + } /> + +); ``` ### Props @@ -53,26 +49,22 @@ displayed in the "Authentication Providers" tab. If you want to supply your own custom list of Authentication Providers, use the `providerSettings` prop: -```ts +```tsx const MyAuthProviders = () => ( - - {someAction} < /ListItemSecondaryAction> - < /ListItem> + + {someAction} + ); const AppRoutes = () => ( -} -/>} -/> -< /Routes> -) -; + path="/settings" + element={} />} + /> + +); ``` > **Note that the list of providers expects to be rendered within a MUI [``](https://material-ui.com/components/lists/)** From 031028c2ac225d51278b3dd5885f4255f6e8ae89 Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Fri, 4 Nov 2022 18:18:29 +0100 Subject: [PATCH 015/155] Revert changes in README.md Signed-off-by: Nikita Karpukhin --- plugins/user-settings/README.md | 37 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md index c1b50710f4..1d15c404e5 100644 --- a/plugins/user-settings/README.md +++ b/plugins/user-settings/README.md @@ -4,14 +4,12 @@ Welcome to the user-settings plugin! ## About the plugin -This plugin provides two components, `` is intended to be used within -the [``](https://backstage.io/storybook/?path=/story/sidebar--sample-sidebar) and displays the signed-in users -profile picture and name. The second component is a settings page where the user can control different settings across -the App. +This plugin provides two components, `` is intended to be used within the [``](https://backstage.io/storybook/?path=/story/sidebar--sample-sidebar) and displays the signed-in users profile picture and name. The second component is a settings page where the user can control different settings across the App. -It also provides a `UserSettingsStorage` implementation of the `StorageApi`, to be used in the frontend as a persistent -alternative to the builtin `WebStorage`. Please -see [the backend README](https://github.com/backstage/backstage/tree/master/plugins/user-settings-backend) +It also provides a `UserSettingsStorage` implementation of the `StorageApi`, to +be used in the frontend as a persistent alternative to the builtin `WebStorage`. +Please see [the backend +README](https://github.com/backstage/backstage/tree/master/plugins/user-settings-backend) for installation instructions. ## Components Usage @@ -19,13 +17,13 @@ for installation instructions. Add the item to the Sidebar: ```tsx -import {Settings as SidebarSettings} from '@backstage/plugin-user-settings'; +import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; - + -< /SidebarPage>; +; ``` Add the page to the App routing: @@ -44,8 +42,7 @@ const AppRoutes = () => ( **Auth Providers** -By default, the plugin provides a list of configured authentication providers fetched from `app-config.yaml` and -displayed in the "Authentication Providers" tab. +By default, the plugin provides a list of configured authentication providers fetched from `app-config.yaml` and displayed in the "Authentication Providers" tab. If you want to supply your own custom list of Authentication Providers, use the `providerSettings` prop: @@ -53,7 +50,7 @@ If you want to supply your own custom list of Authentication Providers, use the const MyAuthProviders = () => ( - {someAction} + {someAction} ); @@ -73,9 +70,10 @@ const AppRoutes = () => ( By default, the plugin renders 3 tabs of settings; GENERAL, AUTHENTICATION PROVIDERS, and FEATURE FLAGS. -If you want to add more options for your users, just pass the extra tabs using `UserSettingsTab` components as children -of the `UserSettingsPage` route. The path is in this case a child of the settings path, in the example below it would -be `/settings/advanced` so that you can easily link to it. +If you want to add more options for your users, +just pass the extra tabs using `UserSettingsTab` components as children of the `UserSettingsPage` route. +The path is in this case a child of the settings path, +in the example below it would be `/settings/advanced` so that you can easily link to it. ```tsx import { @@ -90,14 +88,15 @@ import {
; ``` -To standardize the UI of all setting tabs, make sure you use a similar component structure as the other tabs. You can -take a look at +To standardize the UI of all setting tabs, +make sure you use a similar component structure as the other tabs. +You can take a look at [the example extra tab](https://github.com/backstage/backstage/blob/master/packages/app/src/components/advancedSettings/AdvancedSettings.tsx) we have created in Backstage's demo app. To change the layout altogether, create a custom page in `packages/app/src/components/user-settings/SettingsPage.tsx`: -```typescript jsx +```tsx import React from 'react'; import { SettingsLayout, From 2b35cf99f9aed5932a0e52f312892dea8cdb682c Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Fri, 4 Nov 2022 18:24:17 +0100 Subject: [PATCH 016/155] Fix diff in README.md Signed-off-by: Nikita Karpukhin --- plugins/user-settings/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md index 1d15c404e5..09e095a8e0 100644 --- a/plugins/user-settings/README.md +++ b/plugins/user-settings/README.md @@ -127,10 +127,10 @@ Now register the new settings page in `packages/app/src/App.tsx`: const routes = ( - - }/> - + }> - + {settingsPage} - + +- } /> ++ }> ++ {settingsPage} ++ ); ``` From 17a8e32f39fc359de39fd4fca0da6bf0a4e6e5a2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Nov 2022 12:11:11 +0000 Subject: [PATCH 017/155] Update dependency rc-progress to v3.4.1 Signed-off-by: Renovate Bot --- .changeset/renovate-778b2fa.md | 7 +++++++ packages/core-components/package.json | 2 +- plugins/codescene/package.json | 2 +- plugins/sonarqube/package.json | 2 +- yarn.lock | 20 +++++++++++++++++--- 5 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 .changeset/renovate-778b2fa.md diff --git a/.changeset/renovate-778b2fa.md b/.changeset/renovate-778b2fa.md new file mode 100644 index 0000000000..f4f57e448b --- /dev/null +++ b/.changeset/renovate-778b2fa.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-codescene': patch +'@backstage/plugin-sonarqube': patch +--- + +Updated dependency `rc-progress` to `3.4.1`. diff --git a/packages/core-components/package.json b/packages/core-components/package.json index c7a667e7e2..3aa65efa8d 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -56,7 +56,7 @@ "pluralize": "^8.0.0", "prop-types": "^15.7.2", "qs": "^6.9.4", - "rc-progress": "3.4.0", + "rc-progress": "3.4.1", "react-helmet": "6.1.0", "react-hook-form": "^7.12.2", "react-markdown": "^8.0.0", diff --git a/plugins/codescene/package.json b/plugins/codescene/package.json index 31970219e3..3863b3ef7f 100644 --- a/plugins/codescene/package.json +++ b/plugins/codescene/package.json @@ -30,7 +30,7 @@ "@material-ui/core": "^4.9.10", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.57", - "rc-progress": "3.4.0", + "rc-progress": "3.4.1", "react-use": "^17.2.4" }, "peerDependencies": { diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 2d217ee57c..12567ae189 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -44,7 +44,7 @@ "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/styles": "^4.10.0", "cross-fetch": "^3.1.5", - "rc-progress": "3.4.0", + "rc-progress": "3.4.1", "react-use": "^17.2.4" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index 0802bef2c2..61a3385dfc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3700,7 +3700,7 @@ __metadata: pluralize: ^8.0.0 prop-types: ^15.7.2 qs: ^6.9.4 - rc-progress: 3.4.0 + rc-progress: 3.4.1 react-helmet: 6.1.0 react-hook-form: ^7.12.2 react-markdown: ^8.0.0 @@ -5412,7 +5412,7 @@ __metadata: "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.48.0 - rc-progress: 3.4.0 + rc-progress: 3.4.1 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -7292,7 +7292,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.48.0 - rc-progress: 3.4.0 + rc-progress: 3.4.1 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -33164,6 +33164,20 @@ __metadata: languageName: node linkType: hard +"rc-progress@npm:3.4.1": + version: 3.4.1 + resolution: "rc-progress@npm:3.4.1" + dependencies: + "@babel/runtime": ^7.10.1 + classnames: ^2.2.6 + rc-util: ^5.16.1 + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: d4dce5231ea29bfa866935a59f05473711f9dfc944f95cc78b0fdcd1508a063983cc58973f54c2b20797f0b0f480c4a2b84aaa2ca185158c14800ec659163880 + languageName: node + linkType: hard + "rc-util@npm:^5.16.1": version: 5.16.1 resolution: "rc-util@npm:5.16.1" From 3232e31a23fcc90553dfa02aa7f5ff02bba4a1df Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 26 Oct 2022 14:30:10 +0200 Subject: [PATCH 018/155] backend: Add support for lifecycle events Signed-off-by: Johan Haals --- packages/backend-app-api/api-report.md | 6 ++ .../src/services/implementations/index.ts | 1 + .../implementations/lifecycleService.ts | 81 +++++++++++++++++++ .../backend-defaults/src/CreateBackend.ts | 2 + packages/backend-plugin-api/api-report.md | 13 +++ .../src/services/definitions/index.ts | 5 ++ .../definitions/lifecycleServiceRef.ts | 42 ++++++++++ .../src/service/CatalogPlugin.ts | 9 ++- 8 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 packages/backend-app-api/src/services/implementations/lifecycleService.ts create mode 100644 packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 4d9fbea4a5..5d200be93c 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -4,6 +4,7 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; +import { BackendLifecycle } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { HttpRouterService } from '@backstage/backend-plugin-api'; @@ -66,6 +67,11 @@ export type HttpRouterFactoryOptions = { indexPlugin?: string; }; +// @public (undocumented) +export const lifecycleFactory: ( + options?: undefined, +) => ServiceFactory; + // @public (undocumented) export const loggerFactory: (options?: undefined) => ServiceFactory; diff --git a/packages/backend-app-api/src/services/implementations/index.ts b/packages/backend-app-api/src/services/implementations/index.ts index 608399bad9..2c26097f48 100644 --- a/packages/backend-app-api/src/services/implementations/index.ts +++ b/packages/backend-app-api/src/services/implementations/index.ts @@ -25,4 +25,5 @@ export { schedulerFactory } from './schedulerService'; export { tokenManagerFactory } from './tokenManagerService'; export { urlReaderFactory } from './urlReaderService'; export { httpRouterFactory } from './httpRouterService'; +export { lifecycleFactory } from './lifecycleService'; export type { HttpRouterFactoryOptions } from './httpRouterService'; diff --git a/packages/backend-app-api/src/services/implementations/lifecycleService.ts b/packages/backend-app-api/src/services/implementations/lifecycleService.ts new file mode 100644 index 0000000000..3549ec31f2 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/lifecycleService.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + BackendLifecycle, + createServiceFactory, + lifecycleServiceRef, + loggerToWinstonLogger, + pluginMetadataServiceRef, + rootLoggerServiceRef, + ShutdownHookOptions, +} from '@backstage/backend-plugin-api'; +import { Logger } from 'winston'; + +class BackendLifecycleImpl { + constructor(private readonly logger: Logger) {} + #shutdownTasks: Array = []; + + addShutdownHook(options: ShutdownHookOptions & { pluginId: string }): void { + this.#shutdownTasks.push(options); + } + + async shutdown(): Promise { + await Promise.all( + this.#shutdownTasks.map(hook => + hook + .fn() + .catch(e => { + this.logger.error( + `Shutdown hook registered by plugin '${hook.pluginId}' failed with: ${e}`, + ); + }) + .then(() => + this.logger.debug( + `Successfully ran hook registered by plugin ${hook.pluginId}`, + ), + ), + ), + ); + } +} + +class PluginScopedLifecycleImpl implements BackendLifecycle { + constructor( + private readonly lifecycle: BackendLifecycleImpl, + private readonly pluginId: string, + ) {} + addShutdownHook(options: ShutdownHookOptions): void { + this.lifecycle.addShutdownHook({ ...options, pluginId: this.pluginId }); + } +} + +/** @public */ +export const lifecycleFactory = createServiceFactory({ + service: lifecycleServiceRef, + deps: { + logger: rootLoggerServiceRef, + plugin: pluginMetadataServiceRef, + }, + async factory({ logger }) { + const rootLifecycle = new BackendLifecycleImpl( + loggerToWinstonLogger(logger), + ); + process.on('SIGTERM', async () => await rootLifecycle.shutdown()); + return async ({ plugin }) => { + return new PluginScopedLifecycleImpl(rootLifecycle, plugin.getId()); + }; + }, +}); diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index b95bd6f35b..9fde31e3bd 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -22,6 +22,7 @@ import { databaseFactory, discoveryFactory, httpRouterFactory, + lifecycleFactory, loggerFactory, permissionsFactory, rootLoggerFactory, @@ -43,6 +44,7 @@ export const defaultServiceFactories = [ tokenManagerFactory, urlReaderFactory, httpRouterFactory, + lifecycleFactory, ]; /** diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 70ab168053..0183707735 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -24,6 +24,11 @@ export interface BackendFeature { register(reg: BackendRegistrationPoints): void; } +// @public (undocumented) +export interface BackendLifecycle { + addShutdownHook(options: ShutdownHookOptions): void; +} + // @public (undocumented) export interface BackendModuleConfig { // (undocumented) @@ -158,6 +163,9 @@ export interface HttpRouterService { // @public (undocumented) export const httpRouterServiceRef: ServiceRef; +// @public (undocumented) +export const lifecycleServiceRef: ServiceRef; + // @public (undocumented) export interface Logger { // (undocumented) @@ -235,6 +243,11 @@ export type ServiceRef< $$ref: 'service'; }; +// @public (undocumented) +export type ShutdownHookOptions = { + fn: () => Promise; +}; + // @public (undocumented) export const tokenManagerServiceRef: ServiceRef; diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index e5f032ef60..19f75ffeb5 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -28,4 +28,9 @@ export { permissionsServiceRef } from './permissionsServiceRef'; export { schedulerServiceRef } from './schedulerServiceRef'; export { rootLoggerServiceRef } from './rootLoggerServiceRef'; export { pluginMetadataServiceRef } from './pluginMetadataServiceRef'; +export { lifecycleServiceRef } from './lifecycleServiceRef'; +export type { + BackendLifecycle, + ShutdownHookOptions, +} from './lifecycleServiceRef'; export type { PluginMetadata } from './pluginMetadataServiceRef'; diff --git a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts new file mode 100644 index 0000000000..ffdd4c414b --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createServiceRef } from '../system/types'; + +/** + * @public + **/ +export type ShutdownHookOptions = { + fn: () => Promise; +}; + +/** + * @public + **/ +export interface BackendLifecycle { + /** + * Register a function to be called and when the backend is shutting down. + */ + addShutdownHook(options: ShutdownHookOptions): void; +} + +/** + * @public + */ +export const lifecycleServiceRef = createServiceRef({ + id: 'core.lifecycle', + scope: 'plugin', +}); diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 64e1e52494..84cad86d9a 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -22,6 +22,7 @@ import { permissionsServiceRef, urlReaderServiceRef, httpRouterServiceRef, + lifecycleServiceRef, } from '@backstage/backend-plugin-api'; import { CatalogBuilder } from './CatalogBuilder'; import { @@ -78,6 +79,7 @@ export const catalogPlugin = createBackendPlugin({ permissions: permissionsServiceRef, database: databaseServiceRef, httpRouter: httpRouterServiceRef, + lifecycle: lifecycleServiceRef, }, async init({ logger, @@ -86,6 +88,7 @@ export const catalogPlugin = createBackendPlugin({ database, permissions, httpRouter, + lifecycle, }) { const winstonLogger = loggerToWinstonLogger(logger); const builder = await CatalogBuilder.create({ @@ -100,7 +103,11 @@ export const catalogPlugin = createBackendPlugin({ const { processingEngine, router } = await builder.build(); await processingEngine.start(); - + lifecycle.addShutdownHook({ + fn: async () => { + await processingEngine.stop(); + }, + }); httpRouter.use(router); }, }); From 7cdc99a002bf257876f1b2b6ab014872d1c16dd8 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 9 Nov 2022 15:33:16 +0100 Subject: [PATCH 019/155] Update packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Johan Haals Signed-off-by: Johan Haals --- .../src/services/definitions/lifecycleServiceRef.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts index ffdd4c414b..3e8dd31fd1 100644 --- a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts +++ b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts @@ -28,7 +28,7 @@ export type ShutdownHookOptions = { **/ export interface BackendLifecycle { /** - * Register a function to be called and when the backend is shutting down. + * Register a function to be called when the backend is shutting down. */ addShutdownHook(options: ShutdownHookOptions): void; } From 678a1583039c01b5afe3d9c2b8bea32975c637f1 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 9 Nov 2022 16:02:10 +0100 Subject: [PATCH 020/155] chore: Rename options, add additional logging Signed-off-by: Johan Haals --- .../src/services/implementations/lifecycleService.ts | 12 ++++++++---- .../src/services/definitions/index.ts | 2 +- .../src/services/definitions/lifecycleServiceRef.ts | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/lifecycleService.ts b/packages/backend-app-api/src/services/implementations/lifecycleService.ts index 3549ec31f2..578bd9ca01 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycleService.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycleService.ts @@ -20,19 +20,23 @@ import { loggerToWinstonLogger, pluginMetadataServiceRef, rootLoggerServiceRef, - ShutdownHookOptions, + BackendLifecycleShutdownHook, } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; class BackendLifecycleImpl { constructor(private readonly logger: Logger) {} - #shutdownTasks: Array = []; + #shutdownTasks: Array = + []; - addShutdownHook(options: ShutdownHookOptions & { pluginId: string }): void { + addShutdownHook( + options: BackendLifecycleShutdownHook & { pluginId: string }, + ): void { this.#shutdownTasks.push(options); } async shutdown(): Promise { + this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); await Promise.all( this.#shutdownTasks.map(hook => hook @@ -57,7 +61,7 @@ class PluginScopedLifecycleImpl implements BackendLifecycle { private readonly lifecycle: BackendLifecycleImpl, private readonly pluginId: string, ) {} - addShutdownHook(options: ShutdownHookOptions): void { + addShutdownHook(options: BackendLifecycleShutdownHook): void { this.lifecycle.addShutdownHook({ ...options, pluginId: this.pluginId }); } } diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 19f75ffeb5..cf997a344d 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -31,6 +31,6 @@ export { pluginMetadataServiceRef } from './pluginMetadataServiceRef'; export { lifecycleServiceRef } from './lifecycleServiceRef'; export type { BackendLifecycle, - ShutdownHookOptions, + BackendLifecycleShutdownHook, } from './lifecycleServiceRef'; export type { PluginMetadata } from './pluginMetadataServiceRef'; diff --git a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts index 3e8dd31fd1..4fd9f8daae 100644 --- a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts +++ b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts @@ -19,7 +19,7 @@ import { createServiceRef } from '../system/types'; /** * @public **/ -export type ShutdownHookOptions = { +export type BackendLifecycleShutdownHook = { fn: () => Promise; }; @@ -30,7 +30,7 @@ export interface BackendLifecycle { /** * Register a function to be called when the backend is shutting down. */ - addShutdownHook(options: ShutdownHookOptions): void; + addShutdownHook(options: BackendLifecycleShutdownHook): void; } /** From d0c09089a85eaa5fd33874550a8ed8af4a8c0996 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 10 Nov 2022 15:16:30 +0100 Subject: [PATCH 021/155] Add support for non async hooks Signed-off-by: Johan Haals --- .../src/services/implementations/lifecycleService.ts | 4 ++-- .../src/services/definitions/lifecycleServiceRef.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/lifecycleService.ts b/packages/backend-app-api/src/services/implementations/lifecycleService.ts index 578bd9ca01..81f1b16b53 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycleService.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycleService.ts @@ -39,8 +39,8 @@ class BackendLifecycleImpl { this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); await Promise.all( this.#shutdownTasks.map(hook => - hook - .fn() + Promise.resolve() + .then(() => hook.fn()) .catch(e => { this.logger.error( `Shutdown hook registered by plugin '${hook.pluginId}' failed with: ${e}`, diff --git a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts index 4fd9f8daae..15610e2643 100644 --- a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts +++ b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts @@ -20,7 +20,7 @@ import { createServiceRef } from '../system/types'; * @public **/ export type BackendLifecycleShutdownHook = { - fn: () => Promise; + fn: () => void | Promise; }; /** From 263283c7bd2e5f7197a6b6b53c25b0af99e560ee Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 10 Nov 2022 15:31:42 +0100 Subject: [PATCH 022/155] update api docs Signed-off-by: Johan Haals --- packages/backend-plugin-api/api-report.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 0183707735..b73a85c32e 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -26,9 +26,14 @@ export interface BackendFeature { // @public (undocumented) export interface BackendLifecycle { - addShutdownHook(options: ShutdownHookOptions): void; + addShutdownHook(options: BackendLifecycleShutdownHook): void; } +// @public (undocumented) +export type BackendLifecycleShutdownHook = { + fn: () => void | Promise; +}; + // @public (undocumented) export interface BackendModuleConfig { // (undocumented) @@ -243,11 +248,6 @@ export type ServiceRef< $$ref: 'service'; }; -// @public (undocumented) -export type ShutdownHookOptions = { - fn: () => Promise; -}; - // @public (undocumented) export const tokenManagerServiceRef: ServiceRef; From f3ac47b6c61dc0e2517dcf9b6b7ec26eaf335da6 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 10 Nov 2022 16:27:07 +0100 Subject: [PATCH 023/155] Handle multiple signals, prevent concurrent calls Signed-off-by: Johan Haals --- .../implementations/lifecycleService.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/lifecycleService.ts b/packages/backend-app-api/src/services/implementations/lifecycleService.ts index 81f1b16b53..0ee0946469 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycleService.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycleService.ts @@ -24,8 +24,13 @@ import { } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; +const CALLBACKS = ['SIGTERM', 'SIGINT', 'beforeExit']; class BackendLifecycleImpl { - constructor(private readonly logger: Logger) {} + constructor(private readonly logger: Logger) { + CALLBACKS.map(signal => process.on(signal, () => this.shutdown())); + } + + #isCalled = false; #shutdownTasks: Array = []; @@ -36,6 +41,11 @@ class BackendLifecycleImpl { } async shutdown(): Promise { + if (this.#isCalled) { + return; + } + this.#isCalled = true; + this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); await Promise.all( this.#shutdownTasks.map(hook => @@ -47,8 +57,8 @@ class BackendLifecycleImpl { ); }) .then(() => - this.logger.debug( - `Successfully ran hook registered by plugin ${hook.pluginId}`, + this.logger.info( + `Successfully ran shutdown hook registered by plugin ${hook.pluginId}`, ), ), ), @@ -77,7 +87,6 @@ export const lifecycleFactory = createServiceFactory({ const rootLifecycle = new BackendLifecycleImpl( loggerToWinstonLogger(logger), ); - process.on('SIGTERM', async () => await rootLifecycle.shutdown()); return async ({ plugin }) => { return new PluginScopedLifecycleImpl(rootLifecycle, plugin.getId()); }; From fde99df4c736fe14fe5125402b1488d56bfb6c7f Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Thu, 10 Nov 2022 17:19:57 +0100 Subject: [PATCH 024/155] Rename SubRoute to SettingsLayoutRouteProps Signed-off-by: Nikita Karpukhin --- plugins/user-settings/api-report.md | 4 ++-- .../src/components/SettingsLayout/SettingsLayout.tsx | 6 +++--- .../user-settings/src/components/SettingsLayout/index.ts | 5 ++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/plugins/user-settings/api-report.md b/plugins/user-settings/api-report.md index 1323bb8d7f..6167e343d6 100644 --- a/plugins/user-settings/api-report.md +++ b/plugins/user-settings/api-report.md @@ -47,7 +47,7 @@ export const Settings: (props: { icon?: IconComponent }) => JSX.Element; // @public (undocumented) export const SettingsLayout: { (props: SettingsLayoutProps): JSX.Element; - Route: (props: SubRoute) => null; + Route: (props: SettingsLayoutRouteProps) => null; }; // @public (undocumented) @@ -58,7 +58,7 @@ export type SettingsLayoutProps = { }; // @public (undocumented) -export type SubRoute = { +export type SettingsLayoutRouteProps = { path: string; title: string; children: JSX.Element; diff --git a/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx b/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx index 23394819d6..c120c87122 100644 --- a/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx +++ b/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx @@ -28,7 +28,7 @@ import { } from '@backstage/core-plugin-api'; /** @public */ -export type SubRoute = { +export type SettingsLayoutRouteProps = { path: string; title: string; children: JSX.Element; @@ -37,7 +37,7 @@ export type SubRoute = { const dataKey = 'plugin.user-settings.settingsLayoutRoute'; -const Route: (props: SubRoute) => null = () => null; +const Route: (props: SettingsLayoutRouteProps) => null = () => null; attachComponentData(Route, dataKey, true); // This causes all mount points that are discovered within this route to use the path of the route itself @@ -64,7 +64,7 @@ export const SettingsLayout = (props: SettingsLayoutProps) => { withStrictError: 'Child of SettingsLayout must be an SettingsLayout.Route', }) - .getElements() + .getElements() .map(child => child.props), ); diff --git a/plugins/user-settings/src/components/SettingsLayout/index.ts b/plugins/user-settings/src/components/SettingsLayout/index.ts index 36a5633c94..6e610f8078 100644 --- a/plugins/user-settings/src/components/SettingsLayout/index.ts +++ b/plugins/user-settings/src/components/SettingsLayout/index.ts @@ -14,5 +14,8 @@ * limitations under the License. */ -export type { SettingsLayoutProps, SubRoute } from './SettingsLayout'; +export type { + SettingsLayoutProps, + SettingsLayoutRouteProps, +} from './SettingsLayout'; export { SettingsLayout } from './SettingsLayout'; From c9bbb32e0b67fa80d86ce3154050532398d099dd Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Thu, 10 Nov 2022 17:25:00 +0100 Subject: [PATCH 025/155] Update README Signed-off-by: Nikita Karpukhin --- plugins/user-settings/README.md | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md index 09e095a8e0..1549b8de65 100644 --- a/plugins/user-settings/README.md +++ b/plugins/user-settings/README.md @@ -104,20 +104,16 @@ import { } from '@backstage/plugin-user-settings'; import { AdvancedSettings } from './advancedSettings'; -export const SettingsPage = () => { - return ( - - - - - - - - - ); -}; - -export const settingsPage = ; +export const settingsPage = () => ( + + + + + + + + +); ``` Now register the new settings page in `packages/app/src/App.tsx`: From cf79f4b68a6431ed55f9fae168f5d6346504972c Mon Sep 17 00:00:00 2001 From: Michael Stergianis Date: Thu, 10 Nov 2022 17:21:58 -0500 Subject: [PATCH 026/155] docs: Replaces use of CatalogResultListItem with correct symbol I copied the docs and noticed that the example didn't compile. According to the changelog this is the fix Signed-off-by: Michael Stergianis --- docs/features/search/getting-started.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index 71b9c276d9..726d1557d0 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -68,7 +68,7 @@ export const searchPage = ( switch (result.type) { case 'software-catalog': return ( - { It's good practice for search results to highlight information that was used to return it in the first place! The code below highlights how you might specify a -custom result item component, using the `` component as +custom result item component, using the `` component as an example: ```tsx {7-13} @@ -265,7 +265,7 @@ an example: switch (result.type) { case 'software-catalog': return ( - Date: Thu, 10 Nov 2022 11:37:41 -0600 Subject: [PATCH 027/155] have code follow eslint rules Signed-off-by: Mohamed Mohamed --- .../ExampleComponent/ExampleComponent.test.tsx.hbs | 5 +++-- .../ExampleFetchComponent.test.tsx.hbs | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index ccbebcac40..ae626c0d57 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -4,6 +4,7 @@ import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import { screen } from '@testing-library/react'; import { setupRequestMockHandlers, renderInTestApp, @@ -22,11 +23,11 @@ describe('ExampleComponent', () => { }); it('should render', async () => { - const rendered = await renderInTestApp( + await renderInTestApp( , ); - expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument(); + expect(screen.getByText('Welcome to {{ id }}!')).toBeInTheDocument(); }); }); diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index 95533c899d..0dded1dcc2 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -1,10 +1,11 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { ExampleFetchComponent } from './ExampleFetchComponent'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/test-utils'; + describe('ExampleFetchComponent', () => { const server = setupServer(); // Enable sane handlers for network requests @@ -19,7 +20,7 @@ describe('ExampleFetchComponent', () => { ); }); it('should render', async () => { - const rendered = render(); - expect(await rendered.findByTestId('progress')).toBeInTheDocument(); + await render(); + expect(await screen.findByTestId('progress')).toBeInTheDocument(); }); }); From ddd1c3308d4d838a78342d5167365a5c6e054d57 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Wed, 5 Oct 2022 13:58:43 -0400 Subject: [PATCH 028/155] feat(scaffolder): implement custom field explorer Signed-off-by: Phil Kuang --- .changeset/soft-nails-arrive.md | 5 + plugins/scaffolder/api-report.md | 332 ++++++++++++++---- plugins/scaffolder/package.json | 4 +- .../CustomFieldExplorer.tsx | 201 +++++++++++ .../TemplateEditorIntro.tsx | 19 +- .../TemplateEditorPage/TemplateEditorPage.tsx | 13 + .../EntityNamePicker/EntityNamePicker.tsx | 5 +- .../fields/EntityNamePicker/schema.ts | 30 ++ .../fields/EntityPicker/EntityPicker.tsx | 19 +- .../components/fields/EntityPicker/index.ts | 5 +- .../components/fields/EntityPicker/schema.ts | 63 ++++ .../EntityTagsPicker/EntityTagsPicker.tsx | 21 +- .../fields/EntityTagsPicker/index.ts | 5 +- .../fields/EntityTagsPicker/schema.ts | 56 +++ .../OwnedEntityPicker/OwnedEntityPicker.tsx | 23 +- .../fields/OwnedEntityPicker/index.ts | 5 +- .../fields/OwnedEntityPicker/schema.ts | 67 ++++ .../fields/OwnerPicker/OwnerPicker.tsx | 18 +- .../components/fields/OwnerPicker/index.ts | 5 +- .../components/fields/OwnerPicker/schema.ts | 60 ++++ .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 29 +- .../components/fields/RepoUrlPicker/index.ts | 5 +- .../components/fields/RepoUrlPicker/schema.ts | 101 ++++++ plugins/scaffolder/src/extensions/default.ts | 36 +- plugins/scaffolder/src/extensions/index.tsx | 2 + plugins/scaffolder/src/extensions/types.ts | 16 +- plugins/scaffolder/src/index.ts | 1 + plugins/scaffolder/src/plugin.ts | 36 +- yarn.lock | 2 + 29 files changed, 1026 insertions(+), 158 deletions(-) create mode 100644 .changeset/soft-nails-arrive.md create mode 100644 plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx create mode 100644 plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts create mode 100644 plugins/scaffolder/src/components/fields/EntityPicker/schema.ts create mode 100644 plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts create mode 100644 plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts create mode 100644 plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts diff --git a/.changeset/soft-nails-arrive.md b/.changeset/soft-nails-arrive.md new file mode 100644 index 0000000000..e9c0a40a05 --- /dev/null +++ b/.changeset/soft-nails-arrive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Implement Custom Field Explorer to view and play around with available installed custom field extensions diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 158c75e5bd..69512e7af0 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -35,6 +35,7 @@ import { TaskStep } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { UIOptionsType } from '@rjsf/utils'; import { UiSchema } from '@rjsf/utils'; +import { z } from 'zod'; // @alpha export function createNextScaffolderFieldExtension< @@ -57,6 +58,12 @@ export function createScaffolderLayout( options: LayoutOptions, ): Extension>; +// @public +export type CustomFieldExtensionSchema = { + uiOptions?: JSONSchema7; + returnValue?: JSONSchema7; +}; + // @public export type CustomFieldValidator = ( data: TFieldReturnValue, @@ -75,36 +82,78 @@ export const EntityNamePickerFieldExtension: FieldExtensionComponent< // @public export const EntityPickerFieldExtension: FieldExtensionComponent< string, - EntityPickerUiOptions + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } >; // @public -export interface EntityPickerUiOptions { - // (undocumented) - allowArbitraryValues?: boolean; - // (undocumented) - allowedKinds?: string[]; - // (undocumented) - defaultKind?: string; - // (undocumented) - defaultNamespace?: string | false; -} +export type EntityPickerUiOptions = z.infer; + +// @public (undocumented) +export const EntityPickerUiOptionsSchema: z.ZodObject< + { + allowedKinds: z.ZodOptional>; + defaultKind: z.ZodOptional; + allowArbitraryValues: z.ZodOptional; + defaultNamespace: z.ZodOptional< + z.ZodUnion<[z.ZodString, z.ZodLiteral]> + >; + }, + 'strip', + z.ZodTypeAny, + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + }, + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } +>; // @public export const EntityTagsPickerFieldExtension: FieldExtensionComponent< string[], - EntityTagsPickerUiOptions + { + showCounts?: boolean | undefined; + kinds?: string[] | undefined; + helperText?: string | undefined; + } >; // @public -export interface EntityTagsPickerUiOptions { - // (undocumented) - helperText?: string; - // (undocumented) - kinds?: string[]; - // (undocumented) - showCounts?: boolean; -} +export type EntityTagsPickerUiOptions = z.infer< + typeof EntityTagsPickerUiOptionsSchema +>; + +// @public (undocumented) +export const EntityTagsPickerUiOptionsSchema: z.ZodObject< + { + kinds: z.ZodOptional>; + showCounts: z.ZodOptional; + helperText: z.ZodOptional; + }, + 'strip', + z.ZodTypeAny, + { + showCounts?: boolean | undefined; + kinds?: string[] | undefined; + helperText?: string | undefined; + }, + { + showCounts?: boolean | undefined; + kinds?: string[] | undefined; + helperText?: string | undefined; + } +>; // @public export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; @@ -130,6 +179,7 @@ export type FieldExtensionOptions< props: FieldExtensionComponentProps, ) => JSX.Element | null; validation?: CustomFieldValidator; + schema?: CustomFieldExtensionSchema; }; // @public @@ -199,6 +249,7 @@ export type NextFieldExtensionOptions< props: NextFieldExtensionComponentProps, ) => JSX.Element | null; validation?: NextCustomFieldValidator; + schema?: CustomFieldExtensionSchema; }; // @alpha (undocumented) @@ -228,36 +279,80 @@ export const nextSelectedTemplateRouteRef: SubRouteRef< // @public export const OwnedEntityPickerFieldExtension: FieldExtensionComponent< string, - OwnedEntityPickerUiOptions + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } >; // @public -export interface OwnedEntityPickerUiOptions { - // (undocumented) - allowArbitraryValues?: boolean; - // (undocumented) - allowedKinds?: string[]; - // (undocumented) - defaultKind?: string; - // (undocumented) - defaultNamespace?: string | false; -} +export type OwnedEntityPickerUiOptions = z.infer< + typeof OwnedEntityPickerUiOptionsSchema +>; + +// @public (undocumented) +export const OwnedEntityPickerUiOptionsSchema: z.ZodObject< + { + allowedKinds: z.ZodOptional>; + defaultKind: z.ZodOptional; + allowArbitraryValues: z.ZodOptional; + defaultNamespace: z.ZodOptional< + z.ZodUnion<[z.ZodString, z.ZodLiteral]> + >; + }, + 'strip', + z.ZodTypeAny, + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + }, + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } +>; // @public export const OwnerPickerFieldExtension: FieldExtensionComponent< string, - OwnerPickerUiOptions + { + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } >; // @public -export interface OwnerPickerUiOptions { - // (undocumented) - allowArbitraryValues?: boolean; - // (undocumented) - allowedKinds?: string[]; - // (undocumented) - defaultNamespace?: string | false; -} +export type OwnerPickerUiOptions = z.infer; + +// @public (undocumented) +export const OwnerPickerUiOptionsSchema: z.ZodObject< + { + allowedKinds: z.ZodOptional>>; + allowArbitraryValues: z.ZodOptional; + defaultNamespace: z.ZodOptional< + z.ZodUnion<[z.ZodString, z.ZodLiteral]> + >; + }, + 'strip', + z.ZodTypeAny, + { + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + }, + { + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } +>; // @public export const repoPickerValidation: ( @@ -271,31 +366,144 @@ export const repoPickerValidation: ( // @public export const RepoUrlPickerFieldExtension: FieldExtensionComponent< string, - RepoUrlPickerUiOptions + { + allowedOwners?: string[] | undefined; + allowedOrganizations?: string[] | undefined; + allowedRepos?: string[] | undefined; + allowedHosts?: string[] | undefined; + requestUserCredentials?: + | { + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + | undefined; + secretsKey: string; + } + | undefined; + } >; // @public -export interface RepoUrlPickerUiOptions { - // (undocumented) - allowedHosts?: string[]; - // (undocumented) - allowedOrganizations?: string[]; - // (undocumented) - allowedOwners?: string[]; - // (undocumented) - allowedRepos?: string[]; - // (undocumented) - requestUserCredentials?: { - secretsKey: string; - additionalScopes?: { - gerrit?: string[]; - github?: string[]; - gitlab?: string[]; - bitbucket?: string[]; - azure?: string[]; - }; - }; -} +export type RepoUrlPickerUiOptions = z.infer< + typeof RepoUrlPickerUiOptionsSchema +>; + +// @public (undocumented) +export const RepoUrlPickerUiOptionsSchema: z.ZodObject< + { + allowedHosts: z.ZodOptional>; + allowedOrganizations: z.ZodOptional>; + allowedOwners: z.ZodOptional>; + allowedRepos: z.ZodOptional>; + requestUserCredentials: z.ZodOptional< + z.ZodObject< + { + secretsKey: z.ZodString; + additionalScopes: z.ZodOptional< + z.ZodObject< + { + gerrit: z.ZodOptional>; + github: z.ZodOptional>; + gitlab: z.ZodOptional>; + bitbucket: z.ZodOptional>; + azure: z.ZodOptional>; + }, + 'strip', + z.ZodTypeAny, + { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + }, + { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + > + >; + }, + 'strip', + z.ZodTypeAny, + { + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + | undefined; + secretsKey: string; + }, + { + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + | undefined; + secretsKey: string; + } + > + >; + }, + 'strip', + z.ZodTypeAny, + { + allowedOwners?: string[] | undefined; + allowedOrganizations?: string[] | undefined; + allowedRepos?: string[] | undefined; + allowedHosts?: string[] | undefined; + requestUserCredentials?: + | { + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + | undefined; + secretsKey: string; + } + | undefined; + }, + { + allowedOwners?: string[] | undefined; + allowedOrganizations?: string[] | undefined; + allowedRepos?: string[] | undefined; + allowedHosts?: string[] | undefined; + requestUserCredentials?: + | { + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + | undefined; + secretsKey: string; + } + | undefined; + } +>; // @public (undocumented) export const rootRouteRef: RouteRef; diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 214351e2b7..07ad33dd03 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -74,7 +74,9 @@ "react-use": "^17.2.4", "use-immer": "^0.7.0", "yaml": "^2.0.0", - "zen-observable": "^0.8.15" + "zen-observable": "^0.8.15", + "zod": "^3.11.6", + "zod-to-json-schema": "^3.18.1" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx new file mode 100644 index 0000000000..3510968acd --- /dev/null +++ b/plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx @@ -0,0 +1,201 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { StreamLanguage } from '@codemirror/language'; +import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml'; +import { + Button, + Card, + CardContent, + CardHeader, + FormControl, + IconButton, + InputLabel, + makeStyles, + MenuItem, + Select, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import { withTheme } from '@rjsf/core'; +import { Theme as MuiTheme } from '@rjsf/material-ui'; +import CodeMirror from '@uiw/react-codemirror'; +import React, { useCallback, useMemo, useState } from 'react'; +import yaml from 'yaml'; +import { FieldExtensionOptions } from '../../extensions'; +import * as fieldOverrides from '../MultistepJsonForm/FieldOverrides'; +import { TemplateEditorForm } from './TemplateEditorForm'; + +const Form = withTheme(MuiTheme); + +const useStyles = makeStyles(theme => ({ + root: { + gridArea: 'pageContent', + display: 'grid', + gridTemplateAreas: ` + "controls controls" + "fieldForm preview" + `, + gridTemplateRows: 'auto 1fr', + gridTemplateColumns: '1fr 1fr', + }, + controls: { + gridArea: 'controls', + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + margin: theme.spacing(1), + }, + fieldForm: { + gridArea: 'fieldForm', + }, + preview: { + gridArea: 'preview', + }, +})); + +export const CustomFieldExplorer = ({ + customFieldExtensions = [], + onClose, +}: { + customFieldExtensions?: FieldExtensionOptions[]; + onClose?: () => void; +}) => { + const classes = useStyles(); + const fieldOptions = customFieldExtensions.filter(field => !!field.schema); + const [selectedField, setSelectedField] = useState(fieldOptions[0]); + const [fieldFormState, setFieldFormState] = useState({}); + const [formState, setFormState] = useState({}); + const [refreshKey, setRefreshKey] = useState(Date.now()); + const sampleFieldTemplate = useMemo( + () => + yaml.stringify({ + parameters: [ + { + title: `${selectedField.name} Example`, + properties: { + [selectedField.name]: { + type: selectedField.schema?.returnValue?.type, + 'ui:field': selectedField.name, + 'ui:options': fieldFormState, + }, + }, + }, + ], + }), + [fieldFormState, selectedField], + ); + + const fieldComponents = useMemo(() => { + return Object.fromEntries( + customFieldExtensions.map(({ name, component }) => [name, component]), + ); + }, [customFieldExtensions]); + + const handleSelectionChange = useCallback( + selection => { + setSelectedField(selection); + setFieldFormState({}); + setFormState({}); + }, + [setFieldFormState, setFormState, setSelectedField], + ); + + const handleFieldConfigChange = useCallback( + state => { + setFieldFormState(state); + setFormState({}); + // Force TemplateEditorForm to re-render since some fields + // may not be responsive to ui:option changes + setRefreshKey(Date.now()); + }, + [setFieldFormState, setRefreshKey], + ); + + return ( +
+
+ + + Choose Custom Field Extension + + + + + + + +
+
+ + + +
handleFieldConfigChange(e.formData)} + schema={selectedField.schema?.uiOptions || {}} + > + +
+
+
+
+
+ + + + + + + null} + /> +
+
+ ); +}; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorIntro.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorIntro.tsx index dc411ee965..61e6c46964 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorIntro.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorIntro.tsx @@ -44,7 +44,7 @@ const useStyles = makeStyles(theme => ({ interface EditorIntroProps { style?: JSX.IntrinsicElements['div']['style']; - onSelect?: (option: 'local' | 'form') => void; + onSelect?: (option: 'local' | 'form' | 'field-explorer') => void; } export function TemplateEditorIntro(props: EditorIntroProps) { @@ -104,6 +104,22 @@ export function TemplateEditorIntro(props: EditorIntroProps) { ); + const cardFieldExplorer = ( + + props.onSelect?.('field-explorer')}> + + + Custom Field Explorer + + + View and play around with available installed custom field + extensions. + + + + + ); + return (
@@ -121,6 +137,7 @@ export function TemplateEditorIntro(props: EditorIntroProps) { {supportsLoad && cardLoadLocal} {cardFormEditor} {!supportsLoad && cardLoadLocal} + {cardFieldExplorer}
); diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx index b539b6403b..8847b85373 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx @@ -19,6 +19,7 @@ import { TemplateDirectoryAccess, WebFileSystemAccess, } from '../../lib/filesystem'; +import { CustomFieldExplorer } from './CustomFieldExplorer'; import { TemplateEditorIntro } from './TemplateEditorIntro'; import { TemplateEditor } from './TemplateEditor'; import { TemplateFormPreviewer } from './TemplateFormPreviewer'; @@ -32,6 +33,9 @@ type Selection = } | { type: 'form'; + } + | { + type: 'field-explorer'; }; interface TemplateEditorPageProps { @@ -62,6 +66,13 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) { layouts={props.layouts} /> ); + } else if (selection?.type === 'field-explorer') { + content = ( + setSelection(undefined)} + /> + ); } else { content = ( @@ -73,6 +84,8 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) { .catch(() => {}); } else if (option === 'form') { setSelection({ type: 'form' }); + } else if (option === 'field-explorer') { + setSelection({ type: 'field-explorer' }); } }} /> diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx b/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx index 802135a863..eba05cfb92 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx @@ -15,13 +15,16 @@ */ import React from 'react'; import { FieldExtensionComponentProps } from '../../../extensions'; +import { EntityNamePickerReturnValue } from './schema'; import { TextField } from '@material-ui/core'; +export { EntityNamePickerSchema } from './schema'; + /** * EntityName Picker */ export const EntityNamePicker = ( - props: FieldExtensionComponentProps, + props: FieldExtensionComponentProps, ) => { const { onChange, diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts new file mode 100644 index 0000000000..45aa4502ce --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +const EntityNamePickerReturnValueSchema = z.string(); + +export type EntityNamePickerReturnValue = z.infer< + typeof EntityNamePickerReturnValueSchema +>; + +export const EntityNamePickerSchema = { + returnValue: zodToJsonSchema( + EntityNamePickerReturnValueSchema, + ) as JSONSchema7, +}; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index e7883a4721..8fcc412d0a 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -24,19 +24,9 @@ import Autocomplete from '@material-ui/lab/Autocomplete'; import React, { useCallback, useEffect } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { FieldExtensionComponentProps } from '../../../extensions'; +import { EntityPickerReturnValue, EntityPickerUiOptions } from './schema'; -/** - * The input props that can be specified under `ui:options` for the - * `EntityPicker` field extension. - * - * @public - */ -export interface EntityPickerUiOptions { - allowedKinds?: string[]; - defaultKind?: string; - allowArbitraryValues?: boolean; - defaultNamespace?: string | false; -} +export { EntityPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `EntityPicker` @@ -45,7 +35,10 @@ export interface EntityPickerUiOptions { * @public */ export const EntityPicker = ( - props: FieldExtensionComponentProps, + props: FieldExtensionComponentProps< + EntityPickerReturnValue, + EntityPickerUiOptions + >, ) => { const { onChange, diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts index 891b5bef16..d1044e4e67 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts @@ -13,4 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { EntityPickerUiOptions } from './EntityPicker'; +export { + type EntityPickerUiOptions, + EntityPickerUiOptionsSchema, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts new file mode 100644 index 0000000000..5f7d66fc4f --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +/** + * @public + */ +export const EntityPickerUiOptionsSchema = z.object({ + allowedKinds: z + .array(z.string()) + .optional() + .describe('List of kinds of entities to derive options from'), + defaultKind: z + .string() + .optional() + .describe( + 'The default entity kind. Options of this kind will not be prefixed.', + ), + allowArbitraryValues: z + .boolean() + .optional() + .describe('Whether to allow arbitrary user input. Defaults to true'), + defaultNamespace: z + .union([z.string(), z.literal(false)]) + .optional() + .describe( + 'The default namespace. Options with this namespace will not be prefixed.', + ), +}); + +const EntityPickerReturnValueSchema = z.string(); + +/** + * The input props that can be specified under `ui:options` for the + * `EntityPicker` field extension. + * + * @public + */ +export type EntityPickerUiOptions = z.infer; + +export type EntityPickerReturnValue = z.infer< + typeof EntityPickerReturnValueSchema +>; + +export const EntityPickerSchema = { + uiOptions: zodToJsonSchema(EntityPickerUiOptionsSchema) as JSONSchema7, + returnValue: zodToJsonSchema(EntityPickerReturnValueSchema) as JSONSchema7, +}; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx index c0e3aab436..6d5404cee1 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx @@ -23,18 +23,12 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { FormControl, TextField } from '@material-ui/core'; import { Autocomplete } from '@material-ui/lab'; import { FieldExtensionComponentProps } from '../../../extensions'; +import { + EntityTagsPickerReturnValue, + EntityTagsPickerUiOptions, +} from './schema'; -/** - * The input props that can be specified under `ui:options` for the - * `EntityTagsPicker` field extension. - * - * @public - */ -export interface EntityTagsPickerUiOptions { - kinds?: string[]; - showCounts?: boolean; - helperText?: string; -} +export { EntityTagsPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `EntityTagsPicker` @@ -43,7 +37,10 @@ export interface EntityTagsPickerUiOptions { * @public */ export const EntityTagsPicker = ( - props: FieldExtensionComponentProps, + props: FieldExtensionComponentProps< + EntityTagsPickerReturnValue, + EntityTagsPickerUiOptions + >, ) => { const { formData, onChange, uiSchema } = props; const catalogApi = useApi(catalogApiRef); diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts index 9ff6e553a6..850fc17c8e 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts @@ -13,4 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { EntityTagsPickerUiOptions } from './EntityTagsPicker'; +export { + type EntityTagsPickerUiOptions, + EntityTagsPickerUiOptionsSchema, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts new file mode 100644 index 0000000000..ff477903d7 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +/** + * @public + */ +export const EntityTagsPickerUiOptionsSchema = z.object({ + kinds: z + .array(z.string()) + .optional() + .describe('List of kinds of entities to derive tags from'), + showCounts: z + .boolean() + .optional() + .describe('Whether to show usage counts per tag'), + helperText: z.string().optional().describe('Helper text to display'), +}); + +const EntityTagsPickerReturnValueSchema = z.array(z.string()); + +/** + * The input props that can be specified under `ui:options` for the + * `EntityTagsPicker` field extension. + * + * @public + */ +export type EntityTagsPickerUiOptions = z.infer< + typeof EntityTagsPickerUiOptionsSchema +>; + +export type EntityTagsPickerReturnValue = z.infer< + typeof EntityTagsPickerReturnValueSchema +>; + +export const EntityTagsPickerSchema = { + uiOptions: zodToJsonSchema(EntityTagsPickerUiOptionsSchema) as JSONSchema7, + returnValue: zodToJsonSchema( + EntityTagsPickerReturnValueSchema, + ) as JSONSchema7, +}; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx index 2446883a86..47bcb6c9b8 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx @@ -27,20 +27,12 @@ import React, { useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { FieldExtensionComponentProps } from '../../../extensions'; +import { + OwnedEntityPickerReturnValue, + OwnedEntityPickerUiOptions, +} from './schema'; -/** - * The input props that can be specified under `ui:options` for the - * `OwnedEntityPicker` field extension. - * - * @public - */ -export interface OwnedEntityPickerUiOptions { - allowedKinds?: string[]; - defaultKind?: string; - allowArbitraryValues?: boolean; - defaultNamespace?: string | false; -} - +export { OwnedEntityPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `OwnedEntityPicker` * field extension. @@ -48,7 +40,10 @@ export interface OwnedEntityPickerUiOptions { * @public */ export const OwnedEntityPicker = ( - props: FieldExtensionComponentProps, + props: FieldExtensionComponentProps< + OwnedEntityPickerReturnValue, + OwnedEntityPickerUiOptions + >, ) => { const { onChange, diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts index 2988ba8cdc..0101eb8845 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts @@ -13,4 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { OwnedEntityPickerUiOptions } from './OwnedEntityPicker'; +export { + type OwnedEntityPickerUiOptions, + OwnedEntityPickerUiOptionsSchema, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts new file mode 100644 index 0000000000..c260fae3cd --- /dev/null +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +/** + * @public + */ +export const OwnedEntityPickerUiOptionsSchema = z.object({ + allowedKinds: z + .array(z.string()) + .optional() + .describe('List of kinds of entities to derive options from'), + defaultKind: z + .string() + .optional() + .describe( + 'The default entity kind. Options of this kind will not be prefixed.', + ), + allowArbitraryValues: z + .boolean() + .optional() + .describe('Whether to allow arbitrary user input. Defaults to true'), + defaultNamespace: z + .union([z.string(), z.literal(false)]) + .optional() + .describe( + 'The default namespace. Options with this namespace will not be prefixed.', + ), +}); + +const OwnedEntityPickerReturnValueSchema = z.string(); + +/** + * The input props that can be specified under `ui:options` for the + * `OwnedEntityPicker` field extension. + * + * @public + */ +export type OwnedEntityPickerUiOptions = z.infer< + typeof OwnedEntityPickerUiOptionsSchema +>; + +export type OwnedEntityPickerReturnValue = z.infer< + typeof OwnedEntityPickerReturnValueSchema +>; + +export const OwnedEntityPickerSchema = { + uiOptions: zodToJsonSchema(OwnedEntityPickerUiOptionsSchema) as JSONSchema7, + returnValue: zodToJsonSchema( + OwnedEntityPickerReturnValueSchema, + ) as JSONSchema7, +}; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx index 3de2b12bd0..10333684d0 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx @@ -16,18 +16,9 @@ import React from 'react'; import { EntityPicker } from '../EntityPicker/EntityPicker'; import { FieldExtensionComponentProps } from '../../../extensions'; +import { OwnerPickerReturnValue, OwnerPickerUiOptions } from './schema'; -/** - * The input props that can be specified under `ui:options` for the - * `OwnerPicker` field extension. - * - * @public - */ -export interface OwnerPickerUiOptions { - allowedKinds?: string[]; - allowArbitraryValues?: boolean; - defaultNamespace?: string | false; -} +export { OwnerPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `OwnerPicker` @@ -36,7 +27,10 @@ export interface OwnerPickerUiOptions { * @public */ export const OwnerPicker = ( - props: FieldExtensionComponentProps, + props: FieldExtensionComponentProps< + OwnerPickerReturnValue, + OwnerPickerUiOptions + >, ) => { const { schema: { title = 'Owner', description = 'The owner of the component' }, diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts index aa26024b5b..5d436358b3 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts @@ -13,4 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { OwnerPickerUiOptions } from './OwnerPicker'; +export { + type OwnerPickerUiOptions, + OwnerPickerUiOptionsSchema, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts new file mode 100644 index 0000000000..634a39ddee --- /dev/null +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +/** + * @public + */ +export const OwnerPickerUiOptionsSchema = z.object({ + allowedKinds: z + .array(z.string()) + .default(['Group', 'User']) + .optional() + .describe( + 'List of kinds of entities to derive options from. Defaults to Group and User', + ), + allowArbitraryValues: z + .boolean() + .optional() + .describe('Whether to allow arbitrary user input. Defaults to true'), + defaultNamespace: z + .union([z.string(), z.literal(false)]) + .optional() + .describe( + 'The default namespace. Options with this namespace will not be prefixed.', + ), +}); + +const OwnerPickerReturnValueSchema = z.string(); + +/** + * The input props that can be specified under `ui:options` for the + * `OwnerPicker` field extension. + * + * @public + */ +export type OwnerPickerUiOptions = z.infer; + +export type OwnerPickerReturnValue = z.infer< + typeof OwnerPickerReturnValueSchema +>; + +export const OwnerPickerSchema = { + uiOptions: zodToJsonSchema(OwnerPickerUiOptionsSchema) as JSONSchema7, + returnValue: zodToJsonSchema(OwnerPickerReturnValueSchema) as JSONSchema7, +}; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 451df857e5..8aca07e995 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -28,32 +28,12 @@ import { FieldExtensionComponentProps } from '../../../extensions'; import { RepoUrlPickerHost } from './RepoUrlPickerHost'; import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName'; import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils'; +import { RepoUrlPickerReturnValue, RepoUrlPickerUiOptions } from './schema'; import { RepoUrlPickerState } from './types'; import useDebounce from 'react-use/lib/useDebounce'; import { useTemplateSecrets } from '../../secrets'; -/** - * The input props that can be specified under `ui:options` for the - * `RepoUrlPicker` field extension. - * - * @public - */ -export interface RepoUrlPickerUiOptions { - allowedHosts?: string[]; - allowedOrganizations?: string[]; - allowedOwners?: string[]; - allowedRepos?: string[]; - requestUserCredentials?: { - secretsKey: string; - additionalScopes?: { - gerrit?: string[]; - github?: string[]; - gitlab?: string[]; - bitbucket?: string[]; - azure?: string[]; - }; - }; -} +export { RepoUrlPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `RepoUrlPicker` @@ -62,7 +42,10 @@ export interface RepoUrlPickerUiOptions { * @public */ export const RepoUrlPicker = ( - props: FieldExtensionComponentProps, + props: FieldExtensionComponentProps< + RepoUrlPickerReturnValue, + RepoUrlPickerUiOptions + >, ) => { const { uiSchema, onChange, rawErrors, formData } = props; const [state, setState] = useState( diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts index c5f596a786..34124d0ccd 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts @@ -13,5 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { RepoUrlPickerUiOptions } from './RepoUrlPicker'; +export { + type RepoUrlPickerUiOptions, + RepoUrlPickerUiOptionsSchema, +} from './schema'; export { repoPickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts new file mode 100644 index 0000000000..b870747fd4 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts @@ -0,0 +1,101 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +/** + * @public + */ +export const RepoUrlPickerUiOptionsSchema = z.object({ + allowedHosts: z + .array(z.string()) + .optional() + .describe('List of allowed SCM platform hosts'), + allowedOrganizations: z + .array(z.string()) + .optional() + .describe('List of allowed organizations in the given SCM platform'), + allowedOwners: z + .array(z.string()) + .optional() + .describe('List of allowed owners in the given SCM platform'), + allowedRepos: z + .array(z.string()) + .optional() + .describe('List of allowed repos in the given SCM platform'), + requestUserCredentials: z + .object({ + secretsKey: z + .string() + .describe( + 'Key used within the template secrets context to store the credential', + ), + additionalScopes: z + .object({ + gerrit: z + .array(z.string()) + .optional() + .describe('Additional Gerrit scopes to request'), + github: z + .array(z.string()) + .optional() + .describe('Additional GitHub scopes to request'), + gitlab: z + .array(z.string()) + .optional() + .describe('Additional GitLab scopes to request'), + bitbucket: z + .array(z.string()) + .optional() + .describe('Additional BitBucket scopes to request'), + azure: z + .array(z.string()) + .optional() + .describe('Additional Azure scopes to request'), + }) + .optional() + .describe('Additional permission scopes to request'), + }) + .optional() + .describe( + 'If defined will request user credentials to auth against the given SCM platform', + ), +}); + +const RepoUrlPickerReturnValueSchema = z.string(); + +/** + * The input props that can be specified under `ui:options` for the + * `RepoUrlPicker` field extension. + * + * @public + */ +export type RepoUrlPickerUiOptions = z.infer< + typeof RepoUrlPickerUiOptionsSchema +>; + +export type RepoUrlPickerReturnValue = z.infer< + typeof RepoUrlPickerReturnValueSchema +>; + +// NOTE: There is a bug with this failing validation in the custom field explorer due +// to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if +// requestUserCredentials is not defined +export const RepoUrlPickerSchema = { + uiOptions: zodToJsonSchema(RepoUrlPickerUiOptionsSchema) as JSONSchema7, + returnValue: zodToJsonSchema(RepoUrlPickerReturnValueSchema) as JSONSchema7, +}; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 8081dcb0ca..c6ab5276d0 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -13,40 +13,64 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityPicker } from '../components/fields/EntityPicker/EntityPicker'; -import { EntityNamePicker } from '../components/fields/EntityNamePicker/EntityNamePicker'; +import { + EntityPicker, + EntityPickerSchema, +} from '../components/fields/EntityPicker/EntityPicker'; +import { + EntityNamePicker, + EntityNamePickerSchema, +} from '../components/fields/EntityNamePicker/EntityNamePicker'; import { entityNamePickerValidation } from '../components/fields/EntityNamePicker/validation'; -import { EntityTagsPicker } from '../components/fields/EntityTagsPicker/EntityTagsPicker'; -import { OwnerPicker } from '../components/fields/OwnerPicker/OwnerPicker'; -import { RepoUrlPicker } from '../components/fields/RepoUrlPicker/RepoUrlPicker'; +import { + EntityTagsPicker, + EntityTagsPickerSchema, +} from '../components/fields/EntityTagsPicker/EntityTagsPicker'; +import { + OwnerPicker, + OwnerPickerSchema, +} from '../components/fields/OwnerPicker/OwnerPicker'; +import { + RepoUrlPicker, + RepoUrlPickerSchema, +} from '../components/fields/RepoUrlPicker/RepoUrlPicker'; import { repoPickerValidation } from '../components/fields/RepoUrlPicker/validation'; -import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; +import { + OwnedEntityPicker, + OwnedEntityPickerSchema, +} from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { component: EntityPicker, name: 'EntityPicker', + schema: EntityPickerSchema, }, { component: EntityNamePicker, name: 'EntityNamePicker', validation: entityNamePickerValidation, + schema: EntityNamePickerSchema, }, { component: EntityTagsPicker, name: 'EntityTagsPicker', + schema: EntityTagsPickerSchema, }, { component: RepoUrlPicker, name: 'RepoUrlPicker', validation: repoPickerValidation, + schema: RepoUrlPickerSchema, }, { component: OwnerPicker, name: 'OwnerPicker', + schema: OwnerPickerSchema, }, { component: OwnedEntityPicker, name: 'OwnedEntityPicker', + schema: OwnedEntityPickerSchema, }, ]; diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index 08075b5b9d..72f17edc20 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { + CustomFieldExtensionSchema, CustomFieldValidator, FieldExtensionOptions, FieldExtensionComponentProps, @@ -103,6 +104,7 @@ attachComponentData( ); export type { + CustomFieldExtensionSchema, CustomFieldValidator, FieldExtensionOptions, FieldExtensionComponentProps, diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder/src/extensions/types.ts index 62f17dff15..b15de56fc1 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder/src/extensions/types.ts @@ -15,14 +15,14 @@ */ import { ApiHolder } from '@backstage/core-plugin-api'; import { FieldValidation, FieldProps } from '@rjsf/core'; -import { PropsWithChildren } from 'react'; - import { UIOptionsType, FieldProps as FieldPropsV5, UiSchema as UiSchemaV5, FieldValidation as FieldValidationV5, } from '@rjsf/utils'; +import { PropsWithChildren } from 'react'; +import { JSONSchema7 } from 'json-schema'; /** * Field validation type for Custom Field Extensions. @@ -35,6 +35,16 @@ export type CustomFieldValidator = ( context: { apiHolder: ApiHolder }, ) => void | Promise; +/** + * Type for the Custom Field Extension schema. + * + * @public + */ +export type CustomFieldExtensionSchema = { + uiOptions?: JSONSchema7; + returnValue?: JSONSchema7; +}; + /** * Type for the Custom Field Extension with the * name and components and validation function. @@ -50,6 +60,7 @@ export type FieldExtensionOptions< props: FieldExtensionComponentProps, ) => JSX.Element | null; validation?: CustomFieldValidator; + schema?: CustomFieldExtensionSchema; }; /** @@ -107,4 +118,5 @@ export type NextFieldExtensionOptions< props: NextFieldExtensionComponentProps, ) => JSX.Element | null; validation?: NextCustomFieldValidator; + schema?: CustomFieldExtensionSchema; }; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 7853667101..0e5e2630ac 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -43,6 +43,7 @@ export { ScaffolderFieldExtensions, } from './extensions'; export type { + CustomFieldExtensionSchema, CustomFieldValidator, FieldExtensionOptions, FieldExtensionComponentProps, diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index f9b359d93a..a34105745f 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -16,12 +16,24 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { scaffolderApiRef, ScaffolderClient } from './api'; -import { EntityPicker } from './components/fields/EntityPicker/EntityPicker'; +import { + EntityPicker, + EntityPickerSchema, +} from './components/fields/EntityPicker/EntityPicker'; import { entityNamePickerValidation } from './components/fields/EntityNamePicker'; -import { EntityNamePicker } from './components/fields/EntityNamePicker/EntityNamePicker'; -import { OwnerPicker } from './components/fields/OwnerPicker/OwnerPicker'; +import { + EntityNamePicker, + EntityNamePickerSchema, +} from './components/fields/EntityNamePicker/EntityNamePicker'; +import { + OwnerPicker, + OwnerPickerSchema, +} from './components/fields/OwnerPicker/OwnerPicker'; import { repoPickerValidation } from './components/fields/RepoUrlPicker'; -import { RepoUrlPicker } from './components/fields/RepoUrlPicker/RepoUrlPicker'; +import { + RepoUrlPicker, + RepoUrlPickerSchema, +} from './components/fields/RepoUrlPicker/RepoUrlPicker'; import { createScaffolderFieldExtension } from './extensions'; import { nextRouteRef, @@ -37,8 +49,14 @@ import { fetchApiRef, identityApiRef, } from '@backstage/core-plugin-api'; -import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker/OwnedEntityPicker'; -import { EntityTagsPicker } from './components/fields/EntityTagsPicker/EntityTagsPicker'; +import { + OwnedEntityPicker, + OwnedEntityPickerSchema, +} from './components/fields/OwnedEntityPicker/OwnedEntityPicker'; +import { + EntityTagsPicker, + EntityTagsPickerSchema, +} from './components/fields/EntityTagsPicker/EntityTagsPicker'; /** * The main plugin export for the scaffolder. @@ -82,6 +100,7 @@ export const EntityPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: EntityPicker, name: 'EntityPicker', + schema: EntityPickerSchema, }), ); @@ -95,6 +114,7 @@ export const EntityNamePickerFieldExtension = scaffolderPlugin.provide( component: EntityNamePicker, name: 'EntityNamePicker', validation: entityNamePickerValidation, + schema: EntityNamePickerSchema, }), ); @@ -109,6 +129,7 @@ export const RepoUrlPickerFieldExtension = scaffolderPlugin.provide( component: RepoUrlPicker, name: 'RepoUrlPicker', validation: repoPickerValidation, + schema: RepoUrlPickerSchema, }), ); @@ -121,6 +142,7 @@ export const OwnerPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: OwnerPicker, name: 'OwnerPicker', + schema: OwnerPickerSchema, }), ); @@ -146,6 +168,7 @@ export const OwnedEntityPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: OwnedEntityPicker, name: 'OwnedEntityPicker', + schema: OwnedEntityPickerSchema, }), ); @@ -157,6 +180,7 @@ export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: EntityTagsPicker, name: 'EntityTagsPicker', + schema: EntityTagsPickerSchema, }), ); diff --git a/yarn.lock b/yarn.lock index acc503697d..eb5d0ffe16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6998,6 +6998,8 @@ __metadata: use-immer: ^0.7.0 yaml: ^2.0.0 zen-observable: ^0.8.15 + zod: ^3.11.6 + zod-to-json-schema: ^3.18.1 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 react: ^16.13.1 || ^17.0.0 From aa4438e3415b16c595b2c4ab032956dd1b032284 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Wed, 9 Nov 2022 14:43:13 -0500 Subject: [PATCH 029/155] refactor(scaffolder): abstract away zod schema types Signed-off-by: Phil Kuang --- plugins/scaffolder/api-report.md | 212 +++--------------- .../fields/EntityNamePicker/schema.ts | 14 +- .../components/fields/EntityPicker/schema.ts | 63 +++--- .../fields/EntityTagsPicker/schema.ts | 47 ++-- .../fields/OwnedEntityPicker/schema.ts | 67 +++--- .../components/fields/OwnerPicker/schema.ts | 56 ++--- .../components/fields/RepoUrlPicker/schema.ts | 131 ++++++----- .../scaffolder/src/components/fields/utils.ts | 34 +++ 8 files changed, 253 insertions(+), 371 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/utils.ts diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 69512e7af0..e56b280c05 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -35,7 +35,6 @@ import { TaskStep } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { UIOptionsType } from '@rjsf/utils'; import { UiSchema } from '@rjsf/utils'; -import { z } from 'zod'; // @alpha export function createNextScaffolderFieldExtension< @@ -91,33 +90,19 @@ export const EntityPickerFieldExtension: FieldExtensionComponent< >; // @public -export type EntityPickerUiOptions = z.infer; +export type EntityPickerUiOptions = + typeof EntityPickerUiOptionsSchema.schemaType; // @public (undocumented) -export const EntityPickerUiOptionsSchema: z.ZodObject< - { - allowedKinds: z.ZodOptional>; - defaultKind: z.ZodOptional; - allowArbitraryValues: z.ZodOptional; - defaultNamespace: z.ZodOptional< - z.ZodUnion<[z.ZodString, z.ZodLiteral]> - >; - }, - 'strip', - z.ZodTypeAny, - { +export const EntityPickerUiOptionsSchema: { + jsonSchema: JSONSchema7; + schemaType: { defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; - }, - { - defaultKind?: string | undefined; - defaultNamespace?: string | false | undefined; - allowedKinds?: string[] | undefined; - allowArbitraryValues?: boolean | undefined; - } ->; + }; +}; // @public export const EntityTagsPickerFieldExtension: FieldExtensionComponent< @@ -130,30 +115,18 @@ export const EntityTagsPickerFieldExtension: FieldExtensionComponent< >; // @public -export type EntityTagsPickerUiOptions = z.infer< - typeof EntityTagsPickerUiOptionsSchema ->; +export type EntityTagsPickerUiOptions = + typeof EntityTagsPickerUiOptionsSchema.schemaType; // @public (undocumented) -export const EntityTagsPickerUiOptionsSchema: z.ZodObject< - { - kinds: z.ZodOptional>; - showCounts: z.ZodOptional; - helperText: z.ZodOptional; - }, - 'strip', - z.ZodTypeAny, - { +export const EntityTagsPickerUiOptionsSchema: { + jsonSchema: JSONSchema7; + schemaType: { showCounts?: boolean | undefined; kinds?: string[] | undefined; helperText?: string | undefined; - }, - { - showCounts?: boolean | undefined; - kinds?: string[] | undefined; - helperText?: string | undefined; - } ->; + }; +}; // @public export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; @@ -288,35 +261,19 @@ export const OwnedEntityPickerFieldExtension: FieldExtensionComponent< >; // @public -export type OwnedEntityPickerUiOptions = z.infer< - typeof OwnedEntityPickerUiOptionsSchema ->; +export type OwnedEntityPickerUiOptions = + typeof OwnedEntityPickerUiOptionsSchema.schemaType; // @public (undocumented) -export const OwnedEntityPickerUiOptionsSchema: z.ZodObject< - { - allowedKinds: z.ZodOptional>; - defaultKind: z.ZodOptional; - allowArbitraryValues: z.ZodOptional; - defaultNamespace: z.ZodOptional< - z.ZodUnion<[z.ZodString, z.ZodLiteral]> - >; - }, - 'strip', - z.ZodTypeAny, - { +export const OwnedEntityPickerUiOptionsSchema: { + jsonSchema: JSONSchema7; + schemaType: { defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; - }, - { - defaultKind?: string | undefined; - defaultNamespace?: string | false | undefined; - allowedKinds?: string[] | undefined; - allowArbitraryValues?: boolean | undefined; - } ->; + }; +}; // @public export const OwnerPickerFieldExtension: FieldExtensionComponent< @@ -329,30 +286,17 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent< >; // @public -export type OwnerPickerUiOptions = z.infer; +export type OwnerPickerUiOptions = typeof OwnerPickerUiOptionsSchema.schemaType; // @public (undocumented) -export const OwnerPickerUiOptionsSchema: z.ZodObject< - { - allowedKinds: z.ZodOptional>>; - allowArbitraryValues: z.ZodOptional; - defaultNamespace: z.ZodOptional< - z.ZodUnion<[z.ZodString, z.ZodLiteral]> - >; - }, - 'strip', - z.ZodTypeAny, - { +export const OwnerPickerUiOptionsSchema: { + jsonSchema: JSONSchema7; + schemaType: { defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; - }, - { - defaultNamespace?: string | false | undefined; - allowedKinds?: string[] | undefined; - allowArbitraryValues?: boolean | undefined; - } ->; + }; +}; // @public export const repoPickerValidation: ( @@ -389,81 +333,13 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent< >; // @public -export type RepoUrlPickerUiOptions = z.infer< - typeof RepoUrlPickerUiOptionsSchema ->; +export type RepoUrlPickerUiOptions = + typeof RepoUrlPickerUiOptionsSchema.schemaType; // @public (undocumented) -export const RepoUrlPickerUiOptionsSchema: z.ZodObject< - { - allowedHosts: z.ZodOptional>; - allowedOrganizations: z.ZodOptional>; - allowedOwners: z.ZodOptional>; - allowedRepos: z.ZodOptional>; - requestUserCredentials: z.ZodOptional< - z.ZodObject< - { - secretsKey: z.ZodString; - additionalScopes: z.ZodOptional< - z.ZodObject< - { - gerrit: z.ZodOptional>; - github: z.ZodOptional>; - gitlab: z.ZodOptional>; - bitbucket: z.ZodOptional>; - azure: z.ZodOptional>; - }, - 'strip', - z.ZodTypeAny, - { - azure?: string[] | undefined; - github?: string[] | undefined; - gitlab?: string[] | undefined; - bitbucket?: string[] | undefined; - gerrit?: string[] | undefined; - }, - { - azure?: string[] | undefined; - github?: string[] | undefined; - gitlab?: string[] | undefined; - bitbucket?: string[] | undefined; - gerrit?: string[] | undefined; - } - > - >; - }, - 'strip', - z.ZodTypeAny, - { - additionalScopes?: - | { - azure?: string[] | undefined; - github?: string[] | undefined; - gitlab?: string[] | undefined; - bitbucket?: string[] | undefined; - gerrit?: string[] | undefined; - } - | undefined; - secretsKey: string; - }, - { - additionalScopes?: - | { - azure?: string[] | undefined; - github?: string[] | undefined; - gitlab?: string[] | undefined; - bitbucket?: string[] | undefined; - gerrit?: string[] | undefined; - } - | undefined; - secretsKey: string; - } - > - >; - }, - 'strip', - z.ZodTypeAny, - { +export const RepoUrlPickerUiOptionsSchema: { + jsonSchema: JSONSchema7; + schemaType: { allowedOwners?: string[] | undefined; allowedOrganizations?: string[] | undefined; allowedRepos?: string[] | undefined; @@ -482,28 +358,8 @@ export const RepoUrlPickerUiOptionsSchema: z.ZodObject< secretsKey: string; } | undefined; - }, - { - allowedOwners?: string[] | undefined; - allowedOrganizations?: string[] | undefined; - allowedRepos?: string[] | undefined; - allowedHosts?: string[] | undefined; - requestUserCredentials?: - | { - additionalScopes?: - | { - azure?: string[] | undefined; - github?: string[] | undefined; - gitlab?: string[] | undefined; - bitbucket?: string[] | undefined; - gerrit?: string[] | undefined; - } - | undefined; - secretsKey: string; - } - | undefined; - } ->; + }; +}; // @public (undocumented) export const rootRouteRef: RouteRef; diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts index 45aa4502ce..83e477b6aa 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts @@ -13,18 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JSONSchema7 } from 'json-schema'; import { z } from 'zod'; -import zodToJsonSchema from 'zod-to-json-schema'; +import { makeJsonSchemaFromZod } from '../utils'; -const EntityNamePickerReturnValueSchema = z.string(); +const EntityNamePickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); -export type EntityNamePickerReturnValue = z.infer< - typeof EntityNamePickerReturnValueSchema ->; +export type EntityNamePickerReturnValue = + typeof EntityNamePickerReturnValueSchema.schemaType; export const EntityNamePickerSchema = { - returnValue: zodToJsonSchema( - EntityNamePickerReturnValueSchema, - ) as JSONSchema7, + returnValue: EntityNamePickerReturnValueSchema.jsonSchema, }; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts index 5f7d66fc4f..3e05264a14 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts @@ -13,37 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JSONSchema7 } from 'json-schema'; import { z } from 'zod'; -import zodToJsonSchema from 'zod-to-json-schema'; +import { makeJsonSchemaFromZod } from '../utils'; /** * @public */ -export const EntityPickerUiOptionsSchema = z.object({ - allowedKinds: z - .array(z.string()) - .optional() - .describe('List of kinds of entities to derive options from'), - defaultKind: z - .string() - .optional() - .describe( - 'The default entity kind. Options of this kind will not be prefixed.', - ), - allowArbitraryValues: z - .boolean() - .optional() - .describe('Whether to allow arbitrary user input. Defaults to true'), - defaultNamespace: z - .union([z.string(), z.literal(false)]) - .optional() - .describe( - 'The default namespace. Options with this namespace will not be prefixed.', - ), -}); +export const EntityPickerUiOptionsSchema = makeJsonSchemaFromZod( + z.object({ + allowedKinds: z + .array(z.string()) + .optional() + .describe('List of kinds of entities to derive options from'), + defaultKind: z + .string() + .optional() + .describe( + 'The default entity kind. Options of this kind will not be prefixed.', + ), + allowArbitraryValues: z + .boolean() + .optional() + .describe('Whether to allow arbitrary user input. Defaults to true'), + defaultNamespace: z + .union([z.string(), z.literal(false)]) + .optional() + .describe( + 'The default namespace. Options with this namespace will not be prefixed.', + ), + }), +); -const EntityPickerReturnValueSchema = z.string(); +const EntityPickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); /** * The input props that can be specified under `ui:options` for the @@ -51,13 +52,13 @@ const EntityPickerReturnValueSchema = z.string(); * * @public */ -export type EntityPickerUiOptions = z.infer; +export type EntityPickerUiOptions = + typeof EntityPickerUiOptionsSchema.schemaType; -export type EntityPickerReturnValue = z.infer< - typeof EntityPickerReturnValueSchema ->; +export type EntityPickerReturnValue = + typeof EntityPickerReturnValueSchema.schemaType; export const EntityPickerSchema = { - uiOptions: zodToJsonSchema(EntityPickerUiOptionsSchema) as JSONSchema7, - returnValue: zodToJsonSchema(EntityPickerReturnValueSchema) as JSONSchema7, + uiOptions: EntityPickerUiOptionsSchema.jsonSchema, + returnValue: EntityPickerReturnValueSchema.jsonSchema, }; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts index ff477903d7..e08918f838 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts @@ -13,26 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JSONSchema7 } from 'json-schema'; import { z } from 'zod'; -import zodToJsonSchema from 'zod-to-json-schema'; +import { makeJsonSchemaFromZod } from '../utils'; /** * @public */ -export const EntityTagsPickerUiOptionsSchema = z.object({ - kinds: z - .array(z.string()) - .optional() - .describe('List of kinds of entities to derive tags from'), - showCounts: z - .boolean() - .optional() - .describe('Whether to show usage counts per tag'), - helperText: z.string().optional().describe('Helper text to display'), -}); +export const EntityTagsPickerUiOptionsSchema = makeJsonSchemaFromZod( + z.object({ + kinds: z + .array(z.string()) + .optional() + .describe('List of kinds of entities to derive tags from'), + showCounts: z + .boolean() + .optional() + .describe('Whether to show usage counts per tag'), + helperText: z.string().optional().describe('Helper text to display'), + }), +); -const EntityTagsPickerReturnValueSchema = z.array(z.string()); +const EntityTagsPickerReturnValueSchema = makeJsonSchemaFromZod( + z.array(z.string()), +); /** * The input props that can be specified under `ui:options` for the @@ -40,17 +43,13 @@ const EntityTagsPickerReturnValueSchema = z.array(z.string()); * * @public */ -export type EntityTagsPickerUiOptions = z.infer< - typeof EntityTagsPickerUiOptionsSchema ->; +export type EntityTagsPickerUiOptions = + typeof EntityTagsPickerUiOptionsSchema.schemaType; -export type EntityTagsPickerReturnValue = z.infer< - typeof EntityTagsPickerReturnValueSchema ->; +export type EntityTagsPickerReturnValue = + typeof EntityTagsPickerReturnValueSchema.schemaType; export const EntityTagsPickerSchema = { - uiOptions: zodToJsonSchema(EntityTagsPickerUiOptionsSchema) as JSONSchema7, - returnValue: zodToJsonSchema( - EntityTagsPickerReturnValueSchema, - ) as JSONSchema7, + uiOptions: EntityTagsPickerUiOptionsSchema.jsonSchema, + returnValue: EntityTagsPickerReturnValueSchema.jsonSchema, }; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts index c260fae3cd..e5bfcd9709 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts @@ -13,37 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JSONSchema7 } from 'json-schema'; import { z } from 'zod'; -import zodToJsonSchema from 'zod-to-json-schema'; +import { makeJsonSchemaFromZod } from '../utils'; /** * @public */ -export const OwnedEntityPickerUiOptionsSchema = z.object({ - allowedKinds: z - .array(z.string()) - .optional() - .describe('List of kinds of entities to derive options from'), - defaultKind: z - .string() - .optional() - .describe( - 'The default entity kind. Options of this kind will not be prefixed.', - ), - allowArbitraryValues: z - .boolean() - .optional() - .describe('Whether to allow arbitrary user input. Defaults to true'), - defaultNamespace: z - .union([z.string(), z.literal(false)]) - .optional() - .describe( - 'The default namespace. Options with this namespace will not be prefixed.', - ), -}); +export const OwnedEntityPickerUiOptionsSchema = makeJsonSchemaFromZod( + z.object({ + allowedKinds: z + .array(z.string()) + .optional() + .describe('List of kinds of entities to derive options from'), + defaultKind: z + .string() + .optional() + .describe( + 'The default entity kind. Options of this kind will not be prefixed.', + ), + allowArbitraryValues: z + .boolean() + .optional() + .describe('Whether to allow arbitrary user input. Defaults to true'), + defaultNamespace: z + .union([z.string(), z.literal(false)]) + .optional() + .describe( + 'The default namespace. Options with this namespace will not be prefixed.', + ), + }), +); -const OwnedEntityPickerReturnValueSchema = z.string(); +const OwnedEntityPickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); /** * The input props that can be specified under `ui:options` for the @@ -51,17 +52,13 @@ const OwnedEntityPickerReturnValueSchema = z.string(); * * @public */ -export type OwnedEntityPickerUiOptions = z.infer< - typeof OwnedEntityPickerUiOptionsSchema ->; +export type OwnedEntityPickerUiOptions = + typeof OwnedEntityPickerUiOptionsSchema.schemaType; -export type OwnedEntityPickerReturnValue = z.infer< - typeof OwnedEntityPickerReturnValueSchema ->; +export type OwnedEntityPickerReturnValue = + typeof OwnedEntityPickerReturnValueSchema.schemaType; export const OwnedEntityPickerSchema = { - uiOptions: zodToJsonSchema(OwnedEntityPickerUiOptionsSchema) as JSONSchema7, - returnValue: zodToJsonSchema( - OwnedEntityPickerReturnValueSchema, - ) as JSONSchema7, + uiOptions: OwnedEntityPickerUiOptionsSchema.jsonSchema, + returnValue: OwnedEntityPickerReturnValueSchema.jsonSchema, }; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts index 634a39ddee..ccfe453651 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts @@ -13,34 +13,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JSONSchema7 } from 'json-schema'; import { z } from 'zod'; -import zodToJsonSchema from 'zod-to-json-schema'; +import { makeJsonSchemaFromZod } from '../utils'; /** * @public */ -export const OwnerPickerUiOptionsSchema = z.object({ - allowedKinds: z - .array(z.string()) - .default(['Group', 'User']) - .optional() - .describe( - 'List of kinds of entities to derive options from. Defaults to Group and User', - ), - allowArbitraryValues: z - .boolean() - .optional() - .describe('Whether to allow arbitrary user input. Defaults to true'), - defaultNamespace: z - .union([z.string(), z.literal(false)]) - .optional() - .describe( - 'The default namespace. Options with this namespace will not be prefixed.', - ), -}); +export const OwnerPickerUiOptionsSchema = makeJsonSchemaFromZod( + z.object({ + allowedKinds: z + .array(z.string()) + .default(['Group', 'User']) + .optional() + .describe( + 'List of kinds of entities to derive options from. Defaults to Group and User', + ), + allowArbitraryValues: z + .boolean() + .optional() + .describe('Whether to allow arbitrary user input. Defaults to true'), + defaultNamespace: z + .union([z.string(), z.literal(false)]) + .optional() + .describe( + 'The default namespace. Options with this namespace will not be prefixed.', + ), + }), +); -const OwnerPickerReturnValueSchema = z.string(); +const OwnerPickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); /** * The input props that can be specified under `ui:options` for the @@ -48,13 +49,12 @@ const OwnerPickerReturnValueSchema = z.string(); * * @public */ -export type OwnerPickerUiOptions = z.infer; +export type OwnerPickerUiOptions = typeof OwnerPickerUiOptionsSchema.schemaType; -export type OwnerPickerReturnValue = z.infer< - typeof OwnerPickerReturnValueSchema ->; +export type OwnerPickerReturnValue = + typeof OwnerPickerReturnValueSchema.schemaType; export const OwnerPickerSchema = { - uiOptions: zodToJsonSchema(OwnerPickerUiOptionsSchema) as JSONSchema7, - returnValue: zodToJsonSchema(OwnerPickerReturnValueSchema) as JSONSchema7, + uiOptions: OwnerPickerUiOptionsSchema.jsonSchema, + returnValue: OwnerPickerReturnValueSchema.jsonSchema, }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts index b870747fd4..98d9f76a9c 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts @@ -13,70 +13,71 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JSONSchema7 } from 'json-schema'; import { z } from 'zod'; -import zodToJsonSchema from 'zod-to-json-schema'; +import { makeJsonSchemaFromZod } from '../utils'; /** * @public */ -export const RepoUrlPickerUiOptionsSchema = z.object({ - allowedHosts: z - .array(z.string()) - .optional() - .describe('List of allowed SCM platform hosts'), - allowedOrganizations: z - .array(z.string()) - .optional() - .describe('List of allowed organizations in the given SCM platform'), - allowedOwners: z - .array(z.string()) - .optional() - .describe('List of allowed owners in the given SCM platform'), - allowedRepos: z - .array(z.string()) - .optional() - .describe('List of allowed repos in the given SCM platform'), - requestUserCredentials: z - .object({ - secretsKey: z - .string() - .describe( - 'Key used within the template secrets context to store the credential', - ), - additionalScopes: z - .object({ - gerrit: z - .array(z.string()) - .optional() - .describe('Additional Gerrit scopes to request'), - github: z - .array(z.string()) - .optional() - .describe('Additional GitHub scopes to request'), - gitlab: z - .array(z.string()) - .optional() - .describe('Additional GitLab scopes to request'), - bitbucket: z - .array(z.string()) - .optional() - .describe('Additional BitBucket scopes to request'), - azure: z - .array(z.string()) - .optional() - .describe('Additional Azure scopes to request'), - }) - .optional() - .describe('Additional permission scopes to request'), - }) - .optional() - .describe( - 'If defined will request user credentials to auth against the given SCM platform', - ), -}); +export const RepoUrlPickerUiOptionsSchema = makeJsonSchemaFromZod( + z.object({ + allowedHosts: z + .array(z.string()) + .optional() + .describe('List of allowed SCM platform hosts'), + allowedOrganizations: z + .array(z.string()) + .optional() + .describe('List of allowed organizations in the given SCM platform'), + allowedOwners: z + .array(z.string()) + .optional() + .describe('List of allowed owners in the given SCM platform'), + allowedRepos: z + .array(z.string()) + .optional() + .describe('List of allowed repos in the given SCM platform'), + requestUserCredentials: z + .object({ + secretsKey: z + .string() + .describe( + 'Key used within the template secrets context to store the credential', + ), + additionalScopes: z + .object({ + gerrit: z + .array(z.string()) + .optional() + .describe('Additional Gerrit scopes to request'), + github: z + .array(z.string()) + .optional() + .describe('Additional GitHub scopes to request'), + gitlab: z + .array(z.string()) + .optional() + .describe('Additional GitLab scopes to request'), + bitbucket: z + .array(z.string()) + .optional() + .describe('Additional BitBucket scopes to request'), + azure: z + .array(z.string()) + .optional() + .describe('Additional Azure scopes to request'), + }) + .optional() + .describe('Additional permission scopes to request'), + }) + .optional() + .describe( + 'If defined will request user credentials to auth against the given SCM platform', + ), + }), +); -const RepoUrlPickerReturnValueSchema = z.string(); +const RepoUrlPickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); /** * The input props that can be specified under `ui:options` for the @@ -84,18 +85,16 @@ const RepoUrlPickerReturnValueSchema = z.string(); * * @public */ -export type RepoUrlPickerUiOptions = z.infer< - typeof RepoUrlPickerUiOptionsSchema ->; +export type RepoUrlPickerUiOptions = + typeof RepoUrlPickerUiOptionsSchema.schemaType; -export type RepoUrlPickerReturnValue = z.infer< - typeof RepoUrlPickerReturnValueSchema ->; +export type RepoUrlPickerReturnValue = + typeof RepoUrlPickerReturnValueSchema.schemaType; // NOTE: There is a bug with this failing validation in the custom field explorer due // to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if // requestUserCredentials is not defined export const RepoUrlPickerSchema = { - uiOptions: zodToJsonSchema(RepoUrlPickerUiOptionsSchema) as JSONSchema7, - returnValue: zodToJsonSchema(RepoUrlPickerReturnValueSchema) as JSONSchema7, + uiOptions: RepoUrlPickerUiOptionsSchema.jsonSchema, + returnValue: RepoUrlPickerReturnValueSchema.jsonSchema, }; diff --git a/plugins/scaffolder/src/components/fields/utils.ts b/plugins/scaffolder/src/components/fields/utils.ts new file mode 100644 index 0000000000..b3ebf1e1b4 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/utils.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +/** + * Utility function to convert zod schemas to JSON schemas with + * type inference extraction that abstracts away zod typings + */ +export function makeJsonSchemaFromZod( + schema: T, +): { + jsonSchema: JSONSchema7; + schemaType: T extends z.ZodType ? I : never; +} { + return { + jsonSchema: zodToJsonSchema(schema) as JSONSchema7, + schemaType: null as any, + }; +} From 3e4a2ee77979919564e7f6bfd9a770966e995d5a Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Thu, 10 Nov 2022 13:11:03 -0500 Subject: [PATCH 030/155] docs(scaffolder): add info about custom field explorer Signed-off-by: Phil Kuang --- .../custom-field-explorer.png | Bin 0 -> 151063 bytes .../writing-custom-field-extensions.md | 106 ++++++++++++++++++ plugins/scaffolder/api-report.md | 41 ++++--- .../fields/EntityNamePicker/schema.ts | 4 +- .../components/fields/EntityPicker/schema.ts | 10 +- .../fields/EntityTagsPicker/schema.ts | 8 +- .../fields/OwnedEntityPicker/schema.ts | 8 +- .../components/fields/OwnerPicker/schema.ts | 9 +- .../components/fields/RepoUrlPicker/schema.ts | 9 +- .../scaffolder/src/components/fields/index.ts | 1 + .../scaffolder/src/components/fields/utils.ts | 9 +- 11 files changed, 158 insertions(+), 47 deletions(-) create mode 100644 docs/assets/software-templates/custom-field-explorer.png diff --git a/docs/assets/software-templates/custom-field-explorer.png b/docs/assets/software-templates/custom-field-explorer.png new file mode 100644 index 0000000000000000000000000000000000000000..1af897ac8c4c693f23a679527d41bf9917604bb8 GIT binary patch literal 151063 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 + + +
This is the content for step 2
+
+ + ); +}; + +ConditionalButtons.args = defaultArgs; diff --git a/packages/core-components/src/components/SimpleStepper/SimpleStepper.test.tsx b/packages/core-components/src/components/SimpleStepper/SimpleStepper.test.tsx index 5897054ed5..73baff8db2 100644 --- a/packages/core-components/src/components/SimpleStepper/SimpleStepper.test.tsx +++ b/packages/core-components/src/components/SimpleStepper/SimpleStepper.test.tsx @@ -145,4 +145,32 @@ describe('Stepper', () => { expect(rendered.getByText('FinalStepNext')).toBeInTheDocument(); }); + + it('Handles skipStep property', async () => { + const rendered = await renderInTestApp( + + +
step0
+
+ +
step1
+
+ +
step2
+
+ +
step3
+
+
, + ); + + fireEvent.click(getTextInSlide(rendered, 0)('Next') as Node); + expect(rendered.getByText('step1')).toBeInTheDocument(); + + fireEvent.click(getTextInSlide(rendered, 1)('Skip') as Node); + expect(rendered.getByText('step2')).toBeInTheDocument(); + + fireEvent.click(getTextInSlide(rendered, 2)('Back') as Node); + expect(rendered.getByText('step1')).toBeInTheDocument(); + }); }); diff --git a/packages/core-components/src/components/SimpleStepper/SimpleStepperFooter.tsx b/packages/core-components/src/components/SimpleStepper/SimpleStepperFooter.tsx index 5f4bdbf0c6..0f7d604eb0 100644 --- a/packages/core-components/src/components/SimpleStepper/SimpleStepperFooter.tsx +++ b/packages/core-components/src/components/SimpleStepper/SimpleStepperFooter.tsx @@ -45,6 +45,10 @@ interface NextBtnProps extends CommonBtnProps { last?: boolean; stepIndex: number; } +interface SkipBtnProps extends CommonBtnProps { + disabled?: boolean; + stepIndex: number; +} interface BackBtnProps extends CommonBtnProps { disabled?: boolean; stepIndex: number; @@ -71,6 +75,18 @@ const NextBtn = ({ ); +const SkipBtn = ({ text, handleClick, disabled, stepIndex }: SkipBtnProps) => ( + +); + const BackBtn = ({ text, handleClick, disabled, stepIndex }: BackBtnProps) => (

y)iSUXrP?ZG`Sk7BbS+)!D8YhJ>47T|85pz6i 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 z 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Νu$ULnuIF2KsUoE%*sM+(Q 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 => { + return ( + 0 && !formData} + onChange={onChange} + focused={focused} + /> + ); +}; +``` + +```tsx +// packages/app/src/scaffolder/MyCustomExtensionWithOptions/extensions.ts +... +import { MyCustomExtensionWithOptions, MyCustomExtensionWithOptionsSchema } from './MyCustomExtensionWithOptions'; + +export const MyCustomFieldWithOptionsExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + name: 'MyCustomExtensionWithOptions', + component: MyCustomExtensionWithOptions, + schema: MyCustomExtensionWithOptionsSchema, + }), +); +``` + +We recommend using a library like [zod](https://github.com/colinhacks/zod) to define your schema +and the provided `makeJsonSchemaFromZod` helper utility function to generate both the JSON schema +and types for your field input/output to preventing having to duplicate the definitions: + +```tsx +//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx +... +import { z } from 'zod'; +import { makeJsonSchemaFromZod } from '@backstage/plugin-scaffolder'; + +const MyCustomExtensionWithOptionsUiOptionsSchema = makeJsonSchemaFromZod( + z.object({ + focused: z + .boolean() + .optional() + .describe('Whether to focus this field'), + }), +); + +const MyCustomExtensionWithOptionsReturnValueSchema = makeJsonSchemaFromZod( + z.string(), +); + +export const MyCustomExtensionWithOptionsSchema = { + uiOptions: MyCustomExtensionWithOptionsUiOptionsSchema.schema, + returnValue: MyCustomExtensionWithOptionsReturnValueSchema.schema, +}; + +export const MyCustomExtensionWithOptions = ({ + onChange, + rawErrors, + required, + formData, +}: FieldProps< + typeof MyCustomExtensionWithOptionsReturnValueSchema.type, + typeof MyCustomExtensionWithOptionsUiOptionsSchema.type +>) => { + return ( + 0 && !formData} + onChange={onChange} + focused={focused} + /> + ); +}; +``` diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index e56b280c05..1ef699f462 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -35,6 +35,7 @@ import { TaskStep } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { UIOptionsType } from '@rjsf/utils'; import { UiSchema } from '@rjsf/utils'; +import { z } from 'zod'; // @alpha export function createNextScaffolderFieldExtension< @@ -90,13 +91,12 @@ export const EntityPickerFieldExtension: FieldExtensionComponent< >; // @public -export type EntityPickerUiOptions = - typeof EntityPickerUiOptionsSchema.schemaType; +export type EntityPickerUiOptions = typeof EntityPickerUiOptionsSchema.type; // @public (undocumented) export const EntityPickerUiOptionsSchema: { - jsonSchema: JSONSchema7; - schemaType: { + schema: JSONSchema7; + type: { defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; @@ -116,12 +116,12 @@ export const EntityTagsPickerFieldExtension: FieldExtensionComponent< // @public export type EntityTagsPickerUiOptions = - typeof EntityTagsPickerUiOptionsSchema.schemaType; + typeof EntityTagsPickerUiOptionsSchema.type; // @public (undocumented) export const EntityTagsPickerUiOptionsSchema: { - jsonSchema: JSONSchema7; - schemaType: { + schema: JSONSchema7; + type: { showCounts?: boolean | undefined; kinds?: string[] | undefined; helperText?: string | undefined; @@ -192,6 +192,14 @@ export type LogEvent = { taskId: string; }; +// @public +export function makeJsonSchemaFromZod( + schema: T, +): { + schema: JSONSchema7; + type: T extends z.ZodType ? I : never; +}; + // @alpha export type NextCustomFieldValidator = ( data: TFieldReturnValue, @@ -262,12 +270,12 @@ export const OwnedEntityPickerFieldExtension: FieldExtensionComponent< // @public export type OwnedEntityPickerUiOptions = - typeof OwnedEntityPickerUiOptionsSchema.schemaType; + typeof OwnedEntityPickerUiOptionsSchema.type; // @public (undocumented) export const OwnedEntityPickerUiOptionsSchema: { - jsonSchema: JSONSchema7; - schemaType: { + schema: JSONSchema7; + type: { defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; @@ -286,12 +294,12 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent< >; // @public -export type OwnerPickerUiOptions = typeof OwnerPickerUiOptionsSchema.schemaType; +export type OwnerPickerUiOptions = typeof OwnerPickerUiOptionsSchema.type; // @public (undocumented) export const OwnerPickerUiOptionsSchema: { - jsonSchema: JSONSchema7; - schemaType: { + schema: JSONSchema7; + type: { defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; @@ -333,13 +341,12 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent< >; // @public -export type RepoUrlPickerUiOptions = - typeof RepoUrlPickerUiOptionsSchema.schemaType; +export type RepoUrlPickerUiOptions = typeof RepoUrlPickerUiOptionsSchema.type; // @public (undocumented) export const RepoUrlPickerUiOptionsSchema: { - jsonSchema: JSONSchema7; - schemaType: { + schema: JSONSchema7; + type: { allowedOwners?: string[] | undefined; allowedOrganizations?: string[] | undefined; allowedRepos?: string[] | undefined; diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts index 83e477b6aa..3139839bf4 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts @@ -19,8 +19,8 @@ import { makeJsonSchemaFromZod } from '../utils'; const EntityNamePickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); export type EntityNamePickerReturnValue = - typeof EntityNamePickerReturnValueSchema.schemaType; + typeof EntityNamePickerReturnValueSchema.type; export const EntityNamePickerSchema = { - returnValue: EntityNamePickerReturnValueSchema.jsonSchema, + returnValue: EntityNamePickerReturnValueSchema.schema, }; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts index 3e05264a14..3d14a2bf3e 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts @@ -52,13 +52,11 @@ const EntityPickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); * * @public */ -export type EntityPickerUiOptions = - typeof EntityPickerUiOptionsSchema.schemaType; +export type EntityPickerUiOptions = typeof EntityPickerUiOptionsSchema.type; -export type EntityPickerReturnValue = - typeof EntityPickerReturnValueSchema.schemaType; +export type EntityPickerReturnValue = typeof EntityPickerReturnValueSchema.type; export const EntityPickerSchema = { - uiOptions: EntityPickerUiOptionsSchema.jsonSchema, - returnValue: EntityPickerReturnValueSchema.jsonSchema, + uiOptions: EntityPickerUiOptionsSchema.schema, + returnValue: EntityPickerReturnValueSchema.schema, }; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts index e08918f838..d3a5391385 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts @@ -44,12 +44,12 @@ const EntityTagsPickerReturnValueSchema = makeJsonSchemaFromZod( * @public */ export type EntityTagsPickerUiOptions = - typeof EntityTagsPickerUiOptionsSchema.schemaType; + typeof EntityTagsPickerUiOptionsSchema.type; export type EntityTagsPickerReturnValue = - typeof EntityTagsPickerReturnValueSchema.schemaType; + typeof EntityTagsPickerReturnValueSchema.type; export const EntityTagsPickerSchema = { - uiOptions: EntityTagsPickerUiOptionsSchema.jsonSchema, - returnValue: EntityTagsPickerReturnValueSchema.jsonSchema, + uiOptions: EntityTagsPickerUiOptionsSchema.schema, + returnValue: EntityTagsPickerReturnValueSchema.schema, }; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts index e5bfcd9709..c967448729 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts @@ -53,12 +53,12 @@ const OwnedEntityPickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); * @public */ export type OwnedEntityPickerUiOptions = - typeof OwnedEntityPickerUiOptionsSchema.schemaType; + typeof OwnedEntityPickerUiOptionsSchema.type; export type OwnedEntityPickerReturnValue = - typeof OwnedEntityPickerReturnValueSchema.schemaType; + typeof OwnedEntityPickerReturnValueSchema.type; export const OwnedEntityPickerSchema = { - uiOptions: OwnedEntityPickerUiOptionsSchema.jsonSchema, - returnValue: OwnedEntityPickerReturnValueSchema.jsonSchema, + uiOptions: OwnedEntityPickerUiOptionsSchema.schema, + returnValue: OwnedEntityPickerReturnValueSchema.schema, }; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts index ccfe453651..accc8f80fc 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts @@ -49,12 +49,11 @@ const OwnerPickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); * * @public */ -export type OwnerPickerUiOptions = typeof OwnerPickerUiOptionsSchema.schemaType; +export type OwnerPickerUiOptions = typeof OwnerPickerUiOptionsSchema.type; -export type OwnerPickerReturnValue = - typeof OwnerPickerReturnValueSchema.schemaType; +export type OwnerPickerReturnValue = typeof OwnerPickerReturnValueSchema.type; export const OwnerPickerSchema = { - uiOptions: OwnerPickerUiOptionsSchema.jsonSchema, - returnValue: OwnerPickerReturnValueSchema.jsonSchema, + uiOptions: OwnerPickerUiOptionsSchema.schema, + returnValue: OwnerPickerReturnValueSchema.schema, }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts index 98d9f76a9c..bf8fa3cf5c 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts @@ -85,16 +85,15 @@ const RepoUrlPickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); * * @public */ -export type RepoUrlPickerUiOptions = - typeof RepoUrlPickerUiOptionsSchema.schemaType; +export type RepoUrlPickerUiOptions = typeof RepoUrlPickerUiOptionsSchema.type; export type RepoUrlPickerReturnValue = - typeof RepoUrlPickerReturnValueSchema.schemaType; + typeof RepoUrlPickerReturnValueSchema.type; // NOTE: There is a bug with this failing validation in the custom field explorer due // to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if // requestUserCredentials is not defined export const RepoUrlPickerSchema = { - uiOptions: RepoUrlPickerUiOptionsSchema.jsonSchema, - returnValue: RepoUrlPickerReturnValueSchema.jsonSchema, + uiOptions: RepoUrlPickerUiOptionsSchema.schema, + returnValue: RepoUrlPickerReturnValueSchema.schema, }; diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index 6358934025..e78b92da00 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -18,3 +18,4 @@ export * from './OwnerPicker'; export * from './RepoUrlPicker'; export * from './OwnedEntityPicker'; export * from './EntityTagsPicker'; +export { makeJsonSchemaFromZod } from './utils'; diff --git a/plugins/scaffolder/src/components/fields/utils.ts b/plugins/scaffolder/src/components/fields/utils.ts index b3ebf1e1b4..2f56e31db5 100644 --- a/plugins/scaffolder/src/components/fields/utils.ts +++ b/plugins/scaffolder/src/components/fields/utils.ts @@ -18,17 +18,18 @@ import { z } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; /** + * @public * Utility function to convert zod schemas to JSON schemas with * type inference extraction that abstracts away zod typings */ export function makeJsonSchemaFromZod( schema: T, ): { - jsonSchema: JSONSchema7; - schemaType: T extends z.ZodType ? I : never; + schema: JSONSchema7; + type: T extends z.ZodType ? I : never; } { return { - jsonSchema: zodToJsonSchema(schema) as JSONSchema7, - schemaType: null as any, + schema: zodToJsonSchema(schema) as JSONSchema7, + type: null as any, }; } From f93f263eff7b05547a357db03b9b422d5c586b79 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Fri, 11 Nov 2022 14:51:37 -0600 Subject: [PATCH 031/155] Added custom provider annotation details Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- docs/features/software-catalog/external-integrations.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/features/software-catalog/external-integrations.md b/docs/features/software-catalog/external-integrations.md index 65f97fb8da..f3387d31f0 100644 --- a/docs/features/software-catalog/external-integrations.md +++ b/docs/features/software-catalog/external-integrations.md @@ -159,7 +159,12 @@ Check out the numbered markings - let's go through them one by one. the outcome of that. This example issues a `fetch` to the right service and issues a full refresh of its entity bucket based on that. 5. The method translates the foreign data model to the native `Entity` form, as - expected by the catalog. + expected by the catalog. Make sure that in this method you include the + `backstage.io/managed-by-location` and `backstage.io/managed-by-origin-location` + annotations on your `Entity`. If these are not present they will not show up + in the Catalog and you will see warnings in your logs. The + [Well-known Annotations](./well-known-annotations.md#backstageiomanaged-by-location) + documentation has guidance on what values to use for these. 6. Finally, we issue a "mutation" to the catalog. This persists the entities in our own bucket, along with an optional `locationKey` that's used for conflict checks. But this is a bigger topic - mutations warrant their own explanatory From be40512430f5292c0cce26c87336d1d1b750cc69 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 12 Nov 2022 17:23:20 +0100 Subject: [PATCH 032/155] Revert "chore: (eslint) restrict imports of @material-ui Link" This reverts commit 7539b3674830af1ca98ea3d74ca4be6b290c4473. Signed-off-by: Patrik Oldsberg --- .changeset/great-colts-invite.md | 16 ---------------- packages/cli/config/eslint-factory.js | 11 ----------- 2 files changed, 27 deletions(-) delete mode 100644 .changeset/great-colts-invite.md diff --git a/.changeset/great-colts-invite.md b/.changeset/great-colts-invite.md deleted file mode 100644 index 1ca71580dc..0000000000 --- a/.changeset/great-colts-invite.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@backstage/cli': minor ---- - -Added a new ESLint rule that restricts imports of Link from @material-ui - -The rule can be can be overridden in the following way: - -```diff -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { -+ restrictedImports: [ -+ { name: '@material-ui/core', importNames: [] }, -+ { name: '@material-ui/core/Link', importNames: [] }, -+ ], -}); -``` diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js index 04e23b2077..e213f58e4e 100644 --- a/packages/cli/config/eslint-factory.js +++ b/packages/cli/config/eslint-factory.js @@ -239,17 +239,6 @@ function createConfigForRole(dir, role, extraConfig = {}) { name: '@material-ui/icons/', // because this is possible too ._. message: "Please import '@material-ui/icons/' instead.", }, - { - name: '@material-ui/core', - importNames: ['Link'], - message: - 'Prefer using `Link` from `@backstage/core-components` rather than material-UI', - }, - { - name: '@material-ui/core/Link', - message: - 'Prefer using `Link` from `@backstage/core-components` rather than material-UI', - }, ...require('module').builtinModules, ...(extraConfig.restrictedImports ?? []), ], From 3626b7358e0027cb8ac227c6aea8ce1f27335133 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Sun, 13 Nov 2022 13:30:17 -0600 Subject: [PATCH 033/155] Updated content based on feedback Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- docs/features/software-catalog/external-integrations.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/features/software-catalog/external-integrations.md b/docs/features/software-catalog/external-integrations.md index f3387d31f0..66813cc4d2 100644 --- a/docs/features/software-catalog/external-integrations.md +++ b/docs/features/software-catalog/external-integrations.md @@ -159,10 +159,10 @@ Check out the numbered markings - let's go through them one by one. the outcome of that. This example issues a `fetch` to the right service and issues a full refresh of its entity bucket based on that. 5. The method translates the foreign data model to the native `Entity` form, as - expected by the catalog. Make sure that in this method you include the - `backstage.io/managed-by-location` and `backstage.io/managed-by-origin-location` - annotations on your `Entity`. If these are not present they will not show up - in the Catalog and you will see warnings in your logs. The + expected by the catalog. The `Entity` must include the + `backstage.io/managed-by-location` and + `backstage.io/managed-by-origin-location annotations`; otherwise, it will not + appear in the Catalog and will generate warning logs. The [Well-known Annotations](./well-known-annotations.md#backstageiomanaged-by-location) documentation has guidance on what values to use for these. 6. Finally, we issue a "mutation" to the catalog. This persists the entities in From e982f77fe3f0f651c9007b938c66b7d3710238e9 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 14 Nov 2022 13:46:21 +0100 Subject: [PATCH 034/155] add changeset Signed-off-by: Johan Haals --- .changeset/silly-wolves-remember.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silly-wolves-remember.md diff --git a/.changeset/silly-wolves-remember.md b/.changeset/silly-wolves-remember.md new file mode 100644 index 0000000000..f39812db88 --- /dev/null +++ b/.changeset/silly-wolves-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Registered shutdown hook in experimental catalog plugin. From d6dbf1792bfa0bf5fc5f0ddaf3ab15599f01f544 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 14 Nov 2022 13:54:13 +0100 Subject: [PATCH 035/155] add changesets Signed-off-by: Johan Haals --- .changeset/old-keys-leave.md | 5 +++++ .changeset/twenty-dodos-wash.md | 5 +++++ .changeset/young-turkeys-relax.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/old-keys-leave.md create mode 100644 .changeset/twenty-dodos-wash.md create mode 100644 .changeset/young-turkeys-relax.md diff --git a/.changeset/old-keys-leave.md b/.changeset/old-keys-leave.md new file mode 100644 index 0000000000..d80574b2e4 --- /dev/null +++ b/.changeset/old-keys-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Added `lifecycleFactory` implementation. diff --git a/.changeset/twenty-dodos-wash.md b/.changeset/twenty-dodos-wash.md new file mode 100644 index 0000000000..d21088a953 --- /dev/null +++ b/.changeset/twenty-dodos-wash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Added `lifecycleFactory` to default service factories. diff --git a/.changeset/young-turkeys-relax.md b/.changeset/young-turkeys-relax.md new file mode 100644 index 0000000000..c53cd984a6 --- /dev/null +++ b/.changeset/young-turkeys-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Added initial support for registering shutdown hooks via `lifecycleServiceRef`. From 4bc0ed33ca8db31d3a0f4ffdf6108eff9d307c77 Mon Sep 17 00:00:00 2001 From: Maximilian Vorbrodt Date: Mon, 14 Nov 2022 15:14:33 +0100 Subject: [PATCH 036/155] bring back old scrolling behaviour Signed-off-by: Maximilian Vorbrodt --- .../src/components/DependencyGraph/DependencyGraph.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index 3ecdbedddf..64d39a4425 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -259,8 +259,6 @@ export function DependencyGraph( ); } - container.on('wheel', e => e.preventDefault()); - if (zoom === 'enabled') { enableZoom(); } else if (zoom === 'enable-on-click') { From d4368d1f9498283e9f3d40dbb86e53a15116d91a Mon Sep 17 00:00:00 2001 From: Maximilian Vorbrodt Date: Mon, 14 Nov 2022 15:16:35 +0100 Subject: [PATCH 037/155] make graph fill only visiable space on the page Signed-off-by: Maximilian Vorbrodt --- .../src/components/DependencyGraph/DependencyGraph.tsx | 4 +++- .../src/components/GroupsExplorerContent/GroupsDiagram.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index 64d39a4425..5dd8664107 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -223,6 +223,7 @@ export function DependencyGraph( const maxWidth = Math.max(graphWidth, containerWidth); const maxHeight = Math.max(graphHeight, containerHeight); + const minHeight = Math.min(graphHeight, containerHeight); const containerRef = React.useMemo( () => @@ -393,7 +394,8 @@ export function DependencyGraph( diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx index c366d1c6c1..95c6d86d27 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx @@ -44,7 +44,7 @@ import useAsync from 'react-use/lib/useAsync'; const useStyles = makeStyles((theme: BackstageTheme) => ({ graph: { flex: 1, - minHeight: 0, + minHeight: '100%', }, graphWrapper: { position: 'relative', From fd47108dedca4b5ac49278bac32a0ea6e8fa82ad Mon Sep 17 00:00:00 2001 From: Maximilian Vorbrodt Date: Mon, 14 Nov 2022 15:23:18 +0100 Subject: [PATCH 038/155] update changeset Signed-off-by: Maximilian Vorbrodt --- .changeset/cuddly-jeans-laugh.md | 6 ------ .changeset/lucky-carrots-stare.md | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 .changeset/cuddly-jeans-laugh.md create mode 100644 .changeset/lucky-carrots-stare.md diff --git a/.changeset/cuddly-jeans-laugh.md b/.changeset/cuddly-jeans-laugh.md deleted file mode 100644 index 6dadb85022..0000000000 --- a/.changeset/cuddly-jeans-laugh.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/core-components': patch -'@backstage/plugin-explore': patch ---- - -Adds a background to the group diagram and prevents weird scroll/zoom behavior when hovering the graph. With this fix the user can only zoom in and out while hovering the graph. If the cursor is not hovering the graph the user can scroll up and down on the page. In total this creates more consistent scrolling or zooming behavior. diff --git a/.changeset/lucky-carrots-stare.md b/.changeset/lucky-carrots-stare.md new file mode 100644 index 0000000000..81ad1b7268 --- /dev/null +++ b/.changeset/lucky-carrots-stare.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-explore': patch +--- + +Adds a background to GroupsDiagram and makes the graph fill up all the visible space on the page for, making it so the user does not have to scroll up or down on the page. From af85d98bad67e336328ae71d8f16d94dc0ea2c52 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 14 Nov 2022 15:28:08 +0100 Subject: [PATCH 039/155] fixed deprecation in some files backend-common Signed-off-by: Simon --- .../src/database/connectors/mysql.ts | 2 +- .../src/reading/AwsS3UrlReader.test.ts | 4 +-- .../src/reading/AzureUrlReader.test.ts | 4 +-- .../reading/BitbucketCloudUrlReader.test.ts | 2 +- .../src/reading/BitbucketUrlReader.test.ts | 28 +++++++++---------- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/backend-common/src/database/connectors/mysql.ts b/packages/backend-common/src/database/connectors/mysql.ts index b75dc3d99c..4283ff42bd 100644 --- a/packages/backend-common/src/database/connectors/mysql.ts +++ b/packages/backend-common/src/database/connectors/mysql.ts @@ -115,7 +115,7 @@ export function parseMysqlConnectionString( password, host: hostname, port: Number(port || 3306), - database: decodeURIComponent(pathname.substr(1)), + database: decodeURIComponent(pathname.substring(1)), }; const ssl = searchParams.get('ssl'); diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts index 454b8a2c5a..9f81322eec 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts @@ -247,7 +247,7 @@ describe('AwsS3UrlReader', () => { }); it('returns contents of an object in a bucket', async () => { - const response = await reader.read( + const response = await reader.readUrl( 'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml', ); expect(response.toString().trim()).toBe('site_name: Test'); @@ -255,7 +255,7 @@ describe('AwsS3UrlReader', () => { it('rejects unknown targets', async () => { await expect( - reader.read( + reader.readUrl( 'https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml', ), ).rejects.toThrow( diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index bc088d8bc9..0ad1346c3c 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -116,7 +116,7 @@ describe('AzureUrlReader', () => { treeResponseFactory, }); - const data = await reader.read(url); + const data = await reader.readUrl(url); const res = await JSON.parse(data.toString('utf-8')); expect(res).toEqual(response); }); @@ -145,7 +145,7 @@ describe('AzureUrlReader', () => { logger, treeResponseFactory, }); - await reader.read(url); + await reader.readUrl(url); }).rejects.toThrow(error); }); }); diff --git a/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts index 12ace2755b..ca6e15a005 100644 --- a/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts @@ -161,7 +161,7 @@ describe('BitbucketCloudUrlReader', () => { describe('read', () => { it('rejects unknown targets', async () => { await expect( - reader.read('https://not.bitbucket.com/apa'), + reader.readUrl('https://not.bitbucket.com/apa'), ).rejects.toThrow( 'Incorrect URL: https://not.bitbucket.com/apa, Error: Invalid Bitbucket Cloud URL or file path', ); diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 922480f8e7..c18e779592 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -16,8 +16,8 @@ import { ConfigReader } from '@backstage/config'; import { - BitbucketIntegration, - readBitbucketIntegrationConfig, + BitbucketCloudIntegration, + readBitbucketCloudIntegrationConfig } from '@backstage/integration'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; @@ -27,14 +27,14 @@ import { setupServer } from 'msw/node'; import os from 'os'; import path from 'path'; import { NotModifiedError } from '@backstage/errors'; -import { BitbucketUrlReader } from './BitbucketUrlReader'; +import { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; import { getVoidLogger } from '../logging'; import getRawBody from 'raw-body'; const logger = getVoidLogger(); -describe('BitbucketUrlReader.factory', () => { +describe('BitbucketCloudUrlReader.factory', () => { it('only apply integration configs not inherited from bitbucketCloud or bitbucketServer', () => { const config = new ConfigReader({ integrations: { @@ -57,7 +57,7 @@ describe('BitbucketUrlReader.factory', () => { config: config, }); - const tuples = BitbucketUrlReader.factory({ + const tuples = BitbucketCloudUrlReader.factory({ config, logger, treeResponseFactory, @@ -67,34 +67,32 @@ describe('BitbucketUrlReader.factory', () => { }); }); -describe('BitbucketUrlReader', () => { +describe('BitbucketCloudUrlReader', () => { const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), }); - const bitbucketProcessor = new BitbucketUrlReader( - new BitbucketIntegration( - readBitbucketIntegrationConfig( + const bitbucketProcessor = new BitbucketCloudUrlReader( + new BitbucketCloudIntegration( + readBitbucketCloudIntegrationConfig( new ConfigReader({ host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0', }), ), ), - logger, { treeResponseFactory }, ); - const hostedBitbucketProcessor = new BitbucketUrlReader( - new BitbucketIntegration( - readBitbucketIntegrationConfig( + const hostedBitbucketProcessor = new BitbucketCloudUrlReader( + new BitbucketCloudIntegration( + readBitbucketCloudIntegrationConfig( new ConfigReader({ host: 'bitbucket.mycompany.net', apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', }), ), ), - logger, { treeResponseFactory }, ); @@ -209,7 +207,7 @@ describe('BitbucketUrlReader', () => { describe('read', () => { it('rejects unknown targets', async () => { await expect( - bitbucketProcessor.read('https://not.bitbucket.com/apa'), + bitbucketProcessor.readUrl('https://not.bitbucket.com/apa'), ).rejects.toThrow( 'Incorrect URL: https://not.bitbucket.com/apa, Error: Invalid Bitbucket URL or file path', ); From 4801c60746fab0825860886b8b6b4dbf8993d5c4 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 14 Nov 2022 15:38:18 +0100 Subject: [PATCH 040/155] done with deprecations in backend except BitBucketUrl Signed-off-by: Simon --- .../backend-common/src/reading/FetchUrlReader.test.ts | 6 +++--- .../backend-common/src/reading/GithubUrlReader.test.ts | 4 ++-- .../backend-common/src/reading/GitlabUrlReader.test.ts | 4 ++-- .../src/reading/UrlReaderPredicateMux.test.ts | 4 ++-- .../backend-common/src/reading/UrlReaderPredicateMux.ts | 2 +- packages/backend-common/src/reading/integration.test.ts | 8 ++++---- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/backend-common/src/reading/FetchUrlReader.test.ts b/packages/backend-common/src/reading/FetchUrlReader.test.ts index 75172e83b0..d3e7ff840b 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.test.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.test.ts @@ -171,7 +171,7 @@ describe('FetchUrlReader', () => { describe('read', () => { it('should return etag from the response', async () => { - const buffer = await fetchUrlReader.read( + const buffer = await fetchUrlReader.readUrl( 'https://backstage.io/some-resource', ); expect(buffer.toString()).toBe('content foo'); @@ -179,13 +179,13 @@ describe('FetchUrlReader', () => { it('should throw NotFound if server responds with 404', async () => { await expect( - fetchUrlReader.read('https://backstage.io/not-exists'), + fetchUrlReader.readUrl('https://backstage.io/not-exists'), ).rejects.toThrow(NotFoundError); }); it('should throw Error if server responds with 500', async () => { await expect( - fetchUrlReader.read('https://backstage.io/error'), + fetchUrlReader.readUrl('https://backstage.io/error'), ).rejects.toThrow(Error); }); }); diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index a5f9d26de5..2315d1f9e8 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -92,7 +92,7 @@ describe('GithubUrlReader', () => { describe('implementation', () => { it('rejects unknown targets', async () => { await expect( - githubProcessor.read('https://not.github.com/apa'), + githubProcessor.readUrl('https://not.github.com/apa'), ).rejects.toThrow( 'Incorrect URL: https://not.github.com/apa, Error: Invalid GitHub URL or file path', ); @@ -135,7 +135,7 @@ describe('GithubUrlReader', () => { ), ); - await gheProcessor.read( + await gheProcessor.readUrl( 'https://github.com/backstage/mock/tree/blob/main', ); }); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 29ef3f4ce8..2e229f003c 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -150,7 +150,7 @@ describe('GitlabUrlReader', () => { treeResponseFactory, }); - const data = await reader.read(url); + const data = await reader.readUrl(url); const res = await JSON.parse(data.toString('utf-8')); expect(res).toEqual(response); }); @@ -169,7 +169,7 @@ describe('GitlabUrlReader', () => { logger, treeResponseFactory, }); - await reader.read(url); + await reader.readUrl(url); }).rejects.toThrow(error); }); }); diff --git a/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts b/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts index 5a11e068df..e3d412b9ab 100644 --- a/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts +++ b/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts @@ -41,7 +41,7 @@ describe('UrlReaderPredicateMux', () => { reader: barReader, }); - await mux.read('http://foo/1'); + await mux.readUrl('http://foo/1'); expect(fooReader.read).toHaveBeenCalledWith('http://foo/1'); await mux.readUrl('http://foo/2'); expect(fooReader.readUrl).toHaveBeenCalledWith('http://foo/2', undefined); @@ -50,7 +50,7 @@ describe('UrlReaderPredicateMux', () => { await mux.search('http://foo/4'); expect(fooReader.search).toHaveBeenCalledWith('http://foo/4', undefined); - await mux.read('http://bar/1'); + await mux.readUrl('http://bar/1'); expect(barReader.read).toHaveBeenCalledWith('http://bar/1'); await mux.readUrl('http://bar/2'); expect(barReader.readUrl).toHaveBeenCalledWith('http://bar/2', undefined); diff --git a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts index d5d30605b2..7330e053ff 100644 --- a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts +++ b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts @@ -50,7 +50,7 @@ export class UrlReaderPredicateMux implements UrlReader { for (const { predicate, reader } of this.readers) { if (predicate(parsed)) { - return reader.read(url); + return reader.readUrl(url); } } diff --git a/packages/backend-common/src/reading/integration.test.ts b/packages/backend-common/src/reading/integration.test.ts index 951df29605..0855c86f4f 100644 --- a/packages/backend-common/src/reading/integration.test.ts +++ b/packages/backend-common/src/reading/integration.test.ts @@ -89,7 +89,7 @@ describe.skip('UrlReaders', () => { it( 'should read data from azure', withRetries(3, async () => { - const data = await reader.read( + const data = await reader.readUrl( 'https://dev.azure.com/backstage-verification/test-templates/_git/test-templates?path=%2Ftemplate.yaml', ); expect(data.toString()).toContain('test-template-azure'); @@ -110,7 +110,7 @@ describe.skip('UrlReaders', () => { it( 'should read data from gitlab', withRetries(3, async () => { - const data = await reader.read( + const data = await reader.readUrl( 'https://gitlab.com/backstage-verification/test-templates/-/blob/master/template.yaml', ); expect(data.toString()).toContain('test-template-gitlab'); @@ -131,7 +131,7 @@ describe.skip('UrlReaders', () => { it( 'should read data from bitbucket', withRetries(3, async () => { - const data = await reader.read( + const data = await reader.readUrl( 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', ); expect(data.toString()).toContain('test-template-bitbucket'); @@ -152,7 +152,7 @@ describe.skip('UrlReaders', () => { it( 'should read data from github', withRetries(3, async () => { - const data = await reader.read( + const data = await reader.readUrl( 'https://github.com/backstage-verification/test-templates/blob/master/template.yaml', ); expect(data.toString()).toContain('test-template-github'); From a7bc5d082d43edc404128e4e7315b6e0ed11979f Mon Sep 17 00:00:00 2001 From: Maximilian Vorbrodt Date: Mon, 14 Nov 2022 15:48:35 +0100 Subject: [PATCH 041/155] fix code style issue Signed-off-by: Maximilian Vorbrodt --- .../src/components/DependencyGraph/DependencyGraph.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index 5dd8664107..5b26629082 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -394,7 +394,7 @@ export function DependencyGraph( From da25293910f208d00296af7c7de497a8f8ff7278 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Fri, 11 Nov 2022 14:11:42 -0500 Subject: [PATCH 042/155] refactor(scaffolder): wrap full field schema and prop type def in helper Signed-off-by: Phil Kuang --- .../writing-custom-field-extensions.md | 25 ++-- plugins/scaffolder/api-report.md | 116 ++++++++++-------- .../EntityNamePicker/EntityNamePicker.tsx | 7 +- .../fields/EntityNamePicker/schema.ts | 11 +- .../fields/EntityPicker/EntityPicker.tsx | 10 +- .../components/fields/EntityPicker/index.ts | 5 +- .../components/fields/EntityPicker/schema.ts | 17 ++- .../EntityTagsPicker/EntityTagsPicker.tsx | 13 +- .../fields/EntityTagsPicker/index.ts | 2 +- .../fields/EntityTagsPicker/schema.ts | 21 ++-- .../OwnedEntityPicker/OwnedEntityPicker.tsx | 14 +-- .../fields/OwnedEntityPicker/index.ts | 2 +- .../fields/OwnedEntityPicker/schema.ts | 17 +-- .../fields/OwnerPicker/OwnerPicker.tsx | 10 +- .../components/fields/OwnerPicker/index.ts | 5 +- .../components/fields/OwnerPicker/schema.ts | 16 +-- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 10 +- .../components/fields/RepoUrlPicker/index.ts | 2 +- .../components/fields/RepoUrlPicker/schema.ts | 18 ++- .../scaffolder/src/components/fields/index.ts | 2 +- .../scaffolder/src/components/fields/utils.ts | 45 +++++-- plugins/scaffolder/src/extensions/types.ts | 2 +- 22 files changed, 169 insertions(+), 201 deletions(-) diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index d0a36ecaf7..1d28bd5db9 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -212,7 +212,7 @@ export const MyCustomExtensionWithOptions = ({ rawErrors, required, formData, -}: FieldProps) => { +}: FieldExtensionComponentProps) => { return ( ) => { +}: MyCustomExtensionWithOptionsProps) => { return ( ( // @public export type CustomFieldExtensionSchema = { + returnValue: JSONSchema7; uiOptions?: JSONSchema7; - returnValue?: JSONSchema7; }; // @public @@ -90,19 +90,20 @@ export const EntityPickerFieldExtension: FieldExtensionComponent< } >; -// @public -export type EntityPickerUiOptions = typeof EntityPickerUiOptionsSchema.type; - // @public (undocumented) -export const EntityPickerUiOptionsSchema: { - schema: JSONSchema7; - type: { +export const EntityPickerFieldSchema: FieldSchema< + string, + { defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; - }; -}; + } +>; + +// @public +export type EntityPickerUiOptions = + typeof EntityPickerFieldSchema.uiOptionsType; // @public export const EntityTagsPickerFieldExtension: FieldExtensionComponent< @@ -114,19 +115,19 @@ export const EntityTagsPickerFieldExtension: FieldExtensionComponent< } >; -// @public -export type EntityTagsPickerUiOptions = - typeof EntityTagsPickerUiOptionsSchema.type; - // @public (undocumented) -export const EntityTagsPickerUiOptionsSchema: { - schema: JSONSchema7; - type: { +export const EntityTagsPickerFieldSchema: FieldSchema< + string[], + { showCounts?: boolean | undefined; kinds?: string[] | undefined; helperText?: string | undefined; - }; -}; + } +>; + +// @public +export type EntityTagsPickerUiOptions = + typeof EntityTagsPickerFieldSchema.uiOptionsType; // @public export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; @@ -155,6 +156,16 @@ export type FieldExtensionOptions< schema?: CustomFieldExtensionSchema; }; +// @public +export interface FieldSchema { + // (undocumented) + readonly schema: CustomFieldExtensionSchema; + // (undocumented) + readonly type: FieldExtensionComponentProps; + // (undocumented) + readonly uiOptionsType: TUiOptions; +} + // @public export type LayoutComponent<_TInputProps> = () => null; @@ -193,12 +204,18 @@ export type LogEvent = { }; // @public -export function makeJsonSchemaFromZod( - schema: T, -): { - schema: JSONSchema7; - type: T extends z.ZodType ? I : never; -}; +export function makeFieldSchemaFromZod< + TReturnSchema extends z.ZodType, + TUiOptionsSchema extends z.ZodType = z.ZodType, +>( + returnSchema: TReturnSchema, + uiOptionsSchema?: TUiOptionsSchema, +): FieldSchema< + TReturnSchema extends z.ZodType ? IReturn : never, + TUiOptionsSchema extends z.ZodType + ? IUiOptions + : never +>; // @alpha export type NextCustomFieldValidator = ( @@ -268,20 +285,20 @@ export const OwnedEntityPickerFieldExtension: FieldExtensionComponent< } >; -// @public -export type OwnedEntityPickerUiOptions = - typeof OwnedEntityPickerUiOptionsSchema.type; - // @public (undocumented) -export const OwnedEntityPickerUiOptionsSchema: { - schema: JSONSchema7; - type: { +export const OwnedEntityPickerFieldSchema: FieldSchema< + string, + { defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; - }; -}; + } +>; + +// @public +export type OwnedEntityPickerUiOptions = + typeof OwnedEntityPickerFieldSchema.uiOptionsType; // @public export const OwnerPickerFieldExtension: FieldExtensionComponent< @@ -293,18 +310,18 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent< } >; -// @public -export type OwnerPickerUiOptions = typeof OwnerPickerUiOptionsSchema.type; - // @public (undocumented) -export const OwnerPickerUiOptionsSchema: { - schema: JSONSchema7; - type: { +export const OwnerPickerFieldSchema: FieldSchema< + string, + { defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; - }; -}; + } +>; + +// @public +export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType; // @public export const repoPickerValidation: ( @@ -340,13 +357,10 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent< } >; -// @public -export type RepoUrlPickerUiOptions = typeof RepoUrlPickerUiOptionsSchema.type; - // @public (undocumented) -export const RepoUrlPickerUiOptionsSchema: { - schema: JSONSchema7; - type: { +export const RepoUrlPickerFieldSchema: FieldSchema< + string, + { allowedOwners?: string[] | undefined; allowedOrganizations?: string[] | undefined; allowedRepos?: string[] | undefined; @@ -365,8 +379,12 @@ export const RepoUrlPickerUiOptionsSchema: { secretsKey: string; } | undefined; - }; -}; + } +>; + +// @public +export type RepoUrlPickerUiOptions = + typeof RepoUrlPickerFieldSchema.uiOptionsType; // @public (undocumented) export const rootRouteRef: RouteRef; diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx b/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx index eba05cfb92..555b726917 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { FieldExtensionComponentProps } from '../../../extensions'; -import { EntityNamePickerReturnValue } from './schema'; +import { EntityNamePickerProps } from './schema'; import { TextField } from '@material-ui/core'; export { EntityNamePickerSchema } from './schema'; @@ -23,9 +22,7 @@ export { EntityNamePickerSchema } from './schema'; /** * EntityName Picker */ -export const EntityNamePicker = ( - props: FieldExtensionComponentProps, -) => { +export const EntityNamePicker = (props: EntityNamePickerProps) => { const { onChange, required, diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts index 3139839bf4..27e33befb5 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts @@ -14,13 +14,10 @@ * limitations under the License. */ import { z } from 'zod'; -import { makeJsonSchemaFromZod } from '../utils'; +import { makeFieldSchemaFromZod } from '../utils'; -const EntityNamePickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); +const EntityNamePickerFieldSchema = makeFieldSchemaFromZod(z.string()); -export type EntityNamePickerReturnValue = - typeof EntityNamePickerReturnValueSchema.type; +export const EntityNamePickerSchema = EntityNamePickerFieldSchema.schema; -export const EntityNamePickerSchema = { - returnValue: EntityNamePickerReturnValueSchema.schema, -}; +export type EntityNamePickerProps = typeof EntityNamePickerFieldSchema.type; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index 8fcc412d0a..bc78e00576 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -23,8 +23,7 @@ import FormControl from '@material-ui/core/FormControl'; import Autocomplete from '@material-ui/lab/Autocomplete'; import React, { useCallback, useEffect } from 'react'; import useAsync from 'react-use/lib/useAsync'; -import { FieldExtensionComponentProps } from '../../../extensions'; -import { EntityPickerReturnValue, EntityPickerUiOptions } from './schema'; +import { EntityPickerProps } from './schema'; export { EntityPickerSchema } from './schema'; @@ -34,12 +33,7 @@ export { EntityPickerSchema } from './schema'; * * @public */ -export const EntityPicker = ( - props: FieldExtensionComponentProps< - EntityPickerReturnValue, - EntityPickerUiOptions - >, -) => { +export const EntityPicker = (props: EntityPickerProps) => { const { onChange, schema: { title = 'Entity', description = 'An entity from the catalog' }, diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts index d1044e4e67..b8df596c98 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts @@ -13,7 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { - type EntityPickerUiOptions, - EntityPickerUiOptionsSchema, -} from './schema'; +export { EntityPickerFieldSchema, type EntityPickerUiOptions } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts index 3d14a2bf3e..9236457fc0 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts @@ -14,12 +14,13 @@ * limitations under the License. */ import { z } from 'zod'; -import { makeJsonSchemaFromZod } from '../utils'; +import { makeFieldSchemaFromZod } from '../utils'; /** * @public */ -export const EntityPickerUiOptionsSchema = makeJsonSchemaFromZod( +export const EntityPickerFieldSchema = makeFieldSchemaFromZod( + z.string(), z.object({ allowedKinds: z .array(z.string()) @@ -44,19 +45,15 @@ export const EntityPickerUiOptionsSchema = makeJsonSchemaFromZod( }), ); -const EntityPickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); - /** * The input props that can be specified under `ui:options` for the * `EntityPicker` field extension. * * @public */ -export type EntityPickerUiOptions = typeof EntityPickerUiOptionsSchema.type; +export type EntityPickerUiOptions = + typeof EntityPickerFieldSchema.uiOptionsType; -export type EntityPickerReturnValue = typeof EntityPickerReturnValueSchema.type; +export type EntityPickerProps = typeof EntityPickerFieldSchema.type; -export const EntityPickerSchema = { - uiOptions: EntityPickerUiOptionsSchema.schema, - returnValue: EntityPickerReturnValueSchema.schema, -}; +export const EntityPickerSchema = EntityPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx index 6d5404cee1..549ccdb8aa 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx @@ -22,11 +22,7 @@ import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { FormControl, TextField } from '@material-ui/core'; import { Autocomplete } from '@material-ui/lab'; -import { FieldExtensionComponentProps } from '../../../extensions'; -import { - EntityTagsPickerReturnValue, - EntityTagsPickerUiOptions, -} from './schema'; +import { EntityTagsPickerProps } from './schema'; export { EntityTagsPickerSchema } from './schema'; @@ -36,12 +32,7 @@ export { EntityTagsPickerSchema } from './schema'; * * @public */ -export const EntityTagsPicker = ( - props: FieldExtensionComponentProps< - EntityTagsPickerReturnValue, - EntityTagsPickerUiOptions - >, -) => { +export const EntityTagsPicker = (props: EntityTagsPickerProps) => { const { formData, onChange, uiSchema } = props; const catalogApi = useApi(catalogApiRef); const [tagOptions, setTagOptions] = useState([]); diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts index 850fc17c8e..52bad9a80d 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ export { + EntityTagsPickerFieldSchema, type EntityTagsPickerUiOptions, - EntityTagsPickerUiOptionsSchema, } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts index d3a5391385..6677d16e98 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts @@ -14,12 +14,13 @@ * limitations under the License. */ import { z } from 'zod'; -import { makeJsonSchemaFromZod } from '../utils'; +import { makeFieldSchemaFromZod } from '../utils'; /** * @public */ -export const EntityTagsPickerUiOptionsSchema = makeJsonSchemaFromZod( +export const EntityTagsPickerFieldSchema = makeFieldSchemaFromZod( + z.array(z.string()), z.object({ kinds: z .array(z.string()) @@ -33,9 +34,9 @@ export const EntityTagsPickerUiOptionsSchema = makeJsonSchemaFromZod( }), ); -const EntityTagsPickerReturnValueSchema = makeJsonSchemaFromZod( - z.array(z.string()), -); +export const EntityTagsPickerSchema = EntityTagsPickerFieldSchema.schema; + +export type EntityTagsPickerProps = typeof EntityTagsPickerFieldSchema.type; /** * The input props that can be specified under `ui:options` for the @@ -44,12 +45,4 @@ const EntityTagsPickerReturnValueSchema = makeJsonSchemaFromZod( * @public */ export type EntityTagsPickerUiOptions = - typeof EntityTagsPickerUiOptionsSchema.type; - -export type EntityTagsPickerReturnValue = - typeof EntityTagsPickerReturnValueSchema.type; - -export const EntityTagsPickerSchema = { - uiOptions: EntityTagsPickerUiOptionsSchema.schema, - returnValue: EntityTagsPickerReturnValueSchema.schema, -}; + typeof EntityTagsPickerFieldSchema.uiOptionsType; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx index 47bcb6c9b8..01a174a6a3 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx @@ -26,25 +26,17 @@ import Autocomplete from '@material-ui/lab/Autocomplete'; import React, { useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; -import { FieldExtensionComponentProps } from '../../../extensions'; -import { - OwnedEntityPickerReturnValue, - OwnedEntityPickerUiOptions, -} from './schema'; +import { OwnedEntityPickerProps } from './schema'; export { OwnedEntityPickerSchema } from './schema'; + /** * The underlying component that is rendered in the form for the `OwnedEntityPicker` * field extension. * * @public */ -export const OwnedEntityPicker = ( - props: FieldExtensionComponentProps< - OwnedEntityPickerReturnValue, - OwnedEntityPickerUiOptions - >, -) => { +export const OwnedEntityPicker = (props: OwnedEntityPickerProps) => { const { onChange, schema: { title = 'Entity', description = 'An entity from the catalog' }, diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts index 0101eb8845..985dae1d3c 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ export { + OwnedEntityPickerFieldSchema, type OwnedEntityPickerUiOptions, - OwnedEntityPickerUiOptionsSchema, } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts index c967448729..4190b89f95 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts @@ -14,12 +14,13 @@ * limitations under the License. */ import { z } from 'zod'; -import { makeJsonSchemaFromZod } from '../utils'; +import { makeFieldSchemaFromZod } from '../utils'; /** * @public */ -export const OwnedEntityPickerUiOptionsSchema = makeJsonSchemaFromZod( +export const OwnedEntityPickerFieldSchema = makeFieldSchemaFromZod( + z.string(), z.object({ allowedKinds: z .array(z.string()) @@ -44,8 +45,6 @@ export const OwnedEntityPickerUiOptionsSchema = makeJsonSchemaFromZod( }), ); -const OwnedEntityPickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); - /** * The input props that can be specified under `ui:options` for the * `OwnedEntityPicker` field extension. @@ -53,12 +52,8 @@ const OwnedEntityPickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); * @public */ export type OwnedEntityPickerUiOptions = - typeof OwnedEntityPickerUiOptionsSchema.type; + typeof OwnedEntityPickerFieldSchema.uiOptionsType; -export type OwnedEntityPickerReturnValue = - typeof OwnedEntityPickerReturnValueSchema.type; +export type OwnedEntityPickerProps = typeof OwnedEntityPickerFieldSchema.type; -export const OwnedEntityPickerSchema = { - uiOptions: OwnedEntityPickerUiOptionsSchema.schema, - returnValue: OwnedEntityPickerReturnValueSchema.schema, -}; +export const OwnedEntityPickerSchema = OwnedEntityPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx index 10333684d0..1c4c356906 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx @@ -15,8 +15,7 @@ */ import React from 'react'; import { EntityPicker } from '../EntityPicker/EntityPicker'; -import { FieldExtensionComponentProps } from '../../../extensions'; -import { OwnerPickerReturnValue, OwnerPickerUiOptions } from './schema'; +import { OwnerPickerProps } from './schema'; export { OwnerPickerSchema } from './schema'; @@ -26,12 +25,7 @@ export { OwnerPickerSchema } from './schema'; * * @public */ -export const OwnerPicker = ( - props: FieldExtensionComponentProps< - OwnerPickerReturnValue, - OwnerPickerUiOptions - >, -) => { +export const OwnerPicker = (props: OwnerPickerProps) => { const { schema: { title = 'Owner', description = 'The owner of the component' }, uiSchema, diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts index 5d436358b3..9d94650e04 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts @@ -13,7 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { - type OwnerPickerUiOptions, - OwnerPickerUiOptionsSchema, -} from './schema'; +export { OwnerPickerFieldSchema, type OwnerPickerUiOptions } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts index accc8f80fc..56edef1343 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts @@ -14,12 +14,13 @@ * limitations under the License. */ import { z } from 'zod'; -import { makeJsonSchemaFromZod } from '../utils'; +import { makeFieldSchemaFromZod } from '../utils'; /** * @public */ -export const OwnerPickerUiOptionsSchema = makeJsonSchemaFromZod( +export const OwnerPickerFieldSchema = makeFieldSchemaFromZod( + z.string(), z.object({ allowedKinds: z .array(z.string()) @@ -41,19 +42,14 @@ export const OwnerPickerUiOptionsSchema = makeJsonSchemaFromZod( }), ); -const OwnerPickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); - /** * The input props that can be specified under `ui:options` for the * `OwnerPicker` field extension. * * @public */ -export type OwnerPickerUiOptions = typeof OwnerPickerUiOptionsSchema.type; +export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType; -export type OwnerPickerReturnValue = typeof OwnerPickerReturnValueSchema.type; +export type OwnerPickerProps = typeof OwnerPickerFieldSchema.type; -export const OwnerPickerSchema = { - uiOptions: OwnerPickerUiOptionsSchema.schema, - returnValue: OwnerPickerReturnValueSchema.schema, -}; +export const OwnerPickerSchema = OwnerPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 8aca07e995..827eff6ea2 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -24,11 +24,10 @@ import { GitlabRepoPicker } from './GitlabRepoPicker'; import { AzureRepoPicker } from './AzureRepoPicker'; import { BitbucketRepoPicker } from './BitbucketRepoPicker'; import { GerritRepoPicker } from './GerritRepoPicker'; -import { FieldExtensionComponentProps } from '../../../extensions'; import { RepoUrlPickerHost } from './RepoUrlPickerHost'; import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName'; import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils'; -import { RepoUrlPickerReturnValue, RepoUrlPickerUiOptions } from './schema'; +import { RepoUrlPickerProps } from './schema'; import { RepoUrlPickerState } from './types'; import useDebounce from 'react-use/lib/useDebounce'; import { useTemplateSecrets } from '../../secrets'; @@ -41,12 +40,7 @@ export { RepoUrlPickerSchema } from './schema'; * * @public */ -export const RepoUrlPicker = ( - props: FieldExtensionComponentProps< - RepoUrlPickerReturnValue, - RepoUrlPickerUiOptions - >, -) => { +export const RepoUrlPicker = (props: RepoUrlPickerProps) => { const { uiSchema, onChange, rawErrors, formData } = props; const [state, setState] = useState( parseRepoPickerUrl(formData), diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts index 34124d0ccd..33e5ef1715 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ export { + RepoUrlPickerFieldSchema, type RepoUrlPickerUiOptions, - RepoUrlPickerUiOptionsSchema, } from './schema'; export { repoPickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts index bf8fa3cf5c..8f8674a0a7 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts @@ -14,12 +14,13 @@ * limitations under the License. */ import { z } from 'zod'; -import { makeJsonSchemaFromZod } from '../utils'; +import { makeFieldSchemaFromZod } from '../utils'; /** * @public */ -export const RepoUrlPickerUiOptionsSchema = makeJsonSchemaFromZod( +export const RepoUrlPickerFieldSchema = makeFieldSchemaFromZod( + z.string(), z.object({ allowedHosts: z .array(z.string()) @@ -77,23 +78,18 @@ export const RepoUrlPickerUiOptionsSchema = makeJsonSchemaFromZod( }), ); -const RepoUrlPickerReturnValueSchema = makeJsonSchemaFromZod(z.string()); - /** * The input props that can be specified under `ui:options` for the * `RepoUrlPicker` field extension. * * @public */ -export type RepoUrlPickerUiOptions = typeof RepoUrlPickerUiOptionsSchema.type; +export type RepoUrlPickerUiOptions = + typeof RepoUrlPickerFieldSchema.uiOptionsType; -export type RepoUrlPickerReturnValue = - typeof RepoUrlPickerReturnValueSchema.type; +export type RepoUrlPickerProps = typeof RepoUrlPickerFieldSchema.type; // NOTE: There is a bug with this failing validation in the custom field explorer due // to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if // requestUserCredentials is not defined -export const RepoUrlPickerSchema = { - uiOptions: RepoUrlPickerUiOptionsSchema.schema, - returnValue: RepoUrlPickerReturnValueSchema.schema, -}; +export const RepoUrlPickerSchema = RepoUrlPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index e78b92da00..8d86f601ce 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -18,4 +18,4 @@ export * from './OwnerPicker'; export * from './RepoUrlPicker'; export * from './OwnedEntityPicker'; export * from './EntityTagsPicker'; -export { makeJsonSchemaFromZod } from './utils'; +export { type FieldSchema, makeFieldSchemaFromZod } from './utils'; diff --git a/plugins/scaffolder/src/components/fields/utils.ts b/plugins/scaffolder/src/components/fields/utils.ts index 2f56e31db5..2d3e2aab1e 100644 --- a/plugins/scaffolder/src/components/fields/utils.ts +++ b/plugins/scaffolder/src/components/fields/utils.ts @@ -16,20 +16,47 @@ import { JSONSchema7 } from 'json-schema'; import { z } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; +import { + CustomFieldExtensionSchema, + FieldExtensionComponentProps, +} from '../../extensions'; /** * @public - * Utility function to convert zod schemas to JSON schemas with - * type inference extraction that abstracts away zod typings + * FieldSchema encapsulates a JSONSchema7 along with the + * matching FieldExtensionComponentProps type for a field extension. */ -export function makeJsonSchemaFromZod( - schema: T, -): { - schema: JSONSchema7; - type: T extends z.ZodType ? I : never; -} { +export interface FieldSchema { + readonly schema: CustomFieldExtensionSchema; + readonly type: FieldExtensionComponentProps; + readonly uiOptionsType: TUiOptions; +} + +/** + * @public + * Utility function to convert zod return and UI options schemas to a + * CustomFieldExtensionSchema with FieldExtensionComponentProps type inference + */ +export function makeFieldSchemaFromZod< + TReturnSchema extends z.ZodType, + TUiOptionsSchema extends z.ZodType = z.ZodType, +>( + returnSchema: TReturnSchema, + uiOptionsSchema?: TUiOptionsSchema, +): FieldSchema< + TReturnSchema extends z.ZodType ? IReturn : never, + TUiOptionsSchema extends z.ZodType + ? IUiOptions + : never +> { return { - schema: zodToJsonSchema(schema) as JSONSchema7, + schema: { + returnValue: zodToJsonSchema(returnSchema) as JSONSchema7, + uiOptions: uiOptionsSchema + ? (zodToJsonSchema(uiOptionsSchema) as JSONSchema7) + : undefined, + }, type: null as any, + uiOptionsType: null as any, }; } diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder/src/extensions/types.ts index b15de56fc1..3834b39c0a 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder/src/extensions/types.ts @@ -41,8 +41,8 @@ export type CustomFieldValidator = ( * @public */ export type CustomFieldExtensionSchema = { + returnValue: JSONSchema7; uiOptions?: JSONSchema7; - returnValue?: JSONSchema7; }; /** From e9ad86dd8222c341c7868f5327954fcec7f87d51 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 14 Nov 2022 17:54:55 +0100 Subject: [PATCH 043/155] fixed tests that failed from converting to readUrl Signed-off-by: Simon --- packages/backend-common/src/reading/AwsS3UrlReader.test.ts | 4 +++- packages/backend-common/src/reading/AzureUrlReader.test.ts | 7 +++++-- packages/backend-common/src/reading/AzureUrlReader.ts | 2 +- packages/backend-common/src/reading/FetchUrlReader.test.ts | 5 ++++- .../backend-common/src/reading/GitlabUrlReader.test.ts | 2 +- .../src/reading/UrlReaderPredicateMux.test.ts | 4 ++-- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts index 9f81322eec..dfa1629412 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts @@ -250,7 +250,9 @@ describe('AwsS3UrlReader', () => { const response = await reader.readUrl( 'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml', ); - expect(response.toString().trim()).toBe('site_name: Test'); + const fromStream = await getRawBody(response.stream!()); + + expect(fromStream.toString().trim()).toBe('site_name: Test'); }); it('rejects unknown targets', async () => { diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index 0ad1346c3c..b50aa30804 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -30,6 +30,8 @@ import { NotModifiedError } from '@backstage/errors'; import { getVoidLogger } from '../logging'; import { AzureUrlReader } from './AzureUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; +import getRawBody from 'raw-body'; + const logger = getVoidLogger(); @@ -117,7 +119,8 @@ describe('AzureUrlReader', () => { }); const data = await reader.readUrl(url); - const res = await JSON.parse(data.toString('utf-8')); + const fromStream = await getRawBody(data.stream!()); + const res = await JSON.parse(fromStream.toString()); expect(res).toEqual(response); }); @@ -336,4 +339,4 @@ describe('AzureUrlReader', () => { ).rejects.toThrow(NotModifiedError); }); }); -}); +}); \ No newline at end of file diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index 140496c181..9514b410fd 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -200,4 +200,4 @@ export class AzureUrlReader implements UrlReader { const { host, token } = this.integration.config; return `azure{host=${host},authed=${Boolean(token)}}`; } -} +} \ No newline at end of file diff --git a/packages/backend-common/src/reading/FetchUrlReader.test.ts b/packages/backend-common/src/reading/FetchUrlReader.test.ts index d3e7ff840b..18828f2dd0 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.test.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.test.ts @@ -22,6 +22,8 @@ import { setupServer } from 'msw/node'; import { getVoidLogger } from '../logging'; import { FetchUrlReader } from './FetchUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; +import getRawBody from 'raw-body'; + const fetchUrlReader = new FetchUrlReader(); @@ -174,7 +176,8 @@ describe('FetchUrlReader', () => { const buffer = await fetchUrlReader.readUrl( 'https://backstage.io/some-resource', ); - expect(buffer.toString()).toBe('content foo'); + const fromStream = await getRawBody(buffer.stream!()); + expect(fromStream.toString()).toBe('content foo'); }); it('should throw NotFound if server responds with 404', async () => { diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 2e229f003c..1a31d3051e 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -151,7 +151,7 @@ describe('GitlabUrlReader', () => { }); const data = await reader.readUrl(url); - const res = await JSON.parse(data.toString('utf-8')); + const res = await JSON.parse(data.toString()); expect(res).toEqual(response); }); diff --git a/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts b/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts index e3d412b9ab..38473557e0 100644 --- a/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts +++ b/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts @@ -42,7 +42,7 @@ describe('UrlReaderPredicateMux', () => { }); await mux.readUrl('http://foo/1'); - expect(fooReader.read).toHaveBeenCalledWith('http://foo/1'); + expect(fooReader.readUrl).toHaveBeenCalledWith('http://foo/1', undefined); await mux.readUrl('http://foo/2'); expect(fooReader.readUrl).toHaveBeenCalledWith('http://foo/2', undefined); await mux.readTree('http://foo/3'); @@ -51,7 +51,7 @@ describe('UrlReaderPredicateMux', () => { expect(fooReader.search).toHaveBeenCalledWith('http://foo/4', undefined); await mux.readUrl('http://bar/1'); - expect(barReader.read).toHaveBeenCalledWith('http://bar/1'); + expect(barReader.readUrl).toHaveBeenCalledWith('http://bar/1', undefined); await mux.readUrl('http://bar/2'); expect(barReader.readUrl).toHaveBeenCalledWith('http://bar/2', undefined); await mux.readTree('http://bar/3'); From 1fcd60eb4dfd06cdb8565c6843a215606afc3a83 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 14 Nov 2022 19:11:19 +0100 Subject: [PATCH 044/155] fixed last failing test in backend-common Signed-off-by: Simon --- packages/backend-common/src/reading/GitlabUrlReader.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 1a31d3051e..727fb5d3c0 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -26,6 +26,7 @@ import { getVoidLogger } from '../logging'; import { GitlabUrlReader } from './GitlabUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; import { NotModifiedError, NotFoundError } from '@backstage/errors'; +import getRawBody from 'raw-body'; import { GitLabIntegration, readGitLabIntegrationConfig, @@ -151,7 +152,8 @@ describe('GitlabUrlReader', () => { }); const data = await reader.readUrl(url); - const res = await JSON.parse(data.toString()); + const fromStream = await getRawBody(data.stream!()); + const res = await JSON.parse(fromStream.toString()); expect(res).toEqual(response); }); From fa633304c3bfa48c5ea90153ac6e284d2243c043 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 15 Nov 2022 08:50:29 +0100 Subject: [PATCH 045/155] merge from master and fixed some more deprecations Signed-off-by: Simon --- .../src/validation/CommonValidatorFunctions.test.ts | 2 +- packages/config/src/index.ts | 2 +- .../src/components/SupportButton/SupportButton.tsx | 2 +- yarn.lock | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts index 9a9ec8bcda..a887a9aa2e 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -224,7 +224,7 @@ describe('CommonValidatorFunctions', () => { ['abc xyz', true], ['abc xyz abc.', true], ])(`isValidString %p ? %p`, (value, result) => { - expect(CommonValidatorFunctions.isValidString(value)).toBe(result); + expect(CommonValidatorFunctions.isNonEmptyString(value)).toBe(result); }); it.each([ diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index c8800d0ad2..a795a02b86 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -25,6 +25,6 @@ export type { JsonObject, JsonPrimitive, JsonValue, -} from './deprecatedTypes'; +} from '@backstage/types'; export { ConfigReader } from './reader'; export type { AppConfig, Config } from './types'; diff --git a/packages/core-components/src/components/SupportButton/SupportButton.tsx b/packages/core-components/src/components/SupportButton/SupportButton.tsx index 01de79bdee..4e0bb866f5 100644 --- a/packages/core-components/src/components/SupportButton/SupportButton.tsx +++ b/packages/core-components/src/components/SupportButton/SupportButton.tsx @@ -68,7 +68,7 @@ const SupportListItem = ({ item }: { item: SupportItem }) => { ( + secondary={item.links?.reduce( (prev, link, idx) => [ ...prev, idx > 0 &&
, diff --git a/yarn.lock b/yarn.lock index 0841181a34..c77ebcf329 100644 --- a/yarn.lock +++ b/yarn.lock @@ -38915,7 +38915,7 @@ __metadata: "typescript@patch:typescript@~4.6.0#~builtin, typescript@patch:typescript@~4.6.3#~builtin": version: 4.6.4 - resolution: "typescript@patch:typescript@npm%3A4.6.4#~builtin::version=4.6.4&hash=a1c5e5" + resolution: "typescript@patch:typescript@npm%3A4.6.4#~builtin::version=4.6.4&hash=f456af" bin: tsc: bin/tsc tsserver: bin/tsserver @@ -38925,7 +38925,7 @@ __metadata: "typescript@patch:typescript@~4.7.0#~builtin": version: 4.7.4 - resolution: "typescript@patch:typescript@npm%3A4.7.4#~builtin::version=4.7.4&hash=a1c5e5" + resolution: "typescript@patch:typescript@npm%3A4.7.4#~builtin::version=4.7.4&hash=f456af" bin: tsc: bin/tsc tsserver: bin/tsserver From d1b0d6675480ee65f123c346e179b94a974ee8ce Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 15 Nov 2022 09:13:20 +0100 Subject: [PATCH 046/155] removed blank row Signed-off-by: Simon --- packages/backend-common/src/reading/AzureUrlReader.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index b50aa30804..035f9bcb05 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -32,7 +32,6 @@ import { AzureUrlReader } from './AzureUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; import getRawBody from 'raw-body'; - const logger = getVoidLogger(); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ From 931eb6d3bcd15a4b5964976d90a402b97b349182 Mon Sep 17 00:00:00 2001 From: Nikita Karpukhin Date: Tue, 15 Nov 2022 09:26:00 +0100 Subject: [PATCH 047/155] fix readme Signed-off-by: Nikita Karpukhin --- plugins/user-settings/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md index 1549b8de65..07e1d6dcbb 100644 --- a/plugins/user-settings/README.md +++ b/plugins/user-settings/README.md @@ -104,7 +104,7 @@ import { } from '@backstage/plugin-user-settings'; import { AdvancedSettings } from './advancedSettings'; -export const settingsPage = () => ( +export const settingsPage = ( From 3b022fedb65d6b995ad44480a404f763fcc245b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 10 Nov 2022 11:56:29 +0100 Subject: [PATCH 048/155] richer errors in the msgraph import steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw Signed-off-by: Simon --- .changeset/clean-socks-call.md | 5 +++++ .../src/microsoftGraph/read.ts | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/clean-socks-call.md diff --git a/.changeset/clean-socks-call.md b/.changeset/clean-socks-call.md new file mode 100644 index 0000000000..9b802618f6 --- /dev/null +++ b/.changeset/clean-socks-call.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Added cause information to logged warnings diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 8e1d9a3776..0ed411bfea 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -122,7 +122,7 @@ export async function readMicrosoftGraphUsers( 120, ); } catch (e) { - options.logger.warn(`Unable to load photo for ${user.id}`); + options.logger.warn(`Unable to load photo for ${user.id}, ${e}`); } const entity = await transformer(user, userPhoto); @@ -206,7 +206,7 @@ export async function readMicrosoftGraphUsersInGroups( expand: options.userExpand, }); } catch (e) { - options.logger.warn(`Unable to load user for ${userId}`); + options.logger.warn(`Unable to load user for ${userId}, ${e}`); } if (user) { try { @@ -217,7 +217,7 @@ export async function readMicrosoftGraphUsersInGroups( 120, ); } catch (e) { - options.logger.warn(`Unable to load userphoto for ${userId}`); + options.logger.warn(`Unable to load userphoto for ${userId}, ${e}`); } const entity = await transformer(user, userPhoto); From 128b3e98fb8d8eb3fbd18ba84794882afdfedd2b Mon Sep 17 00:00:00 2001 From: Casper Thygesen <73483987+cthtrifork@users.noreply.github.com> Date: Tue, 8 Nov 2022 19:48:03 +0100 Subject: [PATCH 049/155] Add default errorHandler() to vault-backend Right now any uncaught error causes backstarte to crash We should use `packages/backend-common/src/middleware/errorHandler.ts` middleware like the other backend plugins Signed-off-by: Casper Thygesen <73483987+cthtrifork@users.noreply.github.com> Signed-off-by: cthtrifork Signed-off-by: Simon --- plugins/vault-backend/src/service/VaultBuilder.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/vault-backend/src/service/VaultBuilder.ts b/plugins/vault-backend/src/service/VaultBuilder.ts index 67260f45ca..b82a5802bb 100644 --- a/plugins/vault-backend/src/service/VaultBuilder.ts +++ b/plugins/vault-backend/src/service/VaultBuilder.ts @@ -20,6 +20,7 @@ import { Logger } from 'winston'; import express, { Router } from 'express'; import { VaultClient } from './vaultApi'; import { TaskRunner, PluginTaskScheduler } from '@backstage/backend-tasks'; +import { errorHandler } from '@backstage/backend-common'; /** * Environment values needed by the VaultBuilder @@ -145,6 +146,7 @@ export class VaultBuilder { res.json({ items: secrets }); }); + router.use(errorHandler()); return router; } } From e2e5242c328bf835f24290101b8a28cb753e6359 Mon Sep 17 00:00:00 2001 From: Casper Thygesen <73483987+cthtrifork@users.noreply.github.com> Date: Tue, 8 Nov 2022 19:51:34 +0100 Subject: [PATCH 050/155] added changeset Signed-off-by: cthtrifork Signed-off-by: Simon --- .changeset/rude-mayflies-heal.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rude-mayflies-heal.md diff --git a/.changeset/rude-mayflies-heal.md b/.changeset/rude-mayflies-heal.md new file mode 100644 index 0000000000..738ade0f3c --- /dev/null +++ b/.changeset/rude-mayflies-heal.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-vault-backend': patch +--- + +Added errorHandler() middleware to vault-backend to prevent errors to cause a crash From 9a4613e53486579a52e8f8eec375d9a8e8d6d948 Mon Sep 17 00:00:00 2001 From: Casper Thygesen <73483987+cthtrifork@users.noreply.github.com> Date: Wed, 9 Nov 2022 06:43:09 +0100 Subject: [PATCH 051/155] Improved patch notes Co-authored-by: Philipp Hugenroth Signed-off-by: Casper Thygesen <73483987+cthtrifork@users.noreply.github.com> Signed-off-by: Simon --- .changeset/rude-mayflies-heal.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rude-mayflies-heal.md b/.changeset/rude-mayflies-heal.md index 738ade0f3c..01cbf01e28 100644 --- a/.changeset/rude-mayflies-heal.md +++ b/.changeset/rude-mayflies-heal.md @@ -2,4 +2,4 @@ '@backstage/plugin-vault-backend': patch --- -Added errorHandler() middleware to vault-backend to prevent errors to cause a crash +Added `errorHandler()` middleware to `router` to prevent crashes caused by fatal errors in plugin backend From 30744a518667fcb269631923fcb7e416036db025 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Nov 2022 13:58:37 +0000 Subject: [PATCH 052/155] build(deps): bump loader-utils from 1.4.0 to 1.4.1 in /storybook Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.1/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.1) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: Simon --- storybook/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/storybook/yarn.lock b/storybook/yarn.lock index f6b5be55d0..a8c455b7a0 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -7875,13 +7875,13 @@ __metadata: linkType: hard "loader-utils@npm:^1.1.0, loader-utils@npm:^1.2.3": - version: 1.4.0 - resolution: "loader-utils@npm:1.4.0" + version: 1.4.1 + resolution: "loader-utils@npm:1.4.1" dependencies: big.js: ^5.2.2 emojis-list: ^3.0.0 json5: ^1.0.1 - checksum: d150b15e7a42ac47d935c8b484b79e44ff6ab4c75df7cc4cb9093350cf014ec0b17bdb60c5d6f91a37b8b218bd63b973e263c65944f58ca2573e402b9a27e717 + checksum: ea0b648cba0194e04a90aab6270619f0e35be009e33a443d9e642e93056cd49e6ca4c9678bd1c777a2392551bc5f4d0f24a87f5040608da1274aa84c6eebb502 languageName: node linkType: hard From 0d33bdc4ac21a798ea2d761ebcf3dd8ffc59ec30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arn=C3=BE=C3=B3r=20J=C3=B3nsson?= Date: Wed, 9 Nov 2022 18:42:19 +0100 Subject: [PATCH 053/155] Add optional step to SimpleStepper Signed-off-by: Arnthor Jonsson Signed-off-by: Simon --- .changeset/cyan-seahorses-itch.md | 5 ++++ .../SimpleStepper/SimpleStepper.stories.tsx | 20 +++++++++++++ .../SimpleStepper/SimpleStepper.test.tsx | 28 +++++++++++++++++++ .../SimpleStepper/SimpleStepperFooter.tsx | 27 ++++++++++++++++++ .../src/components/SimpleStepper/types.ts | 5 ++++ 5 files changed, 85 insertions(+) create mode 100644 .changeset/cyan-seahorses-itch.md diff --git a/.changeset/cyan-seahorses-itch.md b/.changeset/cyan-seahorses-itch.md new file mode 100644 index 0000000000..c3ced5ceb7 --- /dev/null +++ b/.changeset/cyan-seahorses-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': minor +--- + +Add optional step to SimpleStepper diff --git a/packages/core-components/src/components/SimpleStepper/SimpleStepper.stories.tsx b/packages/core-components/src/components/SimpleStepper/SimpleStepper.stories.tsx index 2f3f26f23e..aedd5e6b80 100644 --- a/packages/core-components/src/components/SimpleStepper/SimpleStepper.stories.tsx +++ b/packages/core-components/src/components/SimpleStepper/SimpleStepper.stories.tsx @@ -91,3 +91,23 @@ export const CompletionStep = (args: StepperProps) => { }; CompletionStep.args = defaultArgs; + +export const OptionalStep = (args: StepperProps) => { + return ( + + +