From 27bc65bf11d540e79a5992d1eaf4e35ed9dcac0a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 22 Jun 2021 17:29:26 +0200 Subject: [PATCH 1/4] packages: remove core and core-api Signed-off-by: Patrik Oldsberg --- packages/core-api/.eslintrc.js | 8 - packages/core-api/CHANGELOG.md | 390 ------------- packages/core-api/README.md | 12 - packages/core-api/package.json | 62 --- .../core-api/src/apis/definitions/AlertApi.ts | 44 -- .../src/apis/definitions/AppThemeApi.ts | 83 --- .../src/apis/definitions/ConfigApi.ts | 28 - .../src/apis/definitions/DiscoveryApi.ts | 47 -- .../core-api/src/apis/definitions/ErrorApi.ts | 68 --- .../src/apis/definitions/FeatureFlagsApi.ts | 86 --- .../src/apis/definitions/IdentityApi.ts | 56 -- .../src/apis/definitions/OAuthRequestApi.ts | 133 ----- .../src/apis/definitions/StorageApi.ts | 71 --- .../core-api/src/apis/definitions/auth.ts | 347 ------------ .../core-api/src/apis/definitions/index.ts | 33 -- .../AlertApi/AlertApiForwarder.ts | 33 -- .../apis/implementations/AlertApi/index.ts | 17 - .../AppThemeApi/AppThemeSelector.test.ts | 85 --- .../AppThemeApi/AppThemeSelector.ts | 75 --- .../apis/implementations/AppThemeApi/index.ts | 17 - .../apis/implementations/ConfigApi/index.ts | 17 - .../DiscoveryApi/UrlPatternDiscovery.test.ts | 84 --- .../DiscoveryApi/UrlPatternDiscovery.ts | 58 -- .../implementations/DiscoveryApi/index.ts | 17 - .../implementations/ErrorApi/ErrorAlerter.ts | 39 -- .../ErrorApi/ErrorApiForwarder.ts | 36 -- .../apis/implementations/ErrorApi/index.ts | 18 - .../LocalStorageFeatureFlags.test.tsx | 222 -------- .../LocalStorageFeatureFlags.tsx | 109 ---- .../implementations/FeatureFlagsApi/index.ts | 17 - .../OAuthRequestApi/MockOAuthApi.test.ts | 101 ---- .../OAuthRequestApi/MockOAuthApi.ts | 55 -- .../OAuthPendingRequests.test.ts | 96 ---- .../OAuthRequestApi/OAuthPendingRequests.ts | 126 ----- .../OAuthRequestManager.test.ts | 59 -- .../OAuthRequestApi/OAuthRequestManager.ts | 92 --- .../implementations/OAuthRequestApi/index.ts | 17 - .../StorageApi/WebStorage.test.ts | 173 ------ .../implementations/StorageApi/WebStorage.ts | 94 ---- .../apis/implementations/StorageApi/index.ts | 17 - .../implementations/auth/auth0/Auth0Auth.ts | 46 -- .../apis/implementations/auth/auth0/index.ts | 17 - .../auth/github/GithubAuth.test.ts | 29 - .../implementations/auth/github/GithubAuth.ts | 140 ----- .../apis/implementations/auth/github/index.ts | 18 - .../apis/implementations/auth/github/types.ts | 27 - .../auth/gitlab/GitlabAuth.test.ts | 50 -- .../implementations/auth/gitlab/GitlabAuth.ts | 45 -- .../apis/implementations/auth/gitlab/index.ts | 17 - .../auth/google/GoogleAuth.test.ts | 69 --- .../implementations/auth/google/GoogleAuth.ts | 68 --- .../apis/implementations/auth/google/index.ts | 17 - .../src/apis/implementations/auth/index.ts | 25 - .../auth/microsoft/MicrosoftAuth.ts | 52 -- .../implementations/auth/microsoft/index.ts | 17 - .../auth/oauth2/OAuth2.test.ts | 152 ----- .../implementations/auth/oauth2/OAuth2.ts | 179 ------ .../apis/implementations/auth/oauth2/index.ts | 18 - .../apis/implementations/auth/oauth2/types.ts | 28 - .../auth/okta/OktaAuth.test.ts | 61 -- .../implementations/auth/okta/OktaAuth.ts | 71 --- .../apis/implementations/auth/okta/index.ts | 17 - .../auth/onelogin/OneLoginAuth.ts | 82 --- .../implementations/auth/onelogin/index.ts | 17 - .../implementations/auth/saml/SamlAuth.ts | 98 ---- .../apis/implementations/auth/saml/index.ts | 16 - .../apis/implementations/auth/saml/types.ts | 22 - .../src/apis/implementations/auth/types.ts | 28 - .../src/apis/implementations/index.ts | 30 - packages/core-api/src/apis/index.ts | 19 - .../src/apis/system/ApiAggregator.test.ts | 46 -- .../core-api/src/apis/system/ApiAggregator.ts | 39 -- .../apis/system/ApiFactoryRegistry.test.ts | 91 --- .../src/apis/system/ApiFactoryRegistry.ts | 89 --- .../src/apis/system/ApiProvider.test.tsx | 212 ------- .../core-api/src/apis/system/ApiProvider.tsx | 131 ----- .../core-api/src/apis/system/ApiRef.test.ts | 51 -- packages/core-api/src/apis/system/ApiRef.ts | 57 -- .../src/apis/system/ApiRegistry.test.ts | 75 --- .../core-api/src/apis/system/ApiRegistry.ts | 69 --- .../src/apis/system/ApiResolver.test.ts | 266 --------- .../core-api/src/apis/system/ApiResolver.ts | 110 ---- packages/core-api/src/apis/system/helpers.ts | 49 -- packages/core-api/src/apis/system/index.ts | 23 - packages/core-api/src/apis/system/types.ts | 57 -- packages/core-api/src/app/App.test.tsx | 375 ------------- packages/core-api/src/app/App.tsx | 525 ------------------ packages/core-api/src/app/AppContext.test.tsx | 79 --- packages/core-api/src/app/AppContext.tsx | 67 --- packages/core-api/src/app/AppIdentity.ts | 85 --- .../core-api/src/app/AppThemeProvider.tsx | 97 ---- packages/core-api/src/app/index.ts | 18 - packages/core-api/src/app/types.ts | 256 --------- .../src/extensions/componentData.test.tsx | 118 ---- .../core-api/src/extensions/componentData.tsx | 84 --- .../src/extensions/extensions.test.tsx | 76 --- .../core-api/src/extensions/extensions.tsx | 141 ----- packages/core-api/src/extensions/index.ts | 22 - .../src/extensions/traversal.test.tsx | 104 ---- packages/core-api/src/extensions/traversal.ts | 129 ----- packages/core-api/src/icons/icons.tsx | 67 --- packages/core-api/src/icons/index.ts | 18 - packages/core-api/src/icons/types.ts | 34 -- packages/core-api/src/index.ts | 20 - .../DefaultAuthConnector.test.ts | 182 ------ .../lib/AuthConnector/DefaultAuthConnector.ts | 204 ------- .../lib/AuthConnector/DirectAuthConnector.ts | 77 --- .../AuthConnector/MockAuthConnector.test.ts | 35 -- .../lib/AuthConnector/MockAuthConnector.ts | 45 -- .../core-api/src/lib/AuthConnector/index.ts | 19 - .../core-api/src/lib/AuthConnector/types.ts | 30 - .../AuthSessionStore.test.ts | 137 ----- .../AuthSessionManager/AuthSessionStore.ts | 131 ----- .../RefreshingAuthSessionManager.test.ts | 168 ------ .../RefreshingAuthSessionManager.ts | 148 ----- .../AuthSessionManager/SessionStateTracker.ts | 40 -- .../StaticAuthSessionManager.test.ts | 97 ---- .../StaticAuthSessionManager.ts | 83 --- .../src/lib/AuthSessionManager/common.ts | 71 --- .../src/lib/AuthSessionManager/index.ts | 20 - .../src/lib/AuthSessionManager/types.ts | 57 -- .../core-api/src/lib/globalObject.test.ts | 90 --- packages/core-api/src/lib/globalObject.ts | 73 --- packages/core-api/src/lib/index.ts | 20 - packages/core-api/src/lib/loginPopup.test.ts | 218 -------- packages/core-api/src/lib/loginPopup.ts | 143 ----- packages/core-api/src/lib/subjects.test.ts | 178 ------ packages/core-api/src/lib/subjects.ts | 210 ------- .../core-api/src/lib/versionedValues.test.ts | 40 -- packages/core-api/src/lib/versionedValues.ts | 40 -- packages/core-api/src/plugin/Plugin.tsx | 99 ---- .../core-api/src/plugin/collectors.test.tsx | 106 ---- packages/core-api/src/plugin/collectors.ts | 32 -- packages/core-api/src/plugin/index.ts | 33 -- packages/core-api/src/plugin/types.ts | 121 ---- packages/core-api/src/private.ts | 17 - packages/core-api/src/public.ts | 23 - .../src/routing/ExternalRouteRef.test.ts | 119 ---- .../core-api/src/routing/ExternalRouteRef.ts | 37 -- .../core-api/src/routing/FlatRoutes.test.tsx | 111 ---- packages/core-api/src/routing/FlatRoutes.tsx | 87 --- .../core-api/src/routing/RouteRef.test.ts | 94 ---- packages/core-api/src/routing/RouteRef.ts | 44 -- .../src/routing/RouteResolver.test.ts | 293 ---------- .../core-api/src/routing/RouteResolver.ts | 223 -------- .../core-api/src/routing/SubRouteRef.test.ts | 134 ----- packages/core-api/src/routing/SubRouteRef.ts | 34 -- .../core-api/src/routing/collectors.test.tsx | 374 ------------- packages/core-api/src/routing/collectors.tsx | 172 ------ packages/core-api/src/routing/hooks.test.tsx | 405 -------------- packages/core-api/src/routing/hooks.tsx | 119 ---- packages/core-api/src/routing/index.ts | 30 - packages/core-api/src/routing/types.ts | 72 --- packages/core-api/src/routing/validation.ts | 56 -- packages/core-api/src/setupTests.ts | 18 - packages/core-api/src/types.ts | 73 --- packages/core/.eslintrc.js | 8 - packages/core/.npmrc | 1 - packages/core/CHANGELOG.md | 516 ----------------- packages/core/README.md | 12 - packages/core/config.d.ts | 113 ---- packages/core/package.json | 94 ---- .../core/src/api-wrappers/createApp.test.tsx | 117 ---- packages/core/src/api-wrappers/createApp.tsx | 167 ------ packages/core/src/api-wrappers/defaultApis.ts | 220 -------- packages/core/src/api-wrappers/index.ts | 17 - .../AlertDisplay/AlertDisplay.test.tsx | 107 ---- .../components/AlertDisplay/AlertDisplay.tsx | 76 --- .../core/src/components/AlertDisplay/index.ts | 17 - .../src/components/Avatar/Avatar.stories.tsx | 42 -- .../src/components/Avatar/Avatar.test.tsx | 27 - .../core/src/components/Avatar/Avatar.tsx | 59 -- packages/core/src/components/Avatar/index.ts | 16 - .../core/src/components/Avatar/util.test.ts | 37 -- packages/core/src/components/Avatar/utils.ts | 32 -- .../src/components/Button/Button.stories.tsx | 174 ------ .../src/components/Button/Button.test.tsx | 42 -- .../core/src/components/Button/Button.tsx | 32 -- packages/core/src/components/Button/index.ts | 17 - .../CheckboxTree/CheckboxTree.stories.tsx | 105 ---- .../CheckboxTree/CheckboxTree.test.tsx | 59 -- .../components/CheckboxTree/CheckboxTree.tsx | 358 ------------ .../src/components/CheckboxTree/index.tsx | 17 - .../core/src/components/Chip/Chip.stories.tsx | 43 -- .../CodeSnippet/CodeSnippet.stories.tsx | 94 ---- .../CodeSnippet/CodeSnippet.test.tsx | 74 --- .../components/CodeSnippet/CodeSnippet.tsx | 72 --- .../core/src/components/CodeSnippet/index.tsx | 17 - .../CopyTextButton/CopyTextButton.stories.tsx | 42 -- .../CopyTextButton/CopyTextButton.test.tsx | 85 --- .../CopyTextButton/CopyTextButton.tsx | 100 ---- .../src/components/CopyTextButton/index.tsx | 17 - .../DependencyGraph/DefaultLabel.tsx | 35 -- .../DependencyGraph/DefaultNode.tsx | 79 --- .../DependencyGraph.stories.tsx | 178 ------ .../DependencyGraph/DependencyGraph.test.tsx | 106 ---- .../DependencyGraph/DependencyGraph.tsx | 324 ----------- .../components/DependencyGraph/Edge.test.tsx | 100 ---- .../src/components/DependencyGraph/Edge.tsx | 122 ---- .../components/DependencyGraph/Node.test.tsx | 79 --- .../src/components/DependencyGraph/Node.tsx | 76 --- .../components/DependencyGraph/constants.ts | 21 - .../src/components/DependencyGraph/index.ts | 20 - .../src/components/DependencyGraph/types.ts | 88 --- .../src/components/Dialog/Dialog.stories.tsx | 127 ----- .../DismissableBanner.stories.tsx | 114 ---- .../DismissableBanner.test.tsx | 88 --- .../DismissableBanner/DismissableBanner.tsx | 135 ----- .../src/components/DismissableBanner/index.ts | 17 - .../src/components/Drawer/Drawer.stories.tsx | 171 ------ .../EmptyState/EmptyState.stories.tsx | 78 --- .../components/EmptyState/EmptyState.test.tsx | 39 -- .../src/components/EmptyState/EmptyState.tsx | 68 --- .../EmptyState/EmptyStateImage.test.tsx | 49 -- .../components/EmptyState/EmptyStateImage.tsx | 73 --- .../MissingAnnotationEmptyState.tsx | 86 --- .../EmptyState/assets/createComponent.svg | 1 - .../EmptyState/assets/missingAnnotation.svg | 1 - .../components/EmptyState/assets/noBuild.svg | 1 - .../EmptyState/assets/noInformation.svg | 1 - .../core/src/components/EmptyState/index.ts | 18 - .../FeatureCalloutCircular.test.tsx | 154 ----- .../FeatureCalloutCircular.tsx | 199 ------- .../src/components/FeatureDiscovery/index.ts | 17 - .../FeatureDiscovery/lib/usePortal.ts | 99 ---- .../FeatureDiscovery/lib/useShowCallout.ts | 58 -- .../HeaderIconLinkRow/HeaderIconLinkRow.tsx | 43 -- .../HeaderIconLinkRow/IconLinkVertical.tsx | 93 ---- .../src/components/HeaderIconLinkRow/index.ts | 19 - .../HorizontalScrollGrid.stories.tsx | 37 -- .../HorizontalScrollGrid.test.tsx | 87 --- .../HorizontalScrollGrid.tsx | 247 -------- .../components/HorizontalScrollGrid/index.tsx | 17 - .../Lifecycle/Lifecycle.stories.tsx | 43 -- .../components/Lifecycle/Lifecycle.test.tsx | 41 -- .../src/components/Lifecycle/Lifecycle.tsx | 56 -- .../core/src/components/Lifecycle/index.ts | 17 - .../core/src/components/Link/Link.stories.tsx | 92 --- .../core/src/components/Link/Link.test.tsx | 68 --- packages/core/src/components/Link/Link.tsx | 47 -- packages/core/src/components/Link/index.ts | 18 - .../MarkdownContent.stories.tsx | 120 ---- .../MarkdownContent/MarkdownContent.test.tsx | 65 --- .../MarkdownContent/MarkdownContent.tsx | 89 --- .../src/components/MarkdownContent/index.ts | 17 - .../LoginRequestListItem.tsx | 81 --- .../OAuthRequestDialog/OAuthRequestDialog.tsx | 86 --- .../components/OAuthRequestDialog/index.ts | 17 - .../OverflowTooltip.stories.tsx | 48 -- .../OverflowTooltip/OverflowTooltip.test.tsx | 30 - .../OverflowTooltip/OverflowTooltip.tsx | 57 -- .../src/components/OverflowTooltip/index.ts | 16 - .../components/Progress/Progress.stories.tsx | 25 - .../src/components/Progress/Progress.test.tsx | 34 -- .../core/src/components/Progress/Progress.tsx | 33 -- .../core/src/components/Progress/index.ts | 17 - .../components/ProgressBars/Gauge.stories.tsx | 55 -- .../components/ProgressBars/Gauge.test.tsx | 71 --- .../src/components/ProgressBars/Gauge.tsx | 106 ---- .../ProgressBars/GaugeCard.stories.tsx | 99 ---- .../ProgressBars/GaugeCard.test.tsx | 46 -- .../src/components/ProgressBars/GaugeCard.tsx | 56 -- .../ProgressBars/LinearGauge.stories.tsx | 43 -- .../ProgressBars/LinearGauge.test.tsx | 37 -- .../components/ProgressBars/LinearGauge.tsx | 53 -- .../core/src/components/ProgressBars/index.ts | 19 - .../ResponseErrorPanel/ResponseErrorPanel.tsx | 167 ------ .../components/ResponseErrorPanel/index.ts | 17 - .../src/components/Select/Select.stories.tsx | 57 -- .../src/components/Select/Select.test.tsx | 57 -- .../core/src/components/Select/Select.tsx | 231 -------- packages/core/src/components/Select/index.tsx | 17 - .../Select/static/ClosedDropdown.tsx | 45 -- .../Select/static/OpenedDropdown.tsx | 45 -- .../SimpleStepper/SimpleStepper.stories.tsx | 82 --- .../SimpleStepper/SimpleStepper.test.tsx | 148 ----- .../SimpleStepper/SimpleStepper.tsx | 98 ---- .../SimpleStepper/SimpleStepperFooter.tsx | 156 ------ .../SimpleStepper/SimpleStepperStep.tsx | 61 -- .../src/components/SimpleStepper/index.ts | 18 - .../src/components/SimpleStepper/types.ts | 41 -- .../src/components/Status/Status.stories.tsx | 90 --- .../src/components/Status/Status.test.tsx | 69 --- .../core/src/components/Status/Status.tsx | 130 ----- packages/core/src/components/Status/index.ts | 24 - .../StructuredMetadataTable/MetadataTable.tsx | 109 ---- .../StructuredMetadataTable/README.md | 62 --- .../StructuredMetadataTable.stories.tsx | 73 --- .../StructuredMetadataTable.test.tsx | 118 ---- .../StructuredMetadataTable.tsx | 163 ------ .../StructuredMetadataTable/index.tsx | 17 - .../SupportButton/SupportButton.test.tsx | 66 --- .../SupportButton/SupportButton.tsx | 139 ----- .../src/components/SupportButton/index.ts | 17 - .../TabbedLayout/RoutedTabs.test.tsx | 150 ----- .../components/TabbedLayout/RoutedTabs.tsx | 81 --- .../TabbedLayout/TabbedLayout.stories.tsx | 44 -- .../TabbedLayout/TabbedLayout.test.tsx | 94 ---- .../components/TabbedLayout/TabbedLayout.tsx | 91 --- .../core/src/components/TabbedLayout/index.ts | 17 - .../core/src/components/TabbedLayout/types.ts | 25 - .../core/src/components/Table/Filters.tsx | 157 ------ .../src/components/Table/SubvalueCell.tsx | 45 -- .../src/components/Table/Table.stories.tsx | 332 ----------- .../core/src/components/Table/Table.test.tsx | 68 --- packages/core/src/components/Table/Table.tsx | 508 ----------------- packages/core/src/components/Table/index.ts | 19 - .../core/src/components/Tabs/Tab.test.tsx | 26 - packages/core/src/components/Tabs/Tab.tsx | 62 --- packages/core/src/components/Tabs/TabBar.tsx | 52 -- packages/core/src/components/Tabs/TabIcon.tsx | 60 -- .../core/src/components/Tabs/TabPanel.tsx | 38 -- .../core/src/components/Tabs/Tabs.stories.tsx | 71 --- packages/core/src/components/Tabs/Tabs.tsx | 159 ------ packages/core/src/components/Tabs/index.ts | 17 - packages/core/src/components/Tabs/utils.ts | 28 - .../TrendLine/TrendLine.stories.tsx | 141 ----- .../components/TrendLine/TrendLine.test.tsx | 69 --- .../src/components/TrendLine/TrendLine.tsx | 48 -- .../core/src/components/TrendLine/index.ts | 17 - .../WarningPanel/WarningPanel.stories.tsx | 72 --- .../WarningPanel/WarningPanel.test.tsx | 67 --- .../components/WarningPanel/WarningPanel.tsx | 136 ----- .../core/src/components/WarningPanel/index.ts | 16 - packages/core/src/components/index.ts | 46 -- packages/core/src/hooks/index.ts | 23 - packages/core/src/hooks/useQueryParamState.ts | 95 ---- packages/core/src/hooks/useSupportConfig.ts | 74 --- packages/core/src/index.ts | 22 - .../src/layout/BottomLink/BottomLink.test.tsx | 30 - .../core/src/layout/BottomLink/BottomLink.tsx | 70 --- packages/core/src/layout/BottomLink/index.ts | 18 - .../Breadcrumbs/Breadcrumbs.stories.tsx | 138 ----- .../layout/Breadcrumbs/Breadcrumbs.test.tsx | 54 -- .../src/layout/Breadcrumbs/Breadcrumbs.tsx | 99 ---- packages/core/src/layout/Breadcrumbs/index.ts | 17 - packages/core/src/layout/Content/Content.tsx | 64 --- packages/core/src/layout/Content/index.ts | 17 - .../ContentHeader/ContentHeader.test.tsx | 48 -- .../layout/ContentHeader/ContentHeader.tsx | 116 ---- .../core/src/layout/ContentHeader/index.ts | 17 - .../ErrorBoundary/ErrorBoundary.test.tsx | 71 --- .../layout/ErrorBoundary/ErrorBoundary.tsx | 73 --- .../core/src/layout/ErrorBoundary/index.ts | 17 - .../src/layout/ErrorPage/ErrorPage.test.tsx | 33 -- .../core/src/layout/ErrorPage/ErrorPage.tsx | 85 --- .../core/src/layout/ErrorPage/MicDrop.tsx | 46 -- packages/core/src/layout/ErrorPage/index.ts | 17 - .../core/src/layout/ErrorPage/mic-drop.svg | 1 - .../core/src/layout/Header/Header.stories.tsx | 110 ---- .../core/src/layout/Header/Header.test.tsx | 67 --- packages/core/src/layout/Header/Header.tsx | 217 -------- packages/core/src/layout/Header/index.ts | 17 - .../HeaderActionMenu.test.tsx | 104 ---- .../HeaderActionMenu/HeaderActionMenu.tsx | 106 ---- .../HeaderActionMenu/VerticalMenuIcon.tsx | 25 - .../core/src/layout/HeaderActionMenu/index.ts | 17 - .../layout/HeaderLabel/HeaderLabel.test.tsx | 54 -- .../src/layout/HeaderLabel/HeaderLabel.tsx | 72 --- packages/core/src/layout/HeaderLabel/index.ts | 17 - .../src/layout/HeaderTabs/HeaderTabs.test.tsx | 81 --- .../core/src/layout/HeaderTabs/HeaderTabs.tsx | 97 ---- packages/core/src/layout/HeaderTabs/index.tsx | 18 - .../HomepageTimer/HomepageTimer.test.tsx | 53 -- .../layout/HomepageTimer/HomepageTimer.tsx | 102 ---- .../core/src/layout/HomepageTimer/index.ts | 17 - .../src/layout/InfoCard/InfoCard.stories.tsx | 69 --- .../src/layout/InfoCard/InfoCard.test.tsx | 39 -- .../core/src/layout/InfoCard/InfoCard.tsx | 211 ------- packages/core/src/layout/InfoCard/index.ts | 18 - .../src/layout/ItemCard/ItemCard.stories.tsx | 113 ---- .../src/layout/ItemCard/ItemCard.test.tsx | 61 -- .../core/src/layout/ItemCard/ItemCard.tsx | 104 ---- .../src/layout/ItemCard/ItemCardGrid.test.tsx | 50 -- .../core/src/layout/ItemCard/ItemCardGrid.tsx | 62 --- .../layout/ItemCard/ItemCardHeader.test.tsx | 60 -- .../src/layout/ItemCard/ItemCardHeader.tsx | 93 ---- packages/core/src/layout/ItemCard/index.ts | 21 - .../core/src/layout/Page/Page.stories.tsx | 245 -------- packages/core/src/layout/Page/Page.tsx | 48 -- packages/core/src/layout/Page/index.ts | 17 - packages/core/src/layout/Sidebar/Bar.tsx | 150 ----- packages/core/src/layout/Sidebar/Intro.tsx | 182 ------ .../core/src/layout/Sidebar/Items.test.tsx | 75 --- packages/core/src/layout/Sidebar/Items.tsx | 297 ---------- packages/core/src/layout/Sidebar/Page.tsx | 74 --- .../src/layout/Sidebar/Sidebar.stories.tsx | 56 -- packages/core/src/layout/Sidebar/config.ts | 49 -- packages/core/src/layout/Sidebar/index.ts | 33 -- .../src/layout/Sidebar/localStorage.test.ts | 25 - .../core/src/layout/Sidebar/localStorage.ts | 40 -- .../core/src/layout/SignInPage/SignInPage.tsx | 195 ------- .../src/layout/SignInPage/auth0Provider.tsx | 88 --- .../src/layout/SignInPage/commonProvider.tsx | 96 ---- .../src/layout/SignInPage/customProvider.tsx | 126 ----- .../src/layout/SignInPage/guestProvider.tsx | 61 -- packages/core/src/layout/SignInPage/index.ts | 18 - .../core/src/layout/SignInPage/providers.tsx | 181 ------ .../core/src/layout/SignInPage/styles.tsx | 42 -- packages/core/src/layout/SignInPage/types.ts | 49 -- .../layout/TabbedCard/TabbedCard.stories.tsx | 113 ---- .../src/layout/TabbedCard/TabbedCard.test.tsx | 99 ---- .../core/src/layout/TabbedCard/TabbedCard.tsx | 132 ----- packages/core/src/layout/TabbedCard/index.ts | 17 - packages/core/src/layout/index.ts | 31 -- packages/core/src/setupTests.ts | 17 - 406 files changed, 32821 deletions(-) delete mode 100644 packages/core-api/.eslintrc.js delete mode 100644 packages/core-api/CHANGELOG.md delete mode 100644 packages/core-api/README.md delete mode 100644 packages/core-api/package.json delete mode 100644 packages/core-api/src/apis/definitions/AlertApi.ts delete mode 100644 packages/core-api/src/apis/definitions/AppThemeApi.ts delete mode 100644 packages/core-api/src/apis/definitions/ConfigApi.ts delete mode 100644 packages/core-api/src/apis/definitions/DiscoveryApi.ts delete mode 100644 packages/core-api/src/apis/definitions/ErrorApi.ts delete mode 100644 packages/core-api/src/apis/definitions/FeatureFlagsApi.ts delete mode 100644 packages/core-api/src/apis/definitions/IdentityApi.ts delete mode 100644 packages/core-api/src/apis/definitions/OAuthRequestApi.ts delete mode 100644 packages/core-api/src/apis/definitions/StorageApi.ts delete mode 100644 packages/core-api/src/apis/definitions/auth.ts delete mode 100644 packages/core-api/src/apis/definitions/index.ts delete mode 100644 packages/core-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts delete mode 100644 packages/core-api/src/apis/implementations/AlertApi/index.ts delete mode 100644 packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.test.ts delete mode 100644 packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts delete mode 100644 packages/core-api/src/apis/implementations/AppThemeApi/index.ts delete mode 100644 packages/core-api/src/apis/implementations/ConfigApi/index.ts delete mode 100644 packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.test.ts delete mode 100644 packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts delete mode 100644 packages/core-api/src/apis/implementations/DiscoveryApi/index.ts delete mode 100644 packages/core-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts delete mode 100644 packages/core-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts delete mode 100644 packages/core-api/src/apis/implementations/ErrorApi/index.ts delete mode 100644 packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx delete mode 100644 packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx delete mode 100644 packages/core-api/src/apis/implementations/FeatureFlagsApi/index.ts delete mode 100644 packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts delete mode 100644 packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts delete mode 100644 packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts delete mode 100644 packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts delete mode 100644 packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts delete mode 100644 packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts delete mode 100644 packages/core-api/src/apis/implementations/OAuthRequestApi/index.ts delete mode 100644 packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts delete mode 100644 packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts delete mode 100644 packages/core-api/src/apis/implementations/StorageApi/index.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/auth0/Auth0Auth.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/auth0/index.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/github/index.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/github/types.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/gitlab/index.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/google/index.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/index.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/microsoft/index.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/oauth2/index.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/oauth2/types.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/okta/OktaAuth.test.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/okta/index.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/onelogin/index.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/saml/index.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/saml/types.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/types.ts delete mode 100644 packages/core-api/src/apis/implementations/index.ts delete mode 100644 packages/core-api/src/apis/index.ts delete mode 100644 packages/core-api/src/apis/system/ApiAggregator.test.ts delete mode 100644 packages/core-api/src/apis/system/ApiAggregator.ts delete mode 100644 packages/core-api/src/apis/system/ApiFactoryRegistry.test.ts delete mode 100644 packages/core-api/src/apis/system/ApiFactoryRegistry.ts delete mode 100644 packages/core-api/src/apis/system/ApiProvider.test.tsx delete mode 100644 packages/core-api/src/apis/system/ApiProvider.tsx delete mode 100644 packages/core-api/src/apis/system/ApiRef.test.ts delete mode 100644 packages/core-api/src/apis/system/ApiRef.ts delete mode 100644 packages/core-api/src/apis/system/ApiRegistry.test.ts delete mode 100644 packages/core-api/src/apis/system/ApiRegistry.ts delete mode 100644 packages/core-api/src/apis/system/ApiResolver.test.ts delete mode 100644 packages/core-api/src/apis/system/ApiResolver.ts delete mode 100644 packages/core-api/src/apis/system/helpers.ts delete mode 100644 packages/core-api/src/apis/system/index.ts delete mode 100644 packages/core-api/src/apis/system/types.ts delete mode 100644 packages/core-api/src/app/App.test.tsx delete mode 100644 packages/core-api/src/app/App.tsx delete mode 100644 packages/core-api/src/app/AppContext.test.tsx delete mode 100644 packages/core-api/src/app/AppContext.tsx delete mode 100644 packages/core-api/src/app/AppIdentity.ts delete mode 100644 packages/core-api/src/app/AppThemeProvider.tsx delete mode 100644 packages/core-api/src/app/index.ts delete mode 100644 packages/core-api/src/app/types.ts delete mode 100644 packages/core-api/src/extensions/componentData.test.tsx delete mode 100644 packages/core-api/src/extensions/componentData.tsx delete mode 100644 packages/core-api/src/extensions/extensions.test.tsx delete mode 100644 packages/core-api/src/extensions/extensions.tsx delete mode 100644 packages/core-api/src/extensions/index.ts delete mode 100644 packages/core-api/src/extensions/traversal.test.tsx delete mode 100644 packages/core-api/src/extensions/traversal.ts delete mode 100644 packages/core-api/src/icons/icons.tsx delete mode 100644 packages/core-api/src/icons/index.ts delete mode 100644 packages/core-api/src/icons/types.ts delete mode 100644 packages/core-api/src/index.ts delete mode 100644 packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts delete mode 100644 packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts delete mode 100644 packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts delete mode 100644 packages/core-api/src/lib/AuthConnector/MockAuthConnector.test.ts delete mode 100644 packages/core-api/src/lib/AuthConnector/MockAuthConnector.ts delete mode 100644 packages/core-api/src/lib/AuthConnector/index.ts delete mode 100644 packages/core-api/src/lib/AuthConnector/types.ts delete mode 100644 packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts delete mode 100644 packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts delete mode 100644 packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts delete mode 100644 packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts delete mode 100644 packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts delete mode 100644 packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts delete mode 100644 packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts delete mode 100644 packages/core-api/src/lib/AuthSessionManager/common.ts delete mode 100644 packages/core-api/src/lib/AuthSessionManager/index.ts delete mode 100644 packages/core-api/src/lib/AuthSessionManager/types.ts delete mode 100644 packages/core-api/src/lib/globalObject.test.ts delete mode 100644 packages/core-api/src/lib/globalObject.ts delete mode 100644 packages/core-api/src/lib/index.ts delete mode 100644 packages/core-api/src/lib/loginPopup.test.ts delete mode 100644 packages/core-api/src/lib/loginPopup.ts delete mode 100644 packages/core-api/src/lib/subjects.test.ts delete mode 100644 packages/core-api/src/lib/subjects.ts delete mode 100644 packages/core-api/src/lib/versionedValues.test.ts delete mode 100644 packages/core-api/src/lib/versionedValues.ts delete mode 100644 packages/core-api/src/plugin/Plugin.tsx delete mode 100644 packages/core-api/src/plugin/collectors.test.tsx delete mode 100644 packages/core-api/src/plugin/collectors.ts delete mode 100644 packages/core-api/src/plugin/index.ts delete mode 100644 packages/core-api/src/plugin/types.ts delete mode 100644 packages/core-api/src/private.ts delete mode 100644 packages/core-api/src/public.ts delete mode 100644 packages/core-api/src/routing/ExternalRouteRef.test.ts delete mode 100644 packages/core-api/src/routing/ExternalRouteRef.ts delete mode 100644 packages/core-api/src/routing/FlatRoutes.test.tsx delete mode 100644 packages/core-api/src/routing/FlatRoutes.tsx delete mode 100644 packages/core-api/src/routing/RouteRef.test.ts delete mode 100644 packages/core-api/src/routing/RouteRef.ts delete mode 100644 packages/core-api/src/routing/RouteResolver.test.ts delete mode 100644 packages/core-api/src/routing/RouteResolver.ts delete mode 100644 packages/core-api/src/routing/SubRouteRef.test.ts delete mode 100644 packages/core-api/src/routing/SubRouteRef.ts delete mode 100644 packages/core-api/src/routing/collectors.test.tsx delete mode 100644 packages/core-api/src/routing/collectors.tsx delete mode 100644 packages/core-api/src/routing/hooks.test.tsx delete mode 100644 packages/core-api/src/routing/hooks.tsx delete mode 100644 packages/core-api/src/routing/index.ts delete mode 100644 packages/core-api/src/routing/types.ts delete mode 100644 packages/core-api/src/routing/validation.ts delete mode 100644 packages/core-api/src/setupTests.ts delete mode 100644 packages/core-api/src/types.ts delete mode 100644 packages/core/.eslintrc.js delete mode 100644 packages/core/.npmrc delete mode 100644 packages/core/CHANGELOG.md delete mode 100644 packages/core/README.md delete mode 100644 packages/core/config.d.ts delete mode 100644 packages/core/package.json delete mode 100644 packages/core/src/api-wrappers/createApp.test.tsx delete mode 100644 packages/core/src/api-wrappers/createApp.tsx delete mode 100644 packages/core/src/api-wrappers/defaultApis.ts delete mode 100644 packages/core/src/api-wrappers/index.ts delete mode 100644 packages/core/src/components/AlertDisplay/AlertDisplay.test.tsx delete mode 100644 packages/core/src/components/AlertDisplay/AlertDisplay.tsx delete mode 100644 packages/core/src/components/AlertDisplay/index.ts delete mode 100644 packages/core/src/components/Avatar/Avatar.stories.tsx delete mode 100644 packages/core/src/components/Avatar/Avatar.test.tsx delete mode 100644 packages/core/src/components/Avatar/Avatar.tsx delete mode 100644 packages/core/src/components/Avatar/index.ts delete mode 100644 packages/core/src/components/Avatar/util.test.ts delete mode 100644 packages/core/src/components/Avatar/utils.ts delete mode 100644 packages/core/src/components/Button/Button.stories.tsx delete mode 100644 packages/core/src/components/Button/Button.test.tsx delete mode 100644 packages/core/src/components/Button/Button.tsx delete mode 100644 packages/core/src/components/Button/index.ts delete mode 100644 packages/core/src/components/CheckboxTree/CheckboxTree.stories.tsx delete mode 100644 packages/core/src/components/CheckboxTree/CheckboxTree.test.tsx delete mode 100644 packages/core/src/components/CheckboxTree/CheckboxTree.tsx delete mode 100644 packages/core/src/components/CheckboxTree/index.tsx delete mode 100644 packages/core/src/components/Chip/Chip.stories.tsx delete mode 100644 packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx delete mode 100644 packages/core/src/components/CodeSnippet/CodeSnippet.test.tsx delete mode 100644 packages/core/src/components/CodeSnippet/CodeSnippet.tsx delete mode 100644 packages/core/src/components/CodeSnippet/index.tsx delete mode 100644 packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx delete mode 100644 packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx delete mode 100644 packages/core/src/components/CopyTextButton/CopyTextButton.tsx delete mode 100644 packages/core/src/components/CopyTextButton/index.tsx delete mode 100644 packages/core/src/components/DependencyGraph/DefaultLabel.tsx delete mode 100644 packages/core/src/components/DependencyGraph/DefaultNode.tsx delete mode 100644 packages/core/src/components/DependencyGraph/DependencyGraph.stories.tsx delete mode 100644 packages/core/src/components/DependencyGraph/DependencyGraph.test.tsx delete mode 100644 packages/core/src/components/DependencyGraph/DependencyGraph.tsx delete mode 100644 packages/core/src/components/DependencyGraph/Edge.test.tsx delete mode 100644 packages/core/src/components/DependencyGraph/Edge.tsx delete mode 100644 packages/core/src/components/DependencyGraph/Node.test.tsx delete mode 100644 packages/core/src/components/DependencyGraph/Node.tsx delete mode 100644 packages/core/src/components/DependencyGraph/constants.ts delete mode 100644 packages/core/src/components/DependencyGraph/index.ts delete mode 100644 packages/core/src/components/DependencyGraph/types.ts delete mode 100644 packages/core/src/components/Dialog/Dialog.stories.tsx delete mode 100644 packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx delete mode 100644 packages/core/src/components/DismissableBanner/DismissableBanner.test.tsx delete mode 100644 packages/core/src/components/DismissableBanner/DismissableBanner.tsx delete mode 100644 packages/core/src/components/DismissableBanner/index.ts delete mode 100644 packages/core/src/components/Drawer/Drawer.stories.tsx delete mode 100644 packages/core/src/components/EmptyState/EmptyState.stories.tsx delete mode 100644 packages/core/src/components/EmptyState/EmptyState.test.tsx delete mode 100644 packages/core/src/components/EmptyState/EmptyState.tsx delete mode 100644 packages/core/src/components/EmptyState/EmptyStateImage.test.tsx delete mode 100644 packages/core/src/components/EmptyState/EmptyStateImage.tsx delete mode 100644 packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx delete mode 100644 packages/core/src/components/EmptyState/assets/createComponent.svg delete mode 100644 packages/core/src/components/EmptyState/assets/missingAnnotation.svg delete mode 100644 packages/core/src/components/EmptyState/assets/noBuild.svg delete mode 100644 packages/core/src/components/EmptyState/assets/noInformation.svg delete mode 100644 packages/core/src/components/EmptyState/index.ts delete mode 100644 packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.test.tsx delete mode 100644 packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx delete mode 100644 packages/core/src/components/FeatureDiscovery/index.ts delete mode 100644 packages/core/src/components/FeatureDiscovery/lib/usePortal.ts delete mode 100644 packages/core/src/components/FeatureDiscovery/lib/useShowCallout.ts delete mode 100644 packages/core/src/components/HeaderIconLinkRow/HeaderIconLinkRow.tsx delete mode 100644 packages/core/src/components/HeaderIconLinkRow/IconLinkVertical.tsx delete mode 100644 packages/core/src/components/HeaderIconLinkRow/index.ts delete mode 100644 packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.stories.tsx delete mode 100644 packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.test.tsx delete mode 100644 packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx delete mode 100644 packages/core/src/components/HorizontalScrollGrid/index.tsx delete mode 100644 packages/core/src/components/Lifecycle/Lifecycle.stories.tsx delete mode 100644 packages/core/src/components/Lifecycle/Lifecycle.test.tsx delete mode 100644 packages/core/src/components/Lifecycle/Lifecycle.tsx delete mode 100644 packages/core/src/components/Lifecycle/index.ts delete mode 100644 packages/core/src/components/Link/Link.stories.tsx delete mode 100644 packages/core/src/components/Link/Link.test.tsx delete mode 100644 packages/core/src/components/Link/Link.tsx delete mode 100644 packages/core/src/components/Link/index.ts delete mode 100644 packages/core/src/components/MarkdownContent/MarkdownContent.stories.tsx delete mode 100644 packages/core/src/components/MarkdownContent/MarkdownContent.test.tsx delete mode 100644 packages/core/src/components/MarkdownContent/MarkdownContent.tsx delete mode 100644 packages/core/src/components/MarkdownContent/index.ts delete mode 100644 packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx delete mode 100644 packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx delete mode 100644 packages/core/src/components/OAuthRequestDialog/index.ts delete mode 100644 packages/core/src/components/OverflowTooltip/OverflowTooltip.stories.tsx delete mode 100644 packages/core/src/components/OverflowTooltip/OverflowTooltip.test.tsx delete mode 100644 packages/core/src/components/OverflowTooltip/OverflowTooltip.tsx delete mode 100644 packages/core/src/components/OverflowTooltip/index.ts delete mode 100644 packages/core/src/components/Progress/Progress.stories.tsx delete mode 100644 packages/core/src/components/Progress/Progress.test.tsx delete mode 100644 packages/core/src/components/Progress/Progress.tsx delete mode 100644 packages/core/src/components/Progress/index.ts delete mode 100644 packages/core/src/components/ProgressBars/Gauge.stories.tsx delete mode 100644 packages/core/src/components/ProgressBars/Gauge.test.tsx delete mode 100644 packages/core/src/components/ProgressBars/Gauge.tsx delete mode 100644 packages/core/src/components/ProgressBars/GaugeCard.stories.tsx delete mode 100644 packages/core/src/components/ProgressBars/GaugeCard.test.tsx delete mode 100644 packages/core/src/components/ProgressBars/GaugeCard.tsx delete mode 100644 packages/core/src/components/ProgressBars/LinearGauge.stories.tsx delete mode 100644 packages/core/src/components/ProgressBars/LinearGauge.test.tsx delete mode 100644 packages/core/src/components/ProgressBars/LinearGauge.tsx delete mode 100644 packages/core/src/components/ProgressBars/index.ts delete mode 100644 packages/core/src/components/ResponseErrorPanel/ResponseErrorPanel.tsx delete mode 100644 packages/core/src/components/ResponseErrorPanel/index.ts delete mode 100644 packages/core/src/components/Select/Select.stories.tsx delete mode 100644 packages/core/src/components/Select/Select.test.tsx delete mode 100644 packages/core/src/components/Select/Select.tsx delete mode 100644 packages/core/src/components/Select/index.tsx delete mode 100644 packages/core/src/components/Select/static/ClosedDropdown.tsx delete mode 100644 packages/core/src/components/Select/static/OpenedDropdown.tsx delete mode 100644 packages/core/src/components/SimpleStepper/SimpleStepper.stories.tsx delete mode 100644 packages/core/src/components/SimpleStepper/SimpleStepper.test.tsx delete mode 100644 packages/core/src/components/SimpleStepper/SimpleStepper.tsx delete mode 100644 packages/core/src/components/SimpleStepper/SimpleStepperFooter.tsx delete mode 100644 packages/core/src/components/SimpleStepper/SimpleStepperStep.tsx delete mode 100644 packages/core/src/components/SimpleStepper/index.ts delete mode 100644 packages/core/src/components/SimpleStepper/types.ts delete mode 100644 packages/core/src/components/Status/Status.stories.tsx delete mode 100644 packages/core/src/components/Status/Status.test.tsx delete mode 100644 packages/core/src/components/Status/Status.tsx delete mode 100644 packages/core/src/components/Status/index.ts delete mode 100644 packages/core/src/components/StructuredMetadataTable/MetadataTable.tsx delete mode 100644 packages/core/src/components/StructuredMetadataTable/README.md delete mode 100644 packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx delete mode 100644 packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx delete mode 100644 packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx delete mode 100644 packages/core/src/components/StructuredMetadataTable/index.tsx delete mode 100644 packages/core/src/components/SupportButton/SupportButton.test.tsx delete mode 100644 packages/core/src/components/SupportButton/SupportButton.tsx delete mode 100644 packages/core/src/components/SupportButton/index.ts delete mode 100644 packages/core/src/components/TabbedLayout/RoutedTabs.test.tsx delete mode 100644 packages/core/src/components/TabbedLayout/RoutedTabs.tsx delete mode 100644 packages/core/src/components/TabbedLayout/TabbedLayout.stories.tsx delete mode 100644 packages/core/src/components/TabbedLayout/TabbedLayout.test.tsx delete mode 100644 packages/core/src/components/TabbedLayout/TabbedLayout.tsx delete mode 100644 packages/core/src/components/TabbedLayout/index.ts delete mode 100644 packages/core/src/components/TabbedLayout/types.ts delete mode 100644 packages/core/src/components/Table/Filters.tsx delete mode 100644 packages/core/src/components/Table/SubvalueCell.tsx delete mode 100644 packages/core/src/components/Table/Table.stories.tsx delete mode 100644 packages/core/src/components/Table/Table.test.tsx delete mode 100644 packages/core/src/components/Table/Table.tsx delete mode 100644 packages/core/src/components/Table/index.ts delete mode 100644 packages/core/src/components/Tabs/Tab.test.tsx delete mode 100644 packages/core/src/components/Tabs/Tab.tsx delete mode 100644 packages/core/src/components/Tabs/TabBar.tsx delete mode 100644 packages/core/src/components/Tabs/TabIcon.tsx delete mode 100644 packages/core/src/components/Tabs/TabPanel.tsx delete mode 100644 packages/core/src/components/Tabs/Tabs.stories.tsx delete mode 100644 packages/core/src/components/Tabs/Tabs.tsx delete mode 100644 packages/core/src/components/Tabs/index.ts delete mode 100644 packages/core/src/components/Tabs/utils.ts delete mode 100644 packages/core/src/components/TrendLine/TrendLine.stories.tsx delete mode 100644 packages/core/src/components/TrendLine/TrendLine.test.tsx delete mode 100644 packages/core/src/components/TrendLine/TrendLine.tsx delete mode 100644 packages/core/src/components/TrendLine/index.ts delete mode 100644 packages/core/src/components/WarningPanel/WarningPanel.stories.tsx delete mode 100644 packages/core/src/components/WarningPanel/WarningPanel.test.tsx delete mode 100644 packages/core/src/components/WarningPanel/WarningPanel.tsx delete mode 100644 packages/core/src/components/WarningPanel/index.ts delete mode 100644 packages/core/src/components/index.ts delete mode 100644 packages/core/src/hooks/index.ts delete mode 100644 packages/core/src/hooks/useQueryParamState.ts delete mode 100644 packages/core/src/hooks/useSupportConfig.ts delete mode 100644 packages/core/src/index.ts delete mode 100644 packages/core/src/layout/BottomLink/BottomLink.test.tsx delete mode 100644 packages/core/src/layout/BottomLink/BottomLink.tsx delete mode 100644 packages/core/src/layout/BottomLink/index.ts delete mode 100644 packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx delete mode 100644 packages/core/src/layout/Breadcrumbs/Breadcrumbs.test.tsx delete mode 100644 packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx delete mode 100644 packages/core/src/layout/Breadcrumbs/index.ts delete mode 100644 packages/core/src/layout/Content/Content.tsx delete mode 100644 packages/core/src/layout/Content/index.ts delete mode 100644 packages/core/src/layout/ContentHeader/ContentHeader.test.tsx delete mode 100644 packages/core/src/layout/ContentHeader/ContentHeader.tsx delete mode 100644 packages/core/src/layout/ContentHeader/index.ts delete mode 100644 packages/core/src/layout/ErrorBoundary/ErrorBoundary.test.tsx delete mode 100644 packages/core/src/layout/ErrorBoundary/ErrorBoundary.tsx delete mode 100644 packages/core/src/layout/ErrorBoundary/index.ts delete mode 100644 packages/core/src/layout/ErrorPage/ErrorPage.test.tsx delete mode 100644 packages/core/src/layout/ErrorPage/ErrorPage.tsx delete mode 100644 packages/core/src/layout/ErrorPage/MicDrop.tsx delete mode 100644 packages/core/src/layout/ErrorPage/index.ts delete mode 100644 packages/core/src/layout/ErrorPage/mic-drop.svg delete mode 100644 packages/core/src/layout/Header/Header.stories.tsx delete mode 100644 packages/core/src/layout/Header/Header.test.tsx delete mode 100644 packages/core/src/layout/Header/Header.tsx delete mode 100644 packages/core/src/layout/Header/index.ts delete mode 100644 packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx delete mode 100644 packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.tsx delete mode 100644 packages/core/src/layout/HeaderActionMenu/VerticalMenuIcon.tsx delete mode 100644 packages/core/src/layout/HeaderActionMenu/index.ts delete mode 100644 packages/core/src/layout/HeaderLabel/HeaderLabel.test.tsx delete mode 100644 packages/core/src/layout/HeaderLabel/HeaderLabel.tsx delete mode 100644 packages/core/src/layout/HeaderLabel/index.ts delete mode 100644 packages/core/src/layout/HeaderTabs/HeaderTabs.test.tsx delete mode 100644 packages/core/src/layout/HeaderTabs/HeaderTabs.tsx delete mode 100644 packages/core/src/layout/HeaderTabs/index.tsx delete mode 100644 packages/core/src/layout/HomepageTimer/HomepageTimer.test.tsx delete mode 100644 packages/core/src/layout/HomepageTimer/HomepageTimer.tsx delete mode 100644 packages/core/src/layout/HomepageTimer/index.ts delete mode 100644 packages/core/src/layout/InfoCard/InfoCard.stories.tsx delete mode 100644 packages/core/src/layout/InfoCard/InfoCard.test.tsx delete mode 100644 packages/core/src/layout/InfoCard/InfoCard.tsx delete mode 100644 packages/core/src/layout/InfoCard/index.ts delete mode 100644 packages/core/src/layout/ItemCard/ItemCard.stories.tsx delete mode 100644 packages/core/src/layout/ItemCard/ItemCard.test.tsx delete mode 100644 packages/core/src/layout/ItemCard/ItemCard.tsx delete mode 100644 packages/core/src/layout/ItemCard/ItemCardGrid.test.tsx delete mode 100644 packages/core/src/layout/ItemCard/ItemCardGrid.tsx delete mode 100644 packages/core/src/layout/ItemCard/ItemCardHeader.test.tsx delete mode 100644 packages/core/src/layout/ItemCard/ItemCardHeader.tsx delete mode 100644 packages/core/src/layout/ItemCard/index.ts delete mode 100644 packages/core/src/layout/Page/Page.stories.tsx delete mode 100644 packages/core/src/layout/Page/Page.tsx delete mode 100644 packages/core/src/layout/Page/index.ts delete mode 100644 packages/core/src/layout/Sidebar/Bar.tsx delete mode 100644 packages/core/src/layout/Sidebar/Intro.tsx delete mode 100644 packages/core/src/layout/Sidebar/Items.test.tsx delete mode 100644 packages/core/src/layout/Sidebar/Items.tsx delete mode 100644 packages/core/src/layout/Sidebar/Page.tsx delete mode 100644 packages/core/src/layout/Sidebar/Sidebar.stories.tsx delete mode 100644 packages/core/src/layout/Sidebar/config.ts delete mode 100644 packages/core/src/layout/Sidebar/index.ts delete mode 100644 packages/core/src/layout/Sidebar/localStorage.test.ts delete mode 100644 packages/core/src/layout/Sidebar/localStorage.ts delete mode 100644 packages/core/src/layout/SignInPage/SignInPage.tsx delete mode 100644 packages/core/src/layout/SignInPage/auth0Provider.tsx delete mode 100644 packages/core/src/layout/SignInPage/commonProvider.tsx delete mode 100644 packages/core/src/layout/SignInPage/customProvider.tsx delete mode 100644 packages/core/src/layout/SignInPage/guestProvider.tsx delete mode 100644 packages/core/src/layout/SignInPage/index.ts delete mode 100644 packages/core/src/layout/SignInPage/providers.tsx delete mode 100644 packages/core/src/layout/SignInPage/styles.tsx delete mode 100644 packages/core/src/layout/SignInPage/types.ts delete mode 100644 packages/core/src/layout/TabbedCard/TabbedCard.stories.tsx delete mode 100644 packages/core/src/layout/TabbedCard/TabbedCard.test.tsx delete mode 100644 packages/core/src/layout/TabbedCard/TabbedCard.tsx delete mode 100644 packages/core/src/layout/TabbedCard/index.ts delete mode 100644 packages/core/src/layout/index.ts delete mode 100644 packages/core/src/setupTests.ts diff --git a/packages/core-api/.eslintrc.js b/packages/core-api/.eslintrc.js deleted file mode 100644 index d592a653c8..0000000000 --- a/packages/core-api/.eslintrc.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], - rules: { - // TODO: add prop types to JS and remove - 'react/prop-types': 0, - 'jest/expect-expect': 0, - }, -}; diff --git a/packages/core-api/CHANGELOG.md b/packages/core-api/CHANGELOG.md deleted file mode 100644 index 016dff8a10..0000000000 --- a/packages/core-api/CHANGELOG.md +++ /dev/null @@ -1,390 +0,0 @@ -# @backstage/core-api - -## 0.2.23 - -### Patch Changes - -- a1c30d7ea: Add deprecation warning to package README. -- Updated dependencies - - @backstage/core-plugin-api@0.1.3 - -## 0.2.22 - -### Patch Changes - -- 9bca2a252: Improve forwards compatibility with `@backstage/core-app-api` and `@backstage/core-plugin-api` by re-using route reference types and factory methods from `@backstage/core-plugin-api`. -- Updated dependencies [75b8537ce] -- Updated dependencies [da8cba44f] - - @backstage/core-plugin-api@0.1.2 - -## 0.2.21 - -### Patch Changes - -- 0160678b1: Made the `RouteRef*` types compatible with the ones exported from `@backstage/core-plugin-api`. -- Updated dependencies [031ccd45f] -- Updated dependencies [e7c5e4b30] - - @backstage/core-plugin-api@0.1.1 - - @backstage/theme@0.2.8 - -## 0.2.20 - -### Patch Changes - -- d597a50c6: Add a global type definition for `Symbol.observable`, fix type checking in projects that didn't already have it defined. - -## 0.2.19 - -### Patch Changes - -- 61c3f927c: Updated the `Observable` type to provide interoperability with `Symbol.observable`, making it compatible with at least `zen-observable` and `RxJS 7`. - - In cases where this change breaks tests that mocked the `Observable` type, the following addition to the mock should fix the breakage: - - ```ts - [Symbol.observable]() { - return this; - }, - ``` - -- 65e6c4541: Remove circular dependencies - -## 0.2.18 - -### Patch Changes - -- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 -- 675a569a9: chore: bump `react-use` dependency in all packages - -## 0.2.17 - -### Patch Changes - -- ab07d77f6: Add support for discovering plugins through the app element tree, removing the need to register them explicitly. -- 50ce875a0: Fixed a potentially confusing error being thrown about misuse of routable extensions where the error was actually something different. -- Updated dependencies [931b21a12] - - @backstage/theme@0.2.6 - -## 0.2.16 - -### Patch Changes - -- 1279a3325: Introduce a `load-chunk` step in the `BootErrorPage` to show make chunk loading - errors visible to the user. -- 4a4681b1b: Improved error messaging for routable extension errors, making it easier to identify the component and mount point that caused the error. -- b051e770c: Fixed a bug with `useRouteRef` where navigating from routes beneath a mount point would often fail. - -## 0.2.15 - -### Patch Changes - -- 76deafd31: Changed the signature of `createRoutableExtension` to include null -- 01ccef4c7: Introduce `useRouteRefParams` to `core-api` to retrieve typed route parameters. -- Updated dependencies [4618774ff] - - @backstage/theme@0.2.5 - -## 0.2.14 - -### Patch Changes - -- a51dc0006: Export `SubRouteRef` type, and allow `SubRouteRef`s to be assigned to `plugin.routes`. -- e7f9b9435: Allow elements to be used multiple times in the app element tree. -- 34ff49b0f: Allow extension components to also return `null` in addition to a `JSX.Element`. -- d88dd219e: Internal refactor to allow for future package splits. As part of this `ApiRef`s are now identified by their ID rather than their reference. -- c8b54c370: Added new Docs Icon to Core Icons -- Updated dependencies [0434853a5] - - @backstage/config@0.1.4 - -## 0.2.13 - -### Patch Changes - -- 13524b80b: Fully deprecate `title` option of `RouteRef`s and introduce `id` instead. -- e74b07578: Fixed a bug where FlatRoutes didn't handle React Fragments properly. -- 6fb4258a8: Add `SubRouteRef`s, which can be used to create a route ref with a fixed path relative to an absolute `RouteRef`. They are useful if you for example have a page that is mounted at a sub route of a routable extension component, and you want other plugins to be able to route to that page. - - For example: - - ```tsx - // routes.ts - const rootRouteRef = createRouteRef({ id: 'root' }); - const detailsRouteRef = createSubRouteRef({ - id: 'root-sub', - parent: rootRouteRef, - path: '/details', - }); - - // plugin.ts - export const myPlugin = createPlugin({ - routes: { - root: rootRouteRef, - details: detailsRouteRef, - } - }) - - export const MyPage = plugin.provide(createRoutableExtension({ - component: () => import('./components/MyPage').then(m => m.MyPage), - mountPoint: rootRouteRef, - })) - - // components/MyPage.tsx - const MyPage = () => ( - - {/* myPlugin.routes.root will take the user to this page */} - }> - - {/* myPlugin.routes.details will take the user to this page */} - }> - - ) - ``` - -- 395885905: Wait for `configApi` to be ready before using `featureFlagsApi` -- Updated dependencies [2089de76b] - - @backstage/theme@0.2.4 - -## 0.2.12 - -### Patch Changes - -- 40c0fdbaa: Added support for optional external route references. By setting `optional: true` when creating an `ExternalRouteRef` it is no longer a requirement to bind the route in the app. If the app isn't bound `useRouteRef` will return `undefined`. -- 2a271d89e: Internal refactor of how component data is access to avoid polluting components and make it possible to bridge across versions. - -## 0.2.11 - -### Patch Changes - -- 3a58084b6: The `FlatRoutes` components now renders the not found page of the app if no routes are matched. -- 1407b34c6: More informative error message for missing ApiContext. -- b6c4f485d: Fix error when querying Backstage Identity with SAML authentication -- 3a58084b6: Created separate `AppContext` type to be returned from `useApp` rather than the `BackstageApp` itself. The `AppContext` type includes but deprecates `getPlugins`, `getProvider`, `getRouter`, and `getRoutes`. In addition, the `AppContext` adds a new `getComponents` method which providers access to the app components. -- Updated dependencies [a1f5e6545] - - @backstage/config@0.1.3 - -## 0.2.10 - -### Patch Changes - -- f10950bd2: Minor refactoring of BackstageApp.getSystemIcons to support custom registered - icons. Custom Icons can be added using: - - ```tsx - import AlarmIcon from '@material-ui/icons/Alarm'; - import MyPersonIcon from './MyPerson'; - - const app = createApp({ - icons: { - user: MyPersonIcon // override system icon - alert: AlarmIcon, // Custom icon - }, - }); - ``` - -- fd3f2a8c0: Export `createExternalRouteRef`, as well as give it an `id` for easier debugging, and fix parameter requirements when used with `useRouteRef`. - -## 0.2.9 - -### Patch Changes - -- ab0892358: Remove test dependencies from production package list - -## 0.2.8 - -### Patch Changes - -- a08c32ced: Add `FlatRoutes` component to replace the top-level `Routes` component from `react-router` within apps, removing the need for manually appending `/*` to paths or sorting routes. -- 86c3c652a: Deprecate `RouteRef` path parameter and member, and remove deprecated `routeRef.createSubRouteRef`. -- 27f2af935: Delay auth loginPopup close to avoid race condition with callers of authFlowHelpers. - -## 0.2.7 - -### Patch Changes - -- d681db2b5: Fix for GitHub and SAML auth not properly updating session state when already logged in. -- 1dc445e89: Introduce new plugin extension API -- Updated dependencies [1dc445e89] - - @backstage/test-utils@0.1.6 - -## 0.2.6 - -### Patch Changes - -- 7dd2ef7d1: Use auth provider ID to create unique session storage keys for GitHub and SAML Auth. - -## 0.2.5 - -### Patch Changes - -- b6557c098: Update ApiFactory type to correctly infer API type and disallow mismatched implementations. - - This fixes for example the following code: - - ```ts - interface MyApi { - myMethod(): void - } - - const myApiRef = createApiRef({...}); - - createApiFactory({ - api: myApiRef, - deps: {}, - // This should've caused an error, since the empty object does not fully implement MyApi - factory: () => ({}), - }) - ``` - -- d8d5a17da: Deprecated the `ConcreteRoute`, `MutableRouteRef`, `AbsoluteRouteRef` types and added a new `RouteRef` type as replacement. - - Deprecated and disabled the `createSubRoute` method of `AbsoluteRouteRef`. - - Add an as of yet unused `params` option to `createRouteRef`. - -- Updated dependencies [e3bd9fc2f] -- Updated dependencies [e1f4e24ef] -- Updated dependencies [1665ae8bb] -- Updated dependencies [e3bd9fc2f] - - @backstage/config@0.1.2 - - @backstage/test-utils@0.1.5 - - @backstage/theme@0.2.2 - -## 0.2.4 - -### Patch Changes - -- b4488ddb0: Added a type alias for PositionError = GeolocationPositionError - - @backstage/test-utils@0.1.4 - -## 0.2.3 - -### Patch Changes - -- 700a212b4: bug fix: issue 3223 - detect mismatching origin and indicate it in the message at auth failure - -## 0.2.2 - -### Patch Changes - -- 9b9e86f8a: export oidc provider - -## 0.2.1 - -### Patch Changes - -- c5bab94ab: Updated the AuthApi `.create` methods to configure the default scope of the corresponding Auth Api. As a result the - default scope is configurable when overwriting the Core Api in the app. - - ``` - GithubAuth.create({ - discoveryApi, - oauthRequestApi, - defaultScopes: ['read:user', 'repo'], - }), - ``` - - Replaced redundant CreateOptions of each Auth Api with the OAuthApiCreateOptions type. - - ``` - export type OAuthApiCreateOptions = AuthApiCreateOptions & { - oauthRequestApi: OAuthRequestApi; - defaultScopes?: string[]; - }; - - export type AuthApiCreateOptions = { - discoveryApi: DiscoveryApi; - environment?: string; - provider?: AuthProvider & { id: string }; - }; - ``` - -- Updated dependencies [4577e377b] - - @backstage/theme@0.2.1 - -## 0.2.0 - -### Minor Changes - -- 819a70229: Add SAML login to backstage - - ![](https://user-images.githubusercontent.com/872486/92251660-bb9e3400-eeff-11ea-86fe-1f2a0262cd31.png) - - ![](https://user-images.githubusercontent.com/872486/93851658-1a76f200-fce3-11ea-990b-26ca1a327a15.png) - -- b79017fd3: Updated the `GithubAuth.create` method to configure the default scope of the GitHub Auth Api. As a result the - default scope is configurable when overwriting the Core Api in the app. - - ``` - GithubAuth.create({ - discoveryApi, - oauthRequestApi, - defaultScopes: ['read:user', 'repo'], - }), - ``` - -- cbab5bbf8: Refactored the FeatureFlagsApi to make it easier to re-implement. Existing usage of particularly getUserFlags can be replaced with isActive() or save(). - -### Patch Changes - -- cbbd271c4: Add initial RouteRefRegistry - - Starting out some work to bring routing back and working as part of the work towards finalizing #1536 - - This is some of the groundwork of an experiment we're working on to enable routing via RouteRefs, while letting the app itself look something like this: - - ```jsx - const App = () => ( - - - - {' '} - // catalogRouteRef - - - - - - - // statusRouteRef - - - - - - - - - - // sentryRouteRef - - - - - - - - - - - - - - - - ); - ``` - - As part of inverting the composition of the app, route refs and routing in general was somewhat broken, intentionally. Right now it's not really possible to easily route to different parts of the app from a plugin, or even different parts of the plugin that are not within the same router. - - The core part of the experiment is to construct a map of ApiRef[] -> path overrides. Each key in the map is the list of route refs to traversed to reach a leaf in the routing tree, and the value is the path override at that point. For example, the above tree would add entries like [techDocsRouteRef] -> '/docs', and [entityRouteRef, apiDocsRouteRef] -> '/api'. By mapping out the entire app in this structure, the idea is that we can navigate to any point in the app using RouteRefs. - - The RouteRefRegistry is an implementation of such a map, and the idea is to add it in master to make it a bit easier to experiment and iterate. This is not an exposed API at this point. - - We've explored a couple of alternatives for how to enable routing, but it's boiled down to either a solution centred around the route map mentioned above, or treating all routes as static and globally unique, with no room for flexibility, customization or conflicts between different plugins. We're starting out pursuing this options 😁. We also expect that a the app-wide routing table will make things like dynamic loading a lot cleaner, as there would be a much more clear handoff between the main chunk and dynamic chunks. - -- 26e69ab1a: Remove cost insights example client from demo app and export from plugin - Create cost insights dev plugin using example client - Make PluginConfig and dependent types public -- Updated dependencies [ae5983387] -- Updated dependencies [0d4459c08] - - @backstage/theme@0.2.0 - - @backstage/test-utils@0.1.2 diff --git a/packages/core-api/README.md b/packages/core-api/README.md deleted file mode 100644 index 5a6430baa7..0000000000 --- a/packages/core-api/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# @backstage/core-api - -**NOTE: This package is deprecated** - -See the [migration documentation](https://backstage.io/docs/tutorials/migrating-away-from-core) for details on how to move to using the newer packages, [`@backstage/core-app-api`](https://www.npmjs.com/package/@backstage/core-app-api), [`@backstage/core-components`](https://www.npmjs.com/package/@backstage/core-components), and [`@backstage/core-plugin-api`](https://www.npmjs.com/package/@backstage/core-plugin-api) - -This package used to provide the core API used by Backstage plugins and apps. - -## Documentation - -- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) diff --git a/packages/core-api/package.json b/packages/core-api/package.json deleted file mode 100644 index c6a6b43401..0000000000 --- a/packages/core-api/package.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "@backstage/core-api", - "description": "Internal Core API used by Backstage plugins and apps", - "version": "0.2.23", - "private": false, - "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" - }, - "homepage": "https://backstage.io", - "repository": { - "type": "git", - "url": "https://github.com/backstage/backstage", - "directory": "packages/core-api" - }, - "keywords": [ - "backstage" - ], - "license": "Apache-2.0", - "main": "src/index.ts", - "types": "src/index.ts", - "scripts": { - "build": "backstage-cli build --outputs types,esm", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" - }, - "dependencies": { - "@backstage/config": "^0.1.4", - "@backstage/core-plugin-api": "^0.1.3", - "@backstage/theme": "^0.2.8", - "@material-ui/core": "^4.11.0", - "@material-ui/icons": "^4.9.1", - "@types/react": "^16.9", - "@types/prop-types": "^15.7.3", - "prop-types": "^15.7.2", - "react": "^16.12.0", - "react-router-dom": "6.0.0-beta.0", - "react-use": "^17.2.4", - "zen-observable": "^0.8.15" - }, - "devDependencies": { - "@backstage/cli": "^0.7.2", - "@backstage/test-utils": "^0.1.14", - "@backstage/test-utils-core": "^0.1.1", - "@testing-library/jest-dom": "^5.10.1", - "@testing-library/react": "^11.2.5", - "@testing-library/react-hooks": "^3.3.0", - "@testing-library/user-event": "^13.1.8", - "@types/jest": "^26.0.7", - "@types/node": "^14.14.32", - "@types/zen-observable": "^0.8.0", - "cross-fetch": "^3.0.6", - "msw": "^0.29.0" - }, - "files": [ - "dist" - ] -} diff --git a/packages/core-api/src/apis/definitions/AlertApi.ts b/packages/core-api/src/apis/definitions/AlertApi.ts deleted file mode 100644 index 22dd97ee38..0000000000 --- a/packages/core-api/src/apis/definitions/AlertApi.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { createApiRef, ApiRef } from '../system'; -import { Observable } from '../../types'; - -export type AlertMessage = { - message: string; - // Severity will default to success since that is what material ui defaults the value to. - severity?: 'success' | 'info' | 'warning' | 'error'; -}; - -/** - * The alert API is used to report alerts to the app, and display them to the user. - */ - -export type AlertApi = { - /** - * Post an alert for handling by the application. - */ - post(alert: AlertMessage): void; - - /** - * Observe alerts posted by other parts of the application. - */ - alert$(): Observable; -}; - -export const alertApiRef: ApiRef = createApiRef({ - id: 'core.alert', - description: 'Used to report alerts and forward them to the app', -}); diff --git a/packages/core-api/src/apis/definitions/AppThemeApi.ts b/packages/core-api/src/apis/definitions/AppThemeApi.ts deleted file mode 100644 index 75a2d6a4ec..0000000000 --- a/packages/core-api/src/apis/definitions/AppThemeApi.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiRef, createApiRef } from '../system'; -import { BackstageTheme } from '@backstage/theme'; -import { Observable } from '../../types'; -import { SvgIconProps } from '@material-ui/core'; - -/** - * Describes a theme provided by the app. - */ -export type AppTheme = { - /** - * ID used to remember theme selections. - */ - id: string; - - /** - * Title of the theme - */ - title: string; - - /** - * Theme variant - */ - variant: 'light' | 'dark'; - - /** - * The specialized MaterialUI theme instance. - */ - theme: BackstageTheme; - - /** - * An Icon for the theme mode setting. - */ - icon?: React.ReactElement; -}; - -/** - * The AppThemeApi gives access to the current app theme, and allows switching - * to other options that have been registered as a part of the App. - */ -export type AppThemeApi = { - /** - * Get a list of available themes. - */ - getInstalledThemes(): AppTheme[]; - - /** - * Observe the currently selected theme. A value of undefined means no specific theme has been selected. - */ - activeThemeId$(): Observable; - - /** - * Get the current theme ID. Returns undefined if no specific theme is selected. - */ - getActiveThemeId(): string | undefined; - - /** - * Set a specific theme to use in the app, overriding the default theme selection. - * - * Clear the selection by passing in undefined. - */ - setActiveThemeId(themeId?: string): void; -}; - -export const appThemeApiRef: ApiRef = createApiRef({ - id: 'core.apptheme', - description: 'API Used to configure the app theme, and enumerate options', -}); diff --git a/packages/core-api/src/apis/definitions/ConfigApi.ts b/packages/core-api/src/apis/definitions/ConfigApi.ts deleted file mode 100644 index c060d878bf..0000000000 --- a/packages/core-api/src/apis/definitions/ConfigApi.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { ApiRef, createApiRef } from '../system'; -import { Config } from '@backstage/config'; - -/** - * The Config API is used to provide a mechanism to access the - * runtime configuration of the system. - */ -export type ConfigApi = Config; - -export const configApiRef: ApiRef = createApiRef({ - id: 'core.config', - description: 'Used to access runtime configuration', -}); diff --git a/packages/core-api/src/apis/definitions/DiscoveryApi.ts b/packages/core-api/src/apis/definitions/DiscoveryApi.ts deleted file mode 100644 index 9ae5c8c4f9..0000000000 --- a/packages/core-api/src/apis/definitions/DiscoveryApi.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { ApiRef, createApiRef } from '../system'; - -/** - * The discovery API is used to provide a mechanism for plugins to - * discover the endpoint to use to talk to their backend counterpart. - * - * The purpose of the discovery API is to allow for many different deployment - * setups and routing methods through a central configuration, instead - * of letting each individual plugin manage that configuration. - * - * Implementations of the discovery API can be a simple as a URL pattern - * using the pluginId, but could also have overrides for individual plugins, - * or query a separate discovery service. - */ -export type DiscoveryApi = { - /** - * Returns the HTTP base backend URL for a given plugin, without a trailing slash. - * - * This method must always be called just before making a request, as opposed to - * fetching the URL when constructing an API client. That is to ensure that more - * flexible routing patterns can be supported. - * - * For example, asking for the URL for `auth` may return something - * like `https://backstage.example.com/api/auth` - */ - getBaseUrl(pluginId: string): Promise; -}; - -export const discoveryApiRef: ApiRef = createApiRef({ - id: 'core.discovery', - description: 'Provides service discovery of backend plugins', -}); diff --git a/packages/core-api/src/apis/definitions/ErrorApi.ts b/packages/core-api/src/apis/definitions/ErrorApi.ts deleted file mode 100644 index 86ca407718..0000000000 --- a/packages/core-api/src/apis/definitions/ErrorApi.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiRef, createApiRef } from '../system'; -import { Observable } from '../../types'; - -/** - * Mirrors the JavaScript Error class, for the purpose of - * providing documentation and optional fields. - */ -type Error = { - name: string; - message: string; - stack?: string; -}; - -/** - * Provides additional information about an error that was posted to the application. - */ -export type ErrorContext = { - // If set to true, this error should not be displayed to the user. Defaults to false. - hidden?: boolean; -}; - -/** - * The error API is used to report errors to the app, and display them to the user. - * - * Plugins can use this API as a method of displaying errors to the user, but also - * to report errors for collection by error reporting services. - * - * If an error can be displayed inline, e.g. as feedback in a form, that should be - * preferred over relying on this API to display the error. The main use of this API - * for displaying errors should be for asynchronous errors, such as a failing background process. - * - * Even if an error is displayed inline, it should still be reported through this API - * if it would be useful to collect or log it for debugging purposes, but with - * the hidden flag set. For example, an error arising from form field validation - * should probably not be reported, while a failed REST call would be useful to report. - */ -export type ErrorApi = { - /** - * Post an error for handling by the application. - */ - post(error: Error, context?: ErrorContext): void; - - /** - * Observe errors posted by other parts of the application. - */ - error$(): Observable<{ error: Error; context?: ErrorContext }>; -}; - -export const errorApiRef: ApiRef = createApiRef({ - id: 'core.error', - description: 'Used to report errors and forward them to the app', -}); diff --git a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts deleted file mode 100644 index 56fcd7cacf..0000000000 --- a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiRef, createApiRef } from '../system'; - -/** - * The feature flags API is used to toggle functionality to users across plugins and Backstage. - * - * Plugins can use this API to register feature flags that they have available - * for users to enable/disable, and this API will centralize the current user's - * state of which feature flags they would like to enable. - * - * This is ideal for Backstage plugins, as well as your own App, to trial incomplete - * or unstable upcoming features. Although there will be a common interface for users - * to enable and disable feature flags, this API acts as another way to enable/disable. - */ - -export type FeatureFlag = { - name: string; - pluginId: string; -}; - -export enum FeatureFlagState { - None = 0, - Active = 1, -} - -/** - * Options to use when saving feature flags. - */ -export type FeatureFlagsSaveOptions = { - /** - * The new feature flag states to save. - */ - states: Record; - - /** - * Whether the saves states should be merged into the existing ones, or replace them. - * - * Defaults to false. - */ - merge?: boolean; -}; - -export type UserFlags = {}; - -export interface FeatureFlagsApi { - /** - * Registers a new feature flag. Once a feature flag has been registered it - * can be toggled by users, and read back to enable or disable features. - */ - registerFlag(flag: FeatureFlag): void; - - /** - * Get a list of all registered flags. - */ - getRegisteredFlags(): FeatureFlag[]; - - /** - * Whether the feature flag with the given name is currently activated for the user. - */ - isActive(name: string): boolean; - - /** - * Save the user's choice of feature flag states. - */ - save(options: FeatureFlagsSaveOptions): void; -} - -export const featureFlagsApiRef: ApiRef = createApiRef({ - id: 'core.featureflags', - description: 'Used to toggle functionality in features across Backstage', -}); diff --git a/packages/core-api/src/apis/definitions/IdentityApi.ts b/packages/core-api/src/apis/definitions/IdentityApi.ts deleted file mode 100644 index 222cf12547..0000000000 --- a/packages/core-api/src/apis/definitions/IdentityApi.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { ApiRef, createApiRef } from '../system'; -import { ProfileInfo } from './auth'; - -/** - * The Identity API used to identify and get information about the signed in user. - */ -export type IdentityApi = { - /** - * The ID of the signed in user. This ID is not meant to be presented to the user, but used - * as an opaque string to pass on to backends or use in frontend logic. - * - * TODO: The intention of the user ID is to be able to tie the user to an identity - * that is known by the catalog and/or identity backend. It should for example - * be possible to fetch all owned components using this ID. - */ - getUserId(): string; - - // TODO: getProfile(): Promise - We want this to be async when added, but needs more work. - /** - * The profile of the signed in user. - */ - getProfile(): ProfileInfo; - - /** - * An OpenID Connect ID Token which proves the identity of the signed in user. - * - * The ID token will be undefined if the signed in user does not have a verified - * identity, such as a demo user or mocked user for e2e tests. - */ - getIdToken(): Promise; - - /** - * Sign out the current user - */ - signOut(): Promise; -}; - -export const identityApiRef: ApiRef = createApiRef({ - id: 'core.identity', - description: 'Provides access to the identity of the signed in user', -}); diff --git a/packages/core-api/src/apis/definitions/OAuthRequestApi.ts b/packages/core-api/src/apis/definitions/OAuthRequestApi.ts deleted file mode 100644 index 4e9b2cdbc8..0000000000 --- a/packages/core-api/src/apis/definitions/OAuthRequestApi.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { IconComponent } from '../../icons/types'; -import { Observable } from '../../types'; -import { ApiRef, createApiRef } from '../system'; - -/** - * Information about the auth provider that we're requesting a login towards. - * - * This should be shown to the user so that they can be informed about what login is being requested - * before a popup is shown. - */ -export type AuthProvider = { - /** - * Title for the auth provider, for example "GitHub" - */ - title: string; - - /** - * Icon for the auth provider. - */ - icon: IconComponent; -}; - -/** - * Describes how to handle auth requests. Both how to show them to the user, and what to do when - * the user accesses the auth request. - */ -export type AuthRequesterOptions = { - /** - * Information about the auth provider, which will be forwarded to auth requests. - */ - provider: AuthProvider; - - /** - * Implementation of the auth flow, which will be called synchronously when - * trigger() is called on an auth requests. - */ - onAuthRequest(scopes: Set): Promise; -}; - -/** - * Function used to trigger new auth requests for a set of scopes. - * - * The returned promise will resolve to the same value returned by the onAuthRequest in the - * AuthRequesterOptions. Or rejected, if the request is rejected. - * - * This function can be called multiple times before the promise resolves. All calls - * will be merged into one request, and the scopes forwarded to the onAuthRequest will be the - * union of all requested scopes. - */ -export type AuthRequester = ( - scopes: Set, -) => Promise; - -/** - * An pending auth request for a single auth provider. The request will remain in this pending - * state until either reject() or trigger() is called. - * - * Any new requests for the same provider are merged into the existing pending request, meaning - * there will only ever be a single pending request for a given provider. - */ -export type PendingAuthRequest = { - /** - * Information about the auth provider, as given in the AuthRequesterOptions - */ - provider: AuthProvider; - - /** - * Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError". - */ - reject: () => void; - - /** - * Trigger the auth request to continue the auth flow, by for example showing a popup. - * - * Synchronously calls onAuthRequest with all scope currently in the request. - */ - trigger(): Promise; -}; - -/** - * Provides helpers for implemented OAuth login flows within Backstage. - */ -export type OAuthRequestApi = { - /** - * A utility for showing login popups or similar things, and merging together multiple requests for - * different scopes into one request that includes all scopes. - * - * The passed in options provide information about the login provider, and how to handle auth requests. - * - * The returned AuthRequester function is used to request login with new scopes. These requests - * are merged together and forwarded to the auth handler, as soon as a consumer of auth requests - * triggers an auth flow. - * - * See AuthRequesterOptions, AuthRequester, and handleAuthRequests for more info. - */ - createAuthRequester( - options: AuthRequesterOptions, - ): AuthRequester; - - /** - * Observers pending auth requests. The returned observable will emit all - * current active auth request, at most one for each created auth requester. - * - * Each request has its own info about the login provider, forwarded from the auth requester options. - * - * Depending on user interaction, the request should either be rejected, or used to trigger the auth handler. - * If the request is rejected, all pending AuthRequester calls will fail with a "RejectedError". - * If a auth is triggered, and the auth handler resolves successfully, then all currently pending - * AuthRequester calls will resolve to the value returned by the onAuthRequest call. - */ - authRequest$(): Observable; -}; - -export const oauthRequestApiRef: ApiRef = createApiRef({ - id: 'core.oauthrequest', - description: 'An API for implementing unified OAuth flows in Backstage', -}); diff --git a/packages/core-api/src/apis/definitions/StorageApi.ts b/packages/core-api/src/apis/definitions/StorageApi.ts deleted file mode 100644 index bc243a65f3..0000000000 --- a/packages/core-api/src/apis/definitions/StorageApi.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiRef, createApiRef } from '../system'; -import { Observable } from '../../types'; -import { ErrorApi } from './ErrorApi'; - -export type StorageValueChange = { - key: string; - newValue?: T; -}; - -export type CreateStorageApiOptions = { - errorApi: ErrorApi; - namespace?: string; -}; - -export interface StorageApi { - /** - * Create a bucket to store data in. - * @param {String} name Namespace for the storage to be stored under, - * will inherit previous namespaces too - */ - forBucket(name: string): StorageApi; - - /** - * Get the current value for persistent data, use observe$ to be notified of updates. - * - * @param {String} key Unique key associated with the data. - * @return {Object} data The data that should is stored. - */ - get(key: string): T | undefined; - - /** - * Remove persistent data. - * - * @param {String} key Unique key associated with the data. - */ - remove(key: string): Promise; - - /** - * Save persistent data, and emit messages to anyone that is using observe$ for this key - * - * @param {String} key Unique key associated with the data. - */ - set(key: string, data: any): Promise; - - /** - * Observe changes on a particular key in the bucket - * @param {String} key Unique key associated with the data - */ - observe$(key: string): Observable>; -} - -export const storageApiRef: ApiRef = createApiRef({ - id: 'core.storage', - description: 'Provides the ability to store data which is unique to the user', -}); diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts deleted file mode 100644 index f71aa5df9c..0000000000 --- a/packages/core-api/src/apis/definitions/auth.ts +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiRef, createApiRef } from '../system'; -import { Observable } from '../../types'; - -/** - * This file contains declarations for common interfaces of auth-related APIs. - * The declarations should be used to signal which type of authentication and - * authorization methods each separate auth provider supports. - * - * For example, a Google OAuth provider that supports OAuth 2 and OpenID Connect, - * would be declared as follows: - * - * const googleAuthApiRef = createApiRef({ ... }) - */ - -/** - * An array of scopes, or a scope string formatted according to the - * auth provider, which is typically a space separated list. - * - * See the documentation for each auth provider for the list of scopes - * supported by each provider. - */ -export type OAuthScope = string | string[]; - -export type AuthRequestOptions = { - /** - * If this is set to true, the user will not be prompted to log in, - * and an empty response will be returned if there is no existing session. - * - * This can be used to perform a check whether the user is logged in, or if you don't - * want to force a user to be logged in, but provide functionality if they already are. - * - * @default false - */ - optional?: boolean; - - /** - * If this is set to true, the request will bypass the regular oauth login modal - * and open the login popup directly. - * - * The method must be called synchronously from a user action for this to work in all browsers. - * - * @default false - */ - instantPopup?: boolean; -}; - -/** - * This API provides access to OAuth 2 credentials. It lets you request access tokens, - * which can be used to act on behalf of the user when talking to APIs. - */ -export type OAuthApi = { - /** - * Requests an OAuth 2 Access Token, optionally with a set of scopes. The access token allows - * you to make requests on behalf of the user, and the copes may grant you broader access, depending - * on the auth provider. - * - * Each auth provider has separate handling of scope, so you need to look at the documentation - * for each one to know what scope you need to request. - * - * This method is cheap and should be called each time an access token is used. Do not for example - * store the access token in React component state, as that could cause the token to expire. Instead - * fetch a new access token for each request. - * - * Be sure to include all required scopes when requesting an access token. When testing your implementation - * it is best to log out the Backstage session and then visit your plugin page directly, as - * you might already have some required scopes in your existing session. Not requesting the correct - * scopes can lead to 403 or other authorization errors, which can be tricky to debug. - * - * If the user has not yet granted access to the provider and the set of requested scopes, the user - * will be prompted to log in. The returned promise will not resolve until the user has - * successfully logged in. The returned promise can be rejected, but only if the user rejects the login request. - */ - getAccessToken( - scope?: OAuthScope, - options?: AuthRequestOptions, - ): Promise; -}; - -/** - * This API provides access to OpenID Connect credentials. It lets you request ID tokens, - * which can be passed to backend services to prove the user's identity. - */ -export type OpenIdConnectApi = { - /** - * Requests an OpenID Connect ID Token. - * - * This method is cheap and should be called each time an ID token is used. Do not for example - * store the id token in React component state, as that could cause the token to expire. Instead - * fetch a new id token for each request. - * - * If the user has not yet logged in to Google inside Backstage, the user will be prompted - * to log in. The returned promise will not resolve until the user has successfully logged in. - * The returned promise can be rejected, but only if the user rejects the login request. - */ - getIdToken(options?: AuthRequestOptions): Promise; -}; - -/** - * This API provides access to profile information of the user from an auth provider. - */ -export type ProfileInfoApi = { - /** - * Get profile information for the user as supplied by this auth provider. - * - * If the optional flag is not set, a session is guaranteed to be returned, while if - * the optional flag is set, the session may be undefined. See @AuthRequestOptions for more details. - */ - getProfile(options?: AuthRequestOptions): Promise; -}; - -/** - * This API provides access to the user's identity within Backstage. - * - * An auth provider that implements this interface can be used to sign-in to backstage. It is - * not intended to be used directly from a plugin, but instead serves as a connection between - * this authentication method and the app's @IdentityApi - */ -export type BackstageIdentityApi = { - /** - * Get the user's identity within Backstage. This should normally not be called directly, - * use the @IdentityApi instead. - * - * If the optional flag is not set, a session is guaranteed to be returned, while if - * the optional flag is set, the session may be undefined. See @AuthRequestOptions for more details. - */ - getBackstageIdentity( - options?: AuthRequestOptions, - ): Promise; -}; - -export type BackstageIdentity = { - /** - * The backstage user ID. - */ - id: string; - - /** - * An ID token that can be used to authenticate the user within Backstage. - */ - idToken: string; -}; - -/** - * Profile information of the user. - */ -export type ProfileInfo = { - /** - * Email ID. - */ - email?: string; - - /** - * Display name that can be presented to the user. - */ - displayName?: string; - - /** - * URL to an avatar image of the user. - */ - picture?: string; -}; - -/** - * Session state values passed to subscribers of the SessionApi. - */ -export enum SessionState { - SignedIn = 'SignedIn', - SignedOut = 'SignedOut', -} - -/** - * The SessionApi provides basic controls for any auth provider that is tied to a persistent session. - */ -export type SessionApi = { - /** - * Sign in with a minimum set of permissions. - */ - signIn(): Promise; - - /** - * Sign out from the current session. This will reload the page. - */ - signOut(): Promise; - - /** - * Observe the current state of the auth session. Emits the current state on subscription. - */ - sessionState$(): Observable; -}; - -/** - * Provides authentication towards Google APIs and identities. - * - * See https://developers.google.com/identity/protocols/googlescopes for a full list of supported scopes. - * - * Note that the ID token payload is only guaranteed to contain the user's numerical Google ID, - * email and expiration information. Do not rely on any other fields, as they might not be present. - */ -export const googleAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.google', - description: 'Provides authentication towards Google APIs and identities', -}); - -/** - * Provides authentication towards GitHub APIs. - * - * See https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/ - * for a full list of supported scopes. - */ -export const githubAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.github', - description: 'Provides authentication towards GitHub APIs', -}); - -/** - * Provides authentication towards Okta APIs. - * - * See https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/ - * for a full list of supported scopes. - */ -export const oktaAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.okta', - description: 'Provides authentication towards Okta APIs', -}); - -/** - * Provides authentication towards GitLab APIs. - * - * See https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token - * for a full list of supported scopes. - */ -export const gitlabAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.gitlab', - description: 'Provides authentication towards GitLab APIs', -}); - -/** - * Provides authentication towards Auth0 APIs. - * - * See https://auth0.com/docs/scopes/current/oidc-scopes - * for a full list of supported scopes. - */ -export const auth0AuthApiRef: ApiRef< - OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.auth0', - description: 'Provides authentication towards Auth0 APIs', -}); - -/** - * Provides authentication towards Microsoft APIs and identities. - * - * For more info and a full list of supported scopes, see: - * - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent - * - https://docs.microsoft.com/en-us/graph/permissions-reference - */ -export const microsoftAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.microsoft', - description: 'Provides authentication towards Microsoft APIs and identities', -}); - -/** - * Provides authentication for custom identity providers. - */ -export const oauth2ApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.oauth2', - description: 'Example of how to use oauth2 custom provider', -}); - -/** - * Provides authentication for custom OpenID Connect identity providers. - */ -export const oidcAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.oidc', - description: 'Example of how to use oidc custom provider', -}); - -/** - * Provides authentication for saml based identity providers - */ -export const samlAuthApiRef: ApiRef< - ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.saml', - description: 'Example of how to use SAML custom provider', -}); - -export const oneloginAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.onelogin', - description: 'Provides authentication towards OneLogin APIs and identities', -}); diff --git a/packages/core-api/src/apis/definitions/index.ts b/packages/core-api/src/apis/definitions/index.ts deleted file mode 100644 index d4350ddbf6..0000000000 --- a/packages/core-api/src/apis/definitions/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// This folder contains definitions for all core APIs. -// -// Plugins should rely on these APIs for functionality as much as possible. -// -// If you think some API definition is missing, please open an Issue or send a PR! - -export * from './auth'; - -export * from './AlertApi'; -export * from './AppThemeApi'; -export * from './ConfigApi'; -export * from './DiscoveryApi'; -export * from './ErrorApi'; -export * from './FeatureFlagsApi'; -export * from './IdentityApi'; -export * from './OAuthRequestApi'; -export * from './StorageApi'; diff --git a/packages/core-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts b/packages/core-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts deleted file mode 100644 index 9f7adc260d..0000000000 --- a/packages/core-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { PublishSubject } from '../../../lib/subjects'; -import { Observable } from '../../../types'; -import { AlertApi, AlertMessage } from '../../definitions'; - -/** - * Base implementation for the AlertApi that simply forwards alerts to consumers. - */ -export class AlertApiForwarder implements AlertApi { - private readonly subject = new PublishSubject(); - - post(alert: AlertMessage) { - this.subject.next(alert); - } - - alert$(): Observable { - return this.subject; - } -} diff --git a/packages/core-api/src/apis/implementations/AlertApi/index.ts b/packages/core-api/src/apis/implementations/AlertApi/index.ts deleted file mode 100644 index d572845809..0000000000 --- a/packages/core-api/src/apis/implementations/AlertApi/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { AlertApiForwarder } from './AlertApiForwarder'; diff --git a/packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.test.ts b/packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.test.ts deleted file mode 100644 index 363e829ac2..0000000000 --- a/packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AppTheme } from '../../definitions'; -import { AppThemeSelector } from './AppThemeSelector'; - -describe('AppThemeSelector', () => { - it('should should select new themes', async () => { - const selector = new AppThemeSelector([]); - - expect(selector.getInstalledThemes()).toEqual([]); - - const subFn = jest.fn(); - selector.activeThemeId$().subscribe(subFn); - expect(selector.getActiveThemeId()).toBe(undefined); - await 'wait a tick'; - expect(subFn).toHaveBeenLastCalledWith(undefined); - - selector.setActiveThemeId('x'); - expect(subFn).toHaveBeenLastCalledWith('x'); - expect(selector.getActiveThemeId()).toBe('x'); - - selector.setActiveThemeId(undefined); - expect(subFn).toHaveBeenLastCalledWith(undefined); - expect(selector.getActiveThemeId()).toBe(undefined); - }); - - it('should return a new array of themes', () => { - const themes = new Array(); - const selector = new AppThemeSelector(themes); - - expect(selector.getInstalledThemes()).toEqual(themes); - expect(selector.getInstalledThemes()).not.toBe(themes); - }); - - it('should store theme in local storage', async () => { - expect(AppThemeSelector.createWithStorage([]).getActiveThemeId()).toBe( - undefined, - ); - localStorage.setItem('theme', 'x'); - expect(AppThemeSelector.createWithStorage([]).getActiveThemeId()).toBe('x'); - localStorage.removeItem('theme'); - expect(AppThemeSelector.createWithStorage([]).getActiveThemeId()).toBe( - undefined, - ); - - const addListenerSpy = jest.spyOn(window, 'addEventListener'); - const selector = AppThemeSelector.createWithStorage([]); - - expect(addListenerSpy).toHaveBeenCalledTimes(1); - expect(addListenerSpy).toHaveBeenCalledWith( - 'storage', - expect.any(Function), - ); - - selector.setActiveThemeId('y'); - await 'wait a tick'; - expect(localStorage.getItem('theme')).toBe('y'); - - selector.setActiveThemeId(undefined); - await 'wait a tick'; - expect(localStorage.getItem('theme')).toBe(null); - - localStorage.setItem('theme', 'z'); - expect(selector.getActiveThemeId()).toBe(undefined); - - const listener = addListenerSpy.mock.calls[0][1] as EventListener; - listener({ key: 'theme' } as StorageEvent); - - expect(selector.getActiveThemeId()).toBe('z'); - }); -}); diff --git a/packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts b/packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts deleted file mode 100644 index 5856199733..0000000000 --- a/packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AppThemeApi, AppTheme } from '../../definitions'; -import { BehaviorSubject } from '../../../lib/subjects'; -import { Observable } from '../../../types'; - -const STORAGE_KEY = 'theme'; - -export class AppThemeSelector implements AppThemeApi { - static createWithStorage(themes: AppTheme[]) { - const selector = new AppThemeSelector(themes); - - if (!window.localStorage) { - return selector; - } - - const initialThemeId = - window.localStorage.getItem(STORAGE_KEY) ?? undefined; - - selector.setActiveThemeId(initialThemeId); - - selector.activeThemeId$().subscribe(themeId => { - if (themeId) { - window.localStorage.setItem(STORAGE_KEY, themeId); - } else { - window.localStorage.removeItem(STORAGE_KEY); - } - }); - - window.addEventListener('storage', event => { - if (event.key === STORAGE_KEY) { - const themeId = localStorage.getItem(STORAGE_KEY) ?? undefined; - selector.setActiveThemeId(themeId); - } - }); - - return selector; - } - - private activeThemeId: string | undefined; - private readonly subject = new BehaviorSubject(undefined); - - constructor(private readonly themes: AppTheme[]) {} - - getInstalledThemes(): AppTheme[] { - return this.themes.slice(); - } - - activeThemeId$(): Observable { - return this.subject; - } - - getActiveThemeId(): string | undefined { - return this.activeThemeId; - } - - setActiveThemeId(themeId?: string): void { - this.activeThemeId = themeId; - this.subject.next(themeId); - } -} diff --git a/packages/core-api/src/apis/implementations/AppThemeApi/index.ts b/packages/core-api/src/apis/implementations/AppThemeApi/index.ts deleted file mode 100644 index b0a314303b..0000000000 --- a/packages/core-api/src/apis/implementations/AppThemeApi/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export * from './AppThemeSelector'; diff --git a/packages/core-api/src/apis/implementations/ConfigApi/index.ts b/packages/core-api/src/apis/implementations/ConfigApi/index.ts deleted file mode 100644 index 708c1d4573..0000000000 --- a/packages/core-api/src/apis/implementations/ConfigApi/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { ConfigReader } from '@backstage/config'; diff --git a/packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.test.ts b/packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.test.ts deleted file mode 100644 index 9cd666e8a3..0000000000 --- a/packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { UrlPatternDiscovery } from './UrlPatternDiscovery'; - -describe('UrlPatternDiscovery', () => { - it('should not require interpolation', async () => { - const discoveryApi = UrlPatternDiscovery.compile('http://example.com'); - await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe( - 'http://example.com', - ); - }); - - it('should use a plain pattern', async () => { - const discoveryApi = UrlPatternDiscovery.compile( - 'http://localhost:7000/{{ pluginId }}', - ); - await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe( - 'http://localhost:7000/my-plugin', - ); - }); - - it('should allow for multiple interpolation points', async () => { - const discoveryApi = UrlPatternDiscovery.compile( - 'https://{{pluginId }}.example.com/api/{{ pluginId}}', - ); - await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe( - 'https://my-plugin.example.com/api/my-plugin', - ); - }); - - it('should validate that the pattern is a valid URL', () => { - expect(() => { - UrlPatternDiscovery.compile('example.com'); - }).toThrow('Invalid discovery URL pattern, Invalid URL: example.com'); - - expect(() => { - UrlPatternDiscovery.compile('http://'); - }).toThrow('Invalid discovery URL pattern, Invalid URL: http://'); - - expect(() => { - UrlPatternDiscovery.compile('abc123'); - }).toThrow('Invalid discovery URL pattern, Invalid URL: abc123'); - - expect(() => { - UrlPatternDiscovery.compile('http://example.com:{{pluginId}}'); - }).toThrow( - 'Invalid discovery URL pattern, Invalid URL: http://example.com:pluginId', - ); - - expect(() => { - UrlPatternDiscovery.compile('/{{pluginId}}'); - }).toThrow('Invalid discovery URL pattern, Invalid URL: /pluginId'); - - expect(() => { - UrlPatternDiscovery.compile('http://localhost/{{pluginId}}?forbidden'); - }).toThrow('Invalid discovery URL pattern, URL must not have a query'); - - expect(() => { - UrlPatternDiscovery.compile('http://localhost/{{pluginId}}#forbidden'); - }).toThrow('Invalid discovery URL pattern, URL must not have a hash'); - - expect(() => { - UrlPatternDiscovery.compile('http://localhost/{{pluginId}}/'); - }).toThrow('Invalid discovery URL pattern, URL must not end with a slash'); - - expect(() => { - UrlPatternDiscovery.compile('http://localhost/'); - }).toThrow('Invalid discovery URL pattern, URL must not end with a slash'); - }); -}); diff --git a/packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts b/packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts deleted file mode 100644 index 10d3d90c63..0000000000 --- a/packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DiscoveryApi } from '../../definitions/DiscoveryApi'; - -/** - * UrlPatternDiscovery is a lightweight DiscoveryApi implementation. - * It uses a single template string to construct URLs for each plugin. - */ -export class UrlPatternDiscovery implements DiscoveryApi { - /** - * Creates a new UrlPatternDiscovery given a template. The the only - * interpolation done for the template is to replace instances of `{{pluginId}}` - * with the ID of the plugin being requested. - * - * Example pattern: `http://localhost:7000/api/{{ pluginId }}` - */ - static compile(pattern: string): UrlPatternDiscovery { - const parts = pattern.split(/\{\{\s*pluginId\s*\}\}/); - - try { - const urlStr = parts.join('pluginId'); - const url = new URL(urlStr); - if (url.hash) { - throw new Error('URL must not have a hash'); - } - if (url.search) { - throw new Error('URL must not have a query'); - } - if (urlStr.endsWith('/')) { - throw new Error('URL must not end with a slash'); - } - } catch (error) { - throw new Error(`Invalid discovery URL pattern, ${error.message}`); - } - - return new UrlPatternDiscovery(parts); - } - - private constructor(private readonly parts: string[]) {} - - async getBaseUrl(pluginId: string): Promise { - return this.parts.join(pluginId); - } -} diff --git a/packages/core-api/src/apis/implementations/DiscoveryApi/index.ts b/packages/core-api/src/apis/implementations/DiscoveryApi/index.ts deleted file mode 100644 index 184346401f..0000000000 --- a/packages/core-api/src/apis/implementations/DiscoveryApi/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { UrlPatternDiscovery } from './UrlPatternDiscovery'; diff --git a/packages/core-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts b/packages/core-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts deleted file mode 100644 index f537205917..0000000000 --- a/packages/core-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { AlertApi, ErrorApi, ErrorContext } from '../../definitions'; - -/** - * Decorates an ErrorApi by also forwarding error messages - * to the alertApi with an 'error' severity. - */ -export class ErrorAlerter implements ErrorApi { - constructor( - private readonly alertApi: AlertApi, - private readonly errorApi: ErrorApi, - ) {} - - post(error: Error, context?: ErrorContext) { - if (!context?.hidden) { - this.alertApi.post({ message: error.message, severity: 'error' }); - } - - return this.errorApi.post(error, context); - } - - error$() { - return this.errorApi.error$(); - } -} diff --git a/packages/core-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts b/packages/core-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts deleted file mode 100644 index 226c4f5488..0000000000 --- a/packages/core-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { PublishSubject } from '../../../lib/subjects'; -import { Observable } from '../../../types'; -import { ErrorApi, ErrorContext } from '../../definitions'; - -/** - * Base implementation for the ErrorApi that simply forwards errors to consumers. - */ -export class ErrorApiForwarder implements ErrorApi { - private readonly subject = new PublishSubject<{ - error: Error; - context?: ErrorContext; - }>(); - - post(error: Error, context?: ErrorContext) { - this.subject.next({ error, context }); - } - - error$(): Observable<{ error: Error; context?: ErrorContext }> { - return this.subject; - } -} diff --git a/packages/core-api/src/apis/implementations/ErrorApi/index.ts b/packages/core-api/src/apis/implementations/ErrorApi/index.ts deleted file mode 100644 index 49e12b4646..0000000000 --- a/packages/core-api/src/apis/implementations/ErrorApi/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { ErrorAlerter } from './ErrorAlerter'; -export { ErrorApiForwarder } from './ErrorApiForwarder'; diff --git a/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx b/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx deleted file mode 100644 index 64ab907d5c..0000000000 --- a/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { LocalStorageFeatureFlags } from './LocalStorageFeatureFlags'; -import { FeatureFlagState, FeatureFlagsApi } from '../../definitions'; - -describe('FeatureFlags', () => { - beforeEach(() => { - window.localStorage.clear(); - }); - - describe('getFlags', () => { - let featureFlags: FeatureFlagsApi; - - beforeEach(() => { - featureFlags = new LocalStorageFeatureFlags(); - }); - - it('returns no flags', () => { - expect(featureFlags.getRegisteredFlags()).toEqual([]); - }); - - it('loads flags from local storage', () => { - window.localStorage.setItem( - 'featureFlags', - JSON.stringify({ - 'feature-flag-one': 1, - 'feature-flag-two': 1, - 'feature-flag-three': 0, - 'feature-flag-four': 2, - 'feature-flag-five': 'not-valid', - }), - ); - - expect(featureFlags.isActive('feature-flag-one')).toBe(true); - expect(featureFlags.isActive('feature-flag-two')).toBe(true); - expect(featureFlags.isActive('feature-flag-three')).toBe(false); - expect(featureFlags.isActive('feature-flag-four')).toBe(false); - expect(featureFlags.isActive('feature-flag-five')).toBe(false); - }); - - it('sets the correct values', () => { - featureFlags.save({ - states: { - 'feature-flag-zero': FeatureFlagState.Active, - }, - }); - - expect(featureFlags.isActive('feature-flag-zero')).toBe(true); - expect(window.localStorage.getItem('featureFlags')).toEqual( - '{"feature-flag-zero":1}', - ); - }); - - it('deletes the correct values', () => { - window.localStorage.setItem( - 'featureFlags', - JSON.stringify({ - 'feature-flag-one': 1, - 'feature-flag-two': 0, - 'feature-flag-tree': 1, - 'feature-flag-four': 0, - }), - ); - - featureFlags.save({ - states: { - 'feature-flag-one': FeatureFlagState.None, - 'feature-flag-two': FeatureFlagState.Active, - }, - }); - - expect(window.localStorage.getItem('featureFlags')).toEqual( - '{"feature-flag-two":1}', - ); - }); - - it('clears all values', () => { - window.localStorage.setItem( - 'featureFlags', - JSON.stringify({ - 'feature-flag-one': 1, - 'feature-flag-two': 1, - 'feature-flag-three': 0, - }), - ); - - expect(featureFlags.isActive('feature-flag-one')).toBe(true); - expect(featureFlags.isActive('feature-flag-two')).toBe(true); - expect(featureFlags.isActive('feature-flag-three')).toBe(false); - - featureFlags.save({ states: {} }); - - expect(featureFlags.isActive('feature-flag-one')).toBe(false); - expect(featureFlags.isActive('feature-flag-two')).toBe(false); - expect(featureFlags.isActive('feature-flag-three')).toBe(false); - - expect(window.localStorage.getItem('featureFlags')).toEqual('{}'); - }); - }); - - describe('getRegisteredFlags', () => { - let featureFlags: FeatureFlagsApi; - - beforeEach(() => { - featureFlags = new LocalStorageFeatureFlags(); - featureFlags.registerFlag({ - name: 'registered-flag-1', - pluginId: 'plugin-one', - }); - featureFlags.registerFlag({ - name: 'registered-flag-2', - pluginId: 'plugin-one', - }); - featureFlags.registerFlag({ - name: 'registered-flag-3', - pluginId: 'plugin-two', - }); - }); - - it('should return an empty list', () => { - featureFlags = new LocalStorageFeatureFlags(); - expect(featureFlags.getRegisteredFlags()).toEqual([]); - }); - - it('should return an valid list', () => { - expect(featureFlags.getRegisteredFlags()).toEqual([ - { name: 'registered-flag-1', pluginId: 'plugin-one' }, - { name: 'registered-flag-2', pluginId: 'plugin-one' }, - { name: 'registered-flag-3', pluginId: 'plugin-two' }, - ]); - }); - - it('should provide a copy of the list of flags', () => { - const flags = featureFlags.getRegisteredFlags(); - expect(flags).toEqual([ - { name: 'registered-flag-1', pluginId: 'plugin-one' }, - { name: 'registered-flag-2', pluginId: 'plugin-one' }, - { name: 'registered-flag-3', pluginId: 'plugin-two' }, - ]); - flags.splice(2, 1); - expect(flags).toEqual([ - { name: 'registered-flag-1', pluginId: 'plugin-one' }, - { name: 'registered-flag-2', pluginId: 'plugin-one' }, - ]); - expect(featureFlags.getRegisteredFlags()).toEqual([ - { name: 'registered-flag-1', pluginId: 'plugin-one' }, - { name: 'registered-flag-2', pluginId: 'plugin-one' }, - { name: 'registered-flag-3', pluginId: 'plugin-two' }, - ]); - }); - - it('should get the correct values', () => { - const getByName = (name: string) => - featureFlags.getRegisteredFlags().find(flag => flag.name === name); - - expect(getByName('registered-flag-0')).toBeUndefined(); - expect(getByName('registered-flag-1')).toEqual({ - name: 'registered-flag-1', - pluginId: 'plugin-one', - }); - expect(getByName('registered-flag-2')).toEqual({ - name: 'registered-flag-2', - pluginId: 'plugin-one', - }); - expect(getByName('registered-flag-3')).toEqual({ - name: 'registered-flag-3', - pluginId: 'plugin-two', - }); - }); - - it('throws an error if length is less than three characters', () => { - expect(() => - featureFlags.registerFlag({ - name: 'ab', - pluginId: 'plugin-three', - }), - ).toThrow(/minimum length of three characters/i); - }); - - it('throws an error if length is greater than 150 characters', () => { - expect(() => - featureFlags.registerFlag({ - name: - 'loremipsumdolorsitametconsecteturadipiscingelitnuncvitaeportaexaullamcorperturpismaurisutmattisnequemorbisediaculisauguevivamuspulvinarcursuseratblandithendreritquisqueuttinciduntmagnavestibulumblanditaugueat', - pluginId: 'plugin-three', - }), - ).toThrow(/not exceed 150 characters/i); - }); - - it('throws an error if name does not start with a lowercase letter', () => { - expect(() => - featureFlags.registerFlag({ - name: '123456789', - pluginId: 'plugin-three', - }), - ).toThrow(/start with a lowercase letter/i); - }); - - it('throws an error if name contains characters other than lowercase letters, numbers and hyphens', () => { - expect(() => - featureFlags.registerFlag({ - name: 'Invalid_Feature_Flag', - pluginId: 'plugin-three', - }), - ).toThrow(/only contain lowercase letters, numbers and hyphens/i); - }); - }); -}); diff --git a/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx b/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx deleted file mode 100644 index 3d47e60de8..0000000000 --- a/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - FeatureFlagState, - FeatureFlagsApi, - FeatureFlag, - FeatureFlagsSaveOptions, -} from '../../definitions'; - -export function validateFlagName(name: string): void { - if (name.length < 3) { - throw new Error( - `The '${name}' feature flag must have a minimum length of three characters.`, - ); - } - - if (name.length > 150) { - throw new Error( - `The '${name}' feature flag must not exceed 150 characters.`, - ); - } - - if (!name.match(/^[a-z]+[a-z0-9-]+$/)) { - throw new Error( - `The '${name}' feature flag must start with a lowercase letter and only contain lowercase letters, numbers and hyphens. ` + - 'Examples: feature-flag-one, alpha, release-2020', - ); - } -} - -/** - * Create the FeatureFlags implementation based on the API. - */ -export class LocalStorageFeatureFlags implements FeatureFlagsApi { - private registeredFeatureFlags: FeatureFlag[] = []; - private flags?: Map; - - registerFlag(flag: FeatureFlag) { - validateFlagName(flag.name); - this.registeredFeatureFlags.push(flag); - } - - getRegisteredFlags(): FeatureFlag[] { - return this.registeredFeatureFlags.slice(); - } - - isActive(name: string): boolean { - if (!this.flags) { - this.flags = this.load(); - } - return this.flags.get(name) === FeatureFlagState.Active; - } - - save(options: FeatureFlagsSaveOptions): void { - if (!this.flags) { - this.flags = this.load(); - } - if (!options.merge) { - this.flags.clear(); - } - for (const [name, state] of Object.entries(options.states)) { - this.flags.set(name, state); - } - - const enabled = Array.from(this.flags.entries()).filter( - ([, state]) => state === FeatureFlagState.Active, - ); - window.localStorage.setItem( - 'featureFlags', - JSON.stringify(Object.fromEntries(enabled)), - ); - } - - private load(): Map { - try { - const jsonStr = window.localStorage.getItem('featureFlags'); - if (!jsonStr) { - return new Map(); - } - const json = JSON.parse(jsonStr) as unknown; - if (typeof json !== 'object' || json === null || Array.isArray(json)) { - return new Map(); - } - - const entries = Object.entries(json).filter(([name, value]) => { - validateFlagName(name); - return value === FeatureFlagState.Active; - }); - - return new Map(entries); - } catch { - return new Map(); - } - } -} diff --git a/packages/core-api/src/apis/implementations/FeatureFlagsApi/index.ts b/packages/core-api/src/apis/implementations/FeatureFlagsApi/index.ts deleted file mode 100644 index fc806c9ae1..0000000000 --- a/packages/core-api/src/apis/implementations/FeatureFlagsApi/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { LocalStorageFeatureFlags } from './LocalStorageFeatureFlags'; diff --git a/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts deleted file mode 100644 index d0f137f0be..0000000000 --- a/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import MockOAuthApi from './MockOAuthApi'; -import PowerIcon from '@material-ui/icons/Power'; - -describe('MockOAuthApi', () => { - it('should trigger all requests', async () => { - const authResult = { is: 'done' }; - const mock = new MockOAuthApi(); - - const authHandler1 = jest.fn().mockImplementation(() => authResult); - const requester1 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, - onAuthRequest: authHandler1, - }); - - const authHandler2 = jest.fn().mockResolvedValue('other'); - const requester2 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, - onAuthRequest: authHandler2, - }); - - const promises = [ - requester1(new Set(['a'])), - requester1(new Set(['b'])), - requester2(new Set(['a', 'b'])), - requester2(new Set(['b', 'c'])), - requester2(new Set(['c', 'a'])), - ]; - - await expect( - Promise.race([Promise.all(promises), 'waiting']), - ).resolves.toBe('waiting'); - - await mock.triggerAll(); - - await expect(Promise.all(promises)).resolves.toEqual([ - authResult, - authResult, - 'other', - 'other', - 'other', - ]); - - expect(authHandler1).toHaveBeenCalledTimes(1); - expect(authHandler1).toHaveBeenCalledWith(new Set(['a', 'b'])); - expect(authHandler2).toHaveBeenCalledTimes(1); - expect(authHandler2).toHaveBeenCalledWith(new Set(['a', 'b', 'c'])); - }); - - it('should reject all requests', async () => { - const mock = new MockOAuthApi(); - - const authHandler1 = jest.fn(); - const requester1 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, - onAuthRequest: authHandler1, - }); - - const authHandler2 = jest.fn(); - const requester2 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, - onAuthRequest: authHandler2, - }); - - const promises = [ - requester1(new Set(['a'])), - requester1(new Set(['b'])), - requester2(new Set(['a', 'b'])), - requester2(new Set(['b', 'c'])), - requester2(new Set(['c', 'a'])), - ]; - - await expect( - Promise.race([Promise.all(promises), 'waiting']), - ).resolves.toBe('waiting'); - - await mock.rejectAll(); - - for (const promise of promises) { - await expect(promise).rejects.toMatchObject({ name: 'RejectedError' }); - } - - expect(authHandler1).not.toHaveBeenCalled(); - expect(authHandler2).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts deleted file mode 100644 index 7c37d702d6..0000000000 --- a/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { OAuthRequestApi, AuthRequesterOptions } from '../../definitions'; -import { OAuthRequestManager } from './OAuthRequestManager'; - -export default class MockOAuthApi implements OAuthRequestApi { - private readonly real = new OAuthRequestManager(); - - createAuthRequester(options: AuthRequesterOptions) { - return this.real.createAuthRequester(options); - } - - authRequest$() { - return this.real.authRequest$(); - } - - async triggerAll() { - await Promise.resolve(); // Wait a tick to allow new requests to get forwarded - - return new Promise(resolve => { - const subscription = this.authRequest$().subscribe(requests => { - subscription.unsubscribe(); - Promise.all(requests.map(request => request.trigger())).then(() => - resolve(), - ); - }); - }); - } - - async rejectAll() { - await Promise.resolve(); // Wait a tick to allow new requests to get forwarded - - return new Promise(resolve => { - const subscription = this.authRequest$().subscribe(requests => { - subscription.unsubscribe(); - requests.map(request => request.reject()); - resolve(); - }); - }); - } -} diff --git a/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts deleted file mode 100644 index 20b0cbba67..0000000000 --- a/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { waitFor } from '@testing-library/react'; -import { OAuthPendingRequests } from './OAuthPendingRequests'; - -describe('OAuthPendingRequests', () => { - it('notifies new observers about current state', async () => { - const target = new OAuthPendingRequests(); - const next = jest.fn(); - const error = jest.fn(); - - const input = new Set(['a', 'b']); - target.pending().subscribe({ next, error }); - target.request(input); - - await waitFor(() => expect(next).toBeCalledTimes(2)); - expect(next.mock.calls[0][0].scopes).toBeUndefined(); - expect(next.mock.calls[1][0].scopes.toString()).toBe(input.toString()); - expect(error.mock.calls.length).toBe(0); - }); - - it('resolves requests and notifies observers', async () => { - const target = new OAuthPendingRequests(); - const next = jest.fn(); - const error = jest.fn(); - - const request1 = target.request(new Set(['a'])); - const request2 = target.request(new Set(['a'])); - target.pending().subscribe({ next, error }); - target.resolve(new Set(['a']), 'session1'); - target.resolve(new Set(['a']), 'session2'); - - await expect(request1).resolves.toBe('session1'); - await expect(request2).resolves.toBe('session1'); - expect(next).toBeCalledTimes(3); // once on subscription, twice on resolve - expect(error).toBeCalledTimes(0); - }); - - it('can resolve through the observable', async () => { - const target = new OAuthPendingRequests(); - const next = jest.fn(pendingRequest => pendingRequest.resolve('done')); - const error = jest.fn(); - - const request1 = target.request(new Set(['a'])); - target.pending().subscribe({ next, error }); - - await expect(request1).resolves.toBe('done'); - expect(next).toBeCalledTimes(2); // once with data on subscription, once empty after resolution - expect(error).toBeCalledTimes(0); - }); - - it('rejects requests and notifies observers only once', async () => { - const target = new OAuthPendingRequests(); - const next = jest.fn(); - const error = jest.fn(); - const rejection = new Error('eek'); - - const request1 = target.request(new Set(['a'])); - const request2 = target.request(new Set(['a'])); - target.pending().subscribe({ next, error }); - target.reject(rejection); - target.resolve(new Set(['a']), 'session'); - - await expect(request1).rejects.toBe(rejection); - await expect(request2).rejects.toBe(rejection); - expect(next).toBeCalledTimes(3); // once on subscription, once or reject, once on resolve - expect(error).toBeCalledTimes(0); - }); - - it('can reject through the observable', async () => { - const target = new OAuthPendingRequests(); - const rejection = new Error('nope'); - const next = jest.fn(pendingRequest => pendingRequest.reject(rejection)); - const error = jest.fn(); - - const request1 = target.request(new Set(['a'])); - target.pending().subscribe({ next, error }); - - await expect(request1).rejects.toBe(rejection); - expect(next).toBeCalledTimes(2); - }); -}); diff --git a/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts deleted file mode 100644 index 9e65ca007f..0000000000 --- a/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { BehaviorSubject } from '../../../lib/subjects'; -import { Observable } from '../../../types'; - -type RequestQueueEntry = { - scopes: Set; - resolve: (value: ResultType | PromiseLike) => void; - reject: (reason: Error) => void; -}; - -export type PendingRequest = { - scopes: Set | undefined; - resolve: (value: ResultType) => void; - reject: (reason: Error) => void; -}; - -export function hasScopes( - searched: Set, - searchFor: Set, -): boolean { - for (const scope of searchFor) { - if (!searched.has(scope)) { - return false; - } - } - return true; -} - -export function joinScopes( - scopes: Set, - ...moreScopess: Set[] -): Set { - const result = new Set(scopes); - - for (const moreScopes of moreScopess) { - for (const scope of moreScopes) { - result.add(scope); - } - } - - return result; -} - -/** - * The OAuthPendingRequests class is a utility for managing and observing - * a stream of requests for oauth scopes for a single provider, and resolving - * them correctly once requests are fulfilled. - */ -export class OAuthPendingRequests { - private requests: RequestQueueEntry[] = []; - private subject = new BehaviorSubject>( - this.getCurrentPending(), - ); - - request(scopes: Set): Promise { - return new Promise((resolve, reject) => { - this.requests.push({ scopes, resolve, reject }); - - this.subject.next(this.getCurrentPending()); - }); - } - - resolve(scopes: Set, result: ResultType): void { - this.requests = this.requests.filter(request => { - if (hasScopes(scopes, request.scopes)) { - request.resolve(result); - return false; - } - return true; - }); - - this.subject.next(this.getCurrentPending()); - } - - reject(error: Error) { - this.requests.forEach(request => request.reject(error)); - this.requests = []; - - this.subject.next(this.getCurrentPending()); - } - - pending(): Observable> { - return this.subject; - } - - private getCurrentPending(): PendingRequest { - const currentScopes = - this.requests.length === 0 - ? undefined - : this.requests - .slice(1) - .reduce( - (acc, current) => joinScopes(acc, current.scopes), - this.requests[0].scopes, - ); - - return { - scopes: currentScopes, - resolve: (value: ResultType) => { - if (currentScopes) { - this.resolve(currentScopes, value); - } - }, - reject: (reason: Error) => { - if (currentScopes) { - this.reject(reason); - } - }, - }; - } -} diff --git a/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts deleted file mode 100644 index d8faf90229..0000000000 --- a/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import ProviderIcon from '@material-ui/icons/AcUnit'; -import { OAuthRequestManager } from './OAuthRequestManager'; - -describe('OAuthRequestManager', () => { - it('should forward a requests', async () => { - const manager = new OAuthRequestManager(); - - const reqSpy = jest.fn(); - manager.authRequest$().subscribe(reqSpy); - - const requester = manager.createAuthRequester({ - provider: { - title: 'My Provider', - icon: ProviderIcon, - }, - onAuthRequest: async () => 'hello', - }); - - expect(reqSpy).toHaveBeenCalledTimes(0); - await 'a tick'; - expect(reqSpy).toHaveBeenCalledTimes(2); - expect(reqSpy).toHaveBeenLastCalledWith([]); - - const req = requester(new Set(['my-scope'])); - - expect(reqSpy).toHaveBeenCalledTimes(3); - expect(reqSpy).toHaveBeenLastCalledWith([ - expect.objectContaining({ - reject: expect.any(Function), - trigger: expect.any(Function), - }), - ]); - - await expect(Promise.race([req, Promise.resolve('not yet')])).resolves.toBe( - 'not yet', - ); - - const [request] = reqSpy.mock.calls[2][0]; - request.trigger(); - - await expect(req).resolves.toBe('hello'); - }); -}); diff --git a/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts deleted file mode 100644 index c8cea322e8..0000000000 --- a/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - OAuthRequestApi, - PendingAuthRequest, - AuthRequester, - AuthRequesterOptions, -} from '../../definitions'; -import { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests'; -import { BehaviorSubject } from '../../../lib/subjects'; -import { Observable } from '../../../types'; - -/** - * The OAuthRequestManager is an implementation of the OAuthRequestApi. - * - * The purpose of this class and the API is to read a stream of incoming requests - * of OAuth access tokens from different providers with varying scope, and funnel - * them all together into a single request for each OAuth provider. - */ -export class OAuthRequestManager implements OAuthRequestApi { - private readonly subject = new BehaviorSubject([]); - private currentRequests: PendingAuthRequest[] = []; - private handlerCount = 0; - - createAuthRequester(options: AuthRequesterOptions): AuthRequester { - const handler = new OAuthPendingRequests(); - - const index = this.handlerCount; - this.handlerCount++; - - handler.pending().subscribe({ - next: scopeRequest => { - const newRequests = this.currentRequests.slice(); - const request = this.makeAuthRequest(scopeRequest, options); - if (!request) { - delete newRequests[index]; - } else { - newRequests[index] = request; - } - this.currentRequests = newRequests; - // Convert from sparse array to array of present items only - this.subject.next(newRequests.filter(Boolean)); - }, - }); - - return scopes => { - return handler.request(scopes); - }; - } - - // Converts the pending request and popup options into a popup request that we can forward to subscribers. - private makeAuthRequest( - request: PendingRequest, - options: AuthRequesterOptions, - ): PendingAuthRequest | undefined { - const { scopes } = request; - if (!scopes) { - return undefined; - } - - return { - provider: options.provider, - trigger: async () => { - const result = await options.onAuthRequest(scopes); - request.resolve(result); - }, - reject: () => { - const error = new Error('Login failed, rejected by user'); - error.name = 'RejectedError'; - request.reject(error); - }, - }; - } - - authRequest$(): Observable { - return this.subject; - } -} diff --git a/packages/core-api/src/apis/implementations/OAuthRequestApi/index.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/index.ts deleted file mode 100644 index 72e42872fd..0000000000 --- a/packages/core-api/src/apis/implementations/OAuthRequestApi/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { OAuthRequestManager } from './OAuthRequestManager'; diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts deleted file mode 100644 index 8551eed4a6..0000000000 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { WebStorage } from './WebStorage'; -import { CreateStorageApiOptions, StorageApi } from '../../definitions'; - -describe('WebStorage Storage API', () => { - const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; - const createWebStorage = ( - args?: Partial, - ): StorageApi => { - return WebStorage.create({ - errorApi: mockErrorApi, - ...args, - }); - }; - it('should return undefined for values which are unset', async () => { - const storage = createWebStorage(); - - expect(storage.get('myfakekey')).toBeUndefined(); - }); - - it('should allow the setting and getting of the simple data structures', async () => { - const storage = createWebStorage(); - - await storage.set('myfakekey', 'helloimastring'); - await storage.set('mysecondfakekey', 1234); - await storage.set('mythirdfakekey', true); - expect(storage.get('myfakekey')).toBe('helloimastring'); - expect(storage.get('mysecondfakekey')).toBe(1234); - expect(storage.get('mythirdfakekey')).toBe(true); - }); - - it('should allow setting of complex datastructures', async () => { - const storage = createWebStorage(); - - const mockData = { - something: 'here', - is: [{ super: { complex: [{ but: 'something', why: true }] } }], - }; - - await storage.set('myfakekey', mockData); - - expect(storage.get('myfakekey')).toEqual(mockData); - }); - - it('should subscribe to key changes when setting a new value', async () => { - const storage = createWebStorage(); - - const wrongKeyNextHandler = jest.fn(); - const selectedKeyNextHandler = jest.fn(); - const mockData = { hello: 'im a great new value' }; - - await new Promise(resolve => { - storage.observe$('correctKey').subscribe({ - next: (...args) => { - selectedKeyNextHandler(...args); - resolve(); - }, - }); - - storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler }); - - storage.set('correctKey', mockData); - }); - - expect(wrongKeyNextHandler).not.toHaveBeenCalled(); - expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); - expect(selectedKeyNextHandler).toHaveBeenCalledWith({ - key: 'correctKey', - newValue: mockData, - }); - }); - - it('should subscribe to key changes when deleting a value', async () => { - const storage = createWebStorage(); - - const wrongKeyNextHandler = jest.fn(); - const selectedKeyNextHandler = jest.fn(); - const mockData = { hello: 'im a great new value' }; - - storage.set('correctKey', mockData); - - await new Promise(resolve => { - storage.observe$('correctKey').subscribe({ - next: (...args) => { - selectedKeyNextHandler(...args); - resolve(); - }, - }); - - storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler }); - - storage.remove('correctKey'); - }); - - expect(wrongKeyNextHandler).not.toHaveBeenCalled(); - expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); - expect(selectedKeyNextHandler).toHaveBeenCalledWith({ - key: 'correctKey', - newValue: undefined, - }); - }); - - it('should be able to create different buckets for different uses', async () => { - const rootStorage = createWebStorage(); - - const firstStorage = rootStorage.forBucket('userSettings'); - const secondStorage = rootStorage.forBucket('profileSettings'); - const keyName = 'blobby'; - - await firstStorage.set(keyName, 'boop'); - await secondStorage.set(keyName, 'deerp'); - - expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName)); - expect(firstStorage.get(keyName)).toBe('boop'); - expect(secondStorage.get(keyName)).toBe('deerp'); - }); - - it('should not clash with other namesapces when creating buckets', async () => { - const rootStorage = createWebStorage(); - - // when getting key test2 it will translate to /profile/something/deep/test2 - const firstStorage = rootStorage - .forBucket('profile') - .forBucket('something') - .forBucket('deep'); - // when getting key deep/test2 it will translate to /profile/something/deep/test2 - const secondStorage = rootStorage.forBucket('profile/something'); - - await firstStorage.set('test2', { error: true }); - - expect(secondStorage.get('deep/test2')).toBe(undefined); - }); - - it('should call the error api when the json can not be parsed in local storage', async () => { - const rootStorage = createWebStorage({ - namespace: '/Test/Mock/Thing', - }); - - localStorage.setItem('/Test/Mock/Thing/key', '{smd: asdouindA}'); - - const value = rootStorage.get('key'); - - expect(value).toBe(undefined); - expect(mockErrorApi.post).toHaveBeenCalledWith(expect.any(Error)); - expect(mockErrorApi.post).toHaveBeenCalledWith( - expect.objectContaining({ - message: 'Error when parsing JSON config from storage for: key', - }), - ); - }); - - it('should return a singleton for the same namespace and same bucket', async () => { - const rootStorage = createWebStorage({ - namespace: '/Test/Mock/Thing/Thing ', - }); - - expect(rootStorage.forBucket('test')).toBe(rootStorage.forBucket('test')); - }); -}); diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts deleted file mode 100644 index 1b4f4a88bb..0000000000 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { - StorageApi, - StorageValueChange, - ErrorApi, - CreateStorageApiOptions, -} from '../../definitions'; -import { Observable } from '../../../types'; -import ObservableImpl from 'zen-observable'; - -const buckets = new Map(); - -export class WebStorage implements StorageApi { - constructor( - private readonly namespace: string, - private readonly errorApi: ErrorApi, - ) {} - - static create(options: CreateStorageApiOptions): WebStorage { - return new WebStorage(options.namespace ?? '', options.errorApi); - } - - get(key: string): T | undefined { - try { - const storage = JSON.parse(localStorage.getItem(this.getKeyName(key))!); - return storage ?? undefined; - } catch (e) { - this.errorApi.post( - new Error(`Error when parsing JSON config from storage for: ${key}`), - ); - } - - return undefined; - } - - forBucket(name: string): WebStorage { - const bucketPath = `${this.namespace}/${name}`; - if (!buckets.has(bucketPath)) { - buckets.set(bucketPath, new WebStorage(bucketPath, this.errorApi)); - } - return buckets.get(bucketPath)!; - } - - async set(key: string, data: T): Promise { - localStorage.setItem(this.getKeyName(key), JSON.stringify(data, null, 2)); - this.notifyChanges({ key, newValue: data }); - } - - async remove(key: string): Promise { - localStorage.removeItem(this.getKeyName(key)); - this.notifyChanges({ key, newValue: undefined }); - } - - observe$(key: string): Observable> { - return this.observable.filter(({ key: messageKey }) => messageKey === key); - } - - private getKeyName(key: string) { - return `${this.namespace}/${encodeURIComponent(key)}`; - } - - private notifyChanges(message: StorageValueChange) { - for (const subscription of this.subscribers) { - subscription.next(message); - } - } - - private subscribers = new Set< - ZenObservable.SubscriptionObserver - >(); - - private readonly observable = new ObservableImpl( - subscriber => { - this.subscribers.add(subscriber); - return () => { - this.subscribers.delete(subscriber); - }; - }, - ); -} diff --git a/packages/core-api/src/apis/implementations/StorageApi/index.ts b/packages/core-api/src/apis/implementations/StorageApi/index.ts deleted file mode 100644 index 2f941381fd..0000000000 --- a/packages/core-api/src/apis/implementations/StorageApi/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { WebStorage } from './WebStorage'; diff --git a/packages/core-api/src/apis/implementations/auth/auth0/Auth0Auth.ts b/packages/core-api/src/apis/implementations/auth/auth0/Auth0Auth.ts deleted file mode 100644 index 2dac460fbe..0000000000 --- a/packages/core-api/src/apis/implementations/auth/auth0/Auth0Auth.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import Auth0Icon from '@material-ui/icons/AcUnit'; -import { auth0AuthApiRef } from '../../../definitions/auth'; -import { OAuth2 } from '../oauth2'; -import { OAuthApiCreateOptions } from '../types'; - -const DEFAULT_PROVIDER = { - id: 'auth0', - title: 'Auth0', - icon: Auth0Icon, -}; - -class Auth0Auth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['openid', `email`, `profile`], - }: OAuthApiCreateOptions): typeof auth0AuthApiRef.T { - return OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider, - environment, - defaultScopes, - }); - } -} - -export default Auth0Auth; diff --git a/packages/core-api/src/apis/implementations/auth/auth0/index.ts b/packages/core-api/src/apis/implementations/auth/auth0/index.ts deleted file mode 100644 index 9daed7d13e..0000000000 --- a/packages/core-api/src/apis/implementations/auth/auth0/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { default as Auth0Auth } from './Auth0Auth'; diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts deleted file mode 100644 index 04bfff028d..0000000000 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import GithubAuth from './GithubAuth'; - -describe('GithubAuth', () => { - it('should get access token', async () => { - const getSession = jest - .fn() - .mockResolvedValue({ providerInfo: { accessToken: 'access-token' } }); - const githubAuth = new GithubAuth({ getSession } as any); - - expect(await githubAuth.getAccessToken()).toBe('access-token'); - expect(getSession).toBeCalledTimes(1); - }); -}); diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts deleted file mode 100644 index 60ee6a2629..0000000000 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import GithubIcon from '@material-ui/icons/AcUnit'; -import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; -import { GithubSession } from './types'; -import { - OAuthApi, - SessionApi, - SessionState, - ProfileInfo, - BackstageIdentity, - AuthRequestOptions, -} from '../../../definitions/auth'; -import { SessionManager } from '../../../../lib/AuthSessionManager/types'; -import { - AuthSessionStore, - StaticAuthSessionManager, -} from '../../../../lib/AuthSessionManager'; -import { Observable } from '../../../../types'; -import { OAuthApiCreateOptions } from '../types'; - -export type GithubAuthResponse = { - providerInfo: { - accessToken: string; - scope: string; - expiresInSeconds: number; - }; - profile: ProfileInfo; - backstageIdentity: BackstageIdentity; -}; - -const DEFAULT_PROVIDER = { - id: 'github', - title: 'GitHub', - icon: GithubIcon, -}; - -class GithubAuth implements OAuthApi, SessionApi { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['read:user'], - }: OAuthApiCreateOptions) { - const connector = new DefaultAuthConnector({ - discoveryApi, - environment, - provider, - oauthRequestApi: oauthRequestApi, - sessionTransform(res: GithubAuthResponse): GithubSession { - return { - ...res, - providerInfo: { - accessToken: res.providerInfo.accessToken, - scopes: GithubAuth.normalizeScope(res.providerInfo.scope), - expiresAt: new Date( - Date.now() + res.providerInfo.expiresInSeconds * 1000, - ), - }, - }; - }, - }); - - const sessionManager = new StaticAuthSessionManager({ - connector, - defaultScopes: new Set(defaultScopes), - sessionScopes: (session: GithubSession) => session.providerInfo.scopes, - }); - - const authSessionStore = new AuthSessionStore({ - manager: sessionManager, - storageKey: `${provider.id}Session`, - sessionScopes: (session: GithubSession) => session.providerInfo.scopes, - }); - - return new GithubAuth(authSessionStore); - } - - constructor(private readonly sessionManager: SessionManager) {} - - async signIn() { - await this.getAccessToken(); - } - - async signOut() { - await this.sessionManager.removeSession(); - } - - sessionState$(): Observable { - return this.sessionManager.sessionState$(); - } - - async getAccessToken(scope?: string, options?: AuthRequestOptions) { - const session = await this.sessionManager.getSession({ - ...options, - scopes: GithubAuth.normalizeScope(scope), - }); - return session?.providerInfo.accessToken ?? ''; - } - - async getBackstageIdentity( - options: AuthRequestOptions = {}, - ): Promise { - const session = await this.sessionManager.getSession(options); - return session?.backstageIdentity; - } - - async getProfile(options: AuthRequestOptions = {}) { - const session = await this.sessionManager.getSession(options); - return session?.profile; - } - - static normalizeScope(scope?: string): Set { - if (!scope) { - return new Set(); - } - - const scopeList = Array.isArray(scope) - ? scope - : scope.split(/[\s|,]/).filter(Boolean); - - return new Set(scopeList); - } -} -export default GithubAuth; diff --git a/packages/core-api/src/apis/implementations/auth/github/index.ts b/packages/core-api/src/apis/implementations/auth/github/index.ts deleted file mode 100644 index ee4334f6fc..0000000000 --- a/packages/core-api/src/apis/implementations/auth/github/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export * from './types'; -export { default as GithubAuth } from './GithubAuth'; diff --git a/packages/core-api/src/apis/implementations/auth/github/types.ts b/packages/core-api/src/apis/implementations/auth/github/types.ts deleted file mode 100644 index 90573d2429..0000000000 --- a/packages/core-api/src/apis/implementations/auth/github/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { BackstageIdentity, ProfileInfo } from '../../../definitions'; - -export type GithubSession = { - providerInfo: { - accessToken: string; - scopes: Set; - expiresAt: Date; - }; - profile: ProfileInfo; - backstageIdentity: BackstageIdentity; -}; diff --git a/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts deleted file mode 100644 index 3346af9b7c..0000000000 --- a/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; -import { UrlPatternDiscovery } from '../../DiscoveryApi'; -import GitlabAuth from './GitlabAuth'; - -const getSession = jest.fn(); - -jest.mock('../../../../lib/AuthSessionManager', () => ({ - ...(jest.requireActual('../../../../lib/AuthSessionManager') as any), - RefreshingAuthSessionManager: class { - getSession = getSession; - }, -})); - -describe('GitlabAuth', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - it.each([ - [ - 'read_user api write_repository', - ['read_user', 'api', 'write_repository'], - ], - ['read_repository sudo', ['read_repository', 'sudo']], - ])(`should normalize scopes correctly - %p`, (scope, scopes) => { - const gitlabAuth = GitlabAuth.create({ - oauthRequestApi: new MockOAuthApi(), - discoveryApi: UrlPatternDiscovery.compile('http://example.com'), - }); - - gitlabAuth.getAccessToken(scope); - expect(getSession).toHaveBeenCalledWith({ scopes: new Set(scopes) }); - }); -}); diff --git a/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts deleted file mode 100644 index 37a3c89dd6..0000000000 --- a/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import GitlabIcon from '@material-ui/icons/AcUnit'; -import { gitlabAuthApiRef } from '../../../definitions/auth'; -import { OAuth2 } from '../oauth2'; -import { OAuthApiCreateOptions } from '../types'; - -const DEFAULT_PROVIDER = { - id: 'gitlab', - title: 'GitLab', - icon: GitlabIcon, -}; - -class GitlabAuth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['read_user'], - }: OAuthApiCreateOptions): typeof gitlabAuthApiRef.T { - return OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider, - environment, - defaultScopes, - }); - } -} -export default GitlabAuth; diff --git a/packages/core-api/src/apis/implementations/auth/gitlab/index.ts b/packages/core-api/src/apis/implementations/auth/gitlab/index.ts deleted file mode 100644 index 935a11dd54..0000000000 --- a/packages/core-api/src/apis/implementations/auth/gitlab/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { default as GitlabAuth } from './GitlabAuth'; diff --git a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts deleted file mode 100644 index f8a1c3a1f5..0000000000 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import GoogleAuth from './GoogleAuth'; -import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; -import { UrlPatternDiscovery } from '../../DiscoveryApi'; - -const PREFIX = 'https://www.googleapis.com/auth/'; - -const getSession = jest.fn(); - -jest.mock('../../../../lib/AuthSessionManager', () => ({ - ...(jest.requireActual('../../../../lib/AuthSessionManager') as any), - RefreshingAuthSessionManager: class { - getSession = getSession; - }, -})); - -describe('GoogleAuth', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - it.each([ - ['email', [`${PREFIX}userinfo.email`]], - ['profile', [`${PREFIX}userinfo.profile`]], - ['openid', ['openid']], - ['userinfo.email', [`${PREFIX}userinfo.email`]], - [ - 'userinfo.profile email', - [`${PREFIX}userinfo.profile`, `${PREFIX}userinfo.email`], - ], - [ - `profile ${PREFIX}userinfo.email`, - [`${PREFIX}userinfo.profile`, `${PREFIX}userinfo.email`], - ], - [`${PREFIX}userinfo.profile`, [`${PREFIX}userinfo.profile`]], - ['a', [`${PREFIX}a`]], - ['a b\tc', [`${PREFIX}a`, `${PREFIX}b`, `${PREFIX}c`]], - [`${PREFIX}a b`, [`${PREFIX}a`, `${PREFIX}b`]], - [`${PREFIX}a`, [`${PREFIX}a`]], - - // Some incorrect scopes that we don't try to fix - [`${PREFIX}email`, [`${PREFIX}email`]], - [`${PREFIX}profile`, [`${PREFIX}profile`]], - [`${PREFIX}openid`, [`${PREFIX}openid`]], - ])(`should normalize scopes correctly - %p`, (scope, scopes) => { - const googleAuth = GoogleAuth.create({ - oauthRequestApi: new MockOAuthApi(), - discoveryApi: UrlPatternDiscovery.compile('http://example.com'), - }); - - googleAuth.getAccessToken(scope); - expect(getSession).toHaveBeenCalledWith({ scopes: new Set(scopes) }); - }); -}); diff --git a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts deleted file mode 100644 index 988ef9c2e0..0000000000 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import GoogleIcon from '@material-ui/icons/AcUnit'; -import { googleAuthApiRef } from '../../../definitions/auth'; -import { OAuth2 } from '../oauth2'; -import { OAuthApiCreateOptions } from '../types'; - -const DEFAULT_PROVIDER = { - id: 'google', - title: 'Google', - icon: GoogleIcon, -}; - -const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; - -class GoogleAuth { - static create({ - discoveryApi, - oauthRequestApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - defaultScopes = [ - 'openid', - `${SCOPE_PREFIX}userinfo.email`, - `${SCOPE_PREFIX}userinfo.profile`, - ], - }: OAuthApiCreateOptions): typeof googleAuthApiRef.T { - return OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider, - environment, - defaultScopes, - scopeTransform(scopes: string[]) { - return scopes.map(scope => { - if (scope === 'openid') { - return scope; - } - - if (scope === 'profile' || scope === 'email') { - return `${SCOPE_PREFIX}userinfo.${scope}`; - } - - if (scope.startsWith(SCOPE_PREFIX)) { - return scope; - } - - return `${SCOPE_PREFIX}${scope}`; - }); - }, - }); - } -} -export default GoogleAuth; diff --git a/packages/core-api/src/apis/implementations/auth/google/index.ts b/packages/core-api/src/apis/implementations/auth/google/index.ts deleted file mode 100644 index 96a8699eab..0000000000 --- a/packages/core-api/src/apis/implementations/auth/google/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { default as GoogleAuth } from './GoogleAuth'; diff --git a/packages/core-api/src/apis/implementations/auth/index.ts b/packages/core-api/src/apis/implementations/auth/index.ts deleted file mode 100644 index 9889a33878..0000000000 --- a/packages/core-api/src/apis/implementations/auth/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export * from './github'; -export * from './gitlab'; -export * from './google'; -export * from './oauth2'; -export * from './okta'; -export * from './saml'; -export * from './auth0'; -export * from './microsoft'; -export * from './onelogin'; diff --git a/packages/core-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts deleted file mode 100644 index 5f0e018add..0000000000 --- a/packages/core-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import MicrosoftIcon from '@material-ui/icons/AcUnit'; -import { microsoftAuthApiRef } from '../../../definitions/auth'; -import { OAuth2 } from '../oauth2'; -import { OAuthApiCreateOptions } from '../types'; - -const DEFAULT_PROVIDER = { - id: 'microsoft', - title: 'Microsoft', - icon: MicrosoftIcon, -}; - -class MicrosoftAuth { - static create({ - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - discoveryApi, - defaultScopes = [ - 'openid', - 'offline_access', - 'profile', - 'email', - 'User.Read', - ], - }: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T { - return OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider, - environment, - defaultScopes, - }); - } -} - -export default MicrosoftAuth; diff --git a/packages/core-api/src/apis/implementations/auth/microsoft/index.ts b/packages/core-api/src/apis/implementations/auth/microsoft/index.ts deleted file mode 100644 index 44bca3d37c..0000000000 --- a/packages/core-api/src/apis/implementations/auth/microsoft/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { default as MicrosoftAuth } from './MicrosoftAuth'; diff --git a/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts b/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts deleted file mode 100644 index 4f030d1c64..0000000000 --- a/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import OAuth2 from './OAuth2'; - -const theFuture = new Date(Date.now() + 3600000); -const thePast = new Date(Date.now() - 10); - -const PREFIX = 'https://www.googleapis.com/auth/'; - -const scopeTransform = (x: string[]) => x; - -describe('OAuth2', () => { - it('should get refreshed access token', async () => { - const getSession = jest.fn().mockResolvedValue({ - providerInfo: { accessToken: 'access-token', expiresAt: theFuture }, - }); - const oauth2 = new OAuth2({ - sessionManager: { getSession } as any, - scopeTransform, - }); - - expect(await oauth2.getAccessToken('my-scope my-scope2')).toBe( - 'access-token', - ); - expect(getSession).toBeCalledTimes(1); - expect(getSession.mock.calls[0][0].scopes).toEqual( - new Set(['my-scope', 'my-scope2']), - ); - }); - - it('should transform scopes', async () => { - const getSession = jest.fn().mockResolvedValue({ - providerInfo: { accessToken: 'access-token', expiresAt: theFuture }, - }); - const oauth2 = new OAuth2({ - sessionManager: { getSession } as any, - scopeTransform: scopes => scopes.map(scope => `my-prefix/${scope}`), - }); - - expect(await oauth2.getAccessToken('my-scope')).toBe('access-token'); - expect(getSession).toBeCalledTimes(1); - expect(getSession.mock.calls[0][0].scopes).toEqual( - new Set(['my-prefix/my-scope']), - ); - }); - - it('should get refreshed id token', async () => { - const getSession = jest.fn().mockResolvedValue({ - providerInfo: { idToken: 'id-token', expiresAt: theFuture }, - }); - const oauth2 = new OAuth2({ - sessionManager: { getSession } as any, - scopeTransform, - }); - - expect(await oauth2.getIdToken()).toBe('id-token'); - expect(getSession).toBeCalledTimes(1); - }); - - it('should get optional id token', async () => { - const getSession = jest.fn().mockResolvedValue({ - providerInfo: { idToken: 'id-token', expiresAt: theFuture }, - }); - const oauth2 = new OAuth2({ - sessionManager: { getSession } as any, - scopeTransform, - }); - - expect(await oauth2.getIdToken({ optional: true })).toBe('id-token'); - expect(getSession).toBeCalledTimes(1); - }); - - it('should share popup closed errors', async () => { - const error = new Error('NOPE'); - error.name = 'RejectedError'; - const getSession = jest - .fn() - .mockResolvedValueOnce({ - providerInfo: { - accessToken: 'access-token', - expiresAt: theFuture, - scopes: new Set([`${PREFIX}not-enough`]), - }, - }) - .mockRejectedValue(error); - const oauth2 = new OAuth2({ - sessionManager: { getSession } as any, - scopeTransform, - }); - - // Make sure we have a session before we do the double request, so that we get past the !this.currentSession check - await expect(oauth2.getAccessToken()).resolves.toBe('access-token'); - - const promise1 = oauth2.getAccessToken('more'); - const promise2 = oauth2.getAccessToken('more'); - await expect(promise1).rejects.toBe(error); - await expect(promise2).rejects.toBe(error); - expect(getSession).toBeCalledTimes(3); - }); - - it('should wait for all session refreshes', async () => { - const initialSession = { - providerInfo: { - idToken: 'token1', - expiresAt: theFuture, - scopes: new Set(), - }, - }; - const getSession = jest - .fn() - .mockResolvedValueOnce(initialSession) - .mockResolvedValue({ - providerInfo: { - idToken: 'token2', - expiresAt: theFuture, - scopes: new Set(), - }, - }); - const oauth2 = new OAuth2({ - sessionManager: { getSession } as any, - scopeTransform, - }); - - // Grab the expired session first - await expect(oauth2.getIdToken()).resolves.toBe('token1'); - expect(getSession).toBeCalledTimes(1); - - initialSession.providerInfo.expiresAt = thePast; - - const promise1 = oauth2.getIdToken(); - const promise2 = oauth2.getIdToken(); - const promise3 = oauth2.getIdToken(); - await expect(promise1).resolves.toBe('token2'); - await expect(promise2).resolves.toBe('token2'); - await expect(promise3).resolves.toBe('token2'); - expect(getSession).toBeCalledTimes(4); // De-duping of session requests happens in client - }); -}); diff --git a/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts deleted file mode 100644 index 10d9792799..0000000000 --- a/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import OAuth2Icon from '@material-ui/icons/AcUnit'; -import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; -import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; -import { SessionManager } from '../../../../lib/AuthSessionManager/types'; -import { Observable } from '../../../../types'; -import { - AuthRequestOptions, - BackstageIdentity, - OAuthApi, - OpenIdConnectApi, - ProfileInfo, - ProfileInfoApi, - SessionState, - SessionApi, - BackstageIdentityApi, -} from '../../../definitions/auth'; -import { OAuth2Session } from './types'; -import { OAuthApiCreateOptions } from '../types'; - -type Options = { - sessionManager: SessionManager; - scopeTransform: (scopes: string[]) => string[]; -}; - -type CreateOptions = OAuthApiCreateOptions & { - scopeTransform?: (scopes: string[]) => string[]; -}; - -export type OAuth2Response = { - providerInfo: { - accessToken: string; - idToken: string; - scope: string; - expiresInSeconds: number; - }; - profile: ProfileInfo; - backstageIdentity: BackstageIdentity; -}; - -const DEFAULT_PROVIDER = { - id: 'oauth2', - title: 'Your Identity Provider', - icon: OAuth2Icon, -}; - -class OAuth2 - implements - OAuthApi, - OpenIdConnectApi, - ProfileInfoApi, - BackstageIdentityApi, - SessionApi { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = [], - scopeTransform = x => x, - }: CreateOptions) { - const connector = new DefaultAuthConnector({ - discoveryApi, - environment, - provider, - oauthRequestApi: oauthRequestApi, - sessionTransform(res: OAuth2Response): OAuth2Session { - return { - ...res, - providerInfo: { - idToken: res.providerInfo.idToken, - accessToken: res.providerInfo.accessToken, - scopes: OAuth2.normalizeScopes( - scopeTransform, - res.providerInfo.scope, - ), - expiresAt: new Date( - Date.now() + res.providerInfo.expiresInSeconds * 1000, - ), - }, - }; - }, - }); - - const sessionManager = new RefreshingAuthSessionManager({ - connector, - defaultScopes: new Set(defaultScopes), - sessionScopes: (session: OAuth2Session) => session.providerInfo.scopes, - sessionShouldRefresh: (session: OAuth2Session) => { - const expiresInSec = - (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000; - return expiresInSec < 60 * 5; - }, - }); - - return new OAuth2({ sessionManager, scopeTransform }); - } - - private readonly sessionManager: SessionManager; - private readonly scopeTransform: (scopes: string[]) => string[]; - - constructor(options: Options) { - this.sessionManager = options.sessionManager; - this.scopeTransform = options.scopeTransform; - } - - async signIn() { - await this.getAccessToken(); - } - - async signOut() { - await this.sessionManager.removeSession(); - } - - sessionState$(): Observable { - return this.sessionManager.sessionState$(); - } - - async getAccessToken( - scope?: string | string[], - options?: AuthRequestOptions, - ) { - const normalizedScopes = OAuth2.normalizeScopes(this.scopeTransform, scope); - const session = await this.sessionManager.getSession({ - ...options, - scopes: normalizedScopes, - }); - return session?.providerInfo.accessToken ?? ''; - } - - async getIdToken(options: AuthRequestOptions = {}) { - const session = await this.sessionManager.getSession(options); - return session?.providerInfo.idToken ?? ''; - } - - async getBackstageIdentity( - options: AuthRequestOptions = {}, - ): Promise { - const session = await this.sessionManager.getSession(options); - return session?.backstageIdentity; - } - - async getProfile(options: AuthRequestOptions = {}) { - const session = await this.sessionManager.getSession(options); - return session?.profile; - } - - private static normalizeScopes( - scopeTransform: (scopes: string[]) => string[], - scopes?: string | string[], - ): Set { - if (!scopes) { - return new Set(); - } - - const scopeList = Array.isArray(scopes) - ? scopes - : scopes.split(/[\s|,]/).filter(Boolean); - - return new Set(scopeTransform(scopeList)); - } -} - -export default OAuth2; diff --git a/packages/core-api/src/apis/implementations/auth/oauth2/index.ts b/packages/core-api/src/apis/implementations/auth/oauth2/index.ts deleted file mode 100644 index 793be515b7..0000000000 --- a/packages/core-api/src/apis/implementations/auth/oauth2/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { default as OAuth2 } from './OAuth2'; -export * from './types'; diff --git a/packages/core-api/src/apis/implementations/auth/oauth2/types.ts b/packages/core-api/src/apis/implementations/auth/oauth2/types.ts deleted file mode 100644 index ca34de9f78..0000000000 --- a/packages/core-api/src/apis/implementations/auth/oauth2/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ProfileInfo, BackstageIdentity } from '../../../definitions'; - -export type OAuth2Session = { - providerInfo: { - idToken: string; - accessToken: string; - scopes: Set; - expiresAt: Date; - }; - profile: ProfileInfo; - backstageIdentity: BackstageIdentity; -}; diff --git a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.test.ts b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.test.ts deleted file mode 100644 index 6489b326ff..0000000000 --- a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import OktaAuth from './OktaAuth'; -import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; -import { UrlPatternDiscovery } from '../../DiscoveryApi'; - -const PREFIX = 'okta.'; - -const getSession = jest.fn(); - -jest.mock('../../../../lib/AuthSessionManager', () => ({ - ...(jest.requireActual('../../../../lib/AuthSessionManager') as any), - RefreshingAuthSessionManager: class { - getSession = getSession; - }, -})); - -describe('OktaAuth', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - it.each([ - ['openid', ['openid']], - ['profile email', ['profile', 'email']], - [`${PREFIX}groups.manage`, [`${PREFIX}groups.manage`]], - ['groups.read', [`${PREFIX}groups.read`]], - [ - `${PREFIX}groups.manage groups.read, openid`, - [`${PREFIX}groups.manage`, `${PREFIX}groups.read`, 'openid'], - ], - [`email\t ${PREFIX}groups.read`, ['email', `${PREFIX}groups.read`]], - - // Some incorrect scopes that we don't try to fix - [`${PREFIX}email`, [`${PREFIX}email`]], - [`${PREFIX}profile`, [`${PREFIX}profile`]], - [`${PREFIX}openid`, [`${PREFIX}openid`]], - ])(`should normalize scopes correctly - %p`, (scope, scopes) => { - const auth = OktaAuth.create({ - oauthRequestApi: new MockOAuthApi(), - discoveryApi: UrlPatternDiscovery.compile('http://example.com'), - }); - - auth.getAccessToken(scope); - expect(getSession).toHaveBeenCalledWith({ scopes: new Set(scopes) }); - }); -}); diff --git a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts deleted file mode 100644 index 3f29a56deb..0000000000 --- a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import OktaIcon from '@material-ui/icons/AcUnit'; -import { oktaAuthApiRef } from '../../../definitions/auth'; -import { OAuth2 } from '../oauth2'; -import { OAuthApiCreateOptions } from '../types'; - -const DEFAULT_PROVIDER = { - id: 'okta', - title: 'Okta', - icon: OktaIcon, -}; - -const OKTA_OIDC_SCOPES: Set = new Set([ - 'openid', - 'profile', - 'email', - 'phone', - 'address', - 'groups', - 'offline_access', -]); - -const OKTA_SCOPE_PREFIX: string = 'okta.'; - -class OktaAuth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['openid', 'email', 'profile', 'offline_access'], - }: OAuthApiCreateOptions): typeof oktaAuthApiRef.T { - return OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider, - environment, - defaultScopes, - scopeTransform(scopes) { - return scopes.map(scope => { - if (OKTA_OIDC_SCOPES.has(scope)) { - return scope; - } - - if (scope.startsWith(OKTA_SCOPE_PREFIX)) { - return scope; - } - - return `${OKTA_SCOPE_PREFIX}${scope}`; - }); - }, - }); - } -} - -export default OktaAuth; diff --git a/packages/core-api/src/apis/implementations/auth/okta/index.ts b/packages/core-api/src/apis/implementations/auth/okta/index.ts deleted file mode 100644 index c20ce4e16b..0000000000 --- a/packages/core-api/src/apis/implementations/auth/okta/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { default as OktaAuth } from './OktaAuth'; diff --git a/packages/core-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts b/packages/core-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts deleted file mode 100644 index 6d34e63669..0000000000 --- a/packages/core-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import OneLoginIcon from '@material-ui/icons/AcUnit'; -import { oneloginAuthApiRef } from '../../../definitions/auth'; -import { - OAuthRequestApi, - AuthProvider, - DiscoveryApi, -} from '../../../definitions'; -import { OAuth2 } from '../oauth2'; - -type CreateOptions = { - discoveryApi: DiscoveryApi; - oauthRequestApi: OAuthRequestApi; - - environment?: string; - provider?: AuthProvider & { id: string }; -}; - -const DEFAULT_PROVIDER = { - id: 'onelogin', - title: 'onelogin', - icon: OneLoginIcon, -}; - -const OIDC_SCOPES: Set = new Set([ - 'openid', - 'profile', - 'email', - 'phone', - 'address', - 'groups', - 'offline_access', -]); - -const SCOPE_PREFIX: string = 'onelogin.'; - -class OneLoginAuth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - }: CreateOptions): typeof oneloginAuthApiRef.T { - return OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider, - environment, - defaultScopes: ['openid', 'email', 'profile', 'offline_access'], - scopeTransform(scopes) { - return scopes.map(scope => { - if (OIDC_SCOPES.has(scope)) { - return scope; - } - - if (scope.startsWith(SCOPE_PREFIX)) { - return scope; - } - - return `${SCOPE_PREFIX}${scope}`; - }); - }, - }); - } -} - -export default OneLoginAuth; diff --git a/packages/core-api/src/apis/implementations/auth/onelogin/index.ts b/packages/core-api/src/apis/implementations/auth/onelogin/index.ts deleted file mode 100644 index e1826f17dd..0000000000 --- a/packages/core-api/src/apis/implementations/auth/onelogin/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { default as OneLoginAuth } from './OneLoginAuth'; diff --git a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts deleted file mode 100644 index b8f41cfc18..0000000000 --- a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import SamlIcon from '@material-ui/icons/AcUnit'; -import { DirectAuthConnector } from '../../../../lib/AuthConnector'; -import { SessionManager } from '../../../../lib/AuthSessionManager/types'; -import { Observable } from '../../../../types'; -import { - ProfileInfo, - BackstageIdentity, - SessionState, - AuthRequestOptions, - ProfileInfoApi, - BackstageIdentityApi, - SessionApi, -} from '../../../definitions/auth'; -import { SamlSession } from './types'; -import { - AuthSessionStore, - StaticAuthSessionManager, -} from '../../../../lib/AuthSessionManager'; -import { AuthApiCreateOptions } from '../types'; - -export type SamlAuthResponse = { - profile: ProfileInfo; - backstageIdentity: BackstageIdentity; -}; - -const DEFAULT_PROVIDER = { - id: 'saml', - title: 'SAML', - icon: SamlIcon, -}; - -class SamlAuth implements ProfileInfoApi, BackstageIdentityApi, SessionApi { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - }: AuthApiCreateOptions) { - const connector = new DirectAuthConnector({ - discoveryApi, - environment, - provider, - }); - - const sessionManager = new StaticAuthSessionManager({ - connector, - }); - - const authSessionStore = new AuthSessionStore({ - manager: sessionManager, - storageKey: `${provider.id}Session`, - }); - - return new SamlAuth(authSessionStore); - } - - sessionState$(): Observable { - return this.sessionManager.sessionState$(); - } - - constructor(private readonly sessionManager: SessionManager) {} - - async signIn() { - await this.getBackstageIdentity({}); - } - async signOut() { - await this.sessionManager.removeSession(); - } - - async getBackstageIdentity( - options: AuthRequestOptions = {}, - ): Promise { - const session = await this.sessionManager.getSession(options); - return session?.backstageIdentity; - } - - async getProfile(options: AuthRequestOptions = {}) { - const session = await this.sessionManager.getSession(options); - return session?.profile; - } -} - -export default SamlAuth; diff --git a/packages/core-api/src/apis/implementations/auth/saml/index.ts b/packages/core-api/src/apis/implementations/auth/saml/index.ts deleted file mode 100644 index 930e6cb115..0000000000 --- a/packages/core-api/src/apis/implementations/auth/saml/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export { default as SamlAuth } from './SamlAuth'; diff --git a/packages/core-api/src/apis/implementations/auth/saml/types.ts b/packages/core-api/src/apis/implementations/auth/saml/types.ts deleted file mode 100644 index 2827c6aa24..0000000000 --- a/packages/core-api/src/apis/implementations/auth/saml/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { ProfileInfo, BackstageIdentity } from '../../../definitions'; - -export type SamlSession = { - userId: string; - profile: ProfileInfo; - backstageIdentity: BackstageIdentity; -}; diff --git a/packages/core-api/src/apis/implementations/auth/types.ts b/packages/core-api/src/apis/implementations/auth/types.ts deleted file mode 100644 index 14202f4e29..0000000000 --- a/packages/core-api/src/apis/implementations/auth/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AuthProvider, DiscoveryApi, OAuthRequestApi } from '../../definitions'; - -export type OAuthApiCreateOptions = AuthApiCreateOptions & { - oauthRequestApi: OAuthRequestApi; - defaultScopes?: string[]; -}; - -export type AuthApiCreateOptions = { - discoveryApi: DiscoveryApi; - environment?: string; - provider?: AuthProvider & { id: string }; -}; diff --git a/packages/core-api/src/apis/implementations/index.ts b/packages/core-api/src/apis/implementations/index.ts deleted file mode 100644 index 31f01f2bff..0000000000 --- a/packages/core-api/src/apis/implementations/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// This folder contains implementations for all core APIs. -// -// Plugins should rely on these APIs for functionality as much as possible. - -export * from './auth'; - -export * from './AlertApi'; -export * from './AppThemeApi'; -export * from './ConfigApi'; -export * from './DiscoveryApi'; -export * from './ErrorApi'; -export * from './FeatureFlagsApi'; -export * from './OAuthRequestApi'; -export * from './StorageApi'; diff --git a/packages/core-api/src/apis/index.ts b/packages/core-api/src/apis/index.ts deleted file mode 100644 index 05b920c88a..0000000000 --- a/packages/core-api/src/apis/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export * from './system'; -export * from './definitions'; -export * from './implementations'; diff --git a/packages/core-api/src/apis/system/ApiAggregator.test.ts b/packages/core-api/src/apis/system/ApiAggregator.test.ts deleted file mode 100644 index 5a7cd12683..0000000000 --- a/packages/core-api/src/apis/system/ApiAggregator.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiAggregator } from './ApiAggregator'; -import { createApiRef } from './ApiRef'; -import { ApiRegistry } from './ApiRegistry'; - -describe('ApiAggregator', () => { - const apiARef = createApiRef({ id: 'a', description: '' }); - const apiBRef = createApiRef({ id: 'b', description: '' }); - - it('should forward implementations', () => { - const agg = new ApiAggregator( - ApiRegistry.from([ - [apiARef, 5], - [apiBRef, 10], - ]), - ); - expect(agg.get(apiARef)).toBe(5); - expect(agg.get(apiBRef)).toBe(10); - }); - - it('should return the first implementation', () => { - const agg = new ApiAggregator( - ApiRegistry.from([ - [apiARef, 1], - [apiARef, 2], - ]), - ); - expect(agg.get(apiARef)).toBe(2); - expect(agg.get(apiBRef)).toBe(undefined); - }); -}); diff --git a/packages/core-api/src/apis/system/ApiAggregator.ts b/packages/core-api/src/apis/system/ApiAggregator.ts deleted file mode 100644 index 28b2827d54..0000000000 --- a/packages/core-api/src/apis/system/ApiAggregator.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiRef, ApiHolder } from './types'; - -/** - * An ApiHolder that queries multiple other holders from for - * an Api implementation, returning the first one encountered.. - */ -export class ApiAggregator implements ApiHolder { - private readonly holders: ApiHolder[]; - - constructor(...holders: ApiHolder[]) { - this.holders = holders; - } - - get(apiRef: ApiRef): T | undefined { - for (const holder of this.holders) { - const api = holder.get(apiRef); - if (api) { - return api; - } - } - return undefined; - } -} diff --git a/packages/core-api/src/apis/system/ApiFactoryRegistry.test.ts b/packages/core-api/src/apis/system/ApiFactoryRegistry.test.ts deleted file mode 100644 index c20d0805b1..0000000000 --- a/packages/core-api/src/apis/system/ApiFactoryRegistry.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiFactoryRegistry } from './ApiFactoryRegistry'; -import { createApiRef } from './ApiRef'; - -const aRef = createApiRef({ id: 'a', description: '' }); -const aFactory1 = { api: aRef, deps: {}, factory: () => 1 }; -const aFactory2 = { api: aRef, deps: {}, factory: () => 2 }; -const bRef = createApiRef({ id: 'b', description: '' }); -const bFactory = { api: bRef, deps: {}, factory: () => 'x' }; -const cRef = createApiRef({ id: 'c', description: '' }); -const cFactory = { api: cRef, deps: {}, factory: () => 'y' }; - -describe('ApiFactoryRegistry', () => { - it('should be empty when created', () => { - const registry = new ApiFactoryRegistry(); - expect(registry.getAllApis()).toEqual(new Set()); - }); - - it('should register a factory', () => { - const registry = new ApiFactoryRegistry(); - expect(registry.register('default', aFactory1)).toBe(true); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.getAllApis()).toEqual(new Set([aRef])); - }); - - it('should prioritize factories based on scope', () => { - const registry = new ApiFactoryRegistry(); - expect(registry.register('default', aFactory1)).toBe(true); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.register('default', aFactory2)).toBe(false); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.register('app', aFactory2)).toBe(true); - expect(registry.get(aRef)).toBe(aFactory2); - expect(registry.register('default', aFactory1)).toBe(false); - expect(registry.get(aRef)).toBe(aFactory2); - expect(registry.register('static', aFactory1)).toBe(true); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.register('static', aFactory2)).toBe(false); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.register('app', aFactory2)).toBe(false); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.getAllApis()).toEqual(new Set([aRef])); - }); - - it('should register multiple factories without conflict', () => { - const registry = new ApiFactoryRegistry(); - expect(registry.register('static', aFactory1)).toBe(true); - expect(registry.register('default', bFactory)).toBe(true); - expect(registry.register('app', cFactory)).toBe(true); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.get(bRef)).toBe(bFactory); - expect(registry.get(cRef)).toBe(cFactory); - expect(registry.getAllApis()).toEqual(new Set([aRef, bRef, cRef])); - }); - - it('should identify ApiRefs by id but still return the correct factory ref when listing all apis', () => { - const ref1 = createApiRef({ id: 'a', description: 'ref1' }); - const ref2 = createApiRef({ id: 'a', description: 'ref2' }); - - const factory1 = { api: ref1, deps: {}, factory: () => 3 }; - const factory2 = { api: ref2, deps: {}, factory: () => 3 }; - - const registry = new ApiFactoryRegistry(); - expect(registry.register('default', factory1)).toBe(true); - expect(registry.register('default', factory2)).toBe(false); - expect(registry.get(ref1)).toEqual(factory1); - expect(registry.get(ref2)).toEqual(factory1); - expect(registry.getAllApis()).toEqual(new Set([ref1])); - - expect(registry.register('app', factory2)).toBe(true); - expect(registry.get(ref1)).toEqual(factory2); - expect(registry.get(ref2)).toEqual(factory2); - expect(registry.getAllApis()).toEqual(new Set([ref2])); - expect(registry.getAllApis()).not.toEqual(new Set([ref1])); - }); -}); diff --git a/packages/core-api/src/apis/system/ApiFactoryRegistry.ts b/packages/core-api/src/apis/system/ApiFactoryRegistry.ts deleted file mode 100644 index 0c37078d42..0000000000 --- a/packages/core-api/src/apis/system/ApiFactoryRegistry.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - ApiRef, - ApiFactoryHolder, - ApiFactory, - AnyApiRef, - AnyApiFactory, -} from './types'; - -type ApiFactoryScope = - | 'default' // Default factories registered by core and plugins - | 'app' // Factories registered in the app, overriding default ones - | 'static'; // APIs that can't be overridden, e.g. config - -enum ScopePriority { - default = 10, - app = 50, - static = 100, -} - -type FactoryTuple = { - priority: number; - factory: AnyApiFactory; -}; - -/** - * ApiFactoryRegistry is an ApiFactoryHolder implementation that enables - * registration of API Factories with different scope. - * - * Each scope has an assigned priority, where factories registered with - * higher priority scopes override ones with lower priority. - */ -export class ApiFactoryRegistry implements ApiFactoryHolder { - private readonly factories = new Map(); - - /** - * Register a new API factory. Returns true if the factory was added - * to the registry. - * - * A factory will not be added to the registry if there is already - * an existing factory with the same or higher priority. - */ - register( - scope: ApiFactoryScope, - factory: ApiFactory, - ) { - const priority = ScopePriority[scope]; - const existing = this.factories.get(factory.api.id); - if (existing && existing.priority >= priority) { - return false; - } - - this.factories.set(factory.api.id, { priority, factory }); - return true; - } - - get( - api: ApiRef, - ): ApiFactory | undefined { - const tuple = this.factories.get(api.id); - if (!tuple) { - return undefined; - } - return tuple.factory as ApiFactory; - } - - getAllApis(): Set { - const refs = new Set(); - for (const { factory } of this.factories.values()) { - refs.add(factory.api); - } - return refs; - } -} diff --git a/packages/core-api/src/apis/system/ApiProvider.test.tsx b/packages/core-api/src/apis/system/ApiProvider.test.tsx deleted file mode 100644 index a212f253ce..0000000000 --- a/packages/core-api/src/apis/system/ApiProvider.test.tsx +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { Context, useContext } from 'react'; -import { ApiProvider, useApi, withApis } from './ApiProvider'; -import { createApiRef } from './ApiRef'; -import { ApiRegistry } from './ApiRegistry'; -import { render } from '@testing-library/react'; -import { withLogCollector } from '@backstage/test-utils-core'; -import { getGlobalSingleton } from '../../lib/globalObject'; -import { ApiHolder, ApiRef } from './types'; -import { VersionedValue } from '../../lib/versionedValues'; - -describe('ApiProvider', () => { - type Api = () => string; - const apiRef = createApiRef({ id: 'x', description: '' }); - const registry = ApiRegistry.from([[apiRef, () => 'hello']]); - - const MyHookConsumer = () => { - const api = useApi(apiRef); - return

hook message: {api()}

; - }; - - const MyHocConsumer = withApis({ getMessage: apiRef })(({ getMessage }) => { - return

hoc message: {getMessage()}

; - }); - - it('should provide apis', () => { - const renderedHook = render( - - - , - ); - renderedHook.getByText('hook message: hello'); - - const renderedHoc = render( - - - , - ); - renderedHoc.getByText('hoc message: hello'); - }); - - it('should provide nested access to apis', () => { - const aRef = createApiRef({ id: 'a', description: '' }); - const bRef = createApiRef({ id: 'b', description: '' }); - - const MyComponent = () => { - const a = useApi(aRef); - const b = useApi(bRef); - return ( -
- a={a} b={b} -
- ); - }; - - const renderedHook = render( - - - - - , - ); - renderedHook.getByText('a=z b=y'); - }); - - it('should ignore deps in prototype', () => { - // 100% coverage + happy typescript = hasOwnProperty + this atrocity - const xRef = createApiRef({ id: 'x', description: '' }); - - const proto = { x: xRef }; - const props = { getMessage: { enumerable: true, value: apiRef } }; - const obj = Object.create(proto, props) as { - getMessage: typeof apiRef; - x: typeof xRef; - }; - - const MyWeirdHocConsumer = withApis(obj)(({ getMessage }) => { - return

hoc message: {getMessage()}

; - }); - - const renderedHoc = render( - - - , - ); - renderedHoc.getByText('hoc message: hello'); - }); - - it('should error if no provider is available', () => { - expect( - withLogCollector(['error'], () => { - expect(() => { - render(); - }).toThrow(/^No ApiProvider available in react context. /); - }).error, - ).toEqual([ - expect.stringMatching( - /^Error: Uncaught \[Error: No ApiProvider available in react context. /, - ), - expect.stringMatching( - /^The above error occurred in the component/, - ), - ]); - - expect( - withLogCollector(['error'], () => { - expect(() => { - render(); - }).toThrow(/^No ApiProvider available in react context. /); - }).error, - ).toEqual([ - expect.stringMatching( - /^Error: Uncaught \[Error: No ApiProvider available in react context. /, - ), - expect.stringMatching( - /^The above error occurred in the component/, - ), - ]); - }); - - it('should error if api is not available', () => { - expect( - withLogCollector(['error'], () => { - expect(() => { - render( - - - , - ); - }).toThrow('No implementation available for apiRef{x}'); - }).error, - ).toEqual([ - expect.stringMatching( - /^Error: Uncaught \[Error: No implementation available for apiRef{x}\]/, - ), - expect.stringMatching( - /^The above error occurred in the component/, - ), - ]); - - expect( - withLogCollector(['error'], () => { - expect(() => { - render( - - - , - ); - }).toThrow('No implementation available for apiRef{x}'); - }).error, - ).toEqual([ - expect.stringMatching( - /^Error: Uncaught \[Error: No implementation available for apiRef{x}\]/, - ), - expect.stringMatching( - /^The above error occurred in the component/, - ), - ]); - }); -}); - -describe('v1 consumer', () => { - const ApiContext = getGlobalSingleton< - Context> - >('api-context'); - - function useMockApiV1(apiRef: ApiRef): T { - const impl = useContext(ApiContext)?.atVersion(1)?.get(apiRef); - if (!impl) { - throw new Error('no impl'); - } - return impl; - } - - type Api = () => string; - const apiRef = createApiRef({ id: 'x', description: '' }); - const registry = ApiRegistry.with(apiRef, () => 'hello'); - - const MyHookConsumerV1 = () => { - const api = useMockApiV1(apiRef); - return

hook message: {api()}

; - }; - - it('should provide apis', () => { - const renderedHook = render( - - - , - ); - renderedHook.getByText('hook message: hello'); - }); -}); diff --git a/packages/core-api/src/apis/system/ApiProvider.tsx b/packages/core-api/src/apis/system/ApiProvider.tsx deleted file mode 100644 index 40e3cddb72..0000000000 --- a/packages/core-api/src/apis/system/ApiProvider.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { - createContext, - useContext, - ReactNode, - PropsWithChildren, - Context, - useMemo, -} from 'react'; -import PropTypes from 'prop-types'; -import { ApiRef, ApiHolder, TypesToApiRefs } from './types'; -import { ApiAggregator } from './ApiAggregator'; -import { - getGlobalSingleton, - getOrCreateGlobalSingleton, -} from '../../lib/globalObject'; -import { - VersionedValue, - createVersionedValueMap, -} from '../../lib/versionedValues'; - -const missingHolderMessage = - 'No ApiProvider available in react context. ' + - 'A common cause of this error is that multiple versions of @backstage/core-api are installed. ' + - `You can check if that is the case using 'yarn backstage-cli versions:check', and can in many cases ` + - `fix the issue either with the --fix flag or using 'yarn backstage-cli versions:bump'`; - -type ApiProviderProps = { - apis: ApiHolder; - children: ReactNode; -}; - -type ApiContextType = VersionedValue<{ 1: ApiHolder }> | undefined; -const ApiContext = getOrCreateGlobalSingleton('api-context', () => - createContext(undefined), -); - -export const ApiProvider = ({ - apis, - children, -}: PropsWithChildren) => { - const parentHolder = useContext(ApiContext)?.atVersion(1); - const versionedValue = useMemo( - () => - createVersionedValueMap({ - 1: parentHolder ? new ApiAggregator(apis, parentHolder) : apis, - }), - [parentHolder, apis], - ); - - return ; -}; - -ApiProvider.propTypes = { - apis: PropTypes.shape({ get: PropTypes.func.isRequired }).isRequired, - children: PropTypes.node, -}; - -export function useApiHolder(): ApiHolder { - const versionedHolder = useContext( - getGlobalSingleton>('api-context'), - ); - - if (!versionedHolder) { - throw new Error(missingHolderMessage); - } - - const apiHolder = versionedHolder.atVersion(1); - if (!apiHolder) { - throw new Error('ApiContext v1 not available'); - } - - return apiHolder; -} - -export function useApi(apiRef: ApiRef): T { - const apiHolder = useApiHolder(); - - const api = apiHolder.get(apiRef); - if (!api) { - throw new Error(`No implementation available for ${apiRef}`); - } - return api; -} - -export function withApis(apis: TypesToApiRefs) { - return function withApisWrapper

( - WrappedComponent: React.ComponentType

, - ) { - const Hoc = (props: PropsWithChildren>) => { - const apiHolder = useApiHolder(); - - const impls = {} as T; - - for (const key in apis) { - if (apis.hasOwnProperty(key)) { - const ref = apis[key]; - - const api = apiHolder.get(ref); - if (!api) { - throw new Error(`No implementation available for ${ref}`); - } - impls[key] = api; - } - } - - return ; - }; - const displayName = - WrappedComponent.displayName || WrappedComponent.name || 'Component'; - - Hoc.displayName = `withApis(${displayName})`; - - return Hoc; - }; -} diff --git a/packages/core-api/src/apis/system/ApiRef.test.ts b/packages/core-api/src/apis/system/ApiRef.test.ts deleted file mode 100644 index 7cd778634c..0000000000 --- a/packages/core-api/src/apis/system/ApiRef.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createApiRef } from './ApiRef'; - -describe('ApiRef', () => { - it('should be created', () => { - const ref = createApiRef({ id: 'abc', description: '123' }); - expect(ref.id).toBe('abc'); - expect(ref.description).toBe('123'); - expect(String(ref)).toBe('apiRef{abc}'); - expect(() => ref.T).toThrow('tried to read ApiRef.T of apiRef{abc}'); - }); - - it('should reject invalid ids', () => { - for (const id of ['a', 'abc', 'ab-c', 'a.b.c', 'a-b.c', 'abc.a-b-c.abc3']) { - expect(createApiRef({ id, description: '123' }).id).toBe(id); - } - - for (const id of [ - '123', - 'ab-3', - 'ab_c', - '.', - '2ac', - 'ab.3a', - '.abc', - 'abc.', - 'ab..s', - '', - '_', - ]) { - expect(() => createApiRef({ id, description: '123' }).id).toThrow( - `API id must only contain period separated lowercase alphanum tokens with dashes, got '${id}'`, - ); - } - }); -}); diff --git a/packages/core-api/src/apis/system/ApiRef.ts b/packages/core-api/src/apis/system/ApiRef.ts deleted file mode 100644 index 5a774474d8..0000000000 --- a/packages/core-api/src/apis/system/ApiRef.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { ApiRef } from './types'; - -export type ApiRefConfig = { - id: string; - description?: string; -}; - -class ApiRefImpl implements ApiRef { - constructor(private readonly config: ApiRefConfig) { - const valid = config.id - .split('.') - .flatMap(part => part.split('-')) - .every(part => part.match(/^[a-z][a-z0-9]*$/)); - if (!valid) { - throw new Error( - `API id must only contain period separated lowercase alphanum tokens with dashes, got '${config.id}'`, - ); - } - } - - get id(): string { - return this.config.id; - } - - get description() { - return this.config.description; - } - - // Utility for getting type of an api, using `typeof apiRef.T` - get T(): T { - throw new Error(`tried to read ApiRef.T of ${this}`); - } - - toString() { - return `apiRef{${this.config.id}}`; - } -} - -export function createApiRef(config: ApiRefConfig): ApiRef { - return new ApiRefImpl(config); -} diff --git a/packages/core-api/src/apis/system/ApiRegistry.test.ts b/packages/core-api/src/apis/system/ApiRegistry.test.ts deleted file mode 100644 index 66fcb70262..0000000000 --- a/packages/core-api/src/apis/system/ApiRegistry.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiRegistry } from './ApiRegistry'; -import { createApiRef } from './ApiRef'; - -describe('ApiRegistry', () => { - const x1Ref = createApiRef({ id: 'x1', description: '' }); - const x1DuplicateRef = createApiRef({ id: 'x1', description: '' }); - const x2Ref = createApiRef({ id: 'x2', description: '' }); - - it('should be created', () => { - const registry = ApiRegistry.from([]); - expect(registry.get(x1Ref)).toBe(undefined); - }); - - it('should be created with APIs', () => { - const registry = ApiRegistry.from([ - [x1Ref, 3], - [x2Ref, 'y'], - ]); - expect(registry.get(x1Ref)).toBe(3); - expect(registry.get(x1DuplicateRef)).toBe(3); - expect(registry.get(x2Ref)).toBe('y'); - }); - - it('should be built', () => { - const registry = ApiRegistry.builder().build(); - expect(registry.get(x1Ref)).toBe(undefined); - expect(registry.get(x1DuplicateRef)).toBe(undefined); - }); - - it('should be built with APIs', () => { - const builder = ApiRegistry.builder(); - builder.add(x1Ref, 3); - builder.add(x2Ref, 'y'); - - const registry = builder.build(); - expect(registry.get(x1Ref)).toBe(3); - expect(registry.get(x1DuplicateRef)).toBe(3); - expect(registry.get(x2Ref)).toBe('y'); - }); - - it('should be created with API', () => { - const reg1 = ApiRegistry.with(x1Ref, 3); - const reg2 = reg1.with(x2Ref, 'y'); - const reg3 = reg2.with(x2Ref, 'z'); - const reg4 = reg3.with(x1Ref, 2); - const reg5 = reg3.with(x1DuplicateRef, 4); - - expect(reg1.get(x1Ref)).toBe(3); - expect(reg1.get(x2Ref)).toBe(undefined); - expect(reg2.get(x1Ref)).toBe(3); - expect(reg2.get(x2Ref)).toBe('y'); - expect(reg3.get(x1Ref)).toBe(3); - expect(reg3.get(x2Ref)).toBe('z'); - expect(reg4.get(x1Ref)).toBe(2); - expect(reg4.get(x2Ref)).toBe('z'); - expect(reg5.get(x1Ref)).toBe(4); - expect(reg5.get(x2Ref)).toBe('z'); - }); -}); diff --git a/packages/core-api/src/apis/system/ApiRegistry.ts b/packages/core-api/src/apis/system/ApiRegistry.ts deleted file mode 100644 index 6fcfe03f62..0000000000 --- a/packages/core-api/src/apis/system/ApiRegistry.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiRef, ApiHolder } from './types'; - -type ApiImpl = readonly [ApiRef, T]; - -class ApiRegistryBuilder { - private apis: [string, unknown][] = []; - - add(api: ApiRef, impl: I): I { - this.apis.push([api.id, impl]); - return impl; - } - - build(): ApiRegistry { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - return new ApiRegistry(new Map(this.apis)); - } -} - -export class ApiRegistry implements ApiHolder { - static builder() { - return new ApiRegistryBuilder(); - } - - static from(apis: ApiImpl[]) { - return new ApiRegistry(new Map(apis.map(([api, impl]) => [api.id, impl]))); - } - - /** - * Creates a new ApiRegistry with a single API implementation. - * - * @param api ApiRef for the API to add - * @param impl Implementation of the API to add - */ - static with(api: ApiRef, impl: T): ApiRegistry { - return new ApiRegistry(new Map([[api.id, impl]])); - } - - constructor(private readonly apis: Map) {} - - /** - * Returns a new ApiRegistry with the provided API added to the existing ones. - * - * @param api ApiRef for the API to add - * @param impl Implementation of the API to add - */ - with(api: ApiRef, impl: T): ApiRegistry { - return new ApiRegistry(new Map([...this.apis, [api.id, impl]])); - } - - get(api: ApiRef): T | undefined { - return this.apis.get(api.id) as T | undefined; - } -} diff --git a/packages/core-api/src/apis/system/ApiResolver.test.ts b/packages/core-api/src/apis/system/ApiResolver.test.ts deleted file mode 100644 index 064a4f8d77..0000000000 --- a/packages/core-api/src/apis/system/ApiResolver.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiResolver } from './ApiResolver'; -import { createApiRef } from './ApiRef'; -import { ApiFactoryRegistry } from './ApiFactoryRegistry'; - -const aRef = createApiRef({ id: 'a', description: '' }); -const otherARef = createApiRef({ id: 'a', description: 'other' }); -const bRef = createApiRef({ id: 'b', description: '' }); -const otherBRef = createApiRef({ id: 'b', description: 'other' }); -const cRef = createApiRef<{ x: string }>({ id: 'c', description: '' }); -const otherCRef = createApiRef<{ x: string }>({ - id: 'c', - description: 'other', -}); - -function createRegistry() { - const registry = new ApiFactoryRegistry(); - registry.register('default', { - api: aRef, - deps: {}, - factory: () => 1, - }); - registry.register('default', { - api: bRef, - deps: {}, - factory: () => 'b', - }); - registry.register('default', { - api: cRef, - deps: { b: otherBRef }, - factory: ({ b }) => ({ x: 'x', b }), - }); - return registry; -} - -function createSelfCyclicRegistry() { - const registry = new ApiFactoryRegistry(); - registry.register('default', { - api: aRef, - deps: { a: aRef }, - factory: () => 1, - }); - return registry; -} - -function createShortCyclicRegistry() { - const registry = new ApiFactoryRegistry(); - registry.register('default', { - api: aRef, - deps: { b: bRef }, - factory: () => 1, - }); - registry.register('default', { - api: bRef, - deps: { a: aRef }, - factory: () => 'x', - }); - return registry; -} - -function createShortCyclicRegistryWithOther() { - const registry = new ApiFactoryRegistry(); - registry.register('default', { - api: aRef, - deps: { b: bRef }, - factory: () => 1, - }); - registry.register('default', { - api: otherBRef, - deps: { a: otherARef }, - factory: () => 'x', - }); - return registry; -} - -function createLongCyclicRegistry() { - const registry = new ApiFactoryRegistry(); - registry.register('default', { - api: aRef, - deps: { b: otherBRef }, - factory: () => 1, - }); - registry.register('default', { - api: bRef, - deps: { c: cRef }, - factory: () => 'b', - }); - registry.register('default', { - api: cRef, - deps: { a: aRef }, - factory: () => ({ x: 'x' }), - }); - return registry; -} - -describe('ApiResolver', () => { - it('should be created empty', () => { - const resolver = new ApiResolver(new ApiFactoryRegistry()); - expect(resolver.get(aRef)).toBe(undefined); - expect(resolver.get(bRef)).toBe(undefined); - expect(resolver.get(otherBRef)).toBe(undefined); - expect(resolver.get(cRef)).toBe(undefined); - }); - - it('should instantiate APIs', () => { - const resolver = new ApiResolver(createRegistry()); - expect(resolver.get(aRef)).toBe(1); - expect(resolver.get(otherARef)).toBe(1); - expect(resolver.get(bRef)).toBe('b'); - expect(resolver.get(otherBRef)).toBe('b'); - expect(resolver.get(cRef)).toEqual({ x: 'x', b: 'b' }); - expect(resolver.get(cRef)).toBe(resolver.get(otherCRef)); - }); - - it('should detect self dependency cycles', () => { - const resolver = new ApiResolver(createSelfCyclicRegistry()); - expect(() => resolver.get(aRef)).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - }); - - it('should detect short dependency cycles', () => { - const resolver = new ApiResolver(createShortCyclicRegistry()); - expect(() => resolver.get(aRef)).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => resolver.get(bRef)).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - }); - - it('should detect short dependency cycles with other refs', () => { - const resolver = new ApiResolver(createShortCyclicRegistryWithOther()); - expect(() => resolver.get(aRef)).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => resolver.get(bRef)).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => resolver.get(otherARef)).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => resolver.get(otherBRef)).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - }); - - it('should detect long dependency cycles', () => { - const resolver = new ApiResolver(createLongCyclicRegistry()); - expect(() => resolver.get(aRef)).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - // Second call for same ref should still throw - expect(() => resolver.get(aRef)).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => resolver.get(bRef)).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => resolver.get(otherBRef)).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => resolver.get(cRef)).toThrow( - 'Circular dependency of api factory for apiRef{c}', - ); - }); - - it('should validate a factory holder', () => { - expect(() => { - ApiResolver.validateFactories(createRegistry(), [ - aRef, - bRef, - otherBRef, - cRef, - ]); - }).not.toThrow(); - }); - - it('should find self cycles with validation', () => { - const self = createSelfCyclicRegistry(); - expect(() => ApiResolver.validateFactories(self, [aRef])).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => ApiResolver.validateFactories(self, [otherARef])).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - }); - - it('should find dependency cycles with validation', () => { - const short = createShortCyclicRegistry(); - expect(() => ApiResolver.validateFactories(short, [aRef])).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => ApiResolver.validateFactories(short, [otherARef])).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => ApiResolver.validateFactories(short, [bRef])).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => ApiResolver.validateFactories(short, [otherBRef])).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - - const shortOther = createShortCyclicRegistryWithOther(); - expect(() => ApiResolver.validateFactories(shortOther, [aRef])).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => - ApiResolver.validateFactories(shortOther, [otherARef]), - ).toThrow('Circular dependency of api factory for apiRef{a}'); - expect(() => ApiResolver.validateFactories(shortOther, [bRef])).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => - ApiResolver.validateFactories(shortOther, [otherBRef]), - ).toThrow('Circular dependency of api factory for apiRef{b}'); - - const long = createLongCyclicRegistry(); - expect(() => - ApiResolver.validateFactories(long, long.getAllApis()), - ).toThrow('Circular dependency of api factory for apiRef{a}'); - expect(() => ApiResolver.validateFactories(long, [bRef])).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => ApiResolver.validateFactories(long, [otherBRef])).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => ApiResolver.validateFactories(long, [cRef])).toThrow( - 'Circular dependency of api factory for apiRef{c}', - ); - }); - - it('should only call factory func once', () => { - const registry = new ApiFactoryRegistry(); - const factory = jest.fn().mockReturnValue(2); - registry.register('default', { - api: aRef, - deps: {}, - factory, - }); - - const resolver = new ApiResolver(registry); - expect(factory).toHaveBeenCalledTimes(0); - expect(resolver.get(aRef)).toBe(2); - expect(factory).toHaveBeenCalledTimes(1); - expect(resolver.get(aRef)).toBe(2); - expect(factory).toHaveBeenCalledTimes(1); - expect(resolver.get(otherARef)).toBe(2); - expect(factory).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/core-api/src/apis/system/ApiResolver.ts b/packages/core-api/src/apis/system/ApiResolver.ts deleted file mode 100644 index 4d69067b43..0000000000 --- a/packages/core-api/src/apis/system/ApiResolver.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - ApiRef, - ApiHolder, - ApiFactoryHolder, - AnyApiRef, - TypesToApiRefs, -} from './types'; - -export class ApiResolver implements ApiHolder { - /** - * Validate factories by making sure that each of the apis can be created - * without hitting any circular dependencies. - */ - static validateFactories( - factories: ApiFactoryHolder, - apis: Iterable, - ) { - for (const api of apis) { - const heap = [api]; - const allDeps = new Set(); - - while (heap.length) { - const apiRef = heap.shift()!; - const factory = factories.get(apiRef); - if (!factory) { - continue; - } - - for (const dep of Object.values(factory.deps)) { - if (dep.id === api.id) { - throw new Error(`Circular dependency of api factory for ${api}`); - } - if (!allDeps.has(dep)) { - allDeps.add(dep); - heap.push(dep); - } - } - } - } - } - - private readonly apis = new Map(); - - constructor(private readonly factories: ApiFactoryHolder) {} - - get(ref: ApiRef): T | undefined { - return this.load(ref); - } - - private load(ref: ApiRef, loading: AnyApiRef[] = []): T | undefined { - const impl = this.apis.get(ref.id); - if (impl) { - return impl as T; - } - - const factory = this.factories.get(ref); - if (!factory) { - return undefined; - } - - if (loading.includes(factory.api)) { - throw new Error(`Circular dependency of api factory for ${factory.api}`); - } - - const deps = this.loadDeps(ref, factory.deps, [...loading, factory.api]); - const api = factory.factory(deps); - this.apis.set(ref.id, api); - return api as T; - } - - private loadDeps( - dependent: ApiRef, - apis: TypesToApiRefs, - loading: AnyApiRef[], - ): T { - const impls = {} as T; - - for (const key in apis) { - if (apis.hasOwnProperty(key)) { - const ref = apis[key]; - - const api = this.load(ref, loading); - if (!api) { - throw new Error( - `No API factory available for dependency ${ref} of dependent ${dependent}`, - ); - } - impls[key] = api; - } - } - - return impls; - } -} diff --git a/packages/core-api/src/apis/system/helpers.ts b/packages/core-api/src/apis/system/helpers.ts deleted file mode 100644 index 8e84dd6c09..0000000000 --- a/packages/core-api/src/apis/system/helpers.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiRef, ApiFactory, TypesToApiRefs } from './types'; - -/** - * Used to infer types for a standalone ApiFactory that isn't immediately passed - * to another function. - * This function doesn't actually do anything, it's only used to infer types. - */ -export function createApiFactory< - Api, - Impl extends Api, - Deps extends { [name in string]: unknown } ->(factory: ApiFactory): ApiFactory; -export function createApiFactory( - api: ApiRef, - instance: Impl, -): ApiFactory; -export function createApiFactory< - Api, - Impl extends Api, - Deps extends { [name in string]: unknown } ->( - factory: ApiFactory | ApiRef, - instance?: Impl, -): ApiFactory { - if ('id' in factory) { - return { - api: factory, - deps: {} as TypesToApiRefs, - factory: () => instance!, - }; - } - return factory; -} diff --git a/packages/core-api/src/apis/system/index.ts b/packages/core-api/src/apis/system/index.ts deleted file mode 100644 index c9b9fac936..0000000000 --- a/packages/core-api/src/apis/system/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { ApiProvider, useApi, useApiHolder } from './ApiProvider'; -export { ApiRegistry } from './ApiRegistry'; -export { ApiResolver } from './ApiResolver'; -export { ApiFactoryRegistry } from './ApiFactoryRegistry'; -export { createApiRef } from './ApiRef'; -export * from './types'; -export * from './helpers'; diff --git a/packages/core-api/src/apis/system/types.ts b/packages/core-api/src/apis/system/types.ts deleted file mode 100644 index 17ceff4649..0000000000 --- a/packages/core-api/src/apis/system/types.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export type ApiRef = { - id: string; - description?: string; - T: T; -}; - -export type AnyApiRef = ApiRef; - -export type ApiRefType = T extends ApiRef ? U : never; - -export type TypesToApiRefs = { [key in keyof T]: ApiRef }; - -export type ApiRefsToTypes }> = { - [key in keyof T]: ApiRefType; -}; - -export type ApiHolder = { - get(api: ApiRef): T | undefined; -}; - -export type ApiFactory< - Api, - Impl extends Api, - Deps extends { [name in string]: unknown } -> = { - api: ApiRef; - deps: TypesToApiRefs; - factory(deps: Deps): Impl; -}; - -export type AnyApiFactory = ApiFactory< - unknown, - unknown, - { [key in string]: unknown } ->; - -export type ApiFactoryHolder = { - get( - api: ApiRef, - ): ApiFactory | undefined; -}; diff --git a/packages/core-api/src/app/App.test.tsx b/packages/core-api/src/app/App.test.tsx deleted file mode 100644 index 5404c111e4..0000000000 --- a/packages/core-api/src/app/App.test.tsx +++ /dev/null @@ -1,375 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - configApiRef, - createApiFactory, - featureFlagsApiRef, - LocalStorageFeatureFlags, -} from '../apis'; -import { renderWithEffects, withLogCollector } from '@backstage/test-utils'; -import { lightTheme } from '@backstage/theme'; -import { render, screen } from '@testing-library/react'; -import React, { PropsWithChildren } from 'react'; -import { BrowserRouter, Routes } from 'react-router-dom'; -import { createRoutableExtension } from '../extensions'; -import { defaultSystemIcons } from '../icons'; -import { createPlugin } from '../plugin'; -import { useRouteRef } from '../routing/hooks'; -import { - createExternalRouteRef, - createRouteRef, - createSubRouteRef, -} from '../routing'; -import { generateBoundRoutes, PrivateAppImpl } from './App'; - -describe('generateBoundRoutes', () => { - it('runs happy path', () => { - const external = { myRoute: createExternalRouteRef({ id: '1' }) }; - const ref = createRouteRef({ id: 'ref-1' }); - const result = generateBoundRoutes(({ bind }) => { - bind(external, { myRoute: ref }); - }); - - expect(result.get(external.myRoute)).toBe(ref); - }); - - it('throws on unknown keys', () => { - const external = { myRoute: createExternalRouteRef({ id: '2' }) }; - const ref = createRouteRef({ id: 'ref-2' }); - expect(() => - generateBoundRoutes(({ bind }) => { - bind(external, { someOtherRoute: ref } as any); - }), - ).toThrow('Key someOtherRoute is not an existing external route'); - }); -}); - -describe('Integration Test', () => { - const plugin1RouteRef = createRouteRef({ id: 'ref-1' }); - const plugin2RouteRef = createRouteRef({ id: 'ref-2', params: ['x'] }); - const subRouteRef1 = createSubRouteRef({ - id: 'sub1', - path: '/sub1', - parent: plugin1RouteRef, - }); - const subRouteRef2 = createSubRouteRef({ - id: 'sub2', - path: '/sub2/:x', - parent: plugin1RouteRef, - }); - const subRouteRef3 = createSubRouteRef({ - id: 'sub3', - path: '/sub3', - parent: plugin2RouteRef, - }); - const subRouteRef4 = createSubRouteRef({ - id: 'sub4', - path: '/sub4/:y', - parent: plugin2RouteRef, - }); - const extRouteRef1 = createExternalRouteRef({ id: 'extRouteRef1' }); - const extRouteRef2 = createExternalRouteRef({ - id: 'extRouteRef2', - params: ['x'], - }); - const extRouteRef3 = createExternalRouteRef({ - id: 'extRouteRef3', - optional: true, - }); - const extRouteRef4 = createExternalRouteRef({ - id: 'extRouteRef4', - optional: true, - params: ['x'], - }); - - const plugin1 = createPlugin({ - id: 'blob', - // Both absolute and sub route refs should be assignable to the plugin routes - routes: { - ref1: plugin1RouteRef, - ref2: plugin2RouteRef, - ref3: subRouteRef1, - }, - externalRoutes: { - extRouteRef1, - extRouteRef2, - extRouteRef3, - extRouteRef4, - }, - }); - - const plugin2 = createPlugin({ - id: 'plugin2', - }); - - const HiddenComponent = plugin2.provide( - createRoutableExtension({ - component: () => Promise.resolve((_: { path?: string }) =>

), - mountPoint: plugin2RouteRef, - }), - ); - - const ExposedComponent = plugin1.provide( - createRoutableExtension({ - component: () => - Promise.resolve((_: PropsWithChildren<{ path?: string }>) => { - const link1 = useRouteRef(plugin1RouteRef); - const link2 = useRouteRef(plugin2RouteRef); - const subLink1 = useRouteRef(subRouteRef1); - const subLink2 = useRouteRef(subRouteRef2); - const subLink3 = useRouteRef(subRouteRef3); - const subLink4 = useRouteRef(subRouteRef4); - const extLink1 = useRouteRef(extRouteRef1); - const extLink2 = useRouteRef(extRouteRef2); - const extLink3 = useRouteRef(extRouteRef3); - const extLink4 = useRouteRef(extRouteRef4); - return ( -
- link1: {link1()} - link2: {link2({ x: 'a' })} - subLink1: {subLink1()} - subLink2: {subLink2({ x: 'a' })} - subLink3: {subLink3({ x: 'b' })} - subLink4: {subLink4({ x: 'c', y: 'd' })} - extLink1: {extLink1()} - extLink2: {extLink2({ x: 'a' })} - extLink3: {extLink3?.() ?? ''} - extLink4: {extLink4?.({ x: 'b' }) ?? ''} -
- ); - }), - mountPoint: plugin1RouteRef, - }), - ); - - const components = { - NotFoundErrorPage: () => null, - BootErrorPage: () => null, - Progress: () => null, - Router: BrowserRouter, - }; - - it('runs happy paths', async () => { - const app = new PrivateAppImpl({ - apis: [], - defaultApis: [], - themes: [ - { - id: 'light', - title: 'Light Theme', - variant: 'light', - theme: lightTheme, - }, - ], - icons: defaultSystemIcons, - plugins: [], - components, - bindRoutes: ({ bind }) => { - bind(plugin1.externalRoutes, { - extRouteRef1: plugin1RouteRef, - extRouteRef2: plugin2RouteRef, - extRouteRef3: subRouteRef1, - extRouteRef4: plugin2RouteRef, - }); - }, - }); - - const Provider = app.getProvider(); - const Router = app.getRouter(); - - await renderWithEffects( - - - - - - - - , - ); - - expect(screen.getByText('link1: /')).toBeInTheDocument(); - expect(screen.getByText('link2: /foo/a')).toBeInTheDocument(); - expect(screen.getByText('subLink1: /sub1')).toBeInTheDocument(); - expect(screen.getByText('subLink2: /sub2/a')).toBeInTheDocument(); - expect(screen.getByText('subLink3: /foo/b/sub3')).toBeInTheDocument(); - expect(screen.getByText('subLink4: /foo/c/sub4/d')).toBeInTheDocument(); - expect(screen.getByText('extLink1: /')).toBeInTheDocument(); - expect(screen.getByText('extLink2: /foo/a')).toBeInTheDocument(); - expect(screen.getByText('extLink3: /sub1')).toBeInTheDocument(); - expect(screen.getByText('extLink4: /foo/b')).toBeInTheDocument(); - - // Plugins should be discovered through element tree - expect(app.getPlugins()).toEqual([plugin1, plugin2]); - }); - - it('runs happy paths without optional routes', async () => { - const app = new PrivateAppImpl({ - apis: [], - defaultApis: [], - themes: [ - { - id: 'light', - title: 'Light Theme', - variant: 'light', - theme: lightTheme, - }, - ], - icons: defaultSystemIcons, - plugins: [], - components, - bindRoutes: ({ bind }) => { - bind(plugin1.externalRoutes, { - extRouteRef1: plugin1RouteRef, - extRouteRef2: plugin2RouteRef, - }); - }, - }); - - const Provider = app.getProvider(); - const Router = app.getRouter(); - - await renderWithEffects( - - - - - - - - , - ); - - expect(screen.getByText('extLink1: /')).toBeInTheDocument(); - expect(screen.getByText('extLink2: /foo')).toBeInTheDocument(); - expect(screen.getByText('extLink3: ')).toBeInTheDocument(); - expect(screen.getByText('extLink4: ')).toBeInTheDocument(); - }); - - it('should wait for the config to load before calling feature flags', async () => { - const storageFlags = new LocalStorageFeatureFlags(); - jest.spyOn(storageFlags, 'registerFlag'); - - const apis = [ - createApiFactory({ - api: featureFlagsApiRef, - deps: { configApi: configApiRef }, - factory() { - return storageFlags; - }, - }), - ]; - - const app = new PrivateAppImpl({ - apis, - defaultApis: [], - themes: [ - { - id: 'light', - title: 'Light Theme', - variant: 'light', - theme: lightTheme, - }, - ], - icons: defaultSystemIcons, - plugins: [ - createPlugin({ - id: 'test', - register: p => p.featureFlags.register('name'), - }), - ], - components, - bindRoutes: ({ bind }) => { - bind(plugin1.externalRoutes, { - extRouteRef1: plugin1RouteRef, - extRouteRef2: plugin2RouteRef, - }); - }, - }); - - const Provider = app.getProvider(); - const Router = app.getRouter(); - - await renderWithEffects( - - - - - - - - , - ); - - expect(storageFlags.registerFlag).toHaveBeenCalledWith({ - name: 'name', - pluginId: 'test', - }); - }); - - it('should throw some error when the route has duplicate params', () => { - const app = new PrivateAppImpl({ - apis: [], - defaultApis: [], - themes: [ - { - id: 'light', - title: 'Light Theme', - variant: 'light', - theme: lightTheme, - }, - ], - icons: defaultSystemIcons, - plugins: [], - components, - bindRoutes: ({ bind }) => { - bind(plugin1.externalRoutes, { - extRouteRef1: plugin1RouteRef, - extRouteRef2: plugin2RouteRef, - }); - }, - }); - - const Provider = app.getProvider(); - const Router = app.getRouter(); - const { error: errorLogs } = withLogCollector(() => { - expect(() => - render( - - - - - - - - - , - ), - ).toThrow( - 'Parameter :thing is duplicated in path /test/:thing/some/:thing', - ); - }); - expect(errorLogs).toEqual([ - expect.stringContaining( - 'Parameter :thing is duplicated in path /test/:thing/some/:thing', - ), - expect.stringContaining( - 'The above error occurred in the component', - ), - ]); - }); -}); diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx deleted file mode 100644 index 3a5e151466..0000000000 --- a/packages/core-api/src/app/App.tsx +++ /dev/null @@ -1,525 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Config } from '@backstage/config'; -import React, { - ComponentType, - PropsWithChildren, - ReactElement, - useEffect, - useMemo, - useState, -} from 'react'; -import { Navigate, Route, Routes } from 'react-router-dom'; -import { useAsync } from 'react-use'; -import { - AnyApiFactory, - ApiHolder, - ApiProvider, - ApiRegistry, - AppTheme, - appThemeApiRef, - AppThemeSelector, - configApiRef, - ConfigReader, - LocalStorageFeatureFlags, - useApi, -} from '../apis'; -import { - AppThemeApi, - ConfigApi, - featureFlagsApiRef, - identityApiRef, -} from '../apis/definitions'; -import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; -import { - childDiscoverer, - routeElementDiscoverer, - traverseElementTree, -} from '../extensions/traversal'; -import { IconComponent, IconComponentMap, IconKey } from '../icons'; -import { BackstagePlugin } from '../plugin'; -import { pluginCollector } from '../plugin/collectors'; -import { AnyRoutes } from '../plugin/types'; -import { RouteRef, ExternalRouteRef, SubRouteRef } from '../routing'; -import { - routeObjectCollector, - routeParentCollector, - routePathCollector, -} from '../routing/collectors'; -import { RoutingProvider } from '../routing/hooks'; -import { validateRoutes } from '../routing/validation'; -import { AppContextProvider } from './AppContext'; -import { AppIdentity } from './AppIdentity'; -import { AppThemeProvider } from './AppThemeProvider'; -import { - AppComponents, - AppConfigLoader, - AppContext, - AppOptions, - AppRouteBinder, - BackstageApp, - SignInPageProps, - SignInResult, -} from './types'; - -export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) { - const result = new Map(); - - if (bindRoutes) { - const bind: AppRouteBinder = (externalRoutes, targetRoutes: AnyRoutes) => { - for (const [key, value] of Object.entries(targetRoutes)) { - const externalRoute = externalRoutes[key]; - if (!externalRoute) { - throw new Error(`Key ${key} is not an existing external route`); - } - if (!value && !externalRoute.optional) { - throw new Error( - `External route ${key} is required but was undefined`, - ); - } - if (value) { - result.set(externalRoute, value); - } - } - }; - bindRoutes({ bind }); - } - - return result; -} - -type FullAppOptions = { - apis: Iterable; - icons: IconComponentMap; - plugins: BackstagePlugin[]; - components: AppComponents; - themes: AppTheme[]; - configLoader?: AppConfigLoader; - defaultApis: Iterable; - bindRoutes?: AppOptions['bindRoutes']; -}; - -function useConfigLoader( - configLoader: AppConfigLoader | undefined, - components: AppComponents, - appThemeApi: AppThemeApi, -): { api: ConfigApi } | { node: JSX.Element } { - // Keeping this synchronous when a config loader isn't set simplifies tests a lot - const hasConfig = Boolean(configLoader); - const config = useAsync(configLoader || (() => Promise.resolve([]))); - - let noConfigNode = undefined; - - if (hasConfig && config.loading) { - const { Progress } = components; - noConfigNode = ; - } else if (config.error) { - const { BootErrorPage } = components; - noConfigNode = ; - } - - // Before the config is loaded we can't use a router, so exit early - if (noConfigNode) { - return { - node: ( - - {noConfigNode} - - ), - }; - } - - const configReader = ConfigReader.fromConfigs(config.value ?? []); - - return { api: configReader }; -} - -class AppContextImpl implements AppContext { - constructor(private readonly app: PrivateAppImpl) {} - - getPlugins(): BackstagePlugin[] { - // eslint-disable-next-line no-console - console.warn('appContext.getPlugins() is deprecated and will be removed'); - return this.app.getPlugins(); - } - - getSystemIcon(key: IconKey): IconComponent | undefined { - return this.app.getSystemIcon(key); - } - - getComponents(): AppComponents { - return this.app.getComponents(); - } - - getProvider(): React.ComponentType<{}> { - // eslint-disable-next-line no-console - console.warn('appContext.getProvider() is deprecated and will be removed'); - return this.app.getProvider(); - } - - getRouter(): React.ComponentType<{}> { - // eslint-disable-next-line no-console - console.warn('appContext.getRouter() is deprecated and will be removed'); - return this.app.getRouter(); - } - - getRoutes(): JSX.Element[] { - // eslint-disable-next-line no-console - console.warn('appContext.getRoutes() is deprecated and will be removed'); - return this.app.getRoutes(); - } -} - -export class PrivateAppImpl implements BackstageApp { - private apiHolder?: ApiHolder; - private configApi?: ConfigApi; - - private readonly apis: Iterable; - private readonly icons: IconComponentMap; - private readonly plugins: Set>; - private readonly components: AppComponents; - private readonly themes: AppTheme[]; - private readonly configLoader?: AppConfigLoader; - private readonly defaultApis: Iterable; - private readonly bindRoutes: AppOptions['bindRoutes']; - - private readonly identityApi = new AppIdentity(); - - constructor(options: FullAppOptions) { - this.apis = options.apis; - this.icons = options.icons; - this.plugins = new Set(options.plugins); - this.components = options.components; - this.themes = options.themes; - this.configLoader = options.configLoader; - this.defaultApis = options.defaultApis; - this.bindRoutes = options.bindRoutes; - } - - getPlugins(): BackstagePlugin[] { - return Array.from(this.plugins); - } - - getSystemIcon(key: IconKey): IconComponent | undefined { - return this.icons[key]; - } - - getComponents(): AppComponents { - return this.components; - } - - getRoutes(): JSX.Element[] { - const routes = new Array(); - - const { NotFoundErrorPage } = this.components; - - for (const plugin of this.plugins.values()) { - for (const output of plugin.output()) { - switch (output.type) { - case 'legacy-route': { - const { path, component: Component } = output; - routes.push( - } />, - ); - break; - } - case 'route': { - const { target, component: Component } = output; - routes.push( - } - />, - ); - break; - } - case 'legacy-redirect-route': { - const { path, target } = output; - routes.push(); - break; - } - case 'redirect-route': { - const { from, to } = output; - routes.push(); - break; - } - default: - break; - } - } - } - - routes.push( - } - />, - ); - - return routes; - } - - getProvider(): ComponentType<{}> { - const appContext = new AppContextImpl(this); - - const Provider = ({ children }: PropsWithChildren<{}>) => { - const appThemeApi = useMemo( - () => AppThemeSelector.createWithStorage(this.themes), - [], - ); - - const { routePaths, routeParents, routeObjects } = useMemo(() => { - const result = traverseElementTree({ - root: children, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routePaths: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, - collectedPlugins: pluginCollector, - }, - }); - - validateRoutes(result.routePaths, result.routeParents); - - // TODO(Rugvip): Restructure the public API so that we can get an immediate view of - // the app, rather than having to wait for the provider to render. - // For now we need to push the additional plugins we find during - // collection and then make sure we initialize things afterwards. - result.collectedPlugins.forEach(plugin => this.plugins.add(plugin)); - this.verifyPlugins(this.plugins); - - // Initialize APIs once all plugins are available - this.getApiHolder(); - - return result; - }, [children]); - - const loadedConfig = useConfigLoader( - this.configLoader, - this.components, - appThemeApi, - ); - - const hasConfigApi = 'api' in loadedConfig; - if (hasConfigApi) { - const { api } = loadedConfig as { api: Config }; - this.configApi = api; - } - - useEffect(() => { - if (hasConfigApi) { - const featureFlagsApi = this.getApiHolder().get(featureFlagsApiRef)!; - - for (const plugin of this.plugins.values()) { - for (const output of plugin.output()) { - switch (output.type) { - case 'feature-flag': { - featureFlagsApi.registerFlag({ - name: output.name, - pluginId: plugin.getId(), - }); - break; - } - default: - break; - } - } - } - } - }, [hasConfigApi, loadedConfig]); - - if ('node' in loadedConfig) { - // Loading or error - return loadedConfig.node; - } - - return ( - - - - - {children} - - - - - ); - }; - return Provider; - } - - getRouter(): ComponentType<{}> { - const { - Router: RouterComponent, - SignInPage: SignInPageComponent, - } = this.components; - - // This wraps the sign-in page and waits for sign-in to be completed before rendering the app - const SignInPageWrapper = ({ - component: Component, - children, - }: { - component: ComponentType; - children: ReactElement; - }) => { - const [result, setResult] = useState(); - - if (result) { - this.identityApi.setSignInResult(result); - return children; - } - - return ; - }; - - const AppRouter = ({ children }: PropsWithChildren<{}>) => { - const configApi = useApi(configApiRef); - - let { pathname } = new URL( - configApi.getOptionalString('app.baseUrl') ?? '/', - 'http://dummy.dev', // baseUrl can be specified as just a path - ); - if (pathname.endsWith('/')) { - pathname = pathname.replace(/\/$/, ''); - } - - // If the app hasn't configured a sign-in page, we just continue as guest. - if (!SignInPageComponent) { - this.identityApi.setSignInResult({ - userId: 'guest', - profile: { - email: 'guest@example.com', - displayName: 'Guest', - }, - }); - - return ( - - - {children}} /> - - - ); - } - - return ( - - - - {children}} /> - - - - ); - }; - - return AppRouter; - } - - private getApiHolder(): ApiHolder { - if (this.apiHolder) { - return this.apiHolder; - } - - const registry = new ApiFactoryRegistry(); - - registry.register('static', { - api: appThemeApiRef, - deps: {}, - factory: () => AppThemeSelector.createWithStorage(this.themes), - }); - registry.register('static', { - api: configApiRef, - deps: {}, - factory: () => { - if (!this.configApi) { - throw new Error( - 'Tried to access config API before config was loaded', - ); - } - return this.configApi; - }, - }); - registry.register('static', { - api: identityApiRef, - deps: {}, - factory: () => this.identityApi, - }); - - // It's possible to replace the feature flag API, but since we must have at least - // one implementation we add it here directly instead of through the defaultApis. - registry.register('default', { - api: featureFlagsApiRef, - deps: {}, - factory: () => new LocalStorageFeatureFlags(), - }); - for (const factory of this.defaultApis) { - registry.register('default', factory); - } - - for (const plugin of this.plugins) { - for (const factory of plugin.getApis()) { - if (!registry.register('default', factory)) { - throw new Error( - `Plugin ${plugin.getId()} tried to register duplicate or forbidden API factory for ${ - factory.api - }`, - ); - } - } - } - - for (const factory of this.apis) { - if (!registry.register('app', factory)) { - throw new Error( - `Duplicate or forbidden API factory for ${factory.api} in app`, - ); - } - } - - ApiResolver.validateFactories(registry, registry.getAllApis()); - - this.apiHolder = new ApiResolver(registry); - - return this.apiHolder; - } - - /** - * @deprecated - */ - verify() {} - - private verifyPlugins(plugins: Iterable) { - const pluginIds = new Set(); - - for (const plugin of plugins) { - const id = plugin.getId(); - if (pluginIds.has(id)) { - throw new Error(`Duplicate plugin found '${id}'`); - } - pluginIds.add(id); - } - } -} diff --git a/packages/core-api/src/app/AppContext.test.tsx b/packages/core-api/src/app/AppContext.test.tsx deleted file mode 100644 index 526b397130..0000000000 --- a/packages/core-api/src/app/AppContext.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useContext, Context } from 'react'; -import { renderHook } from '@testing-library/react-hooks'; -import { VersionedValue } from '../lib/versionedValues'; -import { getGlobalSingleton } from '../lib/globalObject'; -import { AppContext as AppContextV1 } from './types'; -import { AppContextProvider } from './AppContext'; - -describe('v1 consumer', () => { - const AppContext = getGlobalSingleton< - Context> - >('app-context'); - - function useMockAppV1(): AppContextV1 { - const impl = useContext(AppContext)?.atVersion(1); - if (!impl) { - throw new Error('no impl'); - } - return impl; - } - - it('should provide an app context', () => { - const mockContext: AppContextV1 = { - getComponents: jest.fn(), - getSystemIcon: jest.fn(), - getPlugins: jest.fn(), - getProvider: jest.fn(), - getRouter: jest.fn(), - getRoutes: jest.fn(), - }; - - const renderedHook = renderHook(() => useMockAppV1(), { - wrapper: ({ children }) => ( - - ), - }); - const result = renderedHook.result.current; - - expect(mockContext.getComponents).toHaveBeenCalledTimes(0); - result.getComponents(); - expect(mockContext.getComponents).toHaveBeenCalledTimes(1); - - expect(mockContext.getSystemIcon).toHaveBeenCalledTimes(0); - result.getSystemIcon('icon'); - expect(mockContext.getSystemIcon).toHaveBeenCalledTimes(1); - expect(mockContext.getSystemIcon).toHaveBeenCalledWith('icon'); - - expect(mockContext.getPlugins).toHaveBeenCalledTimes(0); - result.getPlugins(); - expect(mockContext.getPlugins).toHaveBeenCalledTimes(1); - - expect(mockContext.getProvider).toHaveBeenCalledTimes(0); - result.getProvider(); - expect(mockContext.getProvider).toHaveBeenCalledTimes(1); - - expect(mockContext.getRouter).toHaveBeenCalledTimes(0); - result.getRouter(); - expect(mockContext.getRouter).toHaveBeenCalledTimes(1); - - expect(mockContext.getRoutes).toHaveBeenCalledTimes(0); - result.getRoutes(); - expect(mockContext.getRoutes).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/core-api/src/app/AppContext.tsx b/packages/core-api/src/app/AppContext.tsx deleted file mode 100644 index ab2d2d7861..0000000000 --- a/packages/core-api/src/app/AppContext.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { - createContext, - PropsWithChildren, - useContext, - Context, - useMemo, -} from 'react'; -import { - VersionedValue, - createVersionedValueMap, -} from '../lib/versionedValues'; -import { - getGlobalSingleton, - getOrCreateGlobalSingleton, -} from '../lib/globalObject'; -import { AppContext as AppContextV1 } from './types'; - -type AppContextType = VersionedValue<{ 1: AppContextV1 }> | undefined; -const AppContext = getOrCreateGlobalSingleton('app-context', () => - createContext(undefined), -); - -type Props = { - appContext: AppContextV1; -}; - -export const AppContextProvider = ({ - appContext, - children, -}: PropsWithChildren) => { - const versionedValue = useMemo( - () => createVersionedValueMap({ 1: appContext }), - [appContext], - ); - - return ; -}; - -export const useApp = (): AppContextV1 => { - const versionedContext = useContext( - getGlobalSingleton>('app-context'), - ); - if (!versionedContext) { - throw new Error('No app context available'); - } - const appContext = versionedContext.atVersion(1); - if (!appContext) { - throw new Error('AppContext v1 not available'); - } - return appContext; -}; diff --git a/packages/core-api/src/app/AppIdentity.ts b/packages/core-api/src/app/AppIdentity.ts deleted file mode 100644 index 7dd87de392..0000000000 --- a/packages/core-api/src/app/AppIdentity.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { IdentityApi, ProfileInfo } from '../apis'; -import { SignInResult } from './types'; - -/** - * Implementation of the connection between the App-wide IdentityApi - * and sign-in page. - */ -export class AppIdentity implements IdentityApi { - private hasIdentity = false; - private userId?: string; - private profile?: ProfileInfo; - private idTokenFunc?: () => Promise; - private signOutFunc?: () => Promise; - - getUserId(): string { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi userId before app was loaded', - ); - } - return this.userId!; - } - - getProfile(): ProfileInfo { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi profile before app was loaded', - ); - } - return this.profile!; - } - - async getIdToken(): Promise { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi idToken before app was loaded', - ); - } - return this.idTokenFunc?.(); - } - - async signOut(): Promise { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi signOutFunc before app was loaded', - ); - } - await this.signOutFunc?.(); - location.reload(); - } - - // This is indirectly called by the sign-in page to continue into the app. - setSignInResult(result: SignInResult) { - if (this.hasIdentity) { - return; - } - if (!result.userId) { - throw new Error('Invalid sign-in result, userId not set'); - } - if (!result.profile) { - throw new Error('Invalid sign-in result, profile not set'); - } - this.hasIdentity = true; - this.userId = result.userId; - this.profile = result.profile; - this.idTokenFunc = result.getIdToken; - this.signOutFunc = result.signOut; - } -} diff --git a/packages/core-api/src/app/AppThemeProvider.tsx b/packages/core-api/src/app/AppThemeProvider.tsx deleted file mode 100644 index b6a88e6fcc..0000000000 --- a/packages/core-api/src/app/AppThemeProvider.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useMemo, useEffect, useState, PropsWithChildren } from 'react'; -import { ThemeProvider, CssBaseline } from '@material-ui/core'; -import { useApi, appThemeApiRef, AppTheme } from '../apis'; -import { useObservable } from 'react-use'; - -// This tries to find the most accurate match, but also falls back to less -// accurate results in order to avoid errors. -function resolveTheme( - themeId: string | undefined, - shouldPreferDark: boolean, - themes: AppTheme[], -) { - if (themeId !== undefined) { - const selectedTheme = themes.find(theme => theme.id === themeId); - if (selectedTheme) { - return selectedTheme; - } - } - - if (shouldPreferDark) { - const darkTheme = themes.find(theme => theme.variant === 'dark'); - if (darkTheme) { - return darkTheme; - } - } - - const lightTheme = themes.find(theme => theme.variant === 'light'); - if (lightTheme) { - return lightTheme; - } - - return themes[0]; -} - -const useShouldPreferDarkTheme = () => { - const mediaQuery = useMemo( - () => window.matchMedia('(prefers-color-scheme: dark)'), - [], - ); - const [shouldPreferDark, setPrefersDark] = useState(mediaQuery.matches); - - useEffect(() => { - const listener = (event: MediaQueryListEvent) => { - setPrefersDark(event.matches); - }; - mediaQuery.addListener(listener); - return () => { - mediaQuery.removeListener(listener); - }; - }, [mediaQuery]); - - return shouldPreferDark; -}; - -export function AppThemeProvider({ children }: PropsWithChildren<{}>) { - const appThemeApi = useApi(appThemeApiRef); - const themeId = useObservable( - appThemeApi.activeThemeId$(), - appThemeApi.getActiveThemeId(), - ); - - // Browser feature detection won't change over time, so ignore lint rule - const shouldPreferDark = Boolean(window.matchMedia) - ? useShouldPreferDarkTheme() // eslint-disable-line react-hooks/rules-of-hooks - : false; - - const appTheme = resolveTheme( - themeId, - shouldPreferDark, - appThemeApi.getInstalledThemes(), - ); - if (!appTheme) { - throw new Error('App has no themes'); - } - - return ( - - {children} - - ); -} diff --git a/packages/core-api/src/app/index.ts b/packages/core-api/src/app/index.ts deleted file mode 100644 index 56e0800809..0000000000 --- a/packages/core-api/src/app/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { useApp } from './AppContext'; -export * from './types'; diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts deleted file mode 100644 index a3eb6f423f..0000000000 --- a/packages/core-api/src/app/types.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ComponentType } from 'react'; -import { IconComponent, IconComponentMap, IconKey } from '../icons/types'; -import { AnyExternalRoutes, BackstagePlugin } from '../plugin/types'; -import { ExternalRouteRef, RouteRef, SubRouteRef } from '../routing/types'; -import { AnyApiFactory } from '../apis/system'; -import { AppTheme, ProfileInfo } from '../apis/definitions'; -import { AppConfig } from '@backstage/config'; - -export type BootErrorPageProps = { - step: 'load-config' | 'load-chunk'; - error: Error; -}; - -export type SignInResult = { - /** - * User ID that will be returned by the IdentityApi - */ - userId: string; - - profile: ProfileInfo; - - /** - * Function used to retrieve an ID token for the signed in user. - */ - getIdToken?: () => Promise; - - /** - * Sign out handler that will be called if the user requests to sign out. - */ - signOut?: () => Promise; -}; - -export type SignInPageProps = { - /** - * Set the sign-in result for the app. This should only be called once. - */ - onResult(result: SignInResult): void; -}; - -export type AppComponents = { - NotFoundErrorPage: ComponentType<{}>; - BootErrorPage: ComponentType; - Progress: ComponentType<{}>; - Router: ComponentType<{}>; - - /** - * An optional sign-in page that will be rendered instead of the AppRouter at startup. - * - * If a sign-in page is set, it will always be shown before the app, and it is up - * to the sign-in page to handle e.g. saving of login methods for subsequent visits. - * - * The sign-in page will be displayed until it has passed up a result to the parent, - * and which point the AppRouter and all of its children will be rendered instead. - */ - SignInPage?: ComponentType; -}; - -/** - * A function that loads in the App config that will be accessible via the ConfigApi. - * - * If multiple config objects are returned in the array, values in the earlier configs - * will override later ones. - */ -export type AppConfigLoader = () => Promise; - -/** - * Extracts a union of the keys in a map whose value extends the given type - */ -type KeysWithType = { - [key in keyof Obj]: Obj[key] extends Type ? key : never; -}[keyof Obj]; - -/** - * Takes a map Map required values and makes all keys matching Keys optional - */ -type PartialKeys< - Map extends { [name in string]: any }, - Keys extends keyof Map -> = Partial> & Required>; - -/** - * Creates a map of target routes with matching parameters based on a map of external routes. - */ -type TargetRouteMap = { - [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef< - infer Params, - any - > - ? RouteRef | SubRouteRef - : never; -}; - -export type AppRouteBinder = ( - externalRoutes: ExternalRoutes, - targetRoutes: PartialKeys< - TargetRouteMap, - KeysWithType> - >, -) => void; - -export type AppOptions = { - /** - * A collection of ApiFactories to register in the application to either - * add add new ones, or override factories provided by default or by plugins. - */ - apis?: Iterable; - - /** - * Supply icons to override the default ones. - */ - icons?: IconComponentMap; - - /** - * A list of all plugins to include in the app. - */ - plugins?: BackstagePlugin[]; - - /** - * Supply components to the app to override the default ones. - */ - components?: Partial; - - /** - * Themes provided as a part of the app. By default two themes are included, one - * light variant of the default backstage theme, and one dark. - * - * This is the default config: - * - * ``` - * [{ - * id: 'light', - * title: 'Light Theme', - * variant: 'light', - * theme: lightTheme, - * icon: , - * }, { - * id: 'dark', - * title: 'Dark Theme', - * variant: 'dark', - * theme: darkTheme, - * icon: , - * }] - * ``` - */ - themes?: AppTheme[]; - - /** - * A function that loads in App configuration that will be accessible via - * the ConfigApi. - * - * Defaults to an empty config. - * - * TODO(Rugvip): Omitting this should instead default to loading in configuration - * that was packaged by the backstage-cli and default docker container boot script. - */ - configLoader?: AppConfigLoader; - - /** - * A function that is used to register associations between cross-plugin route - * references, enabling plugins to navigate between each other. - * - * The `bind` function that is passed in should be used to bind all external - * routes of all used plugins. - * - * ```ts - * bindRoutes({ bind }) { - * bind(docsPlugin.externalRoutes, { - * homePage: managePlugin.routes.managePage, - * }) - * bind(homePagePlugin.externalRoutes, { - * settingsPage: settingsPlugin.routes.settingsPage, - * }) - * } - * ``` - */ - bindRoutes?(context: { bind: AppRouteBinder }): void; -}; - -export type BackstageApp = { - /** - * Returns all plugins registered for the app. - */ - getPlugins(): BackstagePlugin[]; - - /** - * Get a common or custom icon for this app. - */ - getSystemIcon(key: IconKey): IconComponent | undefined; - - /** - * Provider component that should wrap the Router created with getRouter() - * and any other components that need to be within the app context. - */ - getProvider(): ComponentType<{}>; - - /** - * Router component that should wrap the App Routes create with getRoutes() - * and any other components that should only be available while signed in. - */ - getRouter(): ComponentType<{}>; - - /** - * Routes component that contains all routes for plugin pages in the app. - * - * @deprecated Registering routes in plugins is deprecated and this method will be removed. - */ - getRoutes(): JSX.Element[]; -}; - -export type AppContext = { - /** - * @deprecated Will be removed - */ - getPlugins(): BackstagePlugin[]; - - /** - * Get a common or custom icon for this app. - */ - getSystemIcon(key: IconKey): IconComponent | undefined; - - /** - * Get the components registered for various purposes in the app. - */ - getComponents(): AppComponents; - - /** - * @deprecated Will be removed - */ - getProvider(): ComponentType<{}>; - - /** - * @deprecated Will be removed - */ - getRouter(): ComponentType<{}>; - - /** - * @deprecated Will be removed - */ - getRoutes(): JSX.Element[]; -}; diff --git a/packages/core-api/src/extensions/componentData.test.tsx b/packages/core-api/src/extensions/componentData.test.tsx deleted file mode 100644 index 808ab08cf1..0000000000 --- a/packages/core-api/src/extensions/componentData.test.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { attachComponentData, getComponentData } from './componentData'; - -describe('elementData', () => { - it('should attach a single piece of data', () => { - const data = { foo: 'bar' }; - const Component = () => null; - attachComponentData(Component, 'my-data', data); - - const element = ; - expect(getComponentData(element, 'my-data')).toBe(data); - }); - - it('should attach several distinct pieces of data', () => { - const data1 = { foo: 'bar' }; - const data2 = { test: 'value' }; - const Component = () => null; - attachComponentData(Component, 'my-data', data1); - attachComponentData(Component, 'second', data2); - - const element = ; - expect(getComponentData(element, 'my-data')).toBe(data1); - expect(getComponentData(element, 'second')).toBe(data2); - }); - - it('returns undefined for missing data', () => { - const data = { foo: 'bar' }; - const Component1 = () => null; - const Component2 = () => null; - attachComponentData(Component2, 'my-data', data); - - const element1 = ; - const element2 = ; - expect(getComponentData(element1, 'missing')).toBeUndefined(); - expect(getComponentData(element2, 'missing')).toBeUndefined(); - }); - - it('should throw when attempting to overwrite data', () => { - const data = { foo: 'bar' }; - const MyComponent = () => null; - attachComponentData(MyComponent, 'my-data', data); - expect(() => attachComponentData(MyComponent, 'my-data', data)).toThrow( - 'Attempted to attach duplicate data "my-data" to component "MyComponent"', - ); - }); - - describe('works across versions', () => { - function getDataSymbol() { - const Component = () => null; - attachComponentData(Component, 'my-data', {}); - const [symbol] = Object.getOwnPropertySymbols(Component); - return symbol; - } - - it('should should be able to get data from older versions', () => { - const symbol = getDataSymbol(); - - const data = { foo: 'bar' }; - const Component = () => null; - attachComponentData(Component, 'my-data', data); - - const element = ; - expect((element as any).type[symbol].map.get('my-data')).toBe(data); - }); - - it('should should be able to attach data for older versions', () => { - const symbol = getDataSymbol(); - - const data = { foo: 'bar' }; - const Component = () => null; - (Component as any)[symbol] = { - map: new Map([['my-data', data]]), - }; - - const element = ; - expect(getComponentData(element, 'my-data')).toBe(data); - }); - - it('should be able to get data from newer versions', () => { - const data = { foo: 'bar' }; - const Component = () => null; - attachComponentData(Component, 'my-data', data); - - const element = ; - const container = (global as any)[ - '__@backstage/component-data-store__' - ].get(element.type); - expect(container.map.get('my-data')).toBe(data); - }); - - it('should should be able to attach data for newer versions', () => { - const data = { foo: 'bar' }; - const Component = () => null; - (global as any)['__@backstage/component-data-store__'].set(Component, { - map: new Map([['my-data', data]]), - }); - - const element = ; - expect(getComponentData(element, 'my-data')).toBe(data); - }); - }); -}); diff --git a/packages/core-api/src/extensions/componentData.tsx b/packages/core-api/src/extensions/componentData.tsx deleted file mode 100644 index d4975d9eef..0000000000 --- a/packages/core-api/src/extensions/componentData.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ComponentType, ReactNode } from 'react'; -import { getOrCreateGlobalSingleton } from '../lib/globalObject'; - -// TODO(Rugvip): Access via symbol is deprecated, remove once on 0.3.x -const DATA_KEY = Symbol('backstage-component-data'); - -type ComponentWithData

= ComponentType

& { - [DATA_KEY]?: DataContainer; -}; - -type DataContainer = { - map: Map; -}; - -type MaybeComponentNode = ReactNode & { - type?: ComponentType & { [DATA_KEY]?: DataContainer }; -}; - -// The store is bridged across versions using the global object -const store = getOrCreateGlobalSingleton( - 'component-data-store', - () => new WeakMap, DataContainer>(), -); - -export function attachComponentData

( - component: ComponentType

, - type: string, - data: unknown, -) { - const dataComponent = component as ComponentWithData

; - - let container = store.get(component) || dataComponent[DATA_KEY]; - if (!container) { - container = { map: new Map() }; - store.set(component, container); - dataComponent[DATA_KEY] = container; - } - - if (container.map.has(type)) { - const name = component.displayName || component.name; - throw new Error( - `Attempted to attach duplicate data "${type}" to component "${name}"`, - ); - } - - container.map.set(type, data); -} - -export function getComponentData( - node: ReactNode, - type: string, -): T | undefined { - if (!node) { - return undefined; - } - - const component = (node as MaybeComponentNode).type; - if (!component) { - return undefined; - } - - const container = store.get(component) || component[DATA_KEY]; - if (!container) { - return undefined; - } - - return container.map.get(type) as T | undefined; -} diff --git a/packages/core-api/src/extensions/extensions.test.tsx b/packages/core-api/src/extensions/extensions.test.tsx deleted file mode 100644 index bd649bc0f8..0000000000 --- a/packages/core-api/src/extensions/extensions.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { createPlugin } from '../plugin'; -import { createRouteRef } from '../routing'; -import { getComponentData } from './componentData'; -import { - createComponentExtension, - createReactExtension, - createRoutableExtension, -} from './extensions'; - -const plugin = createPlugin({ - id: 'my-plugin', -}); - -describe('extensions', () => { - it('should create a react extension with component data', () => { - const Component = () =>

; - - const extension = createReactExtension({ - component: { - sync: Component, - }, - data: { - myData: { foo: 'bar' }, - }, - }); - - const ExtensionComponent = plugin.provide(extension); - const element = ; - - expect(getComponentData(element, 'core.plugin')).toBe(plugin); - expect(getComponentData(element, 'myData')).toEqual({ foo: 'bar' }); - }); - - it('should create react extensions of different types', () => { - const Component = () =>
; - const routeRef = createRouteRef({ path: '/foo', title: 'Foo' }); - - const extension1 = createComponentExtension({ - component: { - sync: Component, - }, - }); - - const extension2 = createRoutableExtension({ - component: () => Promise.resolve(Component), - mountPoint: routeRef, - }); - - const ExtensionComponent1 = plugin.provide(extension1); - const ExtensionComponent2 = plugin.provide(extension2); - - const element1 = ; - const element2 = ; - - expect(getComponentData(element1, 'core.plugin')).toBe(plugin); - expect(getComponentData(element2, 'core.plugin')).toBe(plugin); - expect(getComponentData(element2, 'core.mountPoint')).toBe(routeRef); - }); -}); diff --git a/packages/core-api/src/extensions/extensions.tsx b/packages/core-api/src/extensions/extensions.tsx deleted file mode 100644 index bca4c25f97..0000000000 --- a/packages/core-api/src/extensions/extensions.tsx +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { lazy, Suspense } from 'react'; -import { useApp } from '../app'; -import { BackstagePlugin, Extension } from '../plugin/types'; -import { RouteRef, useRouteRef } from '../routing'; -import { attachComponentData } from './componentData'; - -type ComponentLoader = - | { - lazy: () => Promise; - } - | { - sync: T; - }; - -// We do not use ComponentType as the return type, since it doesn't let us convey the children prop. -// ComponentType inserts children as an optional prop whether the inner component accepts it or not, -// making it impossible to make the usage of children type safe. -export function createRoutableExtension< - T extends (props: any) => JSX.Element | null ->(options: { - component: () => Promise; - mountPoint: RouteRef; -}): Extension { - const { component, mountPoint } = options; - return createReactExtension({ - component: { - lazy: () => - component().then( - InnerComponent => { - const RoutableExtensionWrapper: any = (props: any) => { - // Validate that the routing is wired up correctly in the App.tsx - try { - useRouteRef(mountPoint); - } catch (error) { - if (error?.message.startsWith('No path for ')) { - throw new Error( - `Routable extension component with mount point ${mountPoint} was not discovered in the app element tree. ` + - 'Routable extension components may not be rendered by other components and must be ' + - 'directly available as an element within the App provider component.', - ); - } - throw error; - } - return ; - }; - - const componentName = - (InnerComponent as { displayName?: string }).displayName || - InnerComponent.name || - 'LazyComponent'; - - RoutableExtensionWrapper.displayName = `RoutableExtension(${componentName})`; - - return RoutableExtensionWrapper as T; - }, - error => { - const RoutableExtensionWrapper: any = (_: any) => { - const app = useApp(); - const { BootErrorPage } = app.getComponents(); - - return ; - }; - return RoutableExtensionWrapper; - }, - ), - }, - data: { - 'core.mountPoint': mountPoint, - }, - }); -} - -// We do not use ComponentType as the return type, since it doesn't let us convey the children prop. -// ComponentType inserts children as an optional prop whether the inner component accepts it or not, -// making it impossible to make the usage of children type safe. -export function createComponentExtension< - T extends (props: any) => JSX.Element | null ->(options: { component: ComponentLoader }): Extension { - const { component } = options; - return createReactExtension({ component }); -} - -// We do not use ComponentType as the return type, since it doesn't let us convey the children prop. -// ComponentType inserts children as an optional prop whether the inner component accepts it or not, -// making it impossible to make the usage of children type safe. -export function createReactExtension< - T extends (props: any) => JSX.Element | null ->(options: { - component: ComponentLoader; - data?: Record; -}): Extension { - const { data = {} } = options; - - let Component: T; - if ('lazy' in options.component) { - const lazyLoader = options.component.lazy; - Component = (lazy(() => - lazyLoader().then(component => ({ default: component })), - ) as unknown) as T; - } else { - Component = options.component.sync; - } - const componentName = - (Component as { displayName?: string }).displayName || - Component.name || - 'Component'; - - return { - expose(plugin: BackstagePlugin) { - const Result: any = (props: any) => ( - - - - ); - - attachComponentData(Result, 'core.plugin', plugin); - for (const [key, value] of Object.entries(data)) { - attachComponentData(Result, key, value); - } - - Result.displayName = `Extension(${componentName})`; - return Result; - }, - }; -} diff --git a/packages/core-api/src/extensions/index.ts b/packages/core-api/src/extensions/index.ts deleted file mode 100644 index 914d76aebf..0000000000 --- a/packages/core-api/src/extensions/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { attachComponentData, getComponentData } from './componentData'; -export { - createReactExtension, - createRoutableExtension, - createComponentExtension, -} from './extensions'; diff --git a/packages/core-api/src/extensions/traversal.test.tsx b/packages/core-api/src/extensions/traversal.test.tsx deleted file mode 100644 index 0acc51fb27..0000000000 --- a/packages/core-api/src/extensions/traversal.test.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { Children, isValidElement } from 'react'; -import { - childDiscoverer, - createCollector, - traverseElementTree, -} from './traversal'; - -describe('discovery', () => { - it('should collect element names', () => { - const root = ( -
-
-

Title

-

Text

-
-
-
-

Title

- Text -
-
- ); - - const { names } = traverseElementTree({ - root, - discoverers: [childDiscoverer], - collectors: { - names: createCollector( - () => Array(), - (acc, el) => { - if (typeof el.type === 'string') { - acc.push(el.type); - } - }, - ), - }, - }); - - expect(names).toEqual([ - 'main', - 'div', - 'hr', - 'div', - 'h1', - 'p', - 'h2', - 'span', - ]); - }); - - it('should collect element names while skipping one level of children', () => { - const root = ( -
-
-

Title

-

Text

-
-
-
-

Title

- Text -
-
- ); - - const { names } = traverseElementTree({ - root, - discoverers: [ - el => - Children.toArray(el.props.children).flatMap(child => - isValidElement(child) ? child?.props?.children : [], - ), - ], - collectors: { - names: createCollector( - () => Array(), - (acc, el) => { - if (typeof el.type === 'string') { - acc.push(el.type); - } - }, - ), - }, - }); - - expect(names).toEqual(['main', 'h1', 'p', 'h2', 'span']); - }); -}); diff --git a/packages/core-api/src/extensions/traversal.ts b/packages/core-api/src/extensions/traversal.ts deleted file mode 100644 index 79f44523b3..0000000000 --- a/packages/core-api/src/extensions/traversal.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { isValidElement, ReactNode, ReactElement, Children } from 'react'; - -export type Discoverer = (element: ReactElement) => ReactNode; - -export type Collector = () => { - accumulator: Result; - visit( - accumulator: Result, - element: ReactElement, - parent: ReactElement | undefined, - context: Context, - ): Context; -}; - -/** - * A function that allows you to traverse a tree of React elements using - * varying methods to discover child nodes and collect data along the way. - */ -export function traverseElementTree(options: { - root: ReactNode; - discoverers: Discoverer[]; - collectors: { [name in keyof Results]: Collector }; -}): Results { - const collectors: { - [name in string]: ReturnType>; - } = {}; - - // Bootstrap all collectors, initializing the accumulators and providing the visitor function - for (const name in options.collectors) { - if (options.collectors.hasOwnProperty(name)) { - collectors[name] = options.collectors[name](); - } - } - - // Internal representation of an element in the tree that we're iterating over - type QueueItem = { - node: ReactNode; - parent: ReactElement | undefined; - contexts: { [name in string]: unknown }; - }; - - const queue = [ - { - node: Children.toArray(options.root), - parent: undefined, - contexts: {}, - } as QueueItem, - ]; - - while (queue.length !== 0) { - const { node, parent, contexts } = queue.shift()!; - - // While the parent and the element we pass on to collectors and discoverers - // have been validated and are known to be React elements, the child nodes - // emitted by the discoverers are not. - Children.forEach(node, element => { - if (!isValidElement(element)) { - return; - } - - const nextContexts: QueueItem['contexts'] = {}; - - // Collectors populate their result data using the current node, and compute - // context for the next iteration - for (const name in collectors) { - if (collectors.hasOwnProperty(name)) { - const collector = collectors[name]; - - nextContexts[name] = collector.visit( - collector.accumulator, - element, - parent, - contexts[name], - ); - } - } - - // Discoverers provide ways to continue the traversal from the current element - for (const discoverer of options.discoverers) { - const children = discoverer(element); - if (children) { - queue.push({ - node: children, - parent: element, - contexts: nextContexts, - }); - } - } - }); - } - - return Object.fromEntries( - Object.entries(collectors).map(([name, c]) => [name, c.accumulator]), - ) as Results; -} - -export function createCollector( - accumulatorFactory: () => Result, - visit: ReturnType>['visit'], -): Collector { - return () => ({ accumulator: accumulatorFactory(), visit }); -} - -export function childDiscoverer(element: ReactElement): ReactNode { - return element.props?.children; -} - -export function routeElementDiscoverer(element: ReactElement): ReactNode { - if (element.props?.path && element.props?.element) { - return element.props?.element; - } - return undefined; -} diff --git a/packages/core-api/src/icons/icons.tsx b/packages/core-api/src/icons/icons.tsx deleted file mode 100644 index c90ee5cfc4..0000000000 --- a/packages/core-api/src/icons/icons.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { SvgIconProps } from '@material-ui/core'; -import MuiMenuBookIcon from '@material-ui/icons/MenuBook'; -import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; -import MuiChatIcon from '@material-ui/icons/Chat'; -import MuiDashboardIcon from '@material-ui/icons/Dashboard'; -import MuiEmailIcon from '@material-ui/icons/Email'; -import MuiGitHubIcon from '@material-ui/icons/GitHub'; -import MuiHelpIcon from '@material-ui/icons/Help'; -import MuiPeopleIcon from '@material-ui/icons/People'; -import MuiPersonIcon from '@material-ui/icons/Person'; -import MuiWarningIcon from '@material-ui/icons/Warning'; -import MuiDocsIcon from '@material-ui/icons/Description'; - -import React from 'react'; -import { useApp } from '../app/AppContext'; -import { IconComponent, IconComponentMap, SystemIconKey } from './types'; - -export const defaultSystemIcons: IconComponentMap = { - brokenImage: MuiBrokenImageIcon, - // To be confirmed: see https://github.com/backstage/backstage/issues/4970 - catalog: MuiMenuBookIcon, - chat: MuiChatIcon, - dashboard: MuiDashboardIcon, - email: MuiEmailIcon, - github: MuiGitHubIcon, - group: MuiPeopleIcon, - help: MuiHelpIcon, - user: MuiPersonIcon, - warning: MuiWarningIcon, - docs: MuiDocsIcon, -}; - -const overridableSystemIcon = (key: SystemIconKey): IconComponent => { - const Component = (props: SvgIconProps) => { - const app = useApp(); - const Icon = app.getSystemIcon(key); - return Icon ? : ; - }; - return Component; -}; - -export const BrokenImageIcon = overridableSystemIcon('brokenImage'); -export const ChatIcon = overridableSystemIcon('chat'); -export const DashboardIcon = overridableSystemIcon('dashboard'); -export const EmailIcon = overridableSystemIcon('email'); -export const GitHubIcon = overridableSystemIcon('github'); -export const GroupIcon = overridableSystemIcon('group'); -export const HelpIcon = overridableSystemIcon('help'); -export const UserIcon = overridableSystemIcon('user'); -export const WarningIcon = overridableSystemIcon('warning'); -export const DocsIcon = overridableSystemIcon('docs'); diff --git a/packages/core-api/src/icons/index.ts b/packages/core-api/src/icons/index.ts deleted file mode 100644 index 10045c4513..0000000000 --- a/packages/core-api/src/icons/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export * from './icons'; -export * from './types'; diff --git a/packages/core-api/src/icons/types.ts b/packages/core-api/src/icons/types.ts deleted file mode 100644 index bfaf9c5122..0000000000 --- a/packages/core-api/src/icons/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ComponentType } from 'react'; -import { SvgIconProps } from '@material-ui/core'; - -export type SystemIconKey = - | 'brokenImage' - | 'chat' - | 'dashboard' - | 'email' - | 'github' - | 'group' - | 'help' - | 'user' - | 'warning' - | 'docs'; - -export type IconComponent = ComponentType; -export type IconKey = SystemIconKey | string; -export type IconComponentMap = { [key in IconKey]: IconComponent }; diff --git a/packages/core-api/src/index.ts b/packages/core-api/src/index.ts deleted file mode 100644 index 9d4b2f770a..0000000000 --- a/packages/core-api/src/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export * from './public'; -import * as privateExports from './private'; - -export default privateExports; diff --git a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts deleted file mode 100644 index 524a0c5709..0000000000 --- a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import ProviderIcon from '@material-ui/icons/AcUnit'; -import { DefaultAuthConnector } from './DefaultAuthConnector'; -import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi'; -import * as loginPopup from '../loginPopup'; -import { UrlPatternDiscovery } from '../../apis'; -import { msw } from '@backstage/test-utils'; -import { setupServer } from 'msw/node'; -import { rest } from 'msw'; - -const defaultOptions = { - discoveryApi: UrlPatternDiscovery.compile('http://my-host/api/{{pluginId}}'), - environment: 'production', - provider: { - id: 'my-provider', - title: 'My Provider', - icon: ProviderIcon, - }, - oauthRequestApi: new MockOAuthApi(), - sessionTransform: ({ expiresInSeconds, ...res }: any) => ({ - ...res, - scopes: new Set(res.scopes.split(' ')), - expiresAt: new Date(Date.now() + expiresInSeconds * 1000), - }), -}; - -describe('DefaultAuthConnector', () => { - const server = setupServer(); - msw.setupDefaultHandlers(server); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should refresh a session', async () => { - server.use( - rest.get('*', (_req, res, ctx) => - res( - ctx.json({ - idToken: 'mock-id-token', - accessToken: 'mock-access-token', - scopes: 'a b c', - expiresInSeconds: '60', - }), - ), - ), - ); - - const helper = new DefaultAuthConnector(defaultOptions); - const session = await helper.refreshSession(); - expect(session.idToken).toBe('mock-id-token'); - expect(session.accessToken).toBe('mock-access-token'); - expect(session.scopes).toEqual(new Set(['a', 'b', 'c'])); - expect(session.expiresAt.getTime()).toBeLessThan(Date.now() + 70000); - expect(session.expiresAt.getTime()).toBeGreaterThan(Date.now() + 50000); - }); - - it('should handle failure to refresh session', async () => { - server.use( - rest.get('*', (_req, res, ctx) => - res(ctx.status(500, 'Error: Network NOPE')), - ), - ); - - const helper = new DefaultAuthConnector(defaultOptions); - await expect(helper.refreshSession()).rejects.toThrow( - 'Auth refresh request failed, Error: Network NOPE', - ); - }); - - it('should handle failure response when refreshing session', async () => { - server.use(rest.get('*', (_req, res, ctx) => res(ctx.status(401, 'NOPE')))); - - const helper = new DefaultAuthConnector(defaultOptions); - await expect(helper.refreshSession()).rejects.toThrow( - 'Auth refresh request failed, NOPE', - ); - }); - - it('should fail if popup was rejected', async () => { - const mockOauth = new MockOAuthApi(); - const helper = new DefaultAuthConnector({ - ...defaultOptions, - oauthRequestApi: mockOauth, - }); - const promise = helper.createSession({ scopes: new Set(['a', 'b']) }); - await mockOauth.rejectAll(); - await expect(promise).rejects.toMatchObject({ name: 'RejectedError' }); - }); - - it('should create a session', async () => { - const mockOauth = new MockOAuthApi(); - const popupSpy = jest - .spyOn(loginPopup, 'showLoginPopup') - .mockResolvedValue({ - idToken: 'my-id-token', - accessToken: 'my-access-token', - scopes: 'a b', - expiresInSeconds: 3600, - }); - const helper = new DefaultAuthConnector({ - ...defaultOptions, - oauthRequestApi: mockOauth, - }); - - const sessionPromise = helper.createSession({ - scopes: new Set(['a', 'b']), - }); - - await mockOauth.triggerAll(); - - expect(popupSpy).toBeCalledTimes(1); - expect(popupSpy.mock.calls[0][0]).toMatchObject({ - url: - 'http://my-host/api/auth/my-provider/start?scope=a%20b&env=production', - }); - - await expect(sessionPromise).resolves.toEqual({ - idToken: 'my-id-token', - accessToken: 'my-access-token', - scopes: expect.any(Set), - expiresAt: expect.any(Date), - }); - }); - - it('should instantly show popup if option is set', async () => { - const popupSpy = jest - .spyOn(loginPopup, 'showLoginPopup') - .mockResolvedValue('my-session'); - const helper = new DefaultAuthConnector({ - ...defaultOptions, - oauthRequestApi: new MockOAuthApi(), - sessionTransform: str => str, - }); - - const sessionPromise = helper.createSession({ - scopes: new Set(), - instantPopup: true, - }); - - await expect(sessionPromise).resolves.toBe('my-session'); - - expect(popupSpy).toBeCalledTimes(1); - }); - - it('should use join func to join scopes', async () => { - const mockOauth = new MockOAuthApi(); - const popupSpy = jest - .spyOn(loginPopup, 'showLoginPopup') - .mockResolvedValue({ scopes: '' }); - const helper = new DefaultAuthConnector({ - ...defaultOptions, - joinScopes: scopes => `-${[...scopes].join('')}-`, - oauthRequestApi: mockOauth, - }); - - helper.createSession({ scopes: new Set(['a', 'b']) }); - - await mockOauth.triggerAll(); - - expect(popupSpy).toBeCalledTimes(1); - expect(popupSpy.mock.calls[0][0]).toMatchObject({ - url: - 'http://my-host/api/auth/my-provider/start?scope=-ab-&env=production', - }); - }); -}); diff --git a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts deleted file mode 100644 index 41a281dc6c..0000000000 --- a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - AuthRequester, - OAuthRequestApi, - AuthProvider, - DiscoveryApi, -} from '../../apis/definitions'; -import { showLoginPopup } from '../loginPopup'; -import { AuthConnector, CreateSessionOptions } from './types'; - -type Options = { - /** - * DiscoveryApi instance used to locate the auth backend endpoint. - */ - discoveryApi: DiscoveryApi; - /** - * Environment hint passed on to auth backend, for example 'production' or 'development' - */ - environment: string; - /** - * Information about the auth provider to be shown to the user. - * The ID Must match the backend auth plugin configuration, for example 'google'. - */ - provider: AuthProvider & { id: string }; - /** - * API used to instantiate an auth requester. - */ - oauthRequestApi: OAuthRequestApi; - /** - * Function used to join together a set of scopes, defaults to joining with a space character. - */ - joinScopes?: (scopes: Set) => string; - /** - * Function used to transform an auth response into the session type. - */ - sessionTransform?(response: any): AuthSession | Promise; -}; - -function defaultJoinScopes(scopes: Set) { - return [...scopes].join(' '); -} - -/** - * DefaultAuthConnector is the default auth connector in Backstage. It talks to the - * backend auth plugin through the standardized API, and requests user permission - * via the OAuthRequestApi. - */ -export class DefaultAuthConnector - implements AuthConnector { - private readonly discoveryApi: DiscoveryApi; - private readonly environment: string; - private readonly provider: AuthProvider & { id: string }; - private readonly joinScopesFunc: (scopes: Set) => string; - private readonly authRequester: AuthRequester; - private readonly sessionTransform: (response: any) => Promise; - - constructor(options: Options) { - const { - discoveryApi, - environment, - provider, - joinScopes = defaultJoinScopes, - oauthRequestApi, - sessionTransform = id => id, - } = options; - - this.authRequester = oauthRequestApi.createAuthRequester({ - provider, - onAuthRequest: scopes => this.showPopup(scopes), - }); - - this.discoveryApi = discoveryApi; - this.environment = environment; - this.provider = provider; - this.joinScopesFunc = joinScopes; - this.sessionTransform = sessionTransform; - } - - async createSession(options: CreateSessionOptions): Promise { - if (options.instantPopup) { - return this.showPopup(options.scopes); - } - return this.authRequester(options.scopes); - } - - async refreshSession(): Promise { - const res = await fetch( - await this.buildUrl('/refresh', { optional: true }), - { - headers: { - 'x-requested-with': 'XMLHttpRequest', - }, - credentials: 'include', - }, - ).catch(error => { - throw new Error(`Auth refresh request failed, ${error}`); - }); - - if (!res.ok) { - const error: any = new Error( - `Auth refresh request failed, ${res.statusText}`, - ); - error.status = res.status; - throw error; - } - - const authInfo = await res.json(); - - if (authInfo.error) { - const error = new Error(authInfo.error.message); - if (authInfo.error.name) { - error.name = authInfo.error.name; - } - throw error; - } - return await this.sessionTransform(authInfo); - } - - async removeSession(): Promise { - const res = await fetch(await this.buildUrl('/logout'), { - method: 'POST', - headers: { - 'x-requested-with': 'XMLHttpRequest', - }, - credentials: 'include', - }).catch(error => { - throw new Error(`Logout request failed, ${error}`); - }); - - if (!res.ok) { - const error: any = new Error(`Logout request failed, ${res.statusText}`); - error.status = res.status; - throw error; - } - } - - private async showPopup(scopes: Set): Promise { - const scope = this.joinScopesFunc(scopes); - const popupUrl = await this.buildUrl('/start', { scope }); - - const payload = await showLoginPopup({ - url: popupUrl, - name: `${this.provider.title} Login`, - origin: new URL(popupUrl).origin, - width: 450, - height: 730, - }); - - return await this.sessionTransform(payload); - } - - private async buildUrl( - path: string, - query?: { [key: string]: string | boolean | undefined }, - ): Promise { - const baseUrl = await this.discoveryApi.getBaseUrl('auth'); - const queryString = this.buildQueryString({ - ...query, - env: this.environment, - }); - - return `${baseUrl}/${this.provider.id}${path}${queryString}`; - } - - private buildQueryString(query?: { - [key: string]: string | boolean | undefined; - }): string { - if (!query) { - return ''; - } - - const queryString = Object.entries(query) - .map(([key, value]) => { - if (typeof value === 'string') { - return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - } else if (value) { - return encodeURIComponent(key); - } - return undefined; - }) - .filter(Boolean) - .join('&'); - - if (!queryString) { - return ''; - } - return `?${queryString}`; - } -} diff --git a/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts deleted file mode 100644 index ea3704f7f9..0000000000 --- a/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { AuthProvider, DiscoveryApi } from '../../apis/definitions'; -import { showLoginPopup } from '../loginPopup'; - -type Options = { - discoveryApi: DiscoveryApi; - environment?: string; - provider: AuthProvider & { id: string }; -}; -export class DirectAuthConnector { - private readonly discoveryApi: DiscoveryApi; - private readonly environment: string | undefined; - private readonly provider: AuthProvider & { id: string }; - - constructor(options: Options) { - const { discoveryApi, environment, provider } = options; - - this.discoveryApi = discoveryApi; - this.environment = environment; - this.provider = provider; - } - - async createSession(): Promise { - const popupUrl = await this.buildUrl('/start'); - const payload = await showLoginPopup({ - url: popupUrl, - name: `${this.provider.title} Login`, - origin: new URL(popupUrl).origin, - width: 450, - height: 730, - }); - - return { - ...payload, - id: payload.profile.email, - }; - } - - async refreshSession(): Promise {} - - async removeSession(): Promise { - const res = await fetch(await this.buildUrl('/logout'), { - method: 'POST', - headers: { - 'x-requested-with': 'XMLHttpRequest', - }, - credentials: 'include', - }).catch(error => { - throw new Error(`Logout request failed, ${error}`); - }); - - if (!res.ok) { - const error: any = new Error(`Logout request failed, ${res.statusText}`); - error.status = res.status; - throw error; - } - } - - private async buildUrl(path: string): Promise { - const baseUrl = await this.discoveryApi.getBaseUrl('auth'); - return `${baseUrl}/${this.provider.id}${path}?env=${this.environment}`; - } -} diff --git a/packages/core-api/src/lib/AuthConnector/MockAuthConnector.test.ts b/packages/core-api/src/lib/AuthConnector/MockAuthConnector.test.ts deleted file mode 100644 index 0758c0ba29..0000000000 --- a/packages/core-api/src/lib/AuthConnector/MockAuthConnector.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { MockAuthConnector, mockAccessToken } from './MockAuthConnector'; - -describe('MockAuthConnector', () => { - it('should return mock tokens', async () => { - const helper = new MockAuthConnector(); - - await expect(helper.createSession()).resolves.toEqual({ - accessToken: mockAccessToken, - expiresAt: expect.any(Date), - scopes: expect.any(String), - }); - - await expect(helper.refreshSession()).resolves.toEqual({ - accessToken: mockAccessToken, - expiresAt: expect.any(Date), - scopes: expect.any(String), - }); - }); -}); diff --git a/packages/core-api/src/lib/AuthConnector/MockAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/MockAuthConnector.ts deleted file mode 100644 index db978e7018..0000000000 --- a/packages/core-api/src/lib/AuthConnector/MockAuthConnector.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AuthConnector } from './types'; - -export const mockAccessToken = 'mock-access-token'; - -type MockSession = { - accessToken: string; - expiresAt: Date; - scopes: string; -}; - -const defaultMockSession: MockSession = { - accessToken: mockAccessToken, - expiresAt: new Date(), - scopes: 'profile email', -}; - -export class MockAuthConnector implements AuthConnector { - constructor(private readonly mockSession: MockSession = defaultMockSession) {} - - async createSession() { - return this.mockSession; - } - - async refreshSession() { - return this.mockSession; - } - - async removeSession() {} -} diff --git a/packages/core-api/src/lib/AuthConnector/index.ts b/packages/core-api/src/lib/AuthConnector/index.ts deleted file mode 100644 index 047583f3ac..0000000000 --- a/packages/core-api/src/lib/AuthConnector/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { DefaultAuthConnector } from './DefaultAuthConnector'; -export { DirectAuthConnector } from './DirectAuthConnector'; -export * from './types'; diff --git a/packages/core-api/src/lib/AuthConnector/types.ts b/packages/core-api/src/lib/AuthConnector/types.ts deleted file mode 100644 index 464a2ed627..0000000000 --- a/packages/core-api/src/lib/AuthConnector/types.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export type CreateSessionOptions = { - scopes: Set; - instantPopup?: boolean; -}; - -/** - * An AuthConnector is responsible for realizing auth session actions - * by for example communicating with a backend or interacting with the user. - */ -export type AuthConnector = { - createSession(options: CreateSessionOptions): Promise; - refreshSession(): Promise; - removeSession(): Promise; -}; diff --git a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts deleted file mode 100644 index 4ceadd51ba..0000000000 --- a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { AuthSessionStore } from './AuthSessionStore'; -import { SessionManager } from './types'; - -const defaultOptions = { - storageKey: 'my-key', - sessionScopes: (session: string) => new Set(session.split(' ')), -}; - -class LocalStorage { - private store: Record = {}; - - getItem(key: string) { - return this.store[key] || null; - } - setItem(key: string, value: string) { - this.store[key] = value.toString(); - } - removeItem(key: string) { - delete this.store[key]; - } -} - -class MockManager implements SessionManager { - setSession = jest.fn(); - getSession = jest.fn(); - removeSession = jest.fn(); - sessionState$ = jest.fn(); -} - -describe('GheAuth AuthSessionStore', () => { - beforeEach(() => { - delete (window as any).localStorage; - (window as any).localStorage = new LocalStorage(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should load session', async () => { - localStorage.setItem('my-key', '"a b c"'); - - const manager = new MockManager(); - const store = new AuthSessionStore({ manager, ...defaultOptions }); - - await expect(store.getSession({})).resolves.toBe('a b c'); - expect(manager.getSession).not.toHaveBeenCalled(); - expect(manager.setSession).toHaveBeenCalledWith('a b c'); - }); - - it('should not use session without enough scope', async () => { - localStorage.setItem('my-key', '"a b c"'); - - const manager = new MockManager(); - manager.getSession.mockResolvedValue('a b c d'); - const store = new AuthSessionStore({ manager, ...defaultOptions }); - - await expect(store.getSession({ scopes: new Set(['d']) })).resolves.toBe( - 'a b c d', - ); - expect(manager.getSession).toHaveBeenCalledTimes(1); - expect(manager.setSession).not.toHaveBeenCalled(); - }); - - it('should not use expired session', async () => { - localStorage.setItem('my-key', '"a b c"'); - - const manager = new MockManager(); - manager.getSession.mockResolvedValue('123'); - const store = new AuthSessionStore({ - manager, - ...defaultOptions, - sessionShouldRefresh: () => true, - }); - - await expect(store.getSession({})).resolves.toBe('123'); - expect(manager.getSession).toHaveBeenCalledTimes(1); - expect(manager.setSession).not.toHaveBeenCalled(); - }); - - it('should not load missing session', async () => { - const manager = new MockManager(); - manager.getSession.mockResolvedValue('123'); - const store = new AuthSessionStore({ manager, ...defaultOptions }); - - await expect(store.getSession({})).resolves.toBe('123'); - expect(manager.getSession).toHaveBeenCalledTimes(1); - expect(manager.setSession).not.toHaveBeenCalled(); - - expect(localStorage.getItem('my-key')).toBe('"123"'); - }); - - it('should ignore bad session values', async () => { - localStorage.setItem('my-key', 'derp'); - - const manager = new MockManager(); - manager.getSession.mockResolvedValue('123'); - const store = new AuthSessionStore({ manager, ...defaultOptions }); - - await expect(store.getSession({})).resolves.toBe('123'); - expect(manager.getSession).toHaveBeenCalledTimes(1); - expect(manager.setSession).not.toHaveBeenCalled(); - }); - - it('should clear session', () => { - localStorage.setItem('my-key', '"a b c"'); - - const manager = new MockManager(); - const store = new AuthSessionStore({ manager, ...defaultOptions }); - store.removeSession(); - - expect(localStorage.getItem('my-key')).toBe(null); - expect(manager.removeSession).toHaveBeenCalled(); - }); - - it('should forward sessionState calls', () => { - const manager = new MockManager(); - const store = new AuthSessionStore({ manager, ...defaultOptions }); - store.sessionState$(); - expect(manager.sessionState$).toHaveBeenCalled(); - }); -}); diff --git a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts deleted file mode 100644 index 057a70e58d..0000000000 --- a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - SessionManager, - MutableSessionManager, - SessionScopesFunc, - SessionShouldRefreshFunc, - GetSessionOptions, -} from './types'; -import { SessionScopeHelper } from './common'; - -type Options = { - /** The connector used for acting on the auth session */ - manager: MutableSessionManager; - /** Storage key to use to store sessions */ - storageKey: string; - /** Used to get the scope of the session */ - sessionScopes?: SessionScopesFunc; - /** Used to check if the session needs to be refreshed, defaults to never refresh */ - sessionShouldRefresh?: SessionShouldRefreshFunc; -}; - -/** - * AuthSessionStore decorates another SessionManager with a functionality - * to store the session in local storage. - * - * Session is serialized to JSON with special support for following types: Set. - */ -export class AuthSessionStore implements SessionManager { - private readonly manager: MutableSessionManager; - private readonly storageKey: string; - private readonly sessionShouldRefreshFunc: SessionShouldRefreshFunc; - private readonly helper: SessionScopeHelper; - - constructor(options: Options) { - const { - manager, - storageKey, - sessionScopes, - sessionShouldRefresh = () => false, - } = options; - - this.manager = manager; - this.storageKey = storageKey; - this.sessionShouldRefreshFunc = sessionShouldRefresh; - this.helper = new SessionScopeHelper({ - sessionScopes, - defaultScopes: new Set(), - }); - } - - async getSession(options: GetSessionOptions): Promise { - const { scopes } = options; - const session = this.loadSession(); - - if (this.helper.sessionExistsAndHasScope(session, scopes)) { - const shouldRefresh = this.sessionShouldRefreshFunc(session!); - - if (!shouldRefresh) { - this.manager.setSession(session!); - return session!; - } - } - - const newSession = await this.manager.getSession(options); - this.saveSession(newSession); - return newSession; - } - - async removeSession() { - localStorage.removeItem(this.storageKey); - await this.manager.removeSession(); - } - - sessionState$() { - return this.manager.sessionState$(); - } - - private loadSession(): T | undefined { - try { - const sessionJson = localStorage.getItem(this.storageKey); - if (sessionJson) { - const session = JSON.parse(sessionJson, (_key, value) => { - if (value?.__type === 'Set') { - return new Set(value.__value); - } - return value; - }); - return session; - } - - return undefined; - } catch (error) { - localStorage.removeItem(this.storageKey); - return undefined; - } - } - - private saveSession(session: T | undefined) { - if (session === undefined) { - localStorage.removeItem(this.storageKey); - } else { - localStorage.setItem( - this.storageKey, - JSON.stringify(session, (_key, value) => { - if (value instanceof Set) { - return { - __type: 'Set', - __value: Array.from(value), - }; - } - return value; - }), - ); - } - } -} diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts deleted file mode 100644 index 99a10002c3..0000000000 --- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager'; -import { SessionState } from '../../apis'; - -const defaultOptions = { - sessionScopes: (session: { scopes: Set }) => session.scopes, - sessionShouldRefresh: (session: { expired: boolean }) => session.expired, -}; - -describe('RefreshingAuthSessionManager', () => { - it('should save result from createSession', async () => { - const createSession = jest.fn().mockResolvedValue({ expired: false }); - const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE')); - const removeSession = jest.fn(); - const manager = new RefreshingAuthSessionManager({ - connector: { createSession, refreshSession, removeSession }, - ...defaultOptions, - } as any); - const stateSubscriber = jest.fn(); - manager.sessionState$().subscribe(stateSubscriber); - - await Promise.resolve(); // Wait a tick for observer to post a value - - expect(stateSubscriber.mock.calls).toEqual([[SessionState.SignedOut]]); - await manager.getSession({}); - expect(createSession).toBeCalledTimes(1); - - expect(stateSubscriber.mock.calls).toEqual([ - [SessionState.SignedOut], - [SessionState.SignedIn], - ]); - await manager.getSession({}); - expect(createSession).toBeCalledTimes(1); - - expect(refreshSession).toBeCalledTimes(1); - expect(stateSubscriber.mock.calls).toEqual([ - [SessionState.SignedOut], - [SessionState.SignedIn], - ]); - - expect(removeSession).toHaveBeenCalledTimes(0); - await manager.removeSession(); - expect(removeSession).toHaveBeenCalledTimes(1); - expect(stateSubscriber.mock.calls).toEqual([ - [SessionState.SignedOut], - [SessionState.SignedIn], - [SessionState.SignedOut], - ]); - }); - - it('should ask consent only if scopes have changed', async () => { - const createSession = jest.fn(); - const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE')); - const manager = new RefreshingAuthSessionManager({ - connector: { createSession, refreshSession }, - ...defaultOptions, - } as any); - - createSession.mockResolvedValue({ - scopes: new Set(['a']), - expired: false, - }); - await manager.getSession({ scopes: new Set(['a']) }); - expect(createSession).toBeCalledTimes(1); - - await manager.getSession({ scopes: new Set(['a']) }); - expect(createSession).toBeCalledTimes(1); - - await manager.getSession({ scopes: new Set(['b']) }); - expect(createSession).toBeCalledTimes(2); - }); - - it('should check for session expiry', async () => { - const createSession = jest.fn(); - const refreshSession = jest - .fn() - .mockRejectedValueOnce(new Error('NOPE')) - .mockResolvedValue({ scopes: new Set(['a']) }); - const manager = new RefreshingAuthSessionManager({ - connector: { createSession, refreshSession }, - ...defaultOptions, - } as any); - - createSession.mockResolvedValue({ - scopes: new Set(['a']), - expired: true, - }); - - await manager.getSession({ scopes: new Set(['a']) }); - expect(createSession).toBeCalledTimes(1); - expect(refreshSession).toBeCalledTimes(1); - - await manager.getSession({ scopes: new Set(['a']) }); - expect(createSession).toBeCalledTimes(1); - expect(refreshSession).toBeCalledTimes(2); - }); - - it('should handle user closed popup', async () => { - const createSession = jest.fn(); - const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE')); - const manager = new RefreshingAuthSessionManager({ - connector: { createSession, refreshSession }, - ...defaultOptions, - } as any); - - createSession.mockRejectedValueOnce(new Error('some error')); - await expect( - manager.getSession({ scopes: new Set(['a']) }), - ).rejects.toThrow('some error'); - }); - - it('should not get optional session', async () => { - const createSession = jest.fn(); - const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE')); - const manager = new RefreshingAuthSessionManager({ - connector: { createSession, refreshSession }, - ...defaultOptions, - } as any); - - expect(await manager.getSession({ optional: true })).toBe(undefined); - expect(createSession).toBeCalledTimes(0); - expect(refreshSession).toBeCalledTimes(1); - }); - - it('should forward option to instantly show auth popup and not attempt refresh', async () => { - const createSession = jest.fn(); - const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE')); - const manager = new RefreshingAuthSessionManager({ - connector: { createSession, refreshSession }, - ...defaultOptions, - } as any); - - expect(await manager.getSession({ instantPopup: true })).toBe(undefined); - expect(createSession).toBeCalledTimes(1); - expect(createSession).toHaveBeenCalledWith({ - scopes: new Set(), - instantPopup: true, - }); - expect(refreshSession).toBeCalledTimes(0); - }); - - it('should remove session straight away', async () => { - const removeSession = jest.fn(); - const manager = new RefreshingAuthSessionManager({ - connector: { removeSession }, - ...defaultOptions, - } as any); - - await manager.removeSession(); - expect(removeSession).toHaveBeenCalled(); - expect(await manager.getSession({ optional: true })).toBe(undefined); - }); -}); diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts deleted file mode 100644 index d31f5e29bf..0000000000 --- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - SessionManager, - SessionScopesFunc, - SessionShouldRefreshFunc, - GetSessionOptions, -} from './types'; -import { AuthConnector } from '../AuthConnector'; -import { SessionScopeHelper, hasScopes } from './common'; -import { SessionStateTracker } from './SessionStateTracker'; - -type Options = { - /** The connector used for acting on the auth session */ - connector: AuthConnector; - /** Used to get the scope of the session */ - sessionScopes: SessionScopesFunc; - /** Used to check if the session needs to be refreshed */ - sessionShouldRefresh: SessionShouldRefreshFunc; - /** The default scopes that should always be present in a session, defaults to none. */ - defaultScopes?: Set; -}; - -/** - * RefreshingAuthSessionManager manages an underlying session that has - * and expiration time and needs to be refreshed periodically. - */ -export class RefreshingAuthSessionManager implements SessionManager { - private readonly connector: AuthConnector; - private readonly helper: SessionScopeHelper; - private readonly sessionScopesFunc: SessionScopesFunc; - private readonly sessionShouldRefreshFunc: SessionShouldRefreshFunc; - private readonly stateTracker = new SessionStateTracker(); - - private refreshPromise?: Promise; - private currentSession: T | undefined; - - constructor(options: Options) { - const { - connector, - defaultScopes = new Set(), - sessionScopes, - sessionShouldRefresh, - } = options; - - this.connector = connector; - this.sessionScopesFunc = sessionScopes; - this.sessionShouldRefreshFunc = sessionShouldRefresh; - this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes }); - } - - async getSession(options: GetSessionOptions): Promise { - if ( - this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes) - ) { - const shouldRefresh = this.sessionShouldRefreshFunc(this.currentSession!); - if (!shouldRefresh) { - return this.currentSession!; - } - - try { - const refreshedSession = await this.collapsedSessionRefresh(); - const currentScopes = this.sessionScopesFunc(this.currentSession!); - const refreshedScopes = this.sessionScopesFunc(refreshedSession); - if (hasScopes(refreshedScopes, currentScopes)) { - this.currentSession = refreshedSession; - } - return refreshedSession; - } catch (error) { - if (options.optional) { - return undefined; - } - throw error; - } - } - - // The user may still have a valid refresh token in their cookies. Attempt to - // initiate a fresh session through the backend using that refresh token. - // - // We skip this check if an instant login popup is requested, as we need to - // stay in a synchronous call stack from the user interaction. The downside - // is that that the user will sometimes be requested to log in even if they - // already had an existing session. - if (!this.currentSession && !options.instantPopup) { - try { - const newSession = await this.collapsedSessionRefresh(); - this.currentSession = newSession; - // The session might not have the scopes requested so go back and check again - return this.getSession(options); - } catch { - // If the refresh attempt fails we assume we don't have a session, so continue to create one. - } - } - - // If we continue here we will show a popup, so exit if this is an optional session request. - if (options.optional) { - return undefined; - } - - // We can call authRequester multiple times, the returned session will contain all requested scopes. - this.currentSession = await this.connector.createSession({ - ...options, - scopes: this.helper.getExtendedScope(this.currentSession, options.scopes), - }); - this.stateTracker.setIsSignedIn(true); - return this.currentSession; - } - - async removeSession() { - this.currentSession = undefined; - await this.connector.removeSession(); - this.stateTracker.setIsSignedIn(false); - } - - sessionState$() { - return this.stateTracker.sessionState$(); - } - - private async collapsedSessionRefresh(): Promise { - if (this.refreshPromise) { - return this.refreshPromise; - } - - this.refreshPromise = this.connector.refreshSession(); - - try { - const session = await this.refreshPromise; - this.stateTracker.setIsSignedIn(true); - return session; - } finally { - delete this.refreshPromise; - } - } -} diff --git a/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts b/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts deleted file mode 100644 index 525cfd7313..0000000000 --- a/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { SessionState } from '../../apis/definitions'; -import { Observable } from '../../types'; -import { BehaviorSubject } from '../subjects'; - -export class SessionStateTracker { - private readonly subject = new BehaviorSubject( - SessionState.SignedOut, - ); - - private signedIn: boolean = false; - - setIsSignedIn(isSignedIn: boolean) { - if (this.signedIn !== isSignedIn) { - this.signedIn = isSignedIn; - this.subject.next( - this.signedIn ? SessionState.SignedIn : SessionState.SignedOut, - ); - } - } - - sessionState$(): Observable { - return this.subject; - } -} diff --git a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts deleted file mode 100644 index 26f489a1b4..0000000000 --- a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { StaticAuthSessionManager } from './StaticAuthSessionManager'; - -const defaultOptions = { - sessionScopes: (session: string) => new Set(session.split(' ')), -}; - -describe('StaticAuthSessionManager', () => { - const baseConnector = { - refreshSession() { - throw new Error('refreshSession should not be called'); - }, - removeSession() { - throw new Error('removeSession should not be called'); - }, - }; - - it('should get session by creating session once', async () => { - const createSession = jest.fn().mockResolvedValue('my-session'); - const manager = new StaticAuthSessionManager({ - connector: { createSession, ...baseConnector }, - ...defaultOptions, - }); - - expect(createSession).toHaveBeenCalledTimes(0); - await expect(manager.getSession({})).resolves.toBe('my-session'); - expect(createSession).toHaveBeenCalledTimes(1); - await expect(manager.getSession({})).resolves.toBe('my-session'); - expect(createSession).toHaveBeenCalledTimes(1); - }); - - it('should fail to get session if user rejects the request', async () => { - const createSession = jest.fn().mockRejectedValue(new Error('NOPE')); - const manager = new StaticAuthSessionManager({ - connector: { createSession, ...baseConnector }, - ...defaultOptions, - }); - - expect(createSession).toHaveBeenCalledTimes(0); - await expect(manager.getSession({})).rejects.toThrow('NOPE'); - expect(createSession).toHaveBeenCalledTimes(1); - await expect(manager.getSession({ optional: true })).resolves.toBe( - undefined, - ); - }); - - it('should only request auth once for same scopes', async () => { - const createSession = jest - .fn() - .mockImplementation(({ scopes }) => [...scopes].join(' ')); - const manager = new StaticAuthSessionManager({ - connector: { createSession, ...baseConnector }, - ...defaultOptions, - }); - - expect(createSession).toHaveBeenCalledTimes(0); - await expect(manager.getSession({ scopes: new Set(['a']) })).resolves.toBe( - 'a', - ); - expect(createSession).toHaveBeenCalledTimes(1); - await expect(manager.getSession({ scopes: new Set(['a']) })).resolves.toBe( - 'a', - ); - expect(createSession).toHaveBeenCalledTimes(1); - await expect(manager.getSession({ scopes: new Set(['b']) })).resolves.toBe( - 'a b', - ); - expect(createSession).toHaveBeenCalledTimes(2); - }); - - it('should remove session and reload', async () => { - const removeSession = jest.fn(); - const manager = new StaticAuthSessionManager({ - connector: { removeSession }, - ...defaultOptions, - } as any); - - await manager.removeSession(); - expect(removeSession).toHaveBeenCalled(); - expect(await manager.getSession({ optional: true })).toBe(undefined); - }); -}); diff --git a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts deleted file mode 100644 index b7940d88c4..0000000000 --- a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { MutableSessionManager, GetSessionOptions } from './types'; -import { AuthConnector } from '../AuthConnector'; -import { SessionScopeHelper } from './common'; -import { SessionStateTracker } from './SessionStateTracker'; - -type Options = { - /** The connector used for acting on the auth session */ - connector: AuthConnector; - /** Used to get the scope of the session */ - sessionScopes?: (session: T) => Set; - /** The default scopes that should always be present in a session, defaults to none. */ - defaultScopes?: Set; -}; - -/** - * StaticAuthSessionManager manages an underlying session that does not expire. - */ -export class StaticAuthSessionManager implements MutableSessionManager { - private readonly connector: AuthConnector; - private readonly helper: SessionScopeHelper; - private readonly stateTracker = new SessionStateTracker(); - - private currentSession: T | undefined; - - constructor(options: Options) { - const { connector, defaultScopes = new Set(), sessionScopes } = options; - - this.connector = connector; - this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes }); - } - - setSession(session: T | undefined): void { - this.currentSession = session; - this.stateTracker.setIsSignedIn(Boolean(session)); - } - - async getSession(options: GetSessionOptions): Promise { - if ( - this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes) - ) { - return this.currentSession; - } - - // If we continue here we will show a popup, so exit if this is an optional session request. - if (options.optional) { - return undefined; - } - - // We can call authRequester multiple times, the returned session will contain all requested scopes. - this.currentSession = await this.connector.createSession({ - ...options, - scopes: this.helper.getExtendedScope(this.currentSession, options.scopes), - }); - this.stateTracker.setIsSignedIn(true); - return this.currentSession; - } - - async removeSession() { - this.currentSession = undefined; - await this.connector.removeSession(); - this.stateTracker.setIsSignedIn(false); - } - - sessionState$() { - return this.stateTracker.sessionState$(); - } -} diff --git a/packages/core-api/src/lib/AuthSessionManager/common.ts b/packages/core-api/src/lib/AuthSessionManager/common.ts deleted file mode 100644 index 002b6c616b..0000000000 --- a/packages/core-api/src/lib/AuthSessionManager/common.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { SessionScopesFunc } from './types'; - -export function hasScopes( - searched: Set, - searchFor: Set, -): boolean { - for (const scope of searchFor) { - if (!searched.has(scope)) { - return false; - } - } - return true; -} - -type ScopeHelperOptions = { - sessionScopes: SessionScopesFunc | undefined; - defaultScopes?: Set; -}; - -export class SessionScopeHelper { - constructor(private readonly options: ScopeHelperOptions) {} - - sessionExistsAndHasScope( - session: T | undefined, - scopes?: Set, - ): boolean { - if (!session) { - return false; - } - if (!scopes) { - return true; - } - if (this.options.sessionScopes === undefined) { - return true; - } - const sessionScopes = this.options.sessionScopes(session); - return hasScopes(sessionScopes, scopes); - } - - getExtendedScope(session: T | undefined, scopes?: Set) { - const newScope = new Set(this.options.defaultScopes); - if (session && this.options.sessionScopes !== undefined) { - const sessionScopes = this.options.sessionScopes(session); - for (const scope of sessionScopes) { - newScope.add(scope); - } - } - if (scopes) { - for (const scope of scopes) { - newScope.add(scope); - } - } - return newScope; - } -} diff --git a/packages/core-api/src/lib/AuthSessionManager/index.ts b/packages/core-api/src/lib/AuthSessionManager/index.ts deleted file mode 100644 index 85ef2013e9..0000000000 --- a/packages/core-api/src/lib/AuthSessionManager/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager'; -export { StaticAuthSessionManager } from './StaticAuthSessionManager'; -export { AuthSessionStore } from './AuthSessionStore'; -export * from './types'; diff --git a/packages/core-api/src/lib/AuthSessionManager/types.ts b/packages/core-api/src/lib/AuthSessionManager/types.ts deleted file mode 100644 index a58f838037..0000000000 --- a/packages/core-api/src/lib/AuthSessionManager/types.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Observable } from '../../types'; -import { SessionState } from '../../apis/definitions'; - -export type GetSessionOptions = { - optional?: boolean; - instantPopup?: boolean; - scopes?: Set; -}; - -/** - * A sessions manager keeps track of the current session and makes sure that - * multiple simultaneous requests for sessions with different scope are handled - * in a correct way. - */ -export type SessionManager = { - getSession(options: GetSessionOptions): Promise; - - removeSession(): Promise; - - sessionState$(): Observable; -}; - -/** - * An extension of the session manager where the session can also be pushed from the manager. - */ -export interface MutableSessionManager extends SessionManager { - setSession(session: T | undefined): void; -} - -/** - * A function called to determine the scopes of a session. - */ -export type SessionScopesFunc = (session: T) => Set; - -/** - * A function called to determine whether it's time for a session to refresh. - * - * This should return true before the session expires, for example, if a session - * expires after 60 minutes, you could return true if the session is older than 45 minutes. - */ -export type SessionShouldRefreshFunc = (session: T) => boolean; diff --git a/packages/core-api/src/lib/globalObject.test.ts b/packages/core-api/src/lib/globalObject.test.ts deleted file mode 100644 index a658b253a6..0000000000 --- a/packages/core-api/src/lib/globalObject.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - getGlobalSingleton, - getOrCreateGlobalSingleton, - setGlobalSingleton, -} from './globalObject'; - -const anyGlobal = global as any; - -describe('getGlobalSingleton', () => { - beforeEach(() => { - delete anyGlobal['__@backstage/my-thing__']; - }); - - it('should return an existing value', () => { - const myThing = {}; - const myOtherThing = {}; - - anyGlobal['__@backstage/my-thing__'] = myThing; - expect(getGlobalSingleton('my-thing')).toBe(myThing); - expect(getGlobalSingleton('my-thing')).toBe(myThing); - anyGlobal['__@backstage/my-thing__'] = myOtherThing; - expect(getGlobalSingleton('my-thing')).toBe(myOtherThing); - }); - - it('should throw if the value is not set', () => { - expect(() => getGlobalSingleton('my-thing')).toThrow( - 'Global my-thing is not set', - ); - }); -}); - -describe('getOrCreateGlobalSingleton', () => { - beforeEach(() => { - delete anyGlobal['__@backstage/my-thing__']; - }); - - it('should return an existing value', () => { - const myThing = {}; - anyGlobal['__@backstage/my-thing__'] = myThing; - - expect(getOrCreateGlobalSingleton('my-thing', () => ({}))).toBe(myThing); - expect(getOrCreateGlobalSingleton('my-thing', () => ({}))).toBe(myThing); - }); - - it('should should create a new value', () => { - const myNewThing = {}; - - expect(anyGlobal['__@backstage/my-thing__']).toBe(undefined); - expect(getOrCreateGlobalSingleton('my-thing', () => myNewThing)).toBe( - myNewThing, - ); - expect(anyGlobal['__@backstage/my-thing__']).toBe(myNewThing); - expect(getOrCreateGlobalSingleton('my-thing', () => ({}))).toBe(myNewThing); - }); -}); - -describe('setGlobalSingleton', () => { - beforeEach(() => { - delete anyGlobal['__@backstage/my-thing__']; - }); - - it('should set a global value', () => { - setGlobalSingleton('my-thing', 'global value'); - - expect(anyGlobal['__@backstage/my-thing__']).toBe('global value'); - }); - - it('should throw if global value is set', () => { - anyGlobal['__@backstage/my-thing__'] = 'already defined'; - expect(() => setGlobalSingleton('my-thing', () => 'global value')).toThrow( - 'Global my-thing is already se', - ); - }); -}); diff --git a/packages/core-api/src/lib/globalObject.ts b/packages/core-api/src/lib/globalObject.ts deleted file mode 100644 index ad70a61110..0000000000 --- a/packages/core-api/src/lib/globalObject.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 -function getGlobalObject() { - if (typeof window !== 'undefined' && window.Math === Math) { - return window; - } - if (typeof self !== 'undefined' && self.Math === Math) { - return self; - } - // eslint-disable-next-line no-new-func - return Function('return this')(); -} - -const globalObject = getGlobalObject(); - -const makeKey = (id: string) => `__@backstage/${id}__`; - -/** - * Used to provide a global singleton value, failing if it is already set. - */ -export function setGlobalSingleton(id: string, value: unknown): void { - const key = makeKey(id); - if (key in globalObject) { - throw new Error(`Global ${id} is already set`); // TODO some sort of special build err - } - globalObject[key] = value; -} - -/** - * Used to access a global singleton value, failing if it is not already set. - */ -export function getGlobalSingleton(id: string): T { - const key = makeKey(id); - if (!(key in globalObject)) { - throw new Error(`Global ${id} is not set`); // TODO some sort of special build err - } - - return globalObject[key]; -} - -/** - * Serializes access to a global singleton value, with the first caller creating the value. - */ -export function getOrCreateGlobalSingleton( - id: string, - supplier: () => T, -): T { - const key = makeKey(id); - - let value = globalObject[key]; - if (value) { - return value; - } - - value = supplier(); - globalObject[key] = value; - return value; -} diff --git a/packages/core-api/src/lib/index.ts b/packages/core-api/src/lib/index.ts deleted file mode 100644 index 1327aab4c2..0000000000 --- a/packages/core-api/src/lib/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export * from './subjects'; -export * from './loginPopup'; -export * from './AuthConnector'; -export * from './AuthSessionManager'; diff --git a/packages/core-api/src/lib/loginPopup.test.ts b/packages/core-api/src/lib/loginPopup.test.ts deleted file mode 100644 index 1eb7c3f8b9..0000000000 --- a/packages/core-api/src/lib/loginPopup.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { showLoginPopup } from './loginPopup'; - -describe('showLoginPopup', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should show an auth popup', async () => { - const popupMock = { closed: false }; - const openSpy = jest - .spyOn(window, 'open') - .mockReturnValue(popupMock as Window); - const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); - const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); - - const payloadPromise = showLoginPopup({ - url: - 'my-origin/api/backend/auth/start?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fa%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fb', - name: 'test-popup', - origin: 'my-origin', - }); - - expect(openSpy).toBeCalledTimes(1); - expect(openSpy.mock.calls[0][0]).toBe( - 'my-origin/api/backend/auth/start?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fa%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fb', - ); - expect(openSpy.mock.calls[0][1]).toBe('test-popup'); - expect(addEventListenerSpy).toBeCalledTimes(1); - expect(removeEventListenerSpy).toBeCalledTimes(0); - - const listener = addEventListenerSpy.mock.calls[0][1] as EventListener; - - await expect(Promise.race([payloadPromise, 'waiting'])).resolves.toBe( - 'waiting', - ); - - listener({} as MessageEvent); - - await expect(Promise.race([payloadPromise, 'waiting'])).resolves.toBe( - 'waiting', - ); - - // None of these should be accepted - listener({ source: popupMock } as MessageEvent); - listener({ origin: 'my-origin' } as MessageEvent); - listener({ data: { type: 'authorization_response' } } as MessageEvent); - listener({ - source: popupMock, - origin: 'my-origin', - data: {}, - } as MessageEvent); - listener({ - source: popupMock, - origin: 'my-origin', - data: { type: 'not-auth-result', response: {} }, - } as MessageEvent); - - await expect(Promise.race([payloadPromise, 'waiting'])).resolves.toBe( - 'waiting', - ); - - const myResponse = {}; - - // This should be accepted as a valid sessions response - listener({ - source: popupMock, - origin: 'my-origin', - data: { - type: 'authorization_response', - response: myResponse, - }, - } as MessageEvent); - - await expect(payloadPromise).resolves.toBe(myResponse); - - expect(openSpy).toBeCalledTimes(1); - expect(addEventListenerSpy).toBeCalledTimes(1); - expect(removeEventListenerSpy).toBeCalledTimes(1); - }); - - it('should fail if popup returns error', async () => { - const popupMock = { closed: false }; - const openSpy = jest - .spyOn(window, 'open') - .mockReturnValue(popupMock as Window); - const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); - const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); - - const payloadPromise = showLoginPopup({ - url: 'url', - name: 'name', - origin: 'my-origin', - }); - - expect(openSpy).toBeCalledTimes(1); - expect(addEventListenerSpy).toBeCalledTimes(1); - expect(removeEventListenerSpy).toBeCalledTimes(0); - - const listener = addEventListenerSpy.mock.calls[0][1] as EventListener; - - listener({ - source: popupMock, - origin: 'my-origin', - data: { - type: 'authorization_response', - error: { - message: 'NOPE', - name: 'NopeError', - }, - }, - } as MessageEvent); - - await expect(payloadPromise).rejects.toThrow({ - name: 'NopeError', - message: 'NOPE', - }); - - expect(openSpy).toBeCalledTimes(1); - expect(addEventListenerSpy).toBeCalledTimes(1); - expect(removeEventListenerSpy).toBeCalledTimes(1); - }); - - it('should fail if popup is closed', async () => { - const openSpy = jest - .spyOn(window, 'open') - .mockReturnValue({ closed: false } as Window); - const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); - const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); - const popupMock = { closed: false }; - - openSpy.mockReturnValue(popupMock as Window); - - const payloadPromise = showLoginPopup({ - url: 'url', - name: 'name', - origin: 'origin', - }); - - expect(openSpy).toBeCalledTimes(1); - expect(addEventListenerSpy).toBeCalledTimes(1); - expect(removeEventListenerSpy).toBeCalledTimes(0); - - const listener = addEventListenerSpy.mock.calls[0][1] as EventListener; - listener({ - source: popupMock, - origin: 'origin', - data: { - type: 'config_info', - targetOrigin: 'http://localhost', - }, - } as MessageEvent); - - setTimeout(() => { - popupMock.closed = true; - }, 150); - await expect(payloadPromise).rejects.toThrow( - 'Login failed, popup was closed', - ); - - expect(openSpy).toBeCalledTimes(1); - expect(addEventListenerSpy).toBeCalledTimes(1); - expect(removeEventListenerSpy).toBeCalledTimes(1); - }); - - it('should indicate if origin does not match', async () => { - const openSpy = jest - .spyOn(window, 'open') - .mockReturnValue({ closed: false } as Window); - const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); - const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); - const popupMock = { closed: false }; - - openSpy.mockReturnValue(popupMock as Window); - - const payloadPromise = showLoginPopup({ - url: 'url', - name: 'name', - origin: 'origin', - }); - - const listener = addEventListenerSpy.mock.calls[0][1] as EventListener; - listener({ - source: popupMock, - origin: 'origin', - data: { - type: 'config_info', - targetOrigin: 'http://differenthost', - }, - } as MessageEvent); - - setTimeout(() => { - popupMock.closed = true; - }, 150); - await expect(payloadPromise).rejects.toThrow( - 'Login failed, Incorrect app origin, expected http://differenthost', - ); - - expect(openSpy).toBeCalledTimes(1); - expect(addEventListenerSpy).toBeCalledTimes(1); - expect(removeEventListenerSpy).toBeCalledTimes(1); - }); -}); diff --git a/packages/core-api/src/lib/loginPopup.ts b/packages/core-api/src/lib/loginPopup.ts deleted file mode 100644 index b6c14d60c9..0000000000 --- a/packages/core-api/src/lib/loginPopup.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Options used to open a login popup. - */ -export type LoginPopupOptions = { - /** - * The URL that the auth popup should point to - */ - url: string; - - /** - * The name of the popup, as in second argument to window.open - */ - name: string; - - /** - * The origin of the final popup page that will post a message to this window. - */ - origin: string; - - /** - * The width of the popup in pixels, defaults to 500 - */ - width?: number; - - /** - * The height of the popup in pixels, defaults to 700 - */ - height?: number; -}; - -type AuthResult = - | { - type: 'authorization_response'; - response: unknown; - } - | { - type: 'authorization_response'; - error: { - name: string; - message: string; - }; - }; - -/** - * Show a popup pointing to a URL that starts an auth flow. Implementing the receiving - * end of the postMessage mechanism outlined in https://tools.ietf.org/html/draft-sakimura-oauth-wmrm-00 - * - * The redirect handler of the flow should use postMessage to communicate back - * to the app window. The message posted to the app must match the AuthResult type. - * - * The returned promise resolves to the response of the message that was posted from the auth popup. - */ -export function showLoginPopup(options: LoginPopupOptions): Promise { - return new Promise((resolve, reject) => { - const width = options.width || 500; - const height = options.height || 700; - const left = window.screen.width / 2 - width / 2; - const top = window.screen.height / 2 - height / 2; - - const popup = window.open( - options.url, - options.name, - `menubar=no,location=no,resizable=no,scrollbars=no,status=no,width=${width},height=${height},top=${top},left=${left}`, - ); - - let targetOrigin = ''; - - if (!popup || typeof popup.closed === 'undefined' || popup.closed) { - const error = new Error('Failed to open auth popup.'); - error.name = 'PopupRejectedError'; - reject(error); - return; - } - - const messageListener = (event: MessageEvent) => { - if (event.source !== popup) { - return; - } - if (event.origin !== options.origin) { - return; - } - const { data } = event; - - if (data.type === 'config_info') { - targetOrigin = data.targetOrigin; - return; - } - - if (data.type !== 'authorization_response') { - return; - } - const authResult = data as AuthResult; - - if ('error' in authResult) { - const error = new Error(authResult.error.message); - error.name = authResult.error.name; - // TODO: proper error type - // error.extra = authResult.error.extra; - reject(error); - } else { - resolve(authResult.response); - } - done(); - }; - - const intervalId = setInterval(() => { - if (popup.closed) { - const errMessage = `Login failed, ${ - targetOrigin && targetOrigin !== window.location.origin - ? `Incorrect app origin, expected ${targetOrigin}` - : 'popup was closed' - }`; - const error = new Error(errMessage); - error.name = 'PopupClosedError'; - reject(error); - done(); - } - }, 100); - - function done() { - window.removeEventListener('message', messageListener); - clearInterval(intervalId); - } - - window.addEventListener('message', messageListener); - }); -} diff --git a/packages/core-api/src/lib/subjects.test.ts b/packages/core-api/src/lib/subjects.test.ts deleted file mode 100644 index eab5757898..0000000000 --- a/packages/core-api/src/lib/subjects.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { PublishSubject, BehaviorSubject } from './subjects'; - -function observerSpy() { - return { - next: jest.fn(), - error: jest.fn(), - complete: jest.fn(), - }; -} - -describe('PublishSubject', () => { - it('should be completed', async () => { - const subj = new PublishSubject(); - subj.complete(); - - const spy = observerSpy(); - subj.subscribe(spy); - await 'a tick'; - - expect(spy.next).not.toHaveBeenCalled(); - expect(spy.error).not.toHaveBeenCalled(); - expect(spy.complete).toHaveBeenCalledTimes(1); - - expect(() => subj.next(1)).toThrow('PublishSubject is closed'); - expect(() => subj.error(new Error())).toThrow('PublishSubject is closed'); - expect(() => subj.complete()).toThrow('PublishSubject is closed'); - - expect(subj.closed).toBe(true); - }); - - it('should forward values to all subscribers', async () => { - const subj = new PublishSubject(); - - const spy1 = observerSpy(); - const spy2 = observerSpy(); - subj.subscribe(spy1); - const sub = subj.subscribe(spy2); - - subj.next(42); - - expect(spy1.next).toHaveBeenCalledTimes(1); - expect(spy1.next).toHaveBeenCalledWith(42); - expect(spy2.next).toHaveBeenCalledTimes(1); - expect(spy2.next).toHaveBeenCalledWith(42); - - sub.unsubscribe(); - - subj.next(1337); - - expect(spy1.next).toHaveBeenCalledTimes(2); - expect(spy1.next).toHaveBeenCalledWith(1337); - expect(spy2.next).toHaveBeenCalledTimes(1); - - expect(spy1.error).not.toHaveBeenCalled(); - expect(spy2.error).not.toHaveBeenCalled(); - expect(spy1.complete).not.toHaveBeenCalled(); - expect(spy2.complete).not.toHaveBeenCalled(); - - expect(subj.closed).toBe(false); - }); - - it('should forward errors', async () => { - const subj = new PublishSubject(); - - const spy1 = observerSpy(); - subj.subscribe(spy1); - - const error = new Error('NOPE'); - subj.error(error); - expect(spy1.error).toHaveBeenCalledWith(error); - expect(spy1.next).not.toHaveBeenCalled(); - expect(spy1.complete).not.toHaveBeenCalled(); - - const spy2 = observerSpy(); - subj.subscribe(spy2); - await 'a tick'; - expect(spy2.error).toHaveBeenCalledWith(error); - expect(spy2.next).not.toHaveBeenCalled(); - expect(spy2.complete).not.toHaveBeenCalled(); - - expect(subj.closed).toBe(true); - }); -}); - -describe('BehaviorSubject', () => { - it('should be completed', async () => { - const subj = new BehaviorSubject(0); - subj.complete(); - - const next = jest.fn(); - const complete = jest.fn(); - subj.subscribe({ next, complete }); - await 'a tick'; - - expect(complete).toHaveBeenCalledTimes(1); - - expect(() => subj.next(1)).toThrow('BehaviorSubject is closed'); - expect(() => subj.error(new Error())).toThrow('BehaviorSubject is closed'); - expect(() => subj.complete()).toThrow('BehaviorSubject is closed'); - - expect(subj.closed).toBe(true); - }); - - it('should forward values to all subscribers', async () => { - const subj = new BehaviorSubject(0); - - const obs1 = jest.fn(); - const obs2 = jest.fn(); - subj.subscribe(obs1); - const sub = subj.subscribe(obs2); - await 'a tick'; - - expect(obs1).toHaveBeenCalledTimes(1); - expect(obs1).toHaveBeenCalledWith(0); - expect(obs2).toHaveBeenCalledTimes(1); - expect(obs2).toHaveBeenCalledWith(0); - - subj.next(42); - - expect(obs1).toHaveBeenCalledTimes(2); - expect(obs1).toHaveBeenCalledWith(42); - expect(obs2).toHaveBeenCalledTimes(2); - expect(obs2).toHaveBeenCalledWith(42); - - sub.unsubscribe(); - - subj.next(1337); - - expect(obs1).toHaveBeenCalledTimes(3); - expect(obs1).toHaveBeenCalledWith(1337); - expect(obs2).toHaveBeenCalledTimes(2); - - expect(subj.closed).toBe(false); - }); - - it('should forward errors', async () => { - const subj = new BehaviorSubject(0); - - const spy1 = observerSpy(); - subj.subscribe(spy1); - await 'a tick'; - - expect(spy1.error).not.toHaveBeenCalled(); - expect(spy1.next).toHaveBeenCalledWith(0); - - const error = new Error('NOPE'); - subj.error(error); - expect(spy1.error).toHaveBeenCalledWith(error); - expect(spy1.next).toHaveBeenCalledWith(0); - expect(spy1.next).toHaveBeenCalledTimes(1); - expect(spy1.complete).not.toHaveBeenCalled(); - - const spy2 = observerSpy(); - subj.subscribe(spy2); - await 'a tick'; - expect(spy2.error).toHaveBeenCalledWith(error); - expect(spy2.next).not.toHaveBeenCalled(); - expect(spy2.complete).not.toHaveBeenCalled(); - - expect(subj.closed).toBe(true); - }); -}); diff --git a/packages/core-api/src/lib/subjects.ts b/packages/core-api/src/lib/subjects.ts deleted file mode 100644 index 4b60596bd9..0000000000 --- a/packages/core-api/src/lib/subjects.ts +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Observable } from '../types'; -import ObservableImpl from 'zen-observable'; - -// TODO(Rugvip): These are stopgap and probably incomplete implementations of subjects. -// If we add a more complete Observables library they should be replaced. - -/** - * A basic implementation of ReactiveX publish subjects. - * - * A subject is a convenient way to create an observable when you want - * to fan out a single value to all subscribers. - * - * See http://reactivex.io/documentation/subject.html - */ -export class PublishSubject - implements Observable, ZenObservable.SubscriptionObserver { - private isClosed = false; - private terminatingError?: Error; - - private readonly observable = new ObservableImpl(subscriber => { - if (this.isClosed) { - if (this.terminatingError) { - subscriber.error(this.terminatingError); - } else { - subscriber.complete(); - } - return () => {}; - } - - this.subscribers.add(subscriber); - return () => { - this.subscribers.delete(subscriber); - }; - }); - - private readonly subscribers = new Set< - ZenObservable.SubscriptionObserver - >(); - - [Symbol.observable]() { - return this; - } - - get closed() { - return this.isClosed; - } - - next(value: T) { - if (this.isClosed) { - throw new Error('PublishSubject is closed'); - } - this.subscribers.forEach(subscriber => subscriber.next(value)); - } - - error(error: Error) { - if (this.isClosed) { - throw new Error('PublishSubject is closed'); - } - this.isClosed = true; - this.terminatingError = error; - this.subscribers.forEach(subscriber => subscriber.error(error)); - } - - complete() { - if (this.isClosed) { - throw new Error('PublishSubject is closed'); - } - this.isClosed = true; - this.subscribers.forEach(subscriber => subscriber.complete()); - } - - subscribe(observer: ZenObservable.Observer): ZenObservable.Subscription; - subscribe( - onNext: (value: T) => void, - onError?: (error: any) => void, - onComplete?: () => void, - ): ZenObservable.Subscription; - subscribe( - onNext: ZenObservable.Observer | ((value: T) => void), - onError?: (error: any) => void, - onComplete?: () => void, - ): ZenObservable.Subscription { - const observer = - typeof onNext === 'function' - ? { - next: onNext, - error: onError, - complete: onComplete, - } - : onNext; - - return this.observable.subscribe(observer); - } -} - -/** - * A basic implementation of ReactiveX behavior subjects. - * - * A subject is a convenient way to create an observable when you want - * to fan out a single value to all subscribers. - * - * The BehaviorSubject will emit the most recently emitted value or error - * whenever a new observer subscribes to the subject. - * - * See http://reactivex.io/documentation/subject.html - */ -export class BehaviorSubject - implements Observable, ZenObservable.SubscriptionObserver { - private isClosed = false; - private currentValue: T; - private terminatingError?: Error; - - constructor(value: T) { - this.currentValue = value; - } - - private readonly observable = new ObservableImpl(subscriber => { - if (this.isClosed) { - if (this.terminatingError) { - subscriber.error(this.terminatingError); - } else { - subscriber.complete(); - } - return () => {}; - } - - subscriber.next(this.currentValue); - - this.subscribers.add(subscriber); - return () => { - this.subscribers.delete(subscriber); - }; - }); - - private readonly subscribers = new Set< - ZenObservable.SubscriptionObserver - >(); - - [Symbol.observable]() { - return this; - } - - get closed() { - return this.isClosed; - } - - next(value: T) { - if (this.isClosed) { - throw new Error('BehaviorSubject is closed'); - } - this.currentValue = value; - this.subscribers.forEach(subscriber => subscriber.next(value)); - } - - error(error: Error) { - if (this.isClosed) { - throw new Error('BehaviorSubject is closed'); - } - this.isClosed = true; - this.terminatingError = error; - this.subscribers.forEach(subscriber => subscriber.error(error)); - } - - complete() { - if (this.isClosed) { - throw new Error('BehaviorSubject is closed'); - } - this.isClosed = true; - this.subscribers.forEach(subscriber => subscriber.complete()); - } - - subscribe(observer: ZenObservable.Observer): ZenObservable.Subscription; - subscribe( - onNext: (value: T) => void, - onError?: (error: any) => void, - onComplete?: () => void, - ): ZenObservable.Subscription; - subscribe( - onNext: ZenObservable.Observer | ((value: T) => void), - onError?: (error: any) => void, - onComplete?: () => void, - ): ZenObservable.Subscription { - const observer = - typeof onNext === 'function' - ? { - next: onNext, - error: onError, - complete: onComplete, - } - : onNext; - - return this.observable.subscribe(observer); - } -} diff --git a/packages/core-api/src/lib/versionedValues.test.ts b/packages/core-api/src/lib/versionedValues.test.ts deleted file mode 100644 index 19f9c8f349..0000000000 --- a/packages/core-api/src/lib/versionedValues.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createVersionedValueMap } from './versionedValues'; - -describe('createVersionedValueMap', () => { - it('should be empty', () => { - const map = createVersionedValueMap({}); - - // @ts-expect-error - expect(map.atVersion(1 as any)).toBe(undefined); - }); - - it('should access values by version', () => { - const map = createVersionedValueMap({ 1: 'v1', 2: 'v2' }); - - expect(map.atVersion(1)).toBe('v1'); - expect(map.atVersion(2)).toBe('v2'); - - // @ts-expect-error - expect(map.atVersion(0)).toBe(undefined); - // @ts-expect-error - expect(map.atVersion(NaN)).toBe(undefined); - // @ts-expect-error - expect(map.atVersion(Infinity)).toBe(undefined); - }); -}); diff --git a/packages/core-api/src/lib/versionedValues.ts b/packages/core-api/src/lib/versionedValues.ts deleted file mode 100644 index 3d0a4a41ae..0000000000 --- a/packages/core-api/src/lib/versionedValues.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * The versioned value interface is a container for a set of values that - * can be looked up by version. It is intended to be used as a container - * for values that can be versioned independently of package versions. - */ -export type VersionedValue = { - atVersion( - version: Version, - ): Versions[Version] | undefined; -}; - -/** - * Creates a container for a map of versioned values that implements VersionedValue. - */ -export function createVersionedValueMap< - Versions extends { [version: number]: any } ->(versions: Versions): VersionedValue { - Object.freeze(versions); - return { - atVersion(version) { - return versions[version]; - }, - }; -} diff --git a/packages/core-api/src/plugin/Plugin.tsx b/packages/core-api/src/plugin/Plugin.tsx deleted file mode 100644 index 8ccdb267ba..0000000000 --- a/packages/core-api/src/plugin/Plugin.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - PluginConfig, - PluginOutput, - BackstagePlugin, - Extension, - AnyRoutes, - AnyExternalRoutes, -} from './types'; -import { AnyApiFactory } from '../apis'; - -export class PluginImpl< - Routes extends AnyRoutes, - ExternalRoutes extends AnyExternalRoutes -> implements BackstagePlugin { - private storedOutput?: PluginOutput[]; - - constructor(private readonly config: PluginConfig) {} - - getId(): string { - return this.config.id; - } - - getApis(): Iterable { - return this.config.apis ?? []; - } - - get routes(): Routes { - return this.config.routes ?? ({} as Routes); - } - - get externalRoutes(): ExternalRoutes { - return this.config.externalRoutes ?? ({} as ExternalRoutes); - } - - output(): PluginOutput[] { - if (this.storedOutput) { - return this.storedOutput; - } - if (!this.config.register) { - return []; - } - - const outputs = new Array(); - - this.config.register({ - router: { - addRoute(target, component, options) { - outputs.push({ - type: 'route', - target, - component, - options, - }); - }, - }, - featureFlags: { - register(name) { - outputs.push({ type: 'feature-flag', name }); - }, - }, - }); - - this.storedOutput = outputs; - return this.storedOutput; - } - - provide(extension: Extension): T { - return extension.expose(this); - } - - toString() { - return `plugin{${this.config.id}}`; - } -} - -export function createPlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {} ->( - config: PluginConfig, -): BackstagePlugin { - return new PluginImpl(config); -} diff --git a/packages/core-api/src/plugin/collectors.test.tsx b/packages/core-api/src/plugin/collectors.test.tsx deleted file mode 100644 index 8c0ba2bc9e..0000000000 --- a/packages/core-api/src/plugin/collectors.test.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { PropsWithChildren } from 'react'; -import { createRouteRef } from '../routing'; -import { createPlugin } from './Plugin'; -import { - createRoutableExtension, - createComponentExtension, -} from '../extensions'; -import { MemoryRouter, Routes, Route } from 'react-router-dom'; -import { - traverseElementTree, - childDiscoverer, - routeElementDiscoverer, -} from '../extensions/traversal'; -import { pluginCollector } from './collectors'; - -const mockConfig = () => ({ path: '/foo', title: 'Foo' }); -const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => ( - <>{children} -); - -const pluginA = createPlugin({ id: 'my-plugin-a' }); -const pluginB = createPlugin({ id: 'my-plugin-b' }); -const pluginC = createPlugin({ id: 'my-plugin-c' }); - -const ref1 = createRouteRef(mockConfig()); -const ref2 = createRouteRef(mockConfig()); - -const Extension1 = pluginA.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockComponent), - mountPoint: ref1, - }), -); -const Extension2 = pluginB.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockComponent), - mountPoint: ref2, - }), -); -const Extension3 = pluginA.provide( - createComponentExtension({ component: { sync: MockComponent } }), -); -const Extension4 = pluginB.provide( - createComponentExtension({ component: { sync: MockComponent } }), -); -const Extension5 = pluginC.provide( - createComponentExtension({ component: { sync: MockComponent } }), -); - -describe('collection', () => { - it('should collect the plugins', () => { - const root = ( - - - -
- -
-
- {[]} - Some text here shouldn't be a problem -
- {null} -
- -
- - {false} - {true} - {0} -
- -
- } /> -
- - - ); - - const { plugins } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - plugins: pluginCollector, - }, - }); - - expect(plugins).toEqual(new Set([pluginA, pluginB, pluginC])); - }); -}); diff --git a/packages/core-api/src/plugin/collectors.ts b/packages/core-api/src/plugin/collectors.ts deleted file mode 100644 index 7e021beb24..0000000000 --- a/packages/core-api/src/plugin/collectors.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { BackstagePlugin } from './types'; -import { getComponentData } from '../extensions'; -import { createCollector } from '../extensions/traversal'; - -export const pluginCollector = createCollector( - () => new Set>(), - (acc, node) => { - const plugin = getComponentData>( - node, - 'core.plugin', - ); - if (plugin) { - acc.add(plugin); - } - }, -); diff --git a/packages/core-api/src/plugin/index.ts b/packages/core-api/src/plugin/index.ts deleted file mode 100644 index 54903ecf17..0000000000 --- a/packages/core-api/src/plugin/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { createPlugin } from './Plugin'; -export type { - BackstagePlugin, - Extension, - FeatureFlagOutput, - FeatureFlagsHooks, - LegacyRedirectRouteOutput, - LegacyRouteOutput, - PluginConfig, - PluginHooks, - PluginOutput, - RedirectRouteOutput, - RouteOptions, - RouteOutput, - RoutePath, - RouterHooks, -} from './types'; diff --git a/packages/core-api/src/plugin/types.ts b/packages/core-api/src/plugin/types.ts deleted file mode 100644 index a00897da43..0000000000 --- a/packages/core-api/src/plugin/types.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ComponentType } from 'react'; -import { RouteRef, SubRouteRef, ExternalRouteRef } from '../routing'; -import { AnyApiFactory } from '../apis/system'; - -export type RouteOptions = { - // Whether the route path must match exactly, defaults to true. - exact?: boolean; -}; - -export type RoutePath = string; - -// Replace with using RouteRefs -export type LegacyRouteOutput = { - type: 'legacy-route'; - path: RoutePath; - component: ComponentType<{}>; - options?: RouteOptions; -}; - -export type RouteOutput = { - type: 'route'; - target: RouteRef; - component: ComponentType<{}>; - options?: RouteOptions; -}; - -export type RedirectRouteOutput = { - type: 'redirect-route'; - from: RouteRef; - to: RouteRef; - options?: RouteOptions; -}; - -export type LegacyRedirectRouteOutput = { - type: 'legacy-redirect-route'; - path: RoutePath; - target: RoutePath; - options?: RouteOptions; -}; - -export type FeatureFlagOutput = { - type: 'feature-flag'; - name: string; -}; - -export type PluginOutput = - | LegacyRouteOutput - | RouteOutput - | LegacyRedirectRouteOutput - | RedirectRouteOutput - | FeatureFlagOutput; - -export type Extension = { - expose(plugin: BackstagePlugin): T; -}; - -export type AnyRoutes = { [name: string]: RouteRef | SubRouteRef }; - -export type AnyExternalRoutes = { [name: string]: ExternalRouteRef }; - -export type BackstagePlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {} -> = { - getId(): string; - output(): PluginOutput[]; - getApis(): Iterable; - provide(extension: Extension): T; - routes: Routes; - externalRoutes: ExternalRoutes; -}; - -export type PluginConfig< - Routes extends AnyRoutes, - ExternalRoutes extends AnyExternalRoutes -> = { - id: string; - apis?: Iterable; - register?(hooks: PluginHooks): void; - routes?: Routes; - externalRoutes?: ExternalRoutes; -}; - -export type PluginHooks = { - /** - * @deprecated All router hooks have been deprecated - */ - router: RouterHooks; - featureFlags: FeatureFlagsHooks; -}; - -export type RouterHooks = { - /** - * @deprecated Use a routable extension instead, see https://backstage.io/docs/plugins/composability#porting-existing-plugins - */ - addRoute( - target: RouteRef, - Component: ComponentType, - options?: RouteOptions, - ): void; -}; - -export type FeatureFlagsHooks = { - register(name: string): void; -}; diff --git a/packages/core-api/src/private.ts b/packages/core-api/src/private.ts deleted file mode 100644 index 56a7546925..0000000000 --- a/packages/core-api/src/private.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { PrivateAppImpl } from './app/App'; diff --git a/packages/core-api/src/public.ts b/packages/core-api/src/public.ts deleted file mode 100644 index 28b078357d..0000000000 --- a/packages/core-api/src/public.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export * from './apis'; -export * from './app'; -export * from './extensions'; -export * from './icons'; -export * from './plugin'; -export * from './routing'; -export * from './types'; diff --git a/packages/core-api/src/routing/ExternalRouteRef.test.ts b/packages/core-api/src/routing/ExternalRouteRef.test.ts deleted file mode 100644 index 458f7d9426..0000000000 --- a/packages/core-api/src/routing/ExternalRouteRef.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AnyParams, ExternalRouteRef } from './types'; -import { createExternalRouteRef, isExternalRouteRef } from './ExternalRouteRef'; -import { isSubRouteRef } from './SubRouteRef'; -import { isRouteRef } from './RouteRef'; - -describe('ExternalRouteRef', () => { - it('should be created', () => { - const routeRef: ExternalRouteRef = createExternalRouteRef({ - id: 'my-route-ref', - }); - expect(routeRef.params).toEqual([]); - expect(routeRef.optional).toBe(false); - expect(String(routeRef)).toBe('routeRef{type=external,id=my-route-ref}'); - expect(isRouteRef(routeRef)).toBe(false); - expect(isSubRouteRef(routeRef)).toBe(false); - expect(isExternalRouteRef(routeRef)).toBe(true); - - expect(isRouteRef({} as ExternalRouteRef)).toBe(false); - }); - - it('should be created as optional', () => { - const routeRef: ExternalRouteRef<{ - x: string; - y: string; - }> = createExternalRouteRef({ - id: 'my-other-route-ref', - params: [], - optional: true, - }); - expect(routeRef.params).toEqual([]); - expect(routeRef.optional).toEqual(true); - }); - - it('should be created with params', () => { - const routeRef: ExternalRouteRef<{ - x: string; - y: string; - }> = createExternalRouteRef({ - id: 'my-other-route-ref', - params: ['x', 'y'], - }); - expect(routeRef.params).toEqual(['x', 'y']); - expect(routeRef.optional).toEqual(false); - }); - - it('should be created as optional with params', () => { - const routeRef: ExternalRouteRef<{ - x: string; - y: string; - }> = createExternalRouteRef({ - id: 'my-other-route-ref', - params: ['x', 'y'], - optional: true, - }); - expect(routeRef.params).toEqual(['x', 'y']); - expect(routeRef.optional).toEqual(true); - }); - - it('should properly infer and validate parameter types and assignments', () => { - function validateType( - _ref: ExternalRouteRef, - ) {} - - const _1 = createExternalRouteRef({ id: '1', params: ['notX'] }); - // @ts-expect-error - validateType<{ x: string }, any>(_1); - validateType<{ notX: string }, any>(_1); - - const _2 = createExternalRouteRef({ - id: '2', - params: ['x'], - optional: true, - }); - // @ts-expect-error - validateType(_2); - validateType<{ x: string }, true>(_2); - - const _3 = createExternalRouteRef({ id: '3', params: ['x', 'y'] }); - // @ts-expect-error - validateType<{ x: string }, any>(_3); - // TODO(Rugvip): Ideally this would fail as well, but settle for validating it at runtime instead - validateType<{ x: string; y: string; z: string }, any>(_3); - validateType<{ x: string; y: string }, false>(_3); - - const _4 = createExternalRouteRef({ id: '4', params: [] }); - // @ts-expect-error - validateType<{ x: string }, any>(_4); - validateType(_4); - - const _5 = createExternalRouteRef({ id: '5' }); - // @ts-expect-error - validateType<{ x: string }, any>(_5); - validateType(_5); - - const _6 = createExternalRouteRef({ id: '6', optional: true }); - // @ts-expect-error - validateType(_6); - validateType(_6); - - // To avoid complains about missing expectations and unused vars - expect([_1, _2, _3, _4, _5, _6].join('')).toEqual(expect.any(String)); - }); -}); diff --git a/packages/core-api/src/routing/ExternalRouteRef.ts b/packages/core-api/src/routing/ExternalRouteRef.ts deleted file mode 100644 index 0bc5809449..0000000000 --- a/packages/core-api/src/routing/ExternalRouteRef.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - RouteRef, - SubRouteRef, - ExternalRouteRef, - routeRefType, - AnyParams, -} from './types'; - -export { createExternalRouteRef } from '@backstage/core-plugin-api'; - -export function isExternalRouteRef< - Params extends AnyParams, - Optional extends boolean ->( - routeRef: - | RouteRef - | SubRouteRef - | ExternalRouteRef, -): routeRef is ExternalRouteRef { - return routeRef[routeRefType] === 'external'; -} diff --git a/packages/core-api/src/routing/FlatRoutes.test.tsx b/packages/core-api/src/routing/FlatRoutes.test.tsx deleted file mode 100644 index 20ee0db235..0000000000 --- a/packages/core-api/src/routing/FlatRoutes.test.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { render, RenderResult } from '@testing-library/react'; -import React, { ReactNode } from 'react'; -import { MemoryRouter, Route, Routes, useOutlet } from 'react-router-dom'; -import { AppContext } from '../app'; -import { AppContextProvider } from '../app/AppContext'; -import { FlatRoutes } from './FlatRoutes'; - -function makeRouteRenderer(node: ReactNode) { - let rendered: RenderResult | undefined = undefined; - return (path: string) => { - const content = ( - ({ - NotFoundErrorPage: () => <>Not Found, - }), - } as unknown) as AppContext - } - > - - - ); - if (rendered) { - rendered.unmount(); - rendered.rerender(content); - } else { - rendered = render(content); - } - return rendered; - }; -} - -describe('FlatRoutes', () => { - it('renders some routes', () => { - const renderRoute = makeRouteRenderer( - - a} /> - b} /> - , - ); - expect(renderRoute('/a').getByText('a')).toBeInTheDocument(); - expect(renderRoute('/b').getByText('b')).toBeInTheDocument(); - expect(renderRoute('/c').getByText('Not Found')).toBeInTheDocument(); - expect(renderRoute('/b').queryByText('Not Found')).not.toBeInTheDocument(); - expect(renderRoute('/a').getByText('a')).toBeInTheDocument(); - }); - - it('is not sensitive to ordering and overlapping routes', () => { - // The '/*' suffixes here are intentional and will be ignored by FlatRoutes - const routes = ( - <> - a-1} /> - a} /> - a-2} /> - - ); - const renderRoute = makeRouteRenderer({routes}); - expect(renderRoute('/a').getByText('a')).toBeInTheDocument(); - expect(renderRoute('/a-1').getByText('a-1')).toBeInTheDocument(); - expect(renderRoute('/a-2').getByText('a-2')).toBeInTheDocument(); - renderRoute('').unmount(); - - // This uses regular Routes from react-router, not that a-2 renders a, which is the behavior we're working around - const renderBadRoute = makeRouteRenderer({routes}); - expect(renderBadRoute('/a').getByText('a')).toBeInTheDocument(); - expect(renderBadRoute('/a-1').getByText('a-1')).toBeInTheDocument(); - expect(renderBadRoute('/a-2').getByText('a')).toBeInTheDocument(); - }); - - it('renders children straight as outlets', () => { - const MyPage = () => { - return <>Outlet: {useOutlet()}; - }; - - // The '/*' suffixes here are intentional and will be ignored by FlatRoutes - const routes = ( - <> - }> - a - - }> - a-b - - }> - b - - - ); - const renderRoute = makeRouteRenderer({routes}); - expect(renderRoute('/a').getByText('Outlet: a')).toBeInTheDocument(); - expect(renderRoute('/a/b').getByText('Outlet: a-b')).toBeInTheDocument(); - expect(renderRoute('/b').getByText('Outlet: b')).toBeInTheDocument(); - }); -}); diff --git a/packages/core-api/src/routing/FlatRoutes.tsx b/packages/core-api/src/routing/FlatRoutes.tsx deleted file mode 100644 index 9280e183be..0000000000 --- a/packages/core-api/src/routing/FlatRoutes.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { ReactNode, Children, isValidElement, Fragment } from 'react'; -import { useRoutes } from 'react-router-dom'; -import { useApp } from '../app'; - -type RouteObject = { - path: string; - element: JSX.Element; - children?: RouteObject[]; -}; - -// Similar to the same function from react-router, this collects routes from the -// children, but only the first level of routes -function createRoutesFromChildren(childrenNode: ReactNode): RouteObject[] { - return Children.toArray(childrenNode).flatMap(child => { - if (!isValidElement(child)) { - return []; - } - - const { children } = child.props; - - if (child.type === Fragment) { - return createRoutesFromChildren(children); - } - - let path = child.props.path as string | undefined; - - // TODO(Rugvip): Work around plugins registering empty paths, remove once deprecated routes are gone - if (path === '') { - return []; - } - path = path?.replace(/\/\*$/, '') ?? '/'; - - return [ - { - path, - element: child, - children: children && [ - { - path: '/*', - element: children, - }, - ], - }, - ]; - }); -} - -type FlatRoutesProps = { - children: ReactNode; -}; - -export const FlatRoutes = (props: FlatRoutesProps): JSX.Element | null => { - const app = useApp(); - const { NotFoundErrorPage } = app.getComponents(); - const routes = createRoutesFromChildren(props.children) - // Routes are sorted to work around a bug where prefixes are unexpectedly matched - .sort((a, b) => b.path.localeCompare(a.path)) - // We make sure all routes have '/*' appended, except '/' - .map(obj => { - obj.path = obj.path === '/' ? '/' : `${obj.path}/*`; - return obj; - }); - - // TODO(Rugvip): Possibly add a way to skip this, like a noNotFoundPage prop - routes.push({ - element: , - path: '/*', - }); - - return useRoutes(routes); -}; diff --git a/packages/core-api/src/routing/RouteRef.test.ts b/packages/core-api/src/routing/RouteRef.test.ts deleted file mode 100644 index 92d218eb5b..0000000000 --- a/packages/core-api/src/routing/RouteRef.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AnyParams, RouteRef } from './types'; -import { createRouteRef, isRouteRef } from './RouteRef'; -import { isSubRouteRef } from './SubRouteRef'; -import { isExternalRouteRef } from './ExternalRouteRef'; -import MyIcon from '@material-ui/icons/AcUnit'; - -describe('RouteRef', () => { - it('should be created', () => { - const routeRef: RouteRef = createRouteRef({ - id: 'my-route-ref', - }); - expect(routeRef.params).toEqual([]); - expect(String(routeRef)).toBe('routeRef{type=absolute,id=my-route-ref}'); - expect(isRouteRef(routeRef)).toBe(true); - expect(isSubRouteRef(routeRef)).toBe(false); - expect(isExternalRouteRef(routeRef)).toBe(false); - - expect(isRouteRef({} as RouteRef)).toBe(false); - }); - - it('should be created with params', () => { - const routeRef: RouteRef<{ - x: string; - y: string; - }> = createRouteRef({ - id: 'my-other-route-ref', - params: ['x', 'y'], - }); - expect(routeRef.params).toEqual(['x', 'y']); - }); - - it('should properly infer and validate parameter types and assignments', () => { - function validateType(_ref: RouteRef) {} - - const _1 = createRouteRef({ id: '1', params: ['x'] }); - // @ts-expect-error - validateType<{ y: string }>(_1); - // @ts-expect-error - validateType(_1); - validateType<{ x: string }>(_1); - - const _2 = createRouteRef({ id: '2', params: ['x', 'y'] }); - // @ts-expect-error - validateType<{ x: string }>(_2); - // @ts-expect-error - validateType(_2); - // @ts-expect-error - validateType<{ x: string; z: string }>(_2); - // TODO(Rugvip): Ideally this would fail as well, but settle for validating it at runtime instead - validateType<{ x: string; y: string; z: string }>(_2); - validateType<{ x: string; y: string }>(_2); - - const _3 = createRouteRef({ id: '3', params: [] }); - // @ts-expect-error - validateType<{ x: string }>(_3); - validateType(_3); - - const _4 = createRouteRef({ id: '4' }); - // @ts-expect-error - validateType<{ x: string }>(_4); - validateType(_4); - - // To avoid complains about missing expectations and unused vars - expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String)); - }); - - it('should support deprecated access', () => { - const routeRef = createRouteRef({ - title: 'My Ref', - path: '/my-path', - icon: MyIcon, - }); - expect(routeRef.title).toBe('My Ref'); - expect(routeRef.path).toBe('/my-path'); - expect(routeRef.icon).toBe(MyIcon); - expect(String(routeRef)).toBe('routeRef{type=absolute,id=My Ref}'); - }); -}); diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts deleted file mode 100644 index 21f28eba86..0000000000 --- a/packages/core-api/src/routing/RouteRef.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - RouteRef, - SubRouteRef, - ExternalRouteRef, - routeRefType, - AnyParams, - ParamKeys, -} from './types'; -import { IconComponent } from '../icons/types'; - -export { createRouteRef } from '@backstage/core-plugin-api'; - -// TODO(Rugvip): Remove this in the next breaking release, it's exported but unused -export type RouteRefConfig = { - params?: ParamKeys; - path?: string; - icon?: IconComponent; - title: string; -}; - -export function isRouteRef( - routeRef: - | RouteRef - | SubRouteRef - | ExternalRouteRef, -): routeRef is RouteRef { - return routeRef[routeRefType] === 'absolute'; -} diff --git a/packages/core-api/src/routing/RouteResolver.test.ts b/packages/core-api/src/routing/RouteResolver.test.ts deleted file mode 100644 index 901c30dfc1..0000000000 --- a/packages/core-api/src/routing/RouteResolver.test.ts +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createRouteRef } from './RouteRef'; -import { createSubRouteRef } from './SubRouteRef'; -import { createExternalRouteRef } from './ExternalRouteRef'; -import { RouteResolver } from './RouteResolver'; -import { ExternalRouteRef, RouteRef, SubRouteRef } from './types'; -import { MATCH_ALL_ROUTE } from './collectors'; - -const element = () => null; -const rest = { element, caseSensitive: false, children: [MATCH_ALL_ROUTE] }; - -const ref1 = createRouteRef({ id: 'rr1' }); -const ref2 = createRouteRef({ id: 'rr2', params: ['x'] }); -const ref3 = createRouteRef({ id: 'rr3', params: ['y'] }); -const subRef1 = createSubRouteRef({ - id: 'srr1', - parent: ref1, - path: '/foo', -}); -const subRef2 = createSubRouteRef({ - id: 'srr2', - parent: ref1, - path: '/foo/:a', -}); -const subRef3 = createSubRouteRef({ - id: 'srr3', - parent: ref2, - path: '/bar', -}); -const subRef4 = createSubRouteRef({ - id: 'srr4', - parent: ref2, - path: '/bar/:a', -}); -const externalRef1 = createExternalRouteRef({ id: 'err1' }); -const externalRef2 = createExternalRouteRef({ - id: 'err2', - optional: true, -}); -const externalRef3 = createExternalRouteRef({ id: 'err3', params: ['x'] }); -const externalRef4 = createExternalRouteRef({ - id: 'err4', - optional: true, - params: ['x'], -}); - -describe('RouteResolver', () => { - it('should not resolve anything with an empty resolver', () => { - const r = new RouteResolver(new Map(), new Map(), [], new Map()); - - expect(r.resolve(ref1, '/')?.()).toBe(undefined); - expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe(undefined); - expect(r.resolve(subRef1, '/')?.()).toBe(undefined); - expect(r.resolve(subRef2, '/')?.({ a: '2a' })).toBe(undefined); - expect(r.resolve(subRef3, '/')?.({ x: '3x' })).toBe(undefined); - expect(r.resolve(subRef4, '/')?.({ x: '4x', a: '4a' })).toBe(undefined); - expect(r.resolve(externalRef1, '/')?.()).toBe(undefined); - expect(r.resolve(externalRef2, '/')?.()).toBe(undefined); - expect(r.resolve(externalRef3, '/')?.({ x: '5x' })).toBe(undefined); - expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(undefined); - }); - - it('should resolve an absolute route', () => { - const r = new RouteResolver( - new Map([[ref1, '/my-route']]), - new Map(), - [{ routeRefs: new Set([ref1]), path: '/my-route', ...rest }], - new Map(), - ); - - expect(r.resolve(ref1, '/')?.()).toBe('/my-route'); - expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe(undefined); - expect(r.resolve(subRef1, '/')?.()).toBe('/my-route/foo'); - expect(r.resolve(subRef2, '/')?.({ a: '2a' })).toBe('/my-route/foo/2a'); - expect(r.resolve(subRef3, '/')?.({ x: '3x' })).toBe(undefined); - expect(r.resolve(subRef4, '/')?.({ x: '4x', a: '4a' })).toBe(undefined); - expect(r.resolve(externalRef1, '/')?.()).toBe(undefined); - expect(r.resolve(externalRef2, '/')?.()).toBe(undefined); - expect(r.resolve(externalRef3, '/')?.({ x: '5x' })).toBe(undefined); - expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(undefined); - }); - - it('should resolve an absolute route with a param and with a parent', () => { - const r = new RouteResolver( - new Map([ - [ref1, '/my-route'], - [ref2, '/my-parent/:x'], - ]), - new Map([[ref2, ref1]]), - [ - { - routeRefs: new Set([ref2]), - path: '/my-parent/:x', - ...rest, - children: [ - MATCH_ALL_ROUTE, - { routeRefs: new Set([ref1]), path: '/my-route', ...rest }, - ], - }, - ], - new Map([ - [externalRef1, ref1], - [externalRef3, ref2], - [externalRef4, subRef3], - ]), - ); - - expect(r.resolve(ref1, '/')?.()).toBe('/my-route'); - expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe('/my-route/my-parent/1x'); - expect(r.resolve(subRef1, '/')?.()).toBe('/my-route/foo'); - expect(r.resolve(subRef2, '/')?.({ a: '2a' })).toBe('/my-route/foo/2a'); - expect(r.resolve(subRef3, '/')?.({ x: '3x' })).toBe( - '/my-route/my-parent/3x/bar', - ); - expect(r.resolve(subRef4, '/')?.({ x: '4x', a: '4a' })).toBe( - '/my-route/my-parent/4x/bar/4a', - ); - expect(r.resolve(externalRef1, '/')?.()).toBe('/my-route'); - expect(r.resolve(externalRef2, '/')?.()).toBe(undefined); - expect(r.resolve(externalRef3, '/')?.({ x: '5x' })).toBe( - '/my-route/my-parent/5x', - ); - expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe( - '/my-route/my-parent/6x/bar', - ); - }); - - it('should resolve the most specific match', () => { - const r = new RouteResolver( - new Map([ - [ref1, '/deep'], - [ref2, '/root/:x'], - [ref3, '/sub/:y'], - ]), - new Map([ - [ref3, ref2], - [ref1, ref3], - ]), - [ - { - routeRefs: new Set([ref2]), - path: '/root/:x', - ...rest, - children: [ - MATCH_ALL_ROUTE, - { - routeRefs: new Set([ref3]), - path: '/sub/:y', - ...rest, - children: [ - MATCH_ALL_ROUTE, - { - routeRefs: new Set([ref1]), - path: '/deep', - ...rest, - }, - ], - }, - ], - }, - ], - new Map(), - ); - - expect(r.resolve(ref2, '/')?.({ x: 'x' })).toBe('/root/x'); - expect(r.resolve(ref3, '/root/x')?.({ y: 'y' })).toBe('/root/x/sub/y'); - - expect(() => r.resolve(ref1, '/')?.()).toThrow( - /^Cannot route.*with parent.*as it has parameters$/, - ); - expect(() => r.resolve(ref1, '/root/x')?.()).toThrow( - /^Cannot route.*with parent.*as it has parameters$/, - ); - expect(r.resolve(ref1, '/root/x/sub/y')?.()).toBe('/root/x/sub/y/deep'); - // Without the MATCH_ALL_ROUTE, we wouldn't properly match the route here - expect(r.resolve(ref1, '/root/x/sub/y/any/nested/path/here')?.()).toBe( - '/root/x/sub/y/deep', - ); - }); - - it('should resolve an absolute route with multiple parents', () => { - const r = new RouteResolver( - new Map([ - [ref1, '/my-route'], - [ref2, '/my-parent/:x'], - [ref3, '/my-grandparent/:y'], - ]), - new Map([ - [ref1, ref2], - [ref2, ref3], - ]), - [ - { - routeRefs: new Set([ref3]), - path: '/my-grandparent/:y', - ...rest, - children: [ - MATCH_ALL_ROUTE, - { - routeRefs: new Set([ref2]), - path: '/my-parent/:x', - ...rest, - children: [ - MATCH_ALL_ROUTE, - { routeRefs: new Set([ref1]), path: '/my-route', ...rest }, - ], - }, - ], - }, - ], - new Map([ - [externalRef1, ref1], - [externalRef3, ref2], - [externalRef4, subRef3], - ]), - ); - - const l = '/my-grandparent/my-y/my-parent/my-x'; - expect(r.resolve(ref1, l)?.()).toBe( - '/my-grandparent/my-y/my-parent/my-x/my-route', - ); - expect(() => r.resolve(ref1, '/')?.()).toThrow( - /^Cannot route.*with parent.*as it has parameters$/, - ); - expect(r.resolve(ref2, l)?.({ x: '1x' })).toBe( - '/my-grandparent/my-y/my-parent/1x', - ); - expect(r.resolve(ref2, '/my-grandparent/my-y')?.({ x: '1x' })).toBe( - '/my-grandparent/my-y/my-parent/1x', - ); - expect(() => r.resolve(ref2, '/')?.({ x: '1x' })).toThrow( - /^Cannot route.*with parent.*as it has parameters$/, - ); - expect(r.resolve(subRef1, l)?.()).toBe( - '/my-grandparent/my-y/my-parent/my-x/my-route/foo', - ); - expect(() => r.resolve(subRef1, '/')?.()).toThrow( - /^Cannot route.*with parent.*as it has parameters$/, - ); - expect(r.resolve(subRef2, l)?.({ a: '2a' })).toBe( - '/my-grandparent/my-y/my-parent/my-x/my-route/foo/2a', - ); - expect(() => r.resolve(subRef2, '/')?.({ a: '2a' })).toThrow( - /^Cannot route.*with parent.*as it has parameters$/, - ); - expect(r.resolve(subRef3, l)?.({ x: '3x' })).toBe( - '/my-grandparent/my-y/my-parent/3x/bar', - ); - expect(r.resolve(subRef3, '/my-grandparent/my-y')?.({ x: '3x' })).toBe( - '/my-grandparent/my-y/my-parent/3x/bar', - ); - expect(r.resolve(subRef4, l)?.({ x: '4x', a: '4a' })).toBe( - '/my-grandparent/my-y/my-parent/4x/bar/4a', - ); - expect( - r.resolve(subRef4, '/my-grandparent/my-y')?.({ x: '4x', a: '4a' }), - ).toBe('/my-grandparent/my-y/my-parent/4x/bar/4a'); - expect(r.resolve(externalRef1, l)?.()).toBe( - '/my-grandparent/my-y/my-parent/my-x/my-route', - ); - expect(() => r.resolve(externalRef1, '/')?.()).toThrow( - /^Cannot route.*with parent.*as it has parameters$/, - ); - expect(r.resolve(externalRef2, l)?.()).toBe(undefined); - expect(r.resolve(externalRef3, l)?.({ x: '5x' })).toBe( - '/my-grandparent/my-y/my-parent/5x', - ); - expect(() => r.resolve(externalRef3, '/')?.({ x: '5x' })).toThrow( - /^Cannot route.*with parent.*as it has parameters$/, - ); - expect(r.resolve(externalRef4, l)?.({ x: '6x' })).toBe( - '/my-grandparent/my-y/my-parent/6x/bar', - ); - expect(() => r.resolve(externalRef4, '/')?.({ x: '6x' })).toThrow( - /^Cannot route.*with parent.*as it has parameters$/, - ); - }); -}); diff --git a/packages/core-api/src/routing/RouteResolver.ts b/packages/core-api/src/routing/RouteResolver.ts deleted file mode 100644 index 4d4096a026..0000000000 --- a/packages/core-api/src/routing/RouteResolver.ts +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { generatePath, matchRoutes } from 'react-router-dom'; -import { - AnyRouteRef, - BackstageRouteObject, - RouteRef, - ExternalRouteRef, - AnyParams, - SubRouteRef, - routeRefType, - RouteFunc, -} from './types'; -import { isRouteRef } from './RouteRef'; -import { isSubRouteRef } from './SubRouteRef'; -import { isExternalRouteRef } from './ExternalRouteRef'; - -// Joins a list of paths together, avoiding trailing and duplicate slashes -function joinPaths(...paths: string[]): string { - const normalized = paths.join('/').replace(/\/\/+/g, '/'); - if (normalized !== '/' && normalized.endsWith('/')) { - return normalized.slice(0, -1); - } - return normalized; -} - -/** - * Resolves the absolute route ref that our target route ref is pointing pointing to, as well - * as the relative target path. - * - * Returns an undefined target ref if one could not be fully resolved. - */ -function resolveTargetRef( - anyRouteRef: AnyRouteRef, - routePaths: Map, - routeBindings: Map, -): readonly [RouteRef | undefined, string] { - // First we figure out which absolute route ref we're dealing with, an if there was an sub route path to append. - // For sub routes it will be the parent path, while for external routes it will be the bound route. - let targetRef: RouteRef; - let subRoutePath = ''; - if (isRouteRef(anyRouteRef)) { - targetRef = anyRouteRef; - } else if (isSubRouteRef(anyRouteRef)) { - targetRef = anyRouteRef.parent; - subRoutePath = anyRouteRef.path; - } else if (isExternalRouteRef(anyRouteRef)) { - const resolvedRoute = routeBindings.get(anyRouteRef); - if (!resolvedRoute) { - return [undefined, '']; - } - if (isRouteRef(resolvedRoute)) { - targetRef = resolvedRoute; - } else if (isSubRouteRef(resolvedRoute)) { - targetRef = resolvedRoute.parent; - subRoutePath = resolvedRoute.path; - } else { - throw new Error( - `ExternalRouteRef was bound to invalid target, ${resolvedRoute}`, - ); - } - } else if (anyRouteRef[routeRefType]) { - throw new Error( - `Unknown or invalid route ref type, ${anyRouteRef[routeRefType]}`, - ); - } else { - throw new Error(`Unknown object passed to useRouteRef, got ${anyRouteRef}`); - } - - // Bail if no absolute path could be resolved - if (!targetRef) { - return [undefined, '']; - } - - // Find the path that our target route is bound to - const resolvedPath = routePaths.get(targetRef); - if (!resolvedPath) { - return [undefined, '']; - } - - // SubRouteRefs join the path from the parent route with its own path - const targetPath = joinPaths(resolvedPath, subRoutePath); - return [targetRef, targetPath]; -} - -/** - * Resolves the complete base path for navigating to the target RouteRef. - */ -function resolveBasePath( - targetRef: RouteRef, - sourceLocation: Parameters[1], - routePaths: Map, - routeParents: Map, - routeObjects: BackstageRouteObject[], -) { - // While traversing the app element tree we build up the routeObjects structure - // used here. It is the same kind of structure that react-router creates, with the - // addition that associated route refs are stored throughout the tree. This lets - // us look up all route refs that can be reached from our source location. - // Because of the similar route object structure, we can use `matchRoutes` from - // react-router to do the lookup of our current location. - const match = matchRoutes(routeObjects, sourceLocation) ?? []; - - // While we search for a common routing root between our current location and - // the target route, we build a list of all route refs we find that we need - // to traverse to reach the target. - const refDiffList = Array(); - - let matchIndex = -1; - for ( - let targetSearchRef: RouteRef | undefined = targetRef; - targetSearchRef; - targetSearchRef = routeParents.get(targetSearchRef) - ) { - // The match contains a list of all ancestral route refs present at our current location - // Starting at the desired target ref and traversing back through its parents, we search - // for a target ref that is present in the match for our current location. When a match - // is found it means we have found a common base to resolve the route from. - matchIndex = match.findIndex(m => - (m.route as BackstageRouteObject).routeRefs.has(targetSearchRef!), - ); - if (matchIndex !== -1) { - break; - } - - // Every time we move a step up in the ancestry of the target ref, we add the current ref - // to the diff list, which ends up being the list of route refs to traverse form the common base - // in order to reach our target. - refDiffList.unshift(targetSearchRef); - } - - // If our target route is present in the initial match we need to construct the final path - // from the parent of the matched route segment. That's to allow the caller of the route - // function to supply their own params. - if (refDiffList.length === 0) { - matchIndex -= 1; - } - - // This is the part of the route tree that the target and source locations have in common. - // We re-use the existing pathname directly along with all params. - const parentPath = matchIndex === -1 ? '' : match[matchIndex].pathname; - - // This constructs the mid section of the path using paths resolved from all route refs - // we need to traverse to reach our target except for the very last one. None of these - // paths are allowed to require any parameters, as the caller would have no way of knowing - // what parameters those are. - const diffPath = joinPaths( - ...refDiffList.slice(0, -1).map(ref => { - const path = routePaths.get(ref); - if (!path) { - throw new Error(`No path for ${ref}`); - } - if (path.includes(':')) { - throw new Error( - `Cannot route to ${targetRef} with parent ${ref} as it has parameters`, - ); - } - return path; - }), - ); - - return parentPath + diffPath; -} - -export class RouteResolver { - constructor( - private readonly routePaths: Map, - private readonly routeParents: Map, - private readonly routeObjects: BackstageRouteObject[], - private readonly routeBindings: Map< - ExternalRouteRef, - RouteRef | SubRouteRef - >, - ) {} - - resolve( - anyRouteRef: - | RouteRef - | SubRouteRef - | ExternalRouteRef, - sourceLocation: Parameters[1], - ): RouteFunc | undefined { - // First figure out what our target absolute ref is, as well as our target path. - const [targetRef, targetPath] = resolveTargetRef( - anyRouteRef, - this.routePaths, - this.routeBindings, - ); - if (!targetRef) { - return undefined; - } - - // Next we figure out the base path, which is the combination of the common parent path - // between our current location and our target location, as well as the additional path - // that is the difference between the parent path and the base of our target location. - const basePath = resolveBasePath( - targetRef, - sourceLocation, - this.routePaths, - this.routeParents, - this.routeObjects, - ); - - const routeFunc: RouteFunc = (...[params]) => { - return basePath + generatePath(targetPath, params); - }; - return routeFunc; - } -} diff --git a/packages/core-api/src/routing/SubRouteRef.test.ts b/packages/core-api/src/routing/SubRouteRef.test.ts deleted file mode 100644 index 0a12b2a153..0000000000 --- a/packages/core-api/src/routing/SubRouteRef.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AnyParams, SubRouteRef } from './types'; -import { createSubRouteRef, isSubRouteRef } from './SubRouteRef'; -import { createRouteRef, isRouteRef } from './RouteRef'; -import { isExternalRouteRef } from './ExternalRouteRef'; - -const parent = createRouteRef({ id: 'parent' }); -const parentX = createRouteRef({ id: 'parent-x', params: ['x'] }); - -describe('SubRouteRef', () => { - it('should be created', () => { - const routeRef: SubRouteRef = createSubRouteRef({ - parent, - id: 'my-route-ref', - path: '/foo', - }); - expect(routeRef.path).toBe('/foo'); - expect(routeRef.parent).toBe(parent); - expect(routeRef.params).toEqual([]); - expect(String(routeRef)).toBe('routeRef{type=sub,id=my-route-ref}'); - expect(isRouteRef(routeRef)).toBe(false); - expect(isSubRouteRef(routeRef)).toBe(true); - expect(isExternalRouteRef(routeRef)).toBe(false); - - expect(isRouteRef({} as SubRouteRef)).toBe(false); - }); - - it('should be created with params', () => { - const routeRef: SubRouteRef<{ bar: string }> = createSubRouteRef({ - parent, - id: 'my-other-route-ref', - path: '/foo/:bar', - }); - expect(routeRef.path).toBe('/foo/:bar'); - expect(routeRef.parent).toBe(parent); - expect(routeRef.params).toEqual(['bar']); - }); - - it('should be created with merged params', () => { - const routeRef: SubRouteRef<{ - x: string; - y: string; - z: string; - }> = createSubRouteRef({ - parent: parentX, - id: 'my-other-route-ref', - path: '/foo/:y/:z', - }); - expect(routeRef.path).toBe('/foo/:y/:z'); - expect(routeRef.parent).toBe(parentX); - expect(routeRef.params).toEqual(['x', 'y', 'z']); - }); - - it('should be created with params from parent', () => { - const routeRef: SubRouteRef<{ x: string }> = createSubRouteRef({ - parent: parentX, - id: 'my-other-route-ref', - path: '/foo/bar', - }); - expect(routeRef.path).toBe('/foo/bar'); - expect(routeRef.parent).toBe(parentX); - expect(routeRef.params).toEqual(['x']); - }); - - it.each([ - ['foo', "SubRouteRef path must start with '/', got 'foo'"], - [':foo', "SubRouteRef path must start with '/', got ':foo'"], - ['', "SubRouteRef path must start with '/', got ''"], - ['/', "SubRouteRef path must not end with '/', got '/'"], - ['/foo/', "SubRouteRef path must not end with '/', got '/foo/'"], - ['/foo/:x', 'SubRouteRef may not have params that overlap with its parent'], - ['/:/foo', "SubRouteRef path has invalid param, got ''"], - ['/:inva:lid/foo', "SubRouteRef path has invalid param, got 'inva:lid'"], - ['/:inva=lid/foo', "SubRouteRef path has invalid param, got 'inva=lid'"], - ])('should throw if path is invalid, %s', (path, message) => { - expect(() => - createSubRouteRef({ path, parent: parentX, id: path }), - ).toThrow(message); - }); - - it('should properly infer and parse path parameters', () => { - function validateType(_ref: SubRouteRef) {} - - const _1 = createSubRouteRef({ id: '1', parent, path: '/foo/bar' }); - // @ts-expect-error - validateType<{ x: string }>(_1); - validateType(_1); - - const _2 = createSubRouteRef({ id: '2', parent, path: '/foo/:x/:y' }); - // @ts-expect-error - validateType(_2); - // @ts-expect-error - validateType<{ x: string; z: string }>(_2); - // @ts-expect-error - validateType<{ y: string }>(_2); - // TODO(Rugvip): Ideally this would fail as well, but settle for validating it at runtime instead - validateType<{ x: string; y: string; z: string }>(_2); - validateType<{ x: string; y: string }>(_2); - - const _3 = createSubRouteRef({ id: '3', parent: parentX, path: '/foo' }); - // @ts-expect-error - validateType(_3); - // @ts-expect-error - validateType<{ y: string }>(_3); - validateType<{ x: string }>(_3); - - const _4 = createSubRouteRef({ id: '4', parent: parentX, path: '/foo/:y' }); - // @ts-expect-error - validateType(_4); - // @ts-expect-error - validateType<{ x: string; z: string }>(_4); - // @ts-expect-error - validateType<{ y: string }>(_4); - validateType<{ x: string; y: string }>(_4); - - // To avoid complains about missing expectations and unused vars - expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String)); - }); -}); diff --git a/packages/core-api/src/routing/SubRouteRef.ts b/packages/core-api/src/routing/SubRouteRef.ts deleted file mode 100644 index b64bce9d8a..0000000000 --- a/packages/core-api/src/routing/SubRouteRef.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - AnyParams, - ExternalRouteRef, - RouteRef, - routeRefType, - SubRouteRef, -} from './types'; - -export { createSubRouteRef } from '@backstage/core-plugin-api'; - -export function isSubRouteRef( - routeRef: - | RouteRef - | SubRouteRef - | ExternalRouteRef, -): routeRef is SubRouteRef { - return routeRef[routeRefType] === 'sub'; -} diff --git a/packages/core-api/src/routing/collectors.test.tsx b/packages/core-api/src/routing/collectors.test.tsx deleted file mode 100644 index 21ef219555..0000000000 --- a/packages/core-api/src/routing/collectors.test.tsx +++ /dev/null @@ -1,374 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { PropsWithChildren } from 'react'; -import { - routePathCollector, - routeParentCollector, - routeObjectCollector, -} from './collectors'; - -import { - traverseElementTree, - childDiscoverer, - routeElementDiscoverer, -} from '../extensions/traversal'; -import { createRouteRef } from './RouteRef'; -import { createPlugin } from '../plugin'; -import { attachComponentData, createRoutableExtension } from '../extensions'; -import { MemoryRouter, Routes, Route } from 'react-router-dom'; -import { RouteRef } from './types'; - -const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => ( - <>{children} -); - -const plugin = createPlugin({ id: 'my-plugin' }); - -const ref1 = createRouteRef({ path: '/foo1', title: 'Foo' }); -const ref2 = createRouteRef({ path: '/foo2', title: 'Foo' }); -const ref3 = createRouteRef({ path: '/foo3', title: 'Foo' }); -const ref4 = createRouteRef({ path: '/foo4', title: 'Foo' }); -const ref5 = createRouteRef({ path: '/foo5', title: 'Foo' }); -const refOrder = [ref1, ref2, ref3, ref4, ref5]; - -const Extension1 = plugin.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockComponent), - mountPoint: ref1, - }), -); -const Extension2 = plugin.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockComponent), - mountPoint: ref2, - }), -); -const Extension3 = plugin.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockComponent), - mountPoint: ref3, - }), -); -const Extension4 = plugin.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockComponent), - mountPoint: ref4, - }), -); -const Extension5 = plugin.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockComponent), - mountPoint: ref5, - }), -); - -const AggregationComponent = ({ - children, -}: PropsWithChildren<{ - path: string; -}>) => <>{children}; - -attachComponentData(AggregationComponent, 'core.gatherMountPoints', true); - -function sortedEntries(map: Map): [RouteRef, T][] { - return Array.from(map).sort( - ([a], [b]) => refOrder.indexOf(a) - refOrder.indexOf(b), - ); -} - -function routeObj( - path: string, - refs: RouteRef[], - children: any[] = [], - type: 'mounted' | 'gathered' = 'mounted', -) { - return { - path: path, - caseSensitive: false, - element: type, - routeRefs: new Set(refs), - children: [ - { - path: '/*', - caseSensitive: false, - element: 'match-all', - routeRefs: new Set(), - }, - ...children, - ], - }; -} - -describe('discovery', () => { - it('should collect routes', () => { - const list = [ -
, -
, -
- -
, - ]; - - const root = ( - - - -
- -
-
- Some text here shouldn't be a problem -
- {null} -
- -
- - {false} - {list} - {true} - {0} -
- -
- } /> -
- - - ); - - const { routes, routeParents, routeObjects } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routes: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, - }, - }); - expect(sortedEntries(routes)).toEqual([ - [ref1, '/foo'], - [ref2, '/bar/:id'], - [ref3, '/baz'], - [ref4, '/divsoup'], - [ref5, '/blop'], - ]); - expect(sortedEntries(routeParents)).toEqual([ - [ref1, undefined], - [ref2, ref1], - [ref3, ref2], - [ref4, undefined], - [ref5, ref1], - ]); - expect(routeObjects).toEqual([ - routeObj( - '/foo', - [ref1], - [ - routeObj('/bar/:id', [ref2], [routeObj('/baz', [ref3])]), - routeObj('/blop', [ref5]), - ], - ), - routeObj('/divsoup', [ref4]), - ]); - }); - - it('should handle all react router Route patterns', () => { - const root = ( - - - - - - - - } - /> - }> - } /> - - - - - ); - - const { routes, routeParents } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routes: routePathCollector, - routeParents: routeParentCollector, - }, - }); - expect(sortedEntries(routes)).toEqual([ - [ref1, '/foo'], - [ref2, '/bar/:id'], - [ref3, '/baz'], - [ref4, '/divsoup'], - [ref5, '/blop'], - ]); - expect(sortedEntries(routeParents)).toEqual([ - [ref1, undefined], - [ref2, ref1], - [ref3, undefined], - [ref4, ref3], - [ref5, ref3], - ]); - }); - - it('should use the route aggregator key to bind child routes to the same path', () => { - const root = ( - - - - -
- -
- HELLO -
- - - - - - - -
-
- ); - - const { routes, routeParents, routeObjects } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routes: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, - }, - }); - expect(sortedEntries(routes)).toEqual([ - [ref1, '/foo'], - [ref2, '/foo'], - [ref3, '/bar'], - [ref4, '/baz'], - [ref5, '/baz'], - ]); - expect(sortedEntries(routeParents)).toEqual([ - [ref1, undefined], - [ref2, undefined], - [ref3, undefined], - [ref4, ref3], - [ref5, ref3], - ]); - expect(routeObjects).toEqual([ - routeObj('/foo', [ref1, ref2], [], 'gathered'), - routeObj( - '/bar', - [ref3], - [routeObj('/baz', [ref4, ref5], [], 'gathered')], - ), - ]); - }); - - it('should use the route aggregator but stop when encountering explicit path', () => { - const root = ( - - - - - - - - - - - - - - - ); - - const { routes, routeParents, routeObjects } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routes: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, - }, - }); - expect(sortedEntries(routes)).toEqual([ - [ref1, '/foo'], - [ref2, '/bar'], - [ref3, '/baz'], - [ref4, '/blop'], - [ref5, '/bar'], - ]); - expect(sortedEntries(routeParents)).toEqual([ - [ref1, undefined], - [ref2, ref1], - [ref3, ref1], - [ref4, ref3], - [ref5, ref1], - ]); - expect(routeObjects).toEqual([ - routeObj( - '/foo', - [ref1], - [ - routeObj( - '/bar', - [ref2, ref5], - [routeObj('/baz', [ref3], [routeObj('/blop', [ref4])])], - 'gathered', - ), - ], - ), - ]); - }); - - it('should stop gathering mount points after encountering explicit path', () => { - const root = ( - - - - - - - - - - - - ); - - expect(() => { - traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routes: routePathCollector, - routeParents: routeParentCollector, - }, - }); - }).toThrow('Mounted routable extension must have a path'); - }); -}); diff --git a/packages/core-api/src/routing/collectors.tsx b/packages/core-api/src/routing/collectors.tsx deleted file mode 100644 index 7db51e5014..0000000000 --- a/packages/core-api/src/routing/collectors.tsx +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { isValidElement, ReactElement, ReactNode } from 'react'; -import { BackstageRouteObject, RouteRef } from '../routing/types'; -import { getComponentData } from '../extensions'; -import { createCollector } from '../extensions/traversal'; - -function getMountPoint(node: ReactElement): RouteRef | undefined { - const element: ReactNode = node.props?.element; - - let routeRef = getComponentData(node, 'core.mountPoint'); - if (!routeRef && isValidElement(element)) { - routeRef = getComponentData(element, 'core.mountPoint'); - } - - return routeRef; -} - -export const routePathCollector = createCollector( - () => new Map(), - (acc, node, parent, ctxPath: string | undefined) => { - // The context path is used during mount point gathering to assign the same path - // to all discovered mount points - let currentCtxPath = ctxPath; - - if (parent?.props.element === node) { - return currentCtxPath; - } - - // Start gathering mount points when we encounter a mount point gathering flag - if (getComponentData(node, 'core.gatherMountPoints')) { - const path: string | undefined = node.props?.path; - if (!path) { - throw new Error('Mount point gatherer must have a path'); - } - currentCtxPath = path; - } - - const routeRef = getMountPoint(node); - if (routeRef) { - let path: string | undefined = node.props?.path; - // If we're gathering mount points we use the context path as out path, unless - // the element has its own path, in which case we use that instead and stop gathering - if (currentCtxPath) { - if (path) { - currentCtxPath = undefined; - } else { - path = currentCtxPath; - } - } - if (!path) { - throw new Error('Mounted routable extension must have a path'); - } - acc.set(routeRef, path); - } - return currentCtxPath; - }, -); - -export const routeParentCollector = createCollector( - () => new Map(), - (acc, node, parent, parentRouteRef?: RouteRef | { sticky: RouteRef }) => { - if (parent?.props.element === node) { - return parentRouteRef; - } - - let nextParent = parentRouteRef; - - const routeRef = getMountPoint(node); - if (routeRef) { - // "sticky" route ref is when we've encountered a mount point gatherer, and we want a - // mount points beneath it to have the same parent, regardless of internal structure - if (parentRouteRef && 'sticky' in parentRouteRef) { - acc.set(routeRef, parentRouteRef.sticky); - - // When we encounter a mount point with an explicit path, we stop gathering - // mount points withing the children and remove the sticky state - if (node.props?.path) { - nextParent = routeRef; - } else { - nextParent = parentRouteRef; - } - } else { - acc.set(routeRef, parentRouteRef); - nextParent = routeRef; - } - } - - // Mount point gatherers are marked as "sticky" - if (getComponentData(node, 'core.gatherMountPoints')) { - return { sticky: nextParent }; - } - - return nextParent; - }, -); - -// We always add a child that matches all subroutes but without any route refs. This makes -// sure that we're always able to match each route no matter how deep the navigation goes. -// The route resolver then takes care of selecting the most specific match in order to find -// mount points that are as deep in the routing tree as possible. -export const MATCH_ALL_ROUTE: BackstageRouteObject = { - caseSensitive: false, - path: '/*', - element: 'match-all', // These elements aren't used, so we add in a bit of debug information - routeRefs: new Set(), -}; - -export const routeObjectCollector = createCollector( - () => Array(), - (acc, node, parent, parentObj: BackstageRouteObject | undefined) => { - const parentChildren = parentObj?.children ?? acc; - if (parent?.props.element === node) { - return parentObj; - } - - const path: string | undefined = node.props?.path; - const caseSensitive: boolean = Boolean(node.props?.caseSensitive); - - const routeRef = getMountPoint(node); - if (routeRef) { - if (path) { - const newObject: BackstageRouteObject = { - caseSensitive, - path, - element: 'mounted', - routeRefs: new Set([routeRef]), - children: [MATCH_ALL_ROUTE], - }; - parentChildren.push(newObject); - return newObject; - } - - parentObj?.routeRefs.add(routeRef); - } - - const isGatherer = getComponentData( - node, - 'core.gatherMountPoints', - ); - if (isGatherer) { - if (!path) { - throw new Error('Mount point gatherer must have a path'); - } - const newObject: BackstageRouteObject = { - caseSensitive, - path, - element: 'gathered', - routeRefs: new Set(), - children: [MATCH_ALL_ROUTE], - }; - parentChildren.push(newObject); - return newObject; - } - - return parentObj; - }, -); diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx deleted file mode 100644 index 3d723053b0..0000000000 --- a/packages/core-api/src/routing/hooks.test.tsx +++ /dev/null @@ -1,405 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { render } from '@testing-library/react'; -import { renderHook } from '@testing-library/react-hooks'; -import React, { - Context, - PropsWithChildren, - ReactElement, - useContext, -} from 'react'; -import { MemoryRouter, Route, Routes } from 'react-router-dom'; -import { createRoutableExtension } from '../extensions'; -import { - childDiscoverer, - routeElementDiscoverer, - traverseElementTree, -} from '../extensions/traversal'; -import { getGlobalSingleton } from '../lib/globalObject'; -import { VersionedValue } from '../lib/versionedValues'; -import { createPlugin } from '../plugin'; -import { - routeObjectCollector, - routeParentCollector, - routePathCollector, -} from './collectors'; -import { createExternalRouteRef } from './ExternalRouteRef'; -import { RoutingProvider, useRouteRef, useRouteRefParams } from './hooks'; -import { createRouteRef, RouteRefConfig } from './RouteRef'; -import { RouteResolver } from './RouteResolver'; -import { AnyRouteRef, ExternalRouteRef, RouteFunc, RouteRef } from './types'; -import { validateRoutes } from './validation'; - -const mockConfig = (extra?: Partial>) => ({ - path: '/unused', - title: 'Unused', - ...extra, -}); -const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => ( - <>{children} -); - -const plugin = createPlugin({ id: 'my-plugin' }); - -const ref1 = createRouteRef(mockConfig({ path: '/wat1' })); -const ref2 = createRouteRef(mockConfig({ path: '/wat2' })); -const ref3 = createRouteRef(mockConfig({ path: '/wat3' })); -const ref4 = createRouteRef(mockConfig({ path: '/wat4' })); -const ref5 = createRouteRef({ - ...mockConfig({ path: '/wat5' }), - params: ['x'], -}); -const eRefA = createExternalRouteRef({ id: '1' }); -const eRefB = createExternalRouteRef({ id: '2' }); -const eRefC = createExternalRouteRef({ id: '3', params: ['y'] }); -const eRefD = createExternalRouteRef({ id: '4', optional: true }); -const eRefE = createExternalRouteRef({ - id: '5', - optional: true, - params: ['z'], -}); - -const MockRouteSource = (props: { - path?: string; - name: string; - routeRef: AnyRouteRef; - params?: T; -}) => { - try { - const routeFunc = useRouteRef(props.routeRef as any) as - | RouteFunc - | undefined; - return ( -
- Path at {props.name}: {routeFunc?.(props.params) ?? ''} -
- ); - } catch (ex) { - return ( -
- Error at {props.name}: {ex.message} -
- ); - } -}; - -const Extension1 = plugin.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockComponent), - mountPoint: ref1, - }), -); -const Extension2 = plugin.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockRouteSource), - mountPoint: ref2, - }), -); -const Extension3 = plugin.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockComponent), - mountPoint: ref3, - }), -); -const Extension4 = plugin.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockRouteSource), - mountPoint: ref4, - }), -); -const Extension5 = plugin.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockComponent), - mountPoint: ref5, - }), -); - -function withRoutingProvider( - root: ReactElement, - routeBindings: [ExternalRouteRef, RouteRef][] = [], -) { - const { routePaths, routeParents, routeObjects } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routePaths: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, - }, - }); - - return ( - - {root} - - ); -} - -describe('discovery', () => { - it('should handle simple routeRef path creation for routeRefs used in other parts of the app', async () => { - const root = ( - - - - - - - - - - - - - - - ); - - const rendered = render( - withRoutingProvider(root, [ - [eRefA, ref3], - [eRefB, ref1], - [eRefC, ref2], - [eRefD, ref1], - ]), - ); - - await expect( - rendered.findByText('Path at inside: /foo/bar'), - ).resolves.toBeInTheDocument(); - expect( - rendered.getByText('Path at insideExternal: /baz'), - ).toBeInTheDocument(); - expect(rendered.getByText('Path at outside: /foo/bar')).toBeInTheDocument(); - expect( - rendered.getByText('Path at outsideExternal1: /foo'), - ).toBeInTheDocument(); - expect( - rendered.getByText('Path at outsideExternal2: /foo/bar'), - ).toBeInTheDocument(); - expect( - rendered.getByText('Path at outsideExternal3: /foo'), - ).toBeInTheDocument(); - expect( - rendered.getByText('Path at outsideExternal4: '), - ).toBeInTheDocument(); - }); - - it('should handle routeRefs with parameters', async () => { - const root = ( - - - - - - - - - ); - - const rendered = render(withRoutingProvider(root)); - - await expect( - rendered.findByText('Path at inside: /foo/bar/bleb'), - ).resolves.toBeInTheDocument(); - expect( - rendered.getByText('Path at outside: /foo/bar/blob'), - ).toBeInTheDocument(); - }); - - it('should handle relative routing within parameterized routePaths', async () => { - const root = ( - - - - - - - - - - - - - ); - - const rendered = render(withRoutingProvider(root)); - - await expect( - rendered.findByText('Path at inside: /foo/blob/baz'), - ).resolves.toBeInTheDocument(); - }); - - it('should throw errors for routing to other routeRefs with unsupported parameters', () => { - const root = ( - - - - - - - - - - - ); - - const rendered = render(withRoutingProvider(root)); - - expect( - rendered.getByText( - `Error at outsideWithParams: Cannot route to ${ref3} with parent ${ref5} as it has parameters`, - ), - ).toBeInTheDocument(); - expect( - rendered.getByText( - `Error at outsideNoParams: Cannot route to ${ref3} with parent ${ref5} as it has parameters`, - ), - ).toBeInTheDocument(); - }); - - it('should handle relative routing of parameterized routePaths with duplicate param names', () => { - const root = ( - - - - - - - - ); - - const { routePaths, routeParents } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routePaths: routePathCollector, - routeParents: routeParentCollector, - }, - }); - - expect(() => validateRoutes(routePaths, routeParents)).toThrow( - 'Parameter :id is duplicated in path /foo/:id/bar/:id', - ); - }); -}); - -describe('v1 consumer', () => { - const RoutingContext = getGlobalSingleton< - Context> - >('routing-context'); - - function useMockRouteRefV1( - routeRef: AnyRouteRef, - location: string, - ): RouteFunc | undefined { - const resolver = useContext(RoutingContext)?.atVersion(1); - if (!resolver) { - throw new Error('no impl'); - } - return resolver.resolve(routeRef, location); - } - - it('should resolve routes', () => { - const routeRef1 = createRouteRef({ id: 'ref1' }); - const routeRef2 = createRouteRef({ id: 'ref2' }); - const routeRef3 = createRouteRef({ id: 'ref3', params: ['x'] }); - - const renderedHook = renderHook( - ({ routeRef }) => useMockRouteRefV1(routeRef, '/'), - { - initialProps: { - routeRef: routeRef1 as AnyRouteRef, - }, - wrapper: ({ children }) => ( - , string>([ - [routeRef2, '/foo'], - [routeRef3, '/bar/:x'], - ]) - } - routeParents={new Map()} - routeObjects={[]} - routeBindings={new Map()} - children={children} - /> - ), - }, - ); - - expect(renderedHook.result.current).toBe(undefined); - renderedHook.rerender({ routeRef: routeRef2 }); - expect(renderedHook.result.current?.()).toBe('/foo'); - renderedHook.rerender({ routeRef: routeRef3 }); - expect(renderedHook.result.current?.({ x: 'my-x' })).toBe('/bar/my-x'); - }); -}); - -describe('useRouteRefParams', () => { - it('should provide types params', () => { - const routeRef = createRouteRef({ - id: 'ref1', - params: ['a', 'b'], - }); - - const Page = () => { - const params: { a: string; b: string } = useRouteRefParams(routeRef); - - return ( -
- {params.a} - {params.b} -
- ); - }; - - const { getByText } = render( - - - - - - - , - ); - - expect(getByText('foo')).toBeInTheDocument(); - expect(getByText('bar')).toBeInTheDocument(); - }); -}); diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx deleted file mode 100644 index d498027efb..0000000000 --- a/packages/core-api/src/routing/hooks.tsx +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { - createContext, - ReactNode, - useContext, - useMemo, - Context, -} from 'react'; -import { useLocation, useParams } from 'react-router-dom'; -import { - BackstageRouteObject, - RouteRef, - ExternalRouteRef, - AnyParams, - SubRouteRef, - RouteFunc, -} from './types'; -import { RouteResolver } from './RouteResolver'; -import { - VersionedValue, - createVersionedValueMap, -} from '../lib/versionedValues'; -import { - getGlobalSingleton, - getOrCreateGlobalSingleton, -} from '../lib/globalObject'; - -type RoutingContextType = VersionedValue<{ 1: RouteResolver }> | undefined; -const RoutingContext = getOrCreateGlobalSingleton('routing-context', () => - createContext(undefined), -); - -export function useRouteRef( - routeRef: ExternalRouteRef, -): Optional extends true ? RouteFunc | undefined : RouteFunc; -export function useRouteRef( - routeRef: RouteRef | SubRouteRef, -): RouteFunc; -export function useRouteRef( - routeRef: - | RouteRef - | SubRouteRef - | ExternalRouteRef, -): RouteFunc | undefined { - const sourceLocation = useLocation(); - const versionedContext = useContext( - getGlobalSingleton>('routing-context'), - ); - const resolver = versionedContext?.atVersion(1); - const routeFunc = useMemo( - () => resolver && resolver.resolve(routeRef, sourceLocation), - [resolver, routeRef, sourceLocation], - ); - - if (!versionedContext) { - throw new Error('useRouteRef used outside of routing context'); - } - if (!resolver) { - throw new Error('RoutingContext v1 not available'); - } - - const isOptional = 'optional' in routeRef && routeRef.optional; - if (!routeFunc && !isOptional) { - throw new Error(`No path for ${routeRef}`); - } - - return routeFunc; -} - -type ProviderProps = { - routePaths: Map; - routeParents: Map; - routeObjects: BackstageRouteObject[]; - routeBindings: Map; - children: ReactNode; -}; - -export const RoutingProvider = ({ - routePaths, - routeParents, - routeObjects, - routeBindings, - children, -}: ProviderProps) => { - const resolver = new RouteResolver( - routePaths, - routeParents, - routeObjects, - routeBindings, - ); - - const versionedValue = createVersionedValueMap({ 1: resolver }); - return ( - - {children} - - ); -}; - -export function useRouteRefParams( - _routeRef: RouteRef | SubRouteRef, -): Params { - return useParams() as Params; -} diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts deleted file mode 100644 index 835568c313..0000000000 --- a/packages/core-api/src/routing/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { createExternalRouteRef } from './ExternalRouteRef'; -export { FlatRoutes } from './FlatRoutes'; -export { useRouteRef, useRouteRefParams } from './hooks'; -export { createRouteRef } from './RouteRef'; -export type { RouteRefConfig } from './RouteRef'; -export { createSubRouteRef } from './SubRouteRef'; -export type { - AbsoluteRouteRef, - ConcreteRoute, - ExternalRouteRef, - MutableRouteRef, - RouteRef, - SubRouteRef, -} from './types'; diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts deleted file mode 100644 index 5ecb64c7fe..0000000000 --- a/packages/core-api/src/routing/types.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - RouteRef, - SubRouteRef, - ExternalRouteRef, -} from '@backstage/core-plugin-api'; -import { getOrCreateGlobalSingleton } from '../lib/globalObject'; - -export type { RouteRef, SubRouteRef, ExternalRouteRef }; - -export type AnyParams = { [param in string]: string } | undefined; -export type ParamKeys = keyof Params extends never - ? [] - : (keyof Params)[]; -export type OptionalParams< - Params extends { [param in string]: string } -> = Params[keyof Params] extends never ? undefined : Params; - -// The extra TS magic here is to require a single params argument if the RouteRef -// had at least one param defined, but require 0 arguments if there are no params defined. -// Without this we'd have to pass in empty object to all parameter-less RouteRefs -// just to make TypeScript happy, or we would have to make the argument optional in -// which case you might forget to pass it in when it is actually required. -export type RouteFunc = ( - ...[params]: Params extends undefined ? readonly [] : readonly [Params] -) => string; - -type RouteRefType = Exclude< - keyof RouteRef, - 'params' | 'path' | 'title' | 'icon' ->; -export const routeRefType: RouteRefType = getOrCreateGlobalSingleton( - 'route-ref-type', - () => Symbol('route-ref-type'), -); - -export type AnyRouteRef = - | RouteRef - | SubRouteRef - | ExternalRouteRef; - -// TODO(Rugvip): None of these should be found in the wild anymore, remove in next minor release -/** @deprecated */ -export type ConcreteRoute = {}; -/** @deprecated */ -export type AbsoluteRouteRef = RouteRef<{}>; -/** @deprecated */ -export type MutableRouteRef = RouteRef<{}>; - -// A duplicate of the react-router RouteObject, but with routeRef added -export interface BackstageRouteObject { - caseSensitive: boolean; - children?: BackstageRouteObject[]; - element: React.ReactNode; - path: string; - routeRefs: Set; -} diff --git a/packages/core-api/src/routing/validation.ts b/packages/core-api/src/routing/validation.ts deleted file mode 100644 index 51078a0130..0000000000 --- a/packages/core-api/src/routing/validation.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AnyRouteRef } from './types'; - -export function validateRoutes( - routePaths: Map, - routeParents: Map, -) { - const notLeafRoutes = new Set(routeParents.values()); - notLeafRoutes.delete(undefined); - - for (const route of routeParents.keys()) { - if (notLeafRoutes.has(route)) { - continue; - } - - let currentRouteRef: AnyRouteRef | undefined = route; - - let fullPath = ''; - while (currentRouteRef) { - const path = routePaths.get(currentRouteRef); - if (!path) { - throw new Error(`No path for ${currentRouteRef}`); - } - fullPath = `${path}${fullPath}`; - currentRouteRef = routeParents.get(currentRouteRef); - } - - const params = fullPath.match(/:(\w+)/g); - if (params) { - for (let j = 0; j < params.length; j++) { - for (let i = j + 1; i < params.length; i++) { - if (params[i] === params[j]) { - throw new Error( - `Parameter ${params[i]} is duplicated in path ${fullPath}`, - ); - } - } - } - } - } -} diff --git a/packages/core-api/src/setupTests.ts b/packages/core-api/src/setupTests.ts deleted file mode 100644 index c1d649f2ad..0000000000 --- a/packages/core-api/src/setupTests.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import '@testing-library/jest-dom'; -import 'cross-fetch/polyfill'; diff --git a/packages/core-api/src/types.ts b/packages/core-api/src/types.ts deleted file mode 100644 index fff4cb1515..0000000000 --- a/packages/core-api/src/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * This file contains non-react related core types used throughout Backstage. - */ - -/** - * Observer interface for consuming an Observer, see TC39. - */ -export type Observer = { - next?(value: T): void; - error?(error: Error): void; - complete?(): void; -}; - -/** - * Subscription returned when subscribing to an Observable, see TC39. - */ -export type Subscription = { - /** - * Cancels the subscription - */ - unsubscribe(): void; - - /** - * Value indicating whether the subscription is closed. - */ - readonly closed: boolean; -}; - -// Declares the global well-known Symbol.observable -// We get the actual runtime polyfill from zen-observable -declare global { - interface SymbolConstructor { - readonly observable: symbol; - } -} - -/** - * Observable sequence of values and errors, see TC39. - * - * https://github.com/tc39/proposal-observable - * - * This is used as a common return type for observable values and can be created - * using many different observable implementations, such as zen-observable or RxJS 5. - */ -export type Observable = { - [Symbol.observable](): Observable; - - /** - * Subscribes to this observable to start receiving new values. - */ - subscribe(observer: Observer): Subscription; - subscribe( - onNext?: (value: T) => void, - onError?: (error: Error) => void, - onComplete?: () => void, - ): Subscription; -}; diff --git a/packages/core/.eslintrc.js b/packages/core/.eslintrc.js deleted file mode 100644 index d592a653c8..0000000000 --- a/packages/core/.eslintrc.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], - rules: { - // TODO: add prop types to JS and remove - 'react/prop-types': 0, - 'jest/expect-expect': 0, - }, -}; diff --git a/packages/core/.npmrc b/packages/core/.npmrc deleted file mode 100644 index 214c29d139..0000000000 --- a/packages/core/.npmrc +++ /dev/null @@ -1 +0,0 @@ -registry=https://registry.npmjs.org/ diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md deleted file mode 100644 index 789aeddc29..0000000000 --- a/packages/core/CHANGELOG.md +++ /dev/null @@ -1,516 +0,0 @@ -# @backstage/core - -## 0.7.14 - -### Patch Changes - -- a1c30d7ea: Add deprecation warning to package README. -- Updated dependencies - - @backstage/core-api@0.2.23 - -## 0.7.13 - -### Patch Changes - -- d4644f592: Use the Backstage `Link` component in the `Button` - -## 0.7.12 - -### Patch Changes - -- 1cf1d351f: Export `CheckboxTree` as we have a storybook for it -- Updated dependencies [e7c5e4b30] -- Updated dependencies [0160678b1] - - @backstage/theme@0.2.8 - - @backstage/core-api@0.2.21 - -## 0.7.11 - -### Patch Changes - -- cc592248b: SignInPage: Show login page while pop-up is being displayed when `auto` prop is set. -- Updated dependencies [d597a50c6] - - @backstage/core-api@0.2.20 - -## 0.7.10 - -### Patch Changes - -- 65e6c4541: Remove circular dependencies -- 5da6a561d: Fix a bug where users are asked to log-in on every page refresh. This is specific to people with only one sign-in provider in their SignInPage component. -- Updated dependencies [61c3f927c] -- Updated dependencies [65e6c4541] - - @backstage/core-api@0.2.19 - -## 0.7.9 - -### Patch Changes - -- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 -- 889d89b6e: Fix state persisted in the URL make search input in the table toolbar lose their - focus. -- 3f988cb63: Add count of older messages when multiple messages exist in AlertDisplay -- 675a569a9: chore: bump `react-use` dependency in all packages -- Updated dependencies [062bbf90f] -- Updated dependencies [675a569a9] - - @backstage/core-api@0.2.18 - -## 0.7.8 - -### Patch Changes - -- f65adcde7: Fix some transitive dependency warnings in yarn -- 80888659b: Bump react-hook-form version to be the same for the entire project. -- Updated dependencies [7b8272fb7] -- Updated dependencies [d8b81fd28] - - @backstage/theme@0.2.7 - - @backstage/config@0.1.5 - -## 0.7.7 - -### Patch Changes - -- 9afcac5af: Allow passing NavLinkProps to SidebarItem component to use in NavLink -- e0c9ed759: Add `if` prop to `EntityLayout.Route` to conditionally render tabs -- 6eaecbd81: Improve owner example value in `MissingAnnotationEmptyState`. - -## 0.7.6 - -### Patch Changes - -- 94da20976: Sort the table filter options by name. -- d8cc7e67a: Exposing Material UI extension point for tabs to be able to add additional information to them -- 99fbef232: Adding Headings for Accessibility on the Scaffolder Plugin -- ab07d77f6: Add support for discovering plugins through the app element tree, removing the need to register them explicitly. -- 937ed39ce: Exported SignInProviderConfig to strongly type SignInPage providers -- 9a9e7a42f: Adding close button on support menu -- 50ce875a0: Fixed a potentially confusing error being thrown about misuse of routable extensions where the error was actually something different. -- Updated dependencies [ab07d77f6] -- Updated dependencies [931b21a12] -- Updated dependencies [50ce875a0] - - @backstage/core-api@0.2.17 - - @backstage/theme@0.2.6 - -## 0.7.5 - -### Patch Changes - -- d0d1c2f7b: Pass `inverse` prop to Gauge from GaugeCard -- 5cafcf452: add debounce time attribute for apis-docs for search, giving more time to the users when they are typing. -- 86a95ba67: exposes undocumented `PageTheme` -- e27cb6c45: Don't use a drag & drop cursor when clicking on disabled `IconLinkVertical`. - -## 0.7.4 - -### Patch Changes - -- 1279a3325: Introduce a `load-chunk` step in the `BootErrorPage` to show make chunk loading - errors visible to the user. -- 4a4681b1b: Improved error messaging for routable extension errors, making it easier to identify the component and mount point that caused the error. -- b051e770c: Fixed a bug with `useRouteRef` where navigating from routes beneath a mount point would often fail. -- 98dd5da71: Add support for multiple links to post-scaffold task summary page -- Updated dependencies [1279a3325] -- Updated dependencies [4a4681b1b] -- Updated dependencies [b051e770c] - - @backstage/core-api@0.2.16 - -## 0.7.3 - -### Patch Changes - -- fcc3ada24: Reuse ResponseErrorList for non ResponseErrors -- 4618774ff: Changed color for Add Item, Support & Choose buttons with low contrast/readability in dark mode -- df59930b3: Fix PropTypes error with OverflowTooltip component -- Updated dependencies [76deafd31] -- Updated dependencies [01ccef4c7] -- Updated dependencies [4618774ff] - - @backstage/core-api@0.2.15 - - @backstage/theme@0.2.5 - -## 0.7.2 - -### Patch Changes - -- 8686eb38c: Add a `ResponseErrorPanel` to render `ResponseError` from `@backstage/errors` -- 9ca0e4009: use local version of lowerCase and upperCase methods -- 34ff49b0f: Allow extension components to also return `null` in addition to a `JSX.Element`. -- Updated dependencies [a51dc0006] -- Updated dependencies [e7f9b9435] -- Updated dependencies [0434853a5] -- Updated dependencies [34ff49b0f] -- Updated dependencies [d88dd219e] -- Updated dependencies [c8b54c370] - - @backstage/core-api@0.2.14 - - @backstage/config@0.1.4 - -## 0.7.1 - -### Patch Changes - -- ff4d666ab: Add support for passing a fetch function instead of data to Table `data` prop. -- 2089de76b: Deprecated `ItemCard`. Added `ItemCardGrid` and `ItemCardHeader` instead, that can be used to compose functionality around regular Material-UI `Card` components instead. -- dc1fc92c8: Add support for non external URI's in the Link component to `to` prop. For example `Slack -- Updated dependencies [13524b80b] -- Updated dependencies [e74b07578] -- Updated dependencies [6fb4258a8] -- Updated dependencies [2089de76b] -- Updated dependencies [395885905] - - @backstage/core-api@0.2.13 - - @backstage/theme@0.2.4 - -## 0.7.0 - -### Minor Changes - -- 4c049a1a1: - Adds onClick and other props to IconLinkVertical; - - - Allows TriggerButton component to render when pager duty key is missing; - - Refactors TriggerButton and PagerDutyCard not to have shared state; - - Removes the `action` prop of the IconLinkVertical component while adding `onClick`. - - Instead of having an action including a button with onClick, now the whole component can be clickable making it easier to implement and having a better UX. - - Before: - - ```ts - const myLink: IconLinkVerticalProps = { - label: 'Click me', - action: - - - - Secondary Button: - Used for actions that cancel, skip, and in general perform negative - functions, etc. -
-
color="secondary" variant="contained"
-
- - -
- - - Tertiary Button: - Used commonly in a ButtonGroup and when the button function itself is - not a primary function on a page. -
-
color="default" variant="outlined"
-
- - -
- - ); -}; - -export const ButtonLinks = () => { - const routeRef = createRouteRef({ - path: '/hello', - title: 'Hi there!', - }); - - const handleClick = () => { - return 'Your click worked!'; - }; - - return ( - <> - - { - // TODO: Refactor to use new routing mechanisms - } - - -   has props for both Material-UI's component as well as for - react-router-dom's Route object. - - - - -   links to a statically defined route. In general, this should be - avoided. - - - - - View URL - -   links to a defined URL using Material-UI's Button. - - - - - Trigger Event - -   triggers an onClick event using Material-UI's Button. - - - - ); -}; diff --git a/packages/core/src/components/Button/Button.test.tsx b/packages/core/src/components/Button/Button.test.tsx deleted file mode 100644 index e3a1074e6f..0000000000 --- a/packages/core/src/components/Button/Button.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { render, fireEvent, act } from '@testing-library/react'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { Button } from './Button'; -import { Route, Routes } from 'react-router'; - -describe(' - , - ), - ); - - expect(() => getByText(testString)).toThrow(); - await act(async () => { - fireEvent.click(getByText(buttonLabel)); - }); - expect(getByText(testString)).toBeInTheDocument(); - }); -}); diff --git a/packages/core/src/components/Button/Button.tsx b/packages/core/src/components/Button/Button.tsx deleted file mode 100644 index 6dd593d776..0000000000 --- a/packages/core/src/components/Button/Button.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Button as MaterialButton, - ButtonProps as MaterialButtonProps, -} from '@material-ui/core'; -import React from 'react'; -import { Link, LinkProps } from '../Link'; - -type Props = MaterialButtonProps & Omit; - -/** - * Thin wrapper on top of material-ui's Button component - * Makes the Button to utilise react-router - */ -export const Button = React.forwardRef((props, ref) => ( - -)); diff --git a/packages/core/src/components/Button/index.ts b/packages/core/src/components/Button/index.ts deleted file mode 100644 index e2aa3aff98..0000000000 --- a/packages/core/src/components/Button/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { Button } from './Button'; diff --git a/packages/core/src/components/CheckboxTree/CheckboxTree.stories.tsx b/packages/core/src/components/CheckboxTree/CheckboxTree.stories.tsx deleted file mode 100644 index 48da8b92b2..0000000000 --- a/packages/core/src/components/CheckboxTree/CheckboxTree.stories.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useState } from 'react'; -import { CheckboxTree } from './CheckboxTree'; - -const CHECKBOX_TREE_ITEMS = [ - { - label: 'Generic subcategory name 1', - options: [ - { - label: 'Option 1', - value: 1, - }, - { - label: 'Option 2', - value: 2, - }, - ], - }, - { - label: 'Generic subcategory name 2', - options: [ - { - label: 'Option 1', - value: 1, - }, - { - label: 'Option 2', - value: 2, - }, - ], - }, - { - label: 'Generic subcategory name 3', - options: [ - { - label: 'Option 1', - value: 1, - }, - { - label: 'Option 2', - value: 2, - }, - ], - }, -]; - -export default { - title: 'Inputs/CheckboxTree', - component: CheckboxTree, -}; - -export const Default = () => ( - {}} - label="default" - subCategories={CHECKBOX_TREE_ITEMS} - /> -); - -export const DynamicTree = () => { - function generateTree(showMore: boolean = false) { - const t = [ - { - label: 'Show more', - options: [], - }, - ]; - - if (showMore) { - t.push({ - label: 'More', - options: [], - }); - } - - return t; - } - - const [tree, setTree] = useState(generateTree()); - - return ( - { - setTree(generateTree(state.some(c => c.category === 'Show more'))); - }} - label="default" - subCategories={tree} - /> - ); -}; diff --git a/packages/core/src/components/CheckboxTree/CheckboxTree.test.tsx b/packages/core/src/components/CheckboxTree/CheckboxTree.test.tsx deleted file mode 100644 index 6c84a311d2..0000000000 --- a/packages/core/src/components/CheckboxTree/CheckboxTree.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { fireEvent, render } from '@testing-library/react'; -import React from 'react'; -import { CheckboxTree } from './CheckboxTree'; - -const CHECKBOX_TREE_ITEMS = [ - { - label: 'Generic subcategory name 1', - options: [ - { - label: 'Option 1', - value: 1, - }, - { - label: 'Option 2', - value: 2, - }, - ], - }, -]; - -const minProps = { - onChange: jest.fn(), - label: 'Default', - subCategories: CHECKBOX_TREE_ITEMS, -}; - -describe('', () => { - it('renders without exploding', async () => { - const { getByText, getByTestId } = render(); - - expect(getByText('Generic subcategory name 1')).toBeInTheDocument(); - const checkbox = await getByTestId('expandable'); - - // Simulate click on expandable arrow - fireEvent.click(checkbox); - - // Simulate click on option - const option = getByText('Option 1'); - expect(getByText('Option 1')).toBeInTheDocument(); - fireEvent.click(option); - expect(minProps.onChange).toHaveBeenCalled(); - }); -}); diff --git a/packages/core/src/components/CheckboxTree/CheckboxTree.tsx b/packages/core/src/components/CheckboxTree/CheckboxTree.tsx deleted file mode 100644 index 7e7155143b..0000000000 --- a/packages/core/src/components/CheckboxTree/CheckboxTree.tsx +++ /dev/null @@ -1,358 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable guard-for-in */ -import { - Checkbox, - Collapse, - List, - ListItem, - ListItemIcon, - ListItemText, - Typography, -} from '@material-ui/core'; -import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; -import ExpandLess from '@material-ui/icons/ExpandLess'; -import ExpandMore from '@material-ui/icons/ExpandMore'; -import produce from 'immer'; -import { isEqual } from 'lodash'; -import React, { useEffect, useReducer } from 'react'; -import { usePrevious } from 'react-use'; - -type IndexedObject = { - [key: string]: T; -}; - -const useStyles = makeStyles((theme: Theme) => - createStyles({ - root: { - width: '100%', - minWidth: 10, - maxWidth: 360, - backgroundColor: 'transparent', - '&:hover': { - backgroundColor: 'transparent', - }, - '&:active': { - animation: 'none', - transform: 'none', - }, - }, - nested: { - paddingLeft: theme.spacing(5), - height: '32px', - '&:hover': { - backgroundColor: 'transparent', - }, - }, - listItemIcon: { - minWidth: 10, - }, - listItem: { - '&:hover': { - backgroundColor: 'transparent', - }, - }, - text: { - '& span, & svg': { - fontWeight: 'normal', - fontSize: 14, - }, - }, - }), -); - -/* SUB_CATEGORY */ - -type SubCategory = { - label: string; - isChecked?: boolean; - isOpen?: boolean; - options?: Option[]; -}; - -type SubCategoryWithIndexedOptions = { - label: string; - isChecked?: boolean; - isOpen?: boolean; - options: IndexedObject