From 99d4936f6c220fca7990bd40423ca6052eddf13c Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Tue, 10 Oct 2023 23:11:21 +0530 Subject: [PATCH 01/61] Add examples for github:webhook scaffolder actions Signed-off-by: parmar-abhinav --- .changeset/sweet-countries-share.md | 5 + .../github/githubWebhook.examples.test.ts | 209 ++++++++++++++++++ .../builtin/github/githubWebhook.examples.ts | 120 ++++++++++ .../actions/builtin/github/githubWebhook.ts | 2 + 4 files changed, 336 insertions(+) create mode 100644 .changeset/sweet-countries-share.md create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.ts diff --git a/.changeset/sweet-countries-share.md b/.changeset/sweet-countries-share.md new file mode 100644 index 0000000000..e1327a74b8 --- /dev/null +++ b/.changeset/sweet-countries-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add examples for `github:webhook` scaffolder action & improve related tests diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.test.ts new file mode 100644 index 0000000000..ce1fe76c53 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.test.ts @@ -0,0 +1,209 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { PassThrough } from 'stream'; +import { createGithubWebhookAction } from './githubWebhook'; +import yaml from 'yaml'; +import { examples } from './githubWebhook.examples'; + +const mockOctokit = { + rest: { + repos: { + createWebhook: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('github:webhook examples', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const defaultWebhookSecret = 'aafdfdivierernfdk23f'; + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = { + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubWebhookAction({ + integrations, + defaultWebhookSecret, + githubCredentialsProvider, + }); + }); + + it('Create a GitHub webhook for a repository', async () => { + const input = yaml.parse(examples[0].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + config: { + url: input.webhookUrl, + content_type: input.contentType, + secret: input.webhookSecret, + insecure_ssl: '0', + }, + events: input.events, + active: input.active, + }); + }); + + it('Create a GitHub webhook with minimal configuration', async () => { + const input = yaml.parse(examples[1].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + config: { + url: 'https://example.com/my-webhook', + content_type: 'form', + secret: defaultWebhookSecret, + insecure_ssl: '0', + }, + events: ['push'], + active: true, + }); + }); + + it('Create a GitHub webhook with custom events', async () => { + const input = yaml.parse(examples[2].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + config: { + url: 'https://example.com/my-webhook', + content_type: 'form', + secret: defaultWebhookSecret, + insecure_ssl: '0', + }, + events: ['push', 'pull_request'], + active: true, + }); + }); + + it('Create a GitHub webhook with JSON content type', async () => { + const input = yaml.parse(examples[3].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + config: { + url: 'https://example.com/my-webhook', + content_type: 'json', + secret: defaultWebhookSecret, + insecure_ssl: '0', + }, + events: ['push'], + active: true, + }); + }); + + it('Create a GitHub webhook with insecure SSL', async () => { + const input = yaml.parse(examples[4].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + config: { + url: 'https://example.com/my-webhook', + content_type: 'form', + secret: defaultWebhookSecret, + insecure_ssl: '1', + }, + events: ['push'], + active: true, + }); + }); + + it('Create an inactive GitHub webhook', async () => { + const input = yaml.parse(examples[5].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + config: { + url: 'https://example.com/my-webhook', + content_type: 'form', + secret: defaultWebhookSecret, + insecure_ssl: '0', + }, + events: ['push'], + active: false, + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.ts new file mode 100644 index 0000000000..b6d4a8f80e --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Create a GitHub webhook for a repository', + example: yaml.stringify({ + steps: [ + { + action: 'github:webhook', + name: 'Create GitHub Webhook', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + webhookUrl: 'https://example.com/my-webhook', + webhookSecret: 'mysecret', + events: ['push'], + active: true, + contentType: 'json', + insecureSsl: false, + token: 'my-github-token', + }, + }, + ], + }), + }, + { + description: 'Create a GitHub webhook with minimal configuration', + example: yaml.stringify({ + steps: [ + { + action: 'github:webhook', + name: 'Create GitHub Webhook', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + webhookUrl: 'https://example.com/my-webhook', + }, + }, + ], + }), + }, + { + description: 'Create a GitHub webhook with custom events', + example: yaml.stringify({ + steps: [ + { + action: 'github:webhook', + name: 'Create GitHub Webhook', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + webhookUrl: 'https://example.com/my-webhook', + events: ['push', 'pull_request'], + }, + }, + ], + }), + }, + { + description: 'Create a GitHub webhook with JSON content type', + example: yaml.stringify({ + steps: [ + { + action: 'github:webhook', + name: 'Create GitHub Webhook', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + webhookUrl: 'https://example.com/my-webhook', + contentType: 'json', + }, + }, + ], + }), + }, + { + description: 'Create a GitHub webhook with insecure SSL', + example: yaml.stringify({ + steps: [ + { + action: 'github:webhook', + name: 'Create GitHub Webhook', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + webhookUrl: 'https://example.com/my-webhook', + insecureSsl: true, + }, + }, + ], + }), + }, + { + description: 'Create an inactive GitHub webhook', + example: yaml.stringify({ + steps: [ + { + action: 'github:webhook', + name: 'Create GitHub Webhook', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + webhookUrl: 'https://example.com/my-webhook', + active: false, + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts index 4d487befc7..5589c951a5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts @@ -24,6 +24,7 @@ import { assertError, InputError } from '@backstage/errors'; import { Octokit } from 'octokit'; import { getOctokitOptions } from './helpers'; import { parseRepoUrl } from '../publish/util'; +import { examples } from './githubWebhook.examples'; /** * Creates new action that creates a webhook for a repository on GitHub. @@ -51,6 +52,7 @@ export function createGithubWebhookAction(options: { }>({ id: 'github:webhook', description: 'Creates webhook for a repository on GitHub.', + examples, schema: { input: { type: 'object', From ded2058de97400263dcd5e99f24e400d7ab0c5c8 Mon Sep 17 00:00:00 2001 From: Niklas Aronsson Date: Wed, 18 Oct 2023 09:10:42 +0200 Subject: [PATCH 02/61] scaffolder cookiecutter: remove usage of mock-fs Signed-off-by: Niklas Aronsson --- .../package.json | 3 +- .../src/actions/fetch/cookiecutter.test.ts | 28 ++++++------------- yarn.lock | 3 +- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 68bbd40294..b2d8016325 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -41,11 +41,10 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", - "@types/mock-fs": "^4.13.0", - "mock-fs": "^5.2.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index f944b6e598..3872b2e302 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -22,8 +22,7 @@ import { import { ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; -import mockFs from 'mock-fs'; -import os from 'os'; +import { createMockDirectory } from '@backstage/backend-test-utils'; import { PassThrough } from 'stream'; import { createFetchCookiecutterAction } from './cookiecutter'; import { join } from 'path'; @@ -47,6 +46,7 @@ jest.mock( ); describe('fetch:cookiecutter', () => { + const mockDir = createMockDirectory(); const integrations = ScmIntegrations.fromConfig( new ConfigReader({ integrations: { @@ -58,7 +58,7 @@ describe('fetch:cookiecutter', () => { }), ); - const mockTmpDir = os.tmpdir(); + const mockTmpDir = mockDir.path; let mockContext: ActionContext<{ url: string; @@ -106,36 +106,26 @@ describe('fetch:cookiecutter', () => { output: jest.fn(), createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), }; - - // mock the temp directory - mockFs({ [mockTmpDir]: {} }); - mockFs({ [`${join(mockTmpDir, 'template')}`]: {} }); + mockDir.clear(); + mockDir.addContent({ template: {} }); commandExists.mockResolvedValue(null); // Mock when run container is called it creates some new files in the mock filesystem containerRunner.runContainer.mockImplementation(async () => { - mockFs({ - [`${join(mockTmpDir, 'intermediate')}`]: { - 'testfile.json': '{}', - }, + mockDir.addContent({ + 'intermediate/testfile.json': '{}', }); }); // Mock when executeShellCommand is called it creates some new files in the mock filesystem executeShellCommand.mockImplementation(async () => { - mockFs({ - [`${join(mockTmpDir, 'intermediate')}`]: { - 'testfile.json': '{}', - }, + mockDir.addContent({ + 'intermediate/testfile.json': '{}', }); }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should throw an error when copyWithoutRender is not an array', async () => { (mockContext.input as any).copyWithoutRender = 'not an array'; diff --git a/yarn.lock b/yarn.lock index 9f79ca0ec4..8fc014912f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8710,6 +8710,7 @@ __metadata: resolution: "@backstage/plugin-scaffolder-backend-module-cookiecutter@workspace:plugins/scaffolder-backend-module-cookiecutter" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" @@ -8718,10 +8719,8 @@ __metadata: "@backstage/types": "workspace:^" "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 - "@types/mock-fs": ^4.13.0 command-exists: ^1.2.9 fs-extra: 10.1.0 - mock-fs: ^5.2.0 msw: ^1.0.0 winston: ^3.2.1 yn: ^4.0.0 From 9ab0572217d79b1ed6f28b1fc8199c39564c5bd6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 15:51:56 +0200 Subject: [PATCH 03/61] core-app-api: add core.type markers for AppRouter and FlatRoutes Co-authored-by: Camila Belo Co-authored-by: Vincenzo Scamporlino Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- .changeset/tidy-camels-boil.md | 5 +++++ packages/core-app-api/src/app/AppRouter.tsx | 3 +++ packages/core-app-api/src/routing/FlatRoutes.tsx | 8 +++++++- 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 .changeset/tidy-camels-boil.md diff --git a/.changeset/tidy-camels-boil.md b/.changeset/tidy-camels-boil.md new file mode 100644 index 0000000000..6e59f96f34 --- /dev/null +++ b/.changeset/tidy-camels-boil.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +Add component data `core.type` marker for `AppRouter` and `FlatRoutes`. diff --git a/packages/core-app-api/src/app/AppRouter.tsx b/packages/core-app-api/src/app/AppRouter.tsx index 9d07be1ffd..7481f6662a 100644 --- a/packages/core-app-api/src/app/AppRouter.tsx +++ b/packages/core-app-api/src/app/AppRouter.tsx @@ -16,6 +16,7 @@ import React, { useContext, ReactNode, ComponentType, useState } from 'react'; import { + attachComponentData, ConfigApi, configApiRef, IdentityApi, @@ -186,3 +187,5 @@ export function AppRouter(props: AppRouterProps) { ); } + +attachComponentData(AppRouter, 'core.type', 'AppRouter'); diff --git a/packages/core-app-api/src/routing/FlatRoutes.tsx b/packages/core-app-api/src/routing/FlatRoutes.tsx index abb7fe9432..46c2270d0d 100644 --- a/packages/core-app-api/src/routing/FlatRoutes.tsx +++ b/packages/core-app-api/src/routing/FlatRoutes.tsx @@ -16,7 +16,11 @@ import React, { ReactNode, useMemo } from 'react'; import { useRoutes } from 'react-router-dom'; -import { useApp, useElementFilter } from '@backstage/core-plugin-api'; +import { + attachComponentData, + useApp, + useElementFilter, +} from '@backstage/core-plugin-api'; import { isReactRouterBeta } from '../app/isReactRouterBeta'; let warned = false; @@ -115,3 +119,5 @@ export const FlatRoutes = (props: FlatRoutesProps): JSX.Element | null => { return useRoutes(withNotFound); }; + +attachComponentData(FlatRoutes, 'core.type', 'FlatRoutes'); From d267d2d92b6d0d961131ad4cd74596b6092faf8d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 15:53:52 +0200 Subject: [PATCH 04/61] core-compat-api: fix plugin and extension duplication in collected routes Co-authored-by: Camila Belo Co-authored-by: Vincenzo Scamporlino Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- .../src/collectLegacyRoutes.test.tsx | 7 ++++ .../src/collectLegacyRoutes.tsx | 37 +++++++++++-------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx index 486c373991..85fed83e8c 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx @@ -30,6 +30,7 @@ describe('collectLegacyRoutes', () => { } /> } /> } /> + } /> , ); @@ -85,6 +86,12 @@ describe('collectLegacyRoutes', () => { disabled: false, defaultConfig: { path: 'puppetdb' }, }, + { + id: 'plugin.puppetDb.page2', + attachTo: { id: 'core.routes', input: 'routes' }, + disabled: false, + defaultConfig: { path: 'puppetdb' }, + }, { id: 'apis.plugin.puppetdb.service', attachTo: { id: 'core', input: 'apis' }, diff --git a/packages/core-compat-api/src/collectLegacyRoutes.tsx b/packages/core-compat-api/src/collectLegacyRoutes.tsx index 2b149e6285..8178de2876 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.tsx @@ -62,7 +62,10 @@ Existing tasks: export function collectLegacyRoutes( flatRoutesElement: JSX.Element, ): BackstagePlugin[] { - const results = new Array(); + const createdPluginIds = new Map< + LegacyBackstagePlugin, + Extension[] + >(); React.Children.forEach( flatRoutesElement.props.children, @@ -93,13 +96,18 @@ export function collectLegacyRoutes( ); const pluginId = plugin.getId(); - const path: string = route.props.path; - const detectedExtensions = new Array>(); + const detectedExtensions = + createdPluginIds.get(plugin) ?? new Array>(); + createdPluginIds.set(plugin, detectedExtensions); + + const path: string = route.props.path; detectedExtensions.push( createPageExtension({ - id: `plugin.${pluginId}.page`, + id: `plugin.${pluginId}.page${ + detectedExtensions.length ? detectedExtensions.length + 1 : '' + }`, defaultPath: path[0] === '/' ? path.slice(1) : path, routeRef: routeRef ? convertLegacyRouteRef(routeRef) : undefined, @@ -115,23 +123,20 @@ export function collectLegacyRoutes( ), }), ); + }, + ); - detectedExtensions.push( + return Array.from(createdPluginIds).map(([plugin, extensions]) => + createPlugin({ + id: plugin.getId(), + extensions: [ + ...extensions, ...Array.from(plugin.getApis()).map(factory => createApiExtension({ factory, }), ), - ); - - results.push( - createPlugin({ - id: plugin.getId(), - extensions: detectedExtensions, - }), - ); - }, + ], + }), ); - - return results; } From 5409341b53f163cec75dec2645ebd6b9de8f170a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 15:54:38 +0200 Subject: [PATCH 05/61] core-compat-api: add convertLegacyApp Co-authored-by: Camila Belo Co-authored-by: Vincenzo Scamporlino Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- .../core-compat-api/src/convertLegacyApp.ts | 138 ++++++++++++++++++ packages/core-compat-api/src/index.ts | 1 + 2 files changed, 139 insertions(+) create mode 100644 packages/core-compat-api/src/convertLegacyApp.ts diff --git a/packages/core-compat-api/src/convertLegacyApp.ts b/packages/core-compat-api/src/convertLegacyApp.ts new file mode 100644 index 0000000000..9343744075 --- /dev/null +++ b/packages/core-compat-api/src/convertLegacyApp.ts @@ -0,0 +1,138 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { + Children, + Fragment, + ReactElement, + ReactNode, + isValidElement, +} from 'react'; +import { + BackstagePlugin, + ExtensionOverrides, + coreExtensionData, + createExtension, + createExtensionInput, + createExtensionOverrides, +} from '@backstage/frontend-plugin-api'; +import { getComponentData } from '@backstage/core-plugin-api'; +import { collectLegacyRoutes } from './collectLegacyRoutes'; + +function selectChildren( + rootNode: ReactNode, + selector?: (element: ReactElement<{ children?: ReactNode }>) => boolean, + strictError?: string, +): Array> { + return Children.toArray(rootNode).flatMap(node => { + if (!isValidElement<{ children?: ReactNode }>(node)) { + return []; + } + + if (node.type === Fragment) { + return selectChildren(node.props.children, selector, strictError); + } + + if (selector === undefined || selector(node)) { + return [node]; + } + + if (strictError) { + throw new Error(strictError); + } + + return selectChildren(node.props.children, selector, strictError); + }); +} + +export function convertLegacyApp( + rootElement: React.JSX.Element, +): (ExtensionOverrides | BackstagePlugin)[] { + const appRouterEls = selectChildren( + rootElement, + el => getComponentData(el, 'core.type') === 'AppRouter', + ); + if (appRouterEls.length !== 1) { + throw new Error( + "Failed to convert legacy app, AppRouter element could not been found. Make sure it's at the top level of the App element tree", + ); + } + + const rootEls = selectChildren( + appRouterEls[0].props.children, + el => + Boolean(el.props.children) && + selectChildren( + el.props.children, + innerEl => getComponentData(innerEl, 'core.type') === 'FlatRoutes', + ).length === 1, + ); + if (rootEls.length !== 1) { + throw new Error( + "Failed to convert legacy app, Root element containing FlatRoutes could not been found. Make sure it's within the AppRouter element of the App element tree", + ); + } + const [rootEl] = rootEls; + + const routesEls = selectChildren( + rootEls[0].props.children, + el => getComponentData(el, 'core.type') === 'FlatRoutes', + ); + if (routesEls.length !== 1) { + throw new Error( + 'Unexpectedly failed to find FlatRoutes in app element tree', + ); + } + const [routesEl] = routesEls; + + const CoreLayoutOverride = createExtension({ + id: 'core.layout', + attachTo: { id: 'core', input: 'root' }, + inputs: { + content: createExtensionInput( + { + element: coreExtensionData.reactElement, + }, + { singleton: true }, + ), + }, + output: { + element: coreExtensionData.reactElement, + }, + factory({ bind, inputs }) { + // Clone the root element, this replaces the FlatRoutes declared in the app with out content input + bind({ + element: React.cloneElement(rootEl, undefined, inputs.content.element), + }); + }, + }); + const CoreNavOverride = createExtension({ + id: 'core.nav', + attachTo: { id: 'core.layout', input: 'nav' }, + output: {}, + factory() {}, + disabled: true, + }); + + const collectedRoutes = collectLegacyRoutes(routesEl); + + return [ + ...collectedRoutes, + createExtensionOverrides({ + extensions: [CoreLayoutOverride, CoreNavOverride], + }), + ]; +} diff --git a/packages/core-compat-api/src/index.ts b/packages/core-compat-api/src/index.ts index 2181b2fa9a..e5f61119a3 100644 --- a/packages/core-compat-api/src/index.ts +++ b/packages/core-compat-api/src/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { collectLegacyRoutes } from './collectLegacyRoutes'; +export { convertLegacyApp } from './convertLegacyApp'; From a4e72d309f11b2a0ac36f410c7dfc8ff167c75c2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 16:01:13 +0200 Subject: [PATCH 06/61] core-compat-api: add test for convertLegacyApp Signed-off-by: Patrik Oldsberg --- .../src/convertLegacyApp.test.tsx | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 packages/core-compat-api/src/convertLegacyApp.test.tsx diff --git a/packages/core-compat-api/src/convertLegacyApp.test.tsx b/packages/core-compat-api/src/convertLegacyApp.test.tsx new file mode 100644 index 0000000000..0f1f6145c9 --- /dev/null +++ b/packages/core-compat-api/src/convertLegacyApp.test.tsx @@ -0,0 +1,129 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AppRouter, FlatRoutes } from '@backstage/core-app-api'; +import { PuppetDbPage } from '@backstage/plugin-puppetdb'; +import { StackstormPage } from '@backstage/plugin-stackstorm'; +import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card'; +import React, { ReactNode } from 'react'; +import { Route } from 'react-router-dom'; +import { convertLegacyApp } from './convertLegacyApp'; + +const Root = ({ children }: { children: ReactNode }) => <>{children}; + +describe('convertLegacyApp', () => { + it('should find and extract root and routes', () => { + const collected = convertLegacyApp( + <> +
+ + +
+ + + } /> + } /> + } /> + } /> + + + + , + ); + + expect( + collected.map((p: any /* TODO */) => ({ + id: p.id, + extensions: p.extensions.map((e: any) => ({ + id: e.id, + attachTo: e.attachTo, + disabled: e.disabled, + defaultConfig: e.configSchema?.parse({}), + })), + })), + ).toEqual([ + { + id: 'score-card', + extensions: [ + { + id: 'plugin.score-card.page', + attachTo: { id: 'core.routes', input: 'routes' }, + disabled: false, + defaultConfig: { path: 'score-board' }, + }, + { + id: 'apis.plugin.scoringdata.service', + attachTo: { id: 'core', input: 'apis' }, + disabled: false, + }, + ], + }, + { + id: 'stackstorm', + extensions: [ + { + id: 'plugin.stackstorm.page', + attachTo: { id: 'core.routes', input: 'routes' }, + disabled: false, + defaultConfig: { path: 'stackstorm' }, + }, + { + id: 'apis.plugin.stackstorm.service', + attachTo: { id: 'core', input: 'apis' }, + disabled: false, + }, + ], + }, + { + id: 'puppetDb', + extensions: [ + { + id: 'plugin.puppetDb.page', + attachTo: { id: 'core.routes', input: 'routes' }, + disabled: false, + defaultConfig: { path: 'puppetdb' }, + }, + { + id: 'plugin.puppetDb.page2', + attachTo: { id: 'core.routes', input: 'routes' }, + disabled: false, + defaultConfig: { path: 'puppetdb' }, + }, + { + id: 'apis.plugin.puppetdb.service', + attachTo: { id: 'core', input: 'apis' }, + disabled: false, + }, + ], + }, + { + id: undefined, + extensions: [ + { + id: 'core.layout', + attachTo: { id: 'core', input: 'root' }, + disabled: false, + }, + { + id: 'core.nav', + attachTo: { id: 'core.layout', input: 'nav' }, + disabled: true, + }, + ], + }, + ]); + }); +}); From 43fcf6818c37bb3eb961c4a7e8f1d56c0a05d909 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 16:01:35 +0200 Subject: [PATCH 07/61] frontend-app-api: note in createExtensionInstance Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/wiring/createExtensionInstance.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts index 5a325dc0a0..767329da7f 100644 --- a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts +++ b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts @@ -63,6 +63,7 @@ function resolveInputs( const undeclaredAttachments = Array.from(attachments.entries()).filter( ([inputName]) => inputMap[inputName] === undefined, ); + // TODO: Make this a warning rather than an error if (undeclaredAttachments.length > 0) { throw new Error( `received undeclared input${ From dd4f4f06f5bbcc1a755d576a3f9a2577f19ff108 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 01:02:27 +0200 Subject: [PATCH 08/61] core-compat-api: update api report + fix Signed-off-by: Patrik Oldsberg --- packages/core-compat-api/api-report.md | 7 +++++++ packages/core-compat-api/src/convertLegacyApp.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/packages/core-compat-api/api-report.md b/packages/core-compat-api/api-report.md index 5175c76ac8..3dd2eb6b22 100644 --- a/packages/core-compat-api/api-report.md +++ b/packages/core-compat-api/api-report.md @@ -6,11 +6,18 @@ /// import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ExtensionOverrides } from '@backstage/frontend-plugin-api'; +import { default as React_2 } from 'react'; // @public (undocumented) export function collectLegacyRoutes( flatRoutesElement: JSX.Element, ): BackstagePlugin[]; +// @public (undocumented) +export function convertLegacyApp( + rootElement: React_2.JSX.Element, +): (ExtensionOverrides | BackstagePlugin)[]; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/core-compat-api/src/convertLegacyApp.ts b/packages/core-compat-api/src/convertLegacyApp.ts index 9343744075..59cb136c65 100644 --- a/packages/core-compat-api/src/convertLegacyApp.ts +++ b/packages/core-compat-api/src/convertLegacyApp.ts @@ -58,6 +58,7 @@ function selectChildren( }); } +/** @public */ export function convertLegacyApp( rootElement: React.JSX.Element, ): (ExtensionOverrides | BackstagePlugin)[] { From 47dc8b84b3bb70fbb94a2b748a35e5da2054171f Mon Sep 17 00:00:00 2001 From: Ankit Anand Date: Thu, 19 Oct 2023 04:43:24 +0530 Subject: [PATCH 09/61] removed mock-fs in scaffolder rails Signed-off-by: Ankit Anand --- .../package.json | 5 ++--- .../src/actions/fetch/rails/index.test.ts | 17 +++++++++-------- yarn.lock | 3 +-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 2bc91478e4..0763690d7c 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -39,13 +39,12 @@ "fs-extra": "^10.0.1" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", - "@types/mock-fs": "^4.13.0", "@types/node": "^18.17.8", - "jest-when": "^3.1.0", - "mock-fs": "^5.2.0" + "jest-when": "^3.1.0" }, "files": [ "dist" diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts index 2a93644f57..10b8466dc2 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts @@ -34,14 +34,14 @@ import { } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; -import mockFs from 'mock-fs'; -import os from 'os'; import { resolve as resolvePath } from 'path'; import { PassThrough } from 'stream'; import { createFetchRailsAction } from './index'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('fetch:rails', () => { + const mockDir = createMockDirectory(); const integrations = ScmIntegrations.fromConfig( new ConfigReader({ integrations: { @@ -53,7 +53,7 @@ describe('fetch:rails', () => { }), ); - const mockTmpDir = os.tmpdir(); + const mockTmpDir = mockDir.path; const mockContext = { input: { url: 'https://rubyonrails.org/generator', @@ -73,6 +73,9 @@ describe('fetch:rails', () => { createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), }; + mockDir.clear(); + mockDir.addContent({ template: {} }); + const mockReader: UrlReader = { readUrl: jest.fn(), readTree: jest.fn(), @@ -90,14 +93,12 @@ describe('fetch:rails', () => { }); beforeEach(() => { - mockFs({ [`${mockContext.workspacePath}/result`]: {} }); + mockDir.addContent({ + result: '{}', + }); jest.clearAllMocks(); }); - afterEach(() => { - mockFs.restore(); - }); - it('should call fetchContents with the correct values', async () => { await action.handler(mockContext); diff --git a/yarn.lock b/yarn.lock index 636d159dd3..5c88d57903 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8494,6 +8494,7 @@ __metadata: resolution: "@backstage/plugin-scaffolder-backend-module-rails@workspace:plugins/scaffolder-backend-module-rails" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" @@ -8502,12 +8503,10 @@ __metadata: "@backstage/types": "workspace:^" "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 - "@types/mock-fs": ^4.13.0 "@types/node": ^18.17.8 command-exists: ^1.2.9 fs-extra: ^10.0.1 jest-when: ^3.1.0 - mock-fs: ^5.2.0 languageName: unknown linkType: soft From 43a2ff70cbe3b59f62cc4356fc03056bad82d4e8 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Wed, 18 Oct 2023 18:43:08 -0500 Subject: [PATCH 10/61] docs: new backend system updates Signed-off-by: Andre Wanlin --- .../architecture/02-backends.md | 2 +- .../building-backends/01-index.md | 12 ++++++------ .../building-backends/08-migrating.md | 19 +++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/docs/backend-system/architecture/02-backends.md b/docs/backend-system/architecture/02-backends.md index 4ba76a5af3..c64024d653 100644 --- a/docs/backend-system/architecture/02-backends.md +++ b/docs/backend-system/architecture/02-backends.md @@ -23,7 +23,7 @@ import scaffolderPlugin from '@backstage/plugin-scaffolder-backend'; const backend = createBackend(); // Install desired features -backend.add(import('@backstage/plugin-catalog-backend')); +backend.add(import('@backstage/plugin-catalog-backend/alpha')); // Features can also be installed using an explicit reference backend.add(scaffolderPlugin()); diff --git a/docs/backend-system/building-backends/01-index.md b/docs/backend-system/building-backends/01-index.md index 7d331b4ec3..c381999429 100644 --- a/docs/backend-system/building-backends/01-index.md +++ b/docs/backend-system/building-backends/01-index.md @@ -24,9 +24,9 @@ import { createBackend } from '@backstage/backend-defaults'; // Omitted in the e const backend = createBackend(); -backend.add(import('@backstage/plugin-app-backend')); -backend.add(import('@backstage/plugin-catalog-backend')); -backend.add(import('@backstage/plugin-scaffolder-backend')); +backend.add(import('@backstage/plugin-app-backend/alpha')); +backend.add(import('@backstage/plugin-catalog-backend/alpha')); +backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); @@ -126,8 +126,8 @@ You can now trim down the `src/index.ts` files to only include the plugins and m ```ts const backend = createBackend(); -backend.add(import('@backstage/plugin-app-backend')); -backend.add(import('@backstage/plugin-catalog-backend')); +backend.add(import('@backstage/plugin-app-backend/alpha')); +backend.add(import('@backstage/plugin-catalog-backend/alpha')); backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); @@ -139,7 +139,7 @@ And `backend-b`, don't forget to clean up dependencies in `package.json` as well ```ts const backend = createBackend(); -backend.add(import('@backstage/plugin-scaffolder-backend')); +backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); backend.start(); ``` diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index 1a380421c1..1e30d0aa9e 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -177,11 +177,10 @@ custom API, so we use a helper function to transform that particular one. To make additions as mentioned above to the environment, you will start to get into the weeds of how the backend system wiring works. You'll need to have a service reference and a service factory that performs the actual creation of -your service. Please see [the services -article](../architecture/03-services.md#defining-a-service) to learn how to -create a service ref and its default factory. You can place that code directly -in the index file for now if you want, or near the actual implementation class -in question. +your service. Please see [the services article](../architecture/03-services.md) +to learn how to create a service ref and its default factory. You can place that +code directly in the index file for now if you want, or near the actual implementation +class in question. In this example, we'll assume that your added environment field is named `example`, and the created ref is named `exampleServiceRef`. @@ -233,7 +232,7 @@ be used in its new form. ```ts title="packages/backend/src/index.ts" const backend = createBackend(); /* highlight-add-next-line */ -backend.add(import('@backstage/plugin-app-backend')); +backend.add(import('@backstage/plugin-app-backend/alpha')); ``` If you need to override the app package name, which otherwise defaults to `"app"`, @@ -248,7 +247,7 @@ A basic installation of the catalog plugin looks as follows. ```ts title="packages/backend/src/index.ts" const backend = createBackend(); /* highlight-add-start */ -backend.add(import('@backstage/plugin-catalog-backend')); +backend.add(import('@backstage/plugin-catalog-backend/alpha')); backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); @@ -296,7 +295,7 @@ const catalogModuleCustomExtensions = createBackendModule({ /* highlight-add-end */ const backend = createBackend(); -backend.add(import('@backstage/plugin-catalog-backend')); +backend.add(import('@backstage/plugin-catalog-backend/alpha')); backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); @@ -390,7 +389,7 @@ A basic installation of the scaffolder plugin looks as follows. ```ts title="packages/backend/src/index.ts" const backend = createBackend(); /* highlight-add-next-line */ -backend.add(import('@backstage/plugin-scaffolder-backend')); +backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); ``` If you have other customizations made to `plugins/scaffolder.ts`, such as adding @@ -429,7 +428,7 @@ const scaffolderModuleCustomExtensions = createBackendModule({ /* highlight-add-end */ const backend = createBackend(); -backend.add(import('@backstage/plugin-scaffolder-backend')); +backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); /* highlight-add-next-line */ backend.add(scaffolderModuleCustomExtensions()); ``` From ad67a33d099cc4689778f37778bf2b169a525f26 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 11:22:23 +0200 Subject: [PATCH 11/61] fix(search-backend-node): non-string field hightlight Signed-off-by: Camila Belo --- .../search-backend-node/src/engines/LunrSearchEngine.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index ae5a541e81..02bd6f7fd7 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -329,11 +329,11 @@ export function parseHighlightFields({ const highlightedField = positions.reduce((content, pos) => { return ( - `${content.substring(0, pos[0])}${preTag}` + - `${content.substring(pos[0], pos[0] + pos[1])}` + - `${postTag}${content.substring(pos[0] + pos[1])}` + `${content.toString().substring(0, pos[0])}${preTag}` + + `${content.toString().substring(pos[0], pos[0] + pos[1])}` + + `${postTag}${content.toString().substring(pos[0] + pos[1])}` ); - }, doc[field]); + }, doc[field] ?? ''); return [field, highlightedField]; }), From e8839fe017b46cf49ff2eede1776e819df205e8c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 11:26:12 +0200 Subject: [PATCH 12/61] feat(stackoverflow-backend): get request params from config Signed-off-by: Camila Belo --- plugins/stack-overflow-backend/config.d.ts | 7 +++++++ .../search/StackOverflowQuestionsCollatorFactory.ts | 13 +++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/plugins/stack-overflow-backend/config.d.ts b/plugins/stack-overflow-backend/config.d.ts index b97f237c19..613a7aa887 100644 --- a/plugins/stack-overflow-backend/config.d.ts +++ b/plugins/stack-overflow-backend/config.d.ts @@ -40,5 +40,12 @@ export interface Config { * @visibility secret */ apiAccessToken?: string; + + /** + * Type representing the request parameters. + */ + requestParams?: { + [key: string]: string | string[] | number; + }; }; } diff --git a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts index c5f8a8c16b..cf1e56cb28 100644 --- a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts +++ b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts @@ -54,7 +54,7 @@ export type StackOverflowQuestionsCollatorFactoryOptions = { apiKey?: string; apiAccessToken?: string; teamName?: string; - requestParams: StackOverflowQuestionsRequestParams; + requestParams?: StackOverflowQuestionsRequestParams; logger: Logger; }; @@ -81,7 +81,11 @@ export class StackOverflowQuestionsCollatorFactory this.apiAccessToken = options.apiAccessToken; this.teamName = options.teamName; this.maxPage = options.maxPage; - this.requestParams = options.requestParams; + this.requestParams = options.requestParams ?? { + tagged: ['backstage'], + site: 'stackoverflow', + pagesize: 100, + }; this.logger = options.logger.child({ documentType: this.type }); } @@ -98,12 +102,17 @@ export class StackOverflowQuestionsCollatorFactory config.getOptionalString('stackoverflow.baseUrl') || 'https://api.stackexchange.com/2.3'; const maxPage = options.maxPage || 100; + const requestParams = + config.getOptional( + 'stackoverflow.requestParams', + ); return new StackOverflowQuestionsCollatorFactory({ baseUrl, maxPage, apiKey, apiAccessToken, teamName, + requestParams, ...options, }); } From 43bcd9fcead0ae6cc9ac4a3e2b5e7f2441fa2ba5 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 11:27:50 +0200 Subject: [PATCH 13/61] feat(stackoverflow-backend): migrate collator to backend system Signed-off-by: Camila Belo --- .../alpha-api-report.md | 11 +++ plugins/stack-overflow-backend/api-report.md | 2 +- plugins/stack-overflow-backend/package.json | 23 ++++-- plugins/stack-overflow-backend/src/alpha.ts | 70 +++++++++++++++++++ 4 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 plugins/stack-overflow-backend/alpha-api-report.md create mode 100644 plugins/stack-overflow-backend/src/alpha.ts diff --git a/plugins/stack-overflow-backend/alpha-api-report.md b/plugins/stack-overflow-backend/alpha-api-report.md new file mode 100644 index 0000000000..93d4cfe5e5 --- /dev/null +++ b/plugins/stack-overflow-backend/alpha-api-report.md @@ -0,0 +1,11 @@ +## API Report File for "@backstage/plugin-stack-overflow-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +const _default: () => BackendFeature; +export default _default; +``` diff --git a/plugins/stack-overflow-backend/api-report.md b/plugins/stack-overflow-backend/api-report.md index 8c601399c1..e8fd710cf6 100644 --- a/plugins/stack-overflow-backend/api-report.md +++ b/plugins/stack-overflow-backend/api-report.md @@ -45,7 +45,7 @@ export type StackOverflowQuestionsCollatorFactoryOptions = { apiKey?: string; apiAccessToken?: string; teamName?: string; - requestParams: StackOverflowQuestionsRequestParams; + requestParams?: StackOverflowQuestionsRequestParams; logger: Logger; }; diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index 320d35d3b6..53857082d2 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -5,9 +5,22 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin" @@ -33,7 +46,10 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/backend-tasks": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "node-fetch": "^2.6.7", "qs": "^6.9.4", @@ -42,7 +58,6 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/plugin-search-backend-node": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/stack-overflow-backend/src/alpha.ts b/plugins/stack-overflow-backend/src/alpha.ts new file mode 100644 index 0000000000..897cbd1ea2 --- /dev/null +++ b/plugins/stack-overflow-backend/src/alpha.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { readTaskScheduleDefinitionFromConfig } from '@backstage/backend-tasks'; +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; +import { StackOverflowQuestionsCollatorFactory } from './search'; + +/** + * @packageDocumentation + * A module for the search backend that exports Stack Overflow modules. + */ + +/** + * Search backend module for the Stack Overflow index. + * + * @alpha + */ +export default createBackendModule({ + moduleId: 'stackoverflowCollator', + pluginId: 'search', + register(env) { + env.registerInit({ + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + discovery: coreServices.discovery, + scheduler: coreServices.scheduler, + indexRegistry: searchIndexRegistryExtensionPoint, + }, + async init({ config, logger, scheduler, indexRegistry }) { + const defaultSchedule = { + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 3 }, + }; + + const schedule = config.has('search.collators.stackoverflow.schedule') + ? readTaskScheduleDefinitionFromConfig( + config.getConfig('search.collators.stackoverflow.schedule'), + ) + : defaultSchedule; + + indexRegistry.addCollator({ + schedule: scheduler.createScheduledTaskRunner(schedule), + factory: StackOverflowQuestionsCollatorFactory.fromConfig(config, { + logger: loggerToWinstonLogger(logger), + }), + }); + }, + }); + }, +}); From 41c2cdf23358e13b29303e92df0da3c4674fd9cb Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 11:32:36 +0200 Subject: [PATCH 14/61] feat(backend-next): install stack overflow search module Signed-off-by: Camila Belo --- packages/backend-next/package.json | 1 + packages/backend-next/src/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index 69aa81a9cb..41864907e5 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -56,6 +56,7 @@ "@backstage/plugin-search-backend-module-techdocs": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-sonarqube-backend": "workspace:^", + "@backstage/plugin-stack-overflow-backend": "workspace:^", "@backstage/plugin-techdocs-backend": "workspace:^", "@backstage/plugin-todo-backend": "workspace:^" }, diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts index bdfbc2c26b..15f5ca9c38 100644 --- a/packages/backend-next/src/index.ts +++ b/packages/backend-next/src/index.ts @@ -46,6 +46,7 @@ backend.add(import('@backstage/plugin-search-backend-module-explore/alpha')); backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); backend.add(import('@backstage/plugin-search-backend/alpha')); backend.add(import('@backstage/plugin-techdocs-backend/alpha')); +backend.add(import('@backstage/plugin-stack-overflow-backend/alpha')); backend.add(import('@backstage/plugin-todo-backend')); backend.add(import('@backstage/plugin-sonarqube-backend')); From b148cdc8dbfc8889371ee221a2a1ba6075a16cf2 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 11:38:24 +0200 Subject: [PATCH 15/61] feat(stackoverflow): migrate to new frontend system Signed-off-by: Camila Belo --- plugins/stack-overflow/alpha-api-report.md | 13 ++++++ plugins/stack-overflow/package.json | 20 +++++++-- plugins/stack-overflow/src/alpha.tsx | 48 ++++++++++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 plugins/stack-overflow/alpha-api-report.md create mode 100644 plugins/stack-overflow/src/alpha.tsx diff --git a/plugins/stack-overflow/alpha-api-report.md b/plugins/stack-overflow/alpha-api-report.md new file mode 100644 index 0000000000..84d7616759 --- /dev/null +++ b/plugins/stack-overflow/alpha-api-report.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/plugin-stack-overflow" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackstagePlugin } from '@backstage/frontend-plugin-api'; + +// @alpha (undocumented) +const _default: BackstagePlugin; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index f60800d1f9..431f8a5b80 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -5,9 +5,22 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.tsx", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.tsx" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "frontend-plugin" @@ -32,6 +45,7 @@ "@backstage/config": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-home-react": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "@backstage/plugin-search-react": "workspace:^", diff --git a/plugins/stack-overflow/src/alpha.tsx b/plugins/stack-overflow/src/alpha.tsx new file mode 100644 index 0000000000..18f0a0df1c --- /dev/null +++ b/plugins/stack-overflow/src/alpha.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { configApiRef, createApiFactory } from '@backstage/core-plugin-api'; +import { + createApiExtension, + createPlugin, +} from '@backstage/frontend-plugin-api'; +import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; +import { StackOverflowClient, stackOverflowApiRef } from './api'; + +/** @alpha */ +const StackOverflowApi = createApiExtension({ + factory: createApiFactory({ + api: stackOverflowApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => StackOverflowClient.fromConfig(configApi), + }), +}); + +/** @alpha */ +const StackOverflowSearchResultListItem = createSearchResultListItemExtension({ + id: 'stack-overflow', + predicate: result => result.type === 'stack-overflow', + component: () => + import('./search/StackOverflowSearchResultListItem').then( + m => m.StackOverflowSearchResultListItem, + ), +}); + +/** @alpha */ +export default createPlugin({ + id: 'stack-overflow', + extensions: [StackOverflowApi, StackOverflowSearchResultListItem], +}); From 8b44bf58f2380d4d0c3806227325397aa5a04259 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 11:41:12 +0200 Subject: [PATCH 16/61] feat(search): add stackoverflow to types accordion Signed-off-by: Camila Belo --- plugins/search/src/alpha.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index 14e7067dfa..913a96d094 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -69,6 +69,25 @@ import { SearchType } from './components/SearchType'; import { UrlUpdater } from './components/SearchPage/SearchPage'; import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; +/** @internal */ +const StackOverflowIcon = () => { + return ( + + + + + ); +}; + /** @alpha */ export const SearchApi = createApiExtension({ factory: { @@ -153,6 +172,11 @@ export const SearchPage = createPageExtension({ name: 'Architecture Decision Records', icon: , }, + { + value: 'stack-overflow', + name: 'Stack Overflow', + icon: , + }, ]} /> From b168d7e7ea16c440d26d68fc0663c5ce6ece71a3 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 12:55:20 +0200 Subject: [PATCH 17/61] chore: add changeset files Signed-off-by: Camila Belo --- .changeset/flat-ducks-buy.md | 5 +++++ .changeset/giant-cycles-end.md | 5 +++++ .changeset/mean-fans-cough.md | 5 +++++ .changeset/nice-pillows-poke.md | 5 +++++ 4 files changed, 20 insertions(+) create mode 100644 .changeset/flat-ducks-buy.md create mode 100644 .changeset/giant-cycles-end.md create mode 100644 .changeset/mean-fans-cough.md create mode 100644 .changeset/nice-pillows-poke.md diff --git a/.changeset/flat-ducks-buy.md b/.changeset/flat-ducks-buy.md new file mode 100644 index 0000000000..5c952faaf1 --- /dev/null +++ b/.changeset/flat-ducks-buy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Fix highlighting for non-string fields on the `Lunr` search engine implementation. diff --git a/.changeset/giant-cycles-end.md b/.changeset/giant-cycles-end.md new file mode 100644 index 0000000000..6cd21fe54e --- /dev/null +++ b/.changeset/giant-cycles-end.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-stack-overflow-backend': patch +--- + +Migrate package to the new Frontend system, the new module is distributed with a `/alpha` subpath. diff --git a/.changeset/mean-fans-cough.md b/.changeset/mean-fans-cough.md new file mode 100644 index 0000000000..8f93cd361c --- /dev/null +++ b/.changeset/mean-fans-cough.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Add Stack Overflow type to the default alpha search page extension types filter. diff --git a/.changeset/nice-pillows-poke.md b/.changeset/nice-pillows-poke.md new file mode 100644 index 0000000000..1a3926f371 --- /dev/null +++ b/.changeset/nice-pillows-poke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-stack-overflow': patch +--- + +Migrate package to the new Backend system, the new module is distributed with a `/alpha` subpath. From dad7d4de5b0b0c1a6307b57cfbc75585a4666fe1 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 08:17:02 +0200 Subject: [PATCH 18/61] chore: update yarn lock Signed-off-by: Camila Belo --- yarn.lock | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 1e2f6fdb5d..bc7aa10d86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9182,11 +9182,13 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-stack-overflow-backend@workspace:plugins/stack-overflow-backend": +"@backstage/plugin-stack-overflow-backend@workspace:^, @backstage/plugin-stack-overflow-backend@workspace:plugins/stack-overflow-backend": version: 0.0.0-use.local resolution: "@backstage/plugin-stack-overflow-backend@workspace:plugins/stack-overflow-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" @@ -9209,6 +9211,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-home-react": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^" @@ -25502,6 +25505,7 @@ __metadata: "@backstage/plugin-search-backend-module-techdocs": "workspace:^" "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-sonarqube-backend": "workspace:^" + "@backstage/plugin-stack-overflow-backend": "workspace:^" "@backstage/plugin-techdocs-backend": "workspace:^" "@backstage/plugin-todo-backend": "workspace:^" languageName: unknown From 847841a89d766ce460039b7202e5c7ef23286e02 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 14:57:42 +0200 Subject: [PATCH 19/61] refactor(stack-overflow-backend): use official default request parameters Signed-off-by: Camila Belo --- .changeset/giant-cycles-end.md | 2 ++ plugins/stack-overflow-backend/README.md | 2 +- .../StackOverflowQuestionsCollatorFactory.ts | 16 +++++++++------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.changeset/giant-cycles-end.md b/.changeset/giant-cycles-end.md index 6cd21fe54e..5d9f8149a8 100644 --- a/.changeset/giant-cycles-end.md +++ b/.changeset/giant-cycles-end.md @@ -3,3 +3,5 @@ --- Migrate package to the new Frontend system, the new module is distributed with a `/alpha` subpath. + +The search collator `requestParams` option is optional now, so its defaults to `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as done in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). diff --git a/plugins/stack-overflow-backend/README.md b/plugins/stack-overflow-backend/README.md index 4575ab58c0..5de997faa6 100644 --- a/plugins/stack-overflow-backend/README.md +++ b/plugins/stack-overflow-backend/README.md @@ -45,7 +45,7 @@ This stack overflow backend plugin is primarily responsible for the following: Before you are able to start index stack overflow questions to search, you need to go through the [search getting started guide](https://backstage.io/docs/features/search/getting-started). -When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, add the following code snippet to add the `StackOverflowQuestionsCollatorFactory`. Note that you can modify the `requestParams`. +When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, add the following code snippet to add the `StackOverflowQuestionsCollatorFactory`. Note that you can optionally modify the `requestParams`, otherwise it will defaults to `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as done in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). > Note: if your `baseUrl` is set to the external stack overflow api `https://api.stackexchange.com/2.2`, you can find optional and required parameters under the official API documentation under [`Usage of /questions GET`](https://api.stackexchange.com/docs/questions) diff --git a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts index cf1e56cb28..3c97937365 100644 --- a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts +++ b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts @@ -81,10 +81,13 @@ export class StackOverflowQuestionsCollatorFactory this.apiAccessToken = options.apiAccessToken; this.teamName = options.teamName; this.maxPage = options.maxPage; + // Sets the same default request parameters as the official API documentation + // See https://api.stackexchange.com/docs/questions this.requestParams = options.requestParams ?? { - tagged: ['backstage'], + order: 'desc', + sort: 'activity', site: 'stackoverflow', - pagesize: 100, + ...(options.requestParams ?? {}), }; this.logger = options.logger.child({ documentType: this.type }); } @@ -102,10 +105,9 @@ export class StackOverflowQuestionsCollatorFactory config.getOptionalString('stackoverflow.baseUrl') || 'https://api.stackexchange.com/2.3'; const maxPage = options.maxPage || 100; - const requestParams = - config.getOptional( - 'stackoverflow.requestParams', - ); + const requestParams = config + .getOptionalConfig('stackoverflow.requestParams') + ?.get(); return new StackOverflowQuestionsCollatorFactory({ baseUrl, maxPage, @@ -184,7 +186,7 @@ export class StackOverflowQuestionsCollatorFactory ); const data = await res.json(); - for (const question of data.items) { + for (const question of data.items ?? []) { yield { title: question.title, location: question.link, From 7b1296c57a4c5659c9c010b17ca3591869985b2b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 15:06:01 +0200 Subject: [PATCH 20/61] refactor(search): use string construction for non-text values Signed-off-by: Camila Belo --- plugins/search-backend-node/src/engines/LunrSearchEngine.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index 02bd6f7fd7..27cfae3fdf 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -329,9 +329,9 @@ export function parseHighlightFields({ const highlightedField = positions.reduce((content, pos) => { return ( - `${content.toString().substring(0, pos[0])}${preTag}` + - `${content.toString().substring(pos[0], pos[0] + pos[1])}` + - `${postTag}${content.toString().substring(pos[0] + pos[1])}` + `${String(content).substring(0, pos[0])}${preTag}` + + `${String(content).substring(pos[0], pos[0] + pos[1])}` + + `${postTag}${String(content).substring(pos[0] + pos[1])}` ); }, doc[field] ?? ''); From a8a4911713c4b2f4ac4da0031caef7b1def9594f Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 15:49:21 +0200 Subject: [PATCH 21/61] refactor: do not install stack overflow plugin on example apps Signed-off-by: Camila Belo --- packages/app-next/package.json | 1 - packages/backend-next/package.json | 1 - packages/backend-next/src/index.ts | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 71d888ee50..5437c66272 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -68,7 +68,6 @@ "@backstage/plugin-search-react": "workspace:^", "@backstage/plugin-sentry": "workspace:^", "@backstage/plugin-shortcuts": "workspace:^", - "@backstage/plugin-stack-overflow": "workspace:^", "@backstage/plugin-stackstorm": "workspace:^", "@backstage/plugin-tech-insights": "workspace:^", "@backstage/plugin-tech-radar": "workspace:^", diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index 41864907e5..69aa81a9cb 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -56,7 +56,6 @@ "@backstage/plugin-search-backend-module-techdocs": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-sonarqube-backend": "workspace:^", - "@backstage/plugin-stack-overflow-backend": "workspace:^", "@backstage/plugin-techdocs-backend": "workspace:^", "@backstage/plugin-todo-backend": "workspace:^" }, diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts index 15f5ca9c38..bdfbc2c26b 100644 --- a/packages/backend-next/src/index.ts +++ b/packages/backend-next/src/index.ts @@ -46,7 +46,6 @@ backend.add(import('@backstage/plugin-search-backend-module-explore/alpha')); backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); backend.add(import('@backstage/plugin-search-backend/alpha')); backend.add(import('@backstage/plugin-techdocs-backend/alpha')); -backend.add(import('@backstage/plugin-stack-overflow-backend/alpha')); backend.add(import('@backstage/plugin-todo-backend')); backend.add(import('@backstage/plugin-sonarqube-backend')); From 27e22c6a21b33b6cc0162995053c5730fcad8a91 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 16:19:16 +0200 Subject: [PATCH 22/61] feat(stackoverflow-backend): extract search stack overflow module Signed-off-by: Camila Belo --- .../.eslintrc.js | 1 + .../README.md | 30 +++++++++ .../alpha-api-report.md | 2 +- .../api-report.md | 56 ++++++++++++++++ .../catalog-info.yaml | 10 +++ .../config.d.ts | 51 +++++++++++++++ .../package.json | 65 +++++++++++++++++++ .../src/alpha.test.ts | 55 ++++++++++++++++ .../src/alpha.ts | 6 +- ...ckOverflowQuestionsCollatorFactory.test.ts | 0 .../StackOverflowQuestionsCollatorFactory.ts | 0 .../src/collators/index.ts | 22 +++++++ .../src}/index.ts | 9 ++- plugins/stack-overflow-backend/api-report.md | 59 ++++------------- plugins/stack-overflow-backend/package.json | 24 ++----- plugins/stack-overflow-backend/src/index.ts | 38 ++++++++++- yarn.lock | 26 ++++++-- 17 files changed, 377 insertions(+), 77 deletions(-) create mode 100644 plugins/search-backend-module-stack-overflow/.eslintrc.js create mode 100644 plugins/search-backend-module-stack-overflow/README.md rename plugins/{stack-overflow-backend => search-backend-module-stack-overflow}/alpha-api-report.md (75%) create mode 100644 plugins/search-backend-module-stack-overflow/api-report.md create mode 100644 plugins/search-backend-module-stack-overflow/catalog-info.yaml create mode 100644 plugins/search-backend-module-stack-overflow/config.d.ts create mode 100644 plugins/search-backend-module-stack-overflow/package.json create mode 100644 plugins/search-backend-module-stack-overflow/src/alpha.test.ts rename plugins/{stack-overflow-backend => search-backend-module-stack-overflow}/src/alpha.ts (90%) rename plugins/{stack-overflow-backend/src/search => search-backend-module-stack-overflow/src/collators}/StackOverflowQuestionsCollatorFactory.test.ts (100%) rename plugins/{stack-overflow-backend/src/search => search-backend-module-stack-overflow/src/collators}/StackOverflowQuestionsCollatorFactory.ts (100%) create mode 100644 plugins/search-backend-module-stack-overflow/src/collators/index.ts rename plugins/{stack-overflow-backend/src/search => search-backend-module-stack-overflow/src}/index.ts (76%) diff --git a/plugins/search-backend-module-stack-overflow/.eslintrc.js b/plugins/search-backend-module-stack-overflow/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search-backend-module-stack-overflow/README.md b/plugins/search-backend-module-stack-overflow/README.md new file mode 100644 index 0000000000..cba9617ea8 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/README.md @@ -0,0 +1,30 @@ +# search-backend-module-stack-overflow + +> DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below. + +This package exports a module that extends the search backend to also indexing the questions exposed by the [`Stack Overflow` API](https://api.stackexchange.com/docs/questions). + +## Installation + +Add the module package as a dependency: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-search-backend-module-stack-overflow +``` + +Add the collator to your backend instance, along with the search plugin itself: + +```tsx +// packages/backend/src/index.ts +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); +backend.add(import('@backstage/plugin-search-backend/alpha')); +backend.add( + import('@backstage/plugin-search-backend-module-stack-overflow/alpha'), +); +backend.start(); +``` + +You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `stackoverflow` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow/config.d.ts) for more details. diff --git a/plugins/stack-overflow-backend/alpha-api-report.md b/plugins/search-backend-module-stack-overflow/alpha-api-report.md similarity index 75% rename from plugins/stack-overflow-backend/alpha-api-report.md rename to plugins/search-backend-module-stack-overflow/alpha-api-report.md index 93d4cfe5e5..9d25cb0a7d 100644 --- a/plugins/stack-overflow-backend/alpha-api-report.md +++ b/plugins/search-backend-module-stack-overflow/alpha-api-report.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/plugin-stack-overflow-backend" +## API Report File for "@backstage/plugin-search-backend-module-stack-overflow" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/plugins/search-backend-module-stack-overflow/api-report.md b/plugins/search-backend-module-stack-overflow/api-report.md new file mode 100644 index 0000000000..795b30cac2 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/api-report.md @@ -0,0 +1,56 @@ +## API Report File for "@backstage/plugin-search-backend-module-stack-overflow" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { Config } from '@backstage/config'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { Logger } from 'winston'; +import { Readable } from 'stream'; + +// @public +export interface StackOverflowDocument extends IndexableDocument { + // (undocumented) + answers: number; + // (undocumented) + tags: string[]; +} + +// @public +export class StackOverflowQuestionsCollatorFactory + implements DocumentCollatorFactory +{ + // (undocumented) + execute(): AsyncGenerator; + // (undocumented) + static fromConfig( + config: Config, + options: StackOverflowQuestionsCollatorFactoryOptions, + ): StackOverflowQuestionsCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + protected requestParams: StackOverflowQuestionsRequestParams; + // (undocumented) + readonly type: string; +} + +// @public +export type StackOverflowQuestionsCollatorFactoryOptions = { + baseUrl?: string; + maxPage?: number; + apiKey?: string; + apiAccessToken?: string; + teamName?: string; + requestParams?: StackOverflowQuestionsRequestParams; + logger: Logger; +}; + +// @public +export type StackOverflowQuestionsRequestParams = { + [key: string]: string | string[] | number; +}; +``` diff --git a/plugins/search-backend-module-stack-overflow/catalog-info.yaml b/plugins/search-backend-module-stack-overflow/catalog-info.yaml new file mode 100644 index 0000000000..9075d1d591 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-module-stack-overflow + title: '@backstage/plugin-search-backend-module-stack-overflow' + description: A module for the search backend that exports stack overflow modules +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: discoverability-maintainers diff --git a/plugins/search-backend-module-stack-overflow/config.d.ts b/plugins/search-backend-module-stack-overflow/config.d.ts new file mode 100644 index 0000000000..d615179ed0 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/config.d.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + /** + * Configuration options for the stack overflow plugin + */ + stackoverflow?: { + /** + * The base url of the Stack Overflow API used for the plugin + */ + baseUrl?: string; + + /** + * The API key to authenticate to Stack Overflow API + * @visibility secret + */ + apiKey?: string; + + /** + * The name of the team for a Stack Overflow for Teams account + */ + teamName?: string; + + /** + * The API Access Token to authenticate to Stack Overflow API + * @visibility secret + */ + apiAccessToken?: string; + + /** + * Type representing the request parameters. + */ + requestParams?: { + [key: string]: string | string[] | number; + }; + }; +} diff --git a/plugins/search-backend-module-stack-overflow/package.json b/plugins/search-backend-module-stack-overflow/package.json new file mode 100644 index 0000000000..64ff4d32d3 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/package.json @@ -0,0 +1,65 @@ +{ + "name": "@backstage/plugin-search-backend-module-stack-overflow", + "description": "A module for the search backend that exports stack overflow modules", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/search-backend-module-stack-overflow" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/backend-tasks": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-search-backend-node": "workspace:^", + "@backstage/plugin-search-common": "workspace:^", + "node-fetch": "^2.6.7", + "qs": "^6.9.4", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "msw": "^1.2.1" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/search-backend-module-stack-overflow/src/alpha.test.ts b/plugins/search-backend-module-stack-overflow/src/alpha.test.ts new file mode 100644 index 0000000000..1dba1ab8c0 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/src/alpha.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; +import searchModuleStackOverflowCollator from './alpha'; + +describe('searchModuleStackOverflowCollator', () => { + const schedule = { + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 3 }, + }; + + it('should register the stack overflow collator to the search index registry extension point with factory and schedule', async () => { + const extensionPointMock = { + addCollator: jest.fn(), + }; + + await startTestBackend({ + extensionPoints: [ + [searchIndexRegistryExtensionPoint, extensionPointMock], + ], + features: [ + searchModuleStackOverflowCollator(), + mockServices.rootConfig.factory({ + data: { + stackoverflow: { + schedule, + }, + }, + }), + ], + }); + + expect(extensionPointMock.addCollator).toHaveBeenCalledTimes(1); + expect(extensionPointMock.addCollator).toHaveBeenCalledWith({ + factory: expect.objectContaining({ type: 'stack-overflow' }), + schedule: expect.objectContaining({ run: expect.any(Function) }), + }); + }); +}); diff --git a/plugins/stack-overflow-backend/src/alpha.ts b/plugins/search-backend-module-stack-overflow/src/alpha.ts similarity index 90% rename from plugins/stack-overflow-backend/src/alpha.ts rename to plugins/search-backend-module-stack-overflow/src/alpha.ts index 897cbd1ea2..85b686c9ed 100644 --- a/plugins/stack-overflow-backend/src/alpha.ts +++ b/plugins/search-backend-module-stack-overflow/src/alpha.ts @@ -21,7 +21,7 @@ import { createBackendModule, } from '@backstage/backend-plugin-api'; import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; -import { StackOverflowQuestionsCollatorFactory } from './search'; +import { StackOverflowQuestionsCollatorFactory } from './collators'; /** * @packageDocumentation @@ -52,9 +52,9 @@ export default createBackendModule({ initialDelay: { seconds: 3 }, }; - const schedule = config.has('search.collators.stackoverflow.schedule') + const schedule = config.has('stackoverflow.schedule') ? readTaskScheduleDefinitionFromConfig( - config.getConfig('search.collators.stackoverflow.schedule'), + config.getConfig('stackoverflow.schedule'), ) : defaultSchedule; diff --git a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.test.ts b/plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.test.ts similarity index 100% rename from plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.test.ts rename to plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.test.ts diff --git a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts b/plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.ts similarity index 100% rename from plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts rename to plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.ts diff --git a/plugins/search-backend-module-stack-overflow/src/collators/index.ts b/plugins/search-backend-module-stack-overflow/src/collators/index.ts new file mode 100644 index 0000000000..1170b52423 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/src/collators/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + type StackOverflowDocument, + type StackOverflowQuestionsRequestParams, + type StackOverflowQuestionsCollatorFactoryOptions, + StackOverflowQuestionsCollatorFactory, +} from './StackOverflowQuestionsCollatorFactory'; diff --git a/plugins/stack-overflow-backend/src/search/index.ts b/plugins/search-backend-module-stack-overflow/src/index.ts similarity index 76% rename from plugins/stack-overflow-backend/src/search/index.ts rename to plugins/search-backend-module-stack-overflow/src/index.ts index ed3e05cb28..eb1f7540de 100644 --- a/plugins/stack-overflow-backend/src/search/index.ts +++ b/plugins/search-backend-module-stack-overflow/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * Copyright 2023 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,4 +14,9 @@ * limitations under the License. */ -export * from './StackOverflowQuestionsCollatorFactory'; +/** + * @packageDocumentation + * A module for the search backend that exports Stack Overflow modules. + */ + +export * from './collators'; diff --git a/plugins/stack-overflow-backend/api-report.md b/plugins/stack-overflow-backend/api-report.md index e8fd710cf6..d113f7a0b6 100644 --- a/plugins/stack-overflow-backend/api-report.md +++ b/plugins/stack-overflow-backend/api-report.md @@ -3,54 +3,21 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// +import { StackOverflowDocument as StackOverflowDocument_2 } from '@backstage/plugin-search-backend-module-stack-overflow'; +import { StackOverflowQuestionsCollatorFactory as StackOverflowQuestionsCollatorFactory_2 } from '@backstage/plugin-search-backend-module-stack-overflow'; +import { StackOverflowQuestionsRequestParams as StackOverflowQuestionsRequestParams_2 } from '@backstage/plugin-search-backend-module-stack-overflow'; -import { Config } from '@backstage/config'; -import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; -import { IndexableDocument } from '@backstage/plugin-search-common'; -import { Logger } from 'winston'; -import { Readable } from 'stream'; +// @public @deprecated (undocumented) +export type StackOverflowDocument = StackOverflowDocument_2; -// @public -export interface StackOverflowDocument extends IndexableDocument { - // (undocumented) - answers: number; - // (undocumented) - tags: string[]; -} +// @public @deprecated (undocumented) +export const StackOverflowQuestionsCollatorFactory: typeof StackOverflowQuestionsCollatorFactory_2; -// @public -export class StackOverflowQuestionsCollatorFactory - implements DocumentCollatorFactory -{ - // (undocumented) - execute(): AsyncGenerator; - // (undocumented) - static fromConfig( - config: Config, - options: StackOverflowQuestionsCollatorFactoryOptions, - ): StackOverflowQuestionsCollatorFactory; - // (undocumented) - getCollator(): Promise; - // (undocumented) - protected requestParams: StackOverflowQuestionsRequestParams; - // (undocumented) - readonly type: string; -} +// @public @deprecated (undocumented) +export type StackOverflowQuestionsCollatorFactoryOptions = + StackOverflowQuestionsCollatorFactory_2; -// @public -export type StackOverflowQuestionsCollatorFactoryOptions = { - baseUrl?: string; - maxPage?: number; - apiKey?: string; - apiAccessToken?: string; - teamName?: string; - requestParams?: StackOverflowQuestionsRequestParams; - logger: Logger; -}; - -// @public -export type StackOverflowQuestionsRequestParams = { - [key: string]: string | string[] | number; -}; +// @public @deprecated (undocumented) +export type StackOverflowQuestionsRequestParams = + StackOverflowQuestionsRequestParams_2; ``` diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index 53857082d2..5235c33f1a 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -5,22 +5,9 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public" - }, - "exports": { - ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", - "./package.json": "./package.json" - }, - "typesVersions": { - "*": { - "alpha": [ - "src/alpha.ts" - ], - "package.json": [ - "package.json" - ] - } + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" }, "backstage": { "role": "backend-plugin" @@ -46,10 +33,8 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", - "@backstage/backend-plugin-api": "workspace:^", - "@backstage/backend-tasks": "workspace:^", "@backstage/config": "workspace:^", - "@backstage/plugin-search-backend-node": "workspace:^", + "@backstage/plugin-search-backend-module-stack-overflow": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "node-fetch": "^2.6.7", "qs": "^6.9.4", @@ -58,6 +43,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-search-backend-node": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/stack-overflow-backend/src/index.ts b/plugins/stack-overflow-backend/src/index.ts index 6e461b8183..269748424b 100644 --- a/plugins/stack-overflow-backend/src/index.ts +++ b/plugins/stack-overflow-backend/src/index.ts @@ -20,4 +20,40 @@ * @packageDocumentation */ -export * from './search'; +import { + StackOverflowDocument as _StackOverflowDocument, + StackOverflowQuestionsRequestParams as _StackOverflowQuestionsRequestParams, + StackOverflowQuestionsCollatorFactory as _StackOverflowQuestionsCollatorFactory, + StackOverflowQuestionsCollatorFactoryOptions as _StackOverflowQuestionsCollatorFactoryOptions, +} from '@backstage/plugin-search-backend-module-stack-overflow'; + +/** + * @public + * @deprecated + * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + */ +export type StackOverflowDocument = _StackOverflowDocument; + +/** + * @public + * @deprecated + * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + */ +export type StackOverflowQuestionsRequestParams = + _StackOverflowQuestionsRequestParams; + +/** + * @public + * @deprecated + * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + */ +export type StackOverflowQuestionsCollatorFactoryOptions = + _StackOverflowQuestionsCollatorFactory; + +/** + * @public + * @deprecated + * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + */ +export const StackOverflowQuestionsCollatorFactory = + _StackOverflowQuestionsCollatorFactory; diff --git a/yarn.lock b/yarn.lock index bc7aa10d86..6e2db68238 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8847,6 +8847,25 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-search-backend-module-stack-overflow@workspace:^, @backstage/plugin-search-backend-module-stack-overflow@workspace:plugins/search-backend-module-stack-overflow": + version: 0.0.0-use.local + resolution: "@backstage/plugin-search-backend-module-stack-overflow@workspace:plugins/search-backend-module-stack-overflow" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-tasks": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-search-backend-node": "workspace:^" + "@backstage/plugin-search-common": "workspace:^" + msw: ^1.2.1 + node-fetch: ^2.6.7 + qs: ^6.9.4 + winston: ^3.2.1 + languageName: unknown + linkType: soft + "@backstage/plugin-search-backend-module-techdocs@workspace:^, @backstage/plugin-search-backend-module-techdocs@workspace:plugins/search-backend-module-techdocs": version: 0.0.0-use.local resolution: "@backstage/plugin-search-backend-module-techdocs@workspace:plugins/search-backend-module-techdocs" @@ -9182,16 +9201,15 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-stack-overflow-backend@workspace:^, @backstage/plugin-stack-overflow-backend@workspace:plugins/stack-overflow-backend": +"@backstage/plugin-stack-overflow-backend@workspace:plugins/stack-overflow-backend": version: 0.0.0-use.local resolution: "@backstage/plugin-stack-overflow-backend@workspace:plugins/stack-overflow-backend" dependencies: "@backstage/backend-common": "workspace:^" - "@backstage/backend-plugin-api": "workspace:^" - "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" + "@backstage/plugin-search-backend-module-stack-overflow": "workspace:^" "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" msw: ^1.0.0 @@ -25317,7 +25335,6 @@ __metadata: "@backstage/plugin-search-react": "workspace:^" "@backstage/plugin-sentry": "workspace:^" "@backstage/plugin-shortcuts": "workspace:^" - "@backstage/plugin-stack-overflow": "workspace:^" "@backstage/plugin-stackstorm": "workspace:^" "@backstage/plugin-tech-insights": "workspace:^" "@backstage/plugin-tech-radar": "workspace:^" @@ -25505,7 +25522,6 @@ __metadata: "@backstage/plugin-search-backend-module-techdocs": "workspace:^" "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-sonarqube-backend": "workspace:^" - "@backstage/plugin-stack-overflow-backend": "workspace:^" "@backstage/plugin-techdocs-backend": "workspace:^" "@backstage/plugin-todo-backend": "workspace:^" languageName: unknown From 20f8768427927c70311301568ce6e608da50c8fc Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 16:31:36 +0200 Subject: [PATCH 23/61] refactor(search): do not add stack overflow types on the default page Signed-off-by: Camila Belo --- plugins/search/src/alpha.tsx | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index 913a96d094..14e7067dfa 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -69,25 +69,6 @@ import { SearchType } from './components/SearchType'; import { UrlUpdater } from './components/SearchPage/SearchPage'; import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; -/** @internal */ -const StackOverflowIcon = () => { - return ( - - - - - ); -}; - /** @alpha */ export const SearchApi = createApiExtension({ factory: { @@ -172,11 +153,6 @@ export const SearchPage = createPageExtension({ name: 'Architecture Decision Records', icon: , }, - { - value: 'stack-overflow', - name: 'Stack Overflow', - icon: , - }, ]} /> From 0ea51599408c1664640b11a595009daf3a61ca15 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 16:55:34 +0200 Subject: [PATCH 24/61] fix: docs link to stack overflow collator Signed-off-by: Camila Belo --- docs/plugins/integrating-search-into-plugins.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/integrating-search-into-plugins.md b/docs/plugins/integrating-search-into-plugins.md index 7fb8e40b20..f2692d4e91 100644 --- a/docs/plugins/integrating-search-into-plugins.md +++ b/docs/plugins/integrating-search-into-plugins.md @@ -22,7 +22,7 @@ Imagine you have a plugin that is responsible for storing FAQ snippets in a data The search platform provides an interface (`DocumentCollatorFactory` from package `@backstage/plugin-search-common`) that allows you to do exactly that. It works by registering each of your entries as a "document" that later represents one search result each. -> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices. +> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices. #### 1. Install collator interface dependencies From 46f0f1700eb803a416bd389cf9d713b2c41939c9 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 17:15:15 +0200 Subject: [PATCH 25/61] fix: update changeset files Signed-off-by: Camila Belo --- .changeset/chilly-terms-behave.md | 5 +++++ .changeset/giant-cycles-end.md | 4 ++-- .changeset/mean-fans-cough.md | 5 ----- .changeset/nice-pillows-poke.md | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 .changeset/chilly-terms-behave.md delete mode 100644 .changeset/mean-fans-cough.md diff --git a/.changeset/chilly-terms-behave.md b/.changeset/chilly-terms-behave.md new file mode 100644 index 0000000000..47250364bf --- /dev/null +++ b/.changeset/chilly-terms-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-stack-overflow': patch +--- + +Extract a package for the Stack Overflow new backend system plugin. diff --git a/.changeset/giant-cycles-end.md b/.changeset/giant-cycles-end.md index 5d9f8149a8..6f637e05dd 100644 --- a/.changeset/giant-cycles-end.md +++ b/.changeset/giant-cycles-end.md @@ -2,6 +2,6 @@ '@backstage/plugin-stack-overflow-backend': patch --- -Migrate package to the new Frontend system, the new module is distributed with a `/alpha` subpath. +Deprecate package in favor of the new `@backstage/plugin-search-backend-module-stack-overflow` module. -The search collator `requestParams` option is optional now, so its defaults to `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as done in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). +The search collator `requestParams` option is optional now, so its default value is `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as defined in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). diff --git a/.changeset/mean-fans-cough.md b/.changeset/mean-fans-cough.md deleted file mode 100644 index 8f93cd361c..0000000000 --- a/.changeset/mean-fans-cough.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search': patch ---- - -Add Stack Overflow type to the default alpha search page extension types filter. diff --git a/.changeset/nice-pillows-poke.md b/.changeset/nice-pillows-poke.md index 1a3926f371..64034c39d2 100644 --- a/.changeset/nice-pillows-poke.md +++ b/.changeset/nice-pillows-poke.md @@ -2,4 +2,4 @@ '@backstage/plugin-stack-overflow': patch --- -Migrate package to the new Backend system, the new module is distributed with a `/alpha` subpath. +Migrate package to the new Frontend system, the new module is distributed with a `/alpha` subpath. From 0eaa698bb5a73a7f13b6bd4d1b51af84068d40f4 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 13 Oct 2023 09:38:33 +0200 Subject: [PATCH 26/61] chore(frontend-plugin-api): install frontend app api Signed-off-by: Camila Belo --- packages/frontend-plugin-api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index baf8cb4575..9cc3355ec3 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -24,7 +24,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@backstage/frontend-app-api": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^14.0.0", @@ -39,6 +38,7 @@ }, "dependencies": { "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-app-api": "workspace:^", "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", "@types/react": "^16.13.1 || ^17.0.0", From 3c39045e2bcee481ae410e6115b8abbc449147dc Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 14 Oct 2023 12:08:57 +0200 Subject: [PATCH 27/61] refactor: rename stack overflow backend module Signed-off-by: Camila Belo --- .changeset/chilly-terms-behave.md | 2 +- .changeset/giant-cycles-end.md | 2 +- .../integrating-search-into-plugins.md | 2 +- .../.eslintrc.js | 0 .../README.md | 97 +++++++++++++++++++ .../alpha-api-report.md | 2 +- .../api-report.md | 2 +- .../catalog-info.yaml | 4 +- .../config.d.ts | 0 .../package.json | 2 +- .../src/alpha.test.ts | 0 .../src/alpha.ts | 0 ...ckOverflowQuestionsCollatorFactory.test.ts | 0 .../StackOverflowQuestionsCollatorFactory.ts | 0 .../src/collators/index.ts | 0 .../src/index.ts | 0 .../README.md | 30 ------ plugins/stack-overflow-backend/api-report.md | 6 +- plugins/stack-overflow-backend/package.json | 2 +- plugins/stack-overflow-backend/src/index.ts | 10 +- yarn.lock | 6 +- 21 files changed, 117 insertions(+), 50 deletions(-) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/.eslintrc.js (100%) create mode 100644 plugins/search-backend-module-stack-overflow-collator/README.md rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/alpha-api-report.md (92%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/api-report.md (98%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/catalog-info.yaml (79%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/config.d.ts (100%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/package.json (99%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/src/alpha.test.ts (100%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/src/alpha.ts (100%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/src/collators/StackOverflowQuestionsCollatorFactory.test.ts (100%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/src/collators/StackOverflowQuestionsCollatorFactory.ts (100%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/src/collators/index.ts (100%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/src/index.ts (100%) delete mode 100644 plugins/search-backend-module-stack-overflow/README.md diff --git a/.changeset/chilly-terms-behave.md b/.changeset/chilly-terms-behave.md index 47250364bf..b514bc45c8 100644 --- a/.changeset/chilly-terms-behave.md +++ b/.changeset/chilly-terms-behave.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-search-backend-module-stack-overflow': patch +'@backstage/plugin-search-backend-module-stack-overflow-collator': patch --- Extract a package for the Stack Overflow new backend system plugin. diff --git a/.changeset/giant-cycles-end.md b/.changeset/giant-cycles-end.md index 6f637e05dd..f69938ea25 100644 --- a/.changeset/giant-cycles-end.md +++ b/.changeset/giant-cycles-end.md @@ -2,6 +2,6 @@ '@backstage/plugin-stack-overflow-backend': patch --- -Deprecate package in favor of the new `@backstage/plugin-search-backend-module-stack-overflow` module. +Deprecate package in favor of the new `@backstage/plugin-search-backend-module-stack-overflow-collator` module. The search collator `requestParams` option is optional now, so its default value is `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as defined in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). diff --git a/docs/plugins/integrating-search-into-plugins.md b/docs/plugins/integrating-search-into-plugins.md index f2692d4e91..28c8b410e3 100644 --- a/docs/plugins/integrating-search-into-plugins.md +++ b/docs/plugins/integrating-search-into-plugins.md @@ -22,7 +22,7 @@ Imagine you have a plugin that is responsible for storing FAQ snippets in a data The search platform provides an interface (`DocumentCollatorFactory` from package `@backstage/plugin-search-common`) that allows you to do exactly that. It works by registering each of your entries as a "document" that later represents one search result each. -> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices. +> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices. #### 1. Install collator interface dependencies diff --git a/plugins/search-backend-module-stack-overflow/.eslintrc.js b/plugins/search-backend-module-stack-overflow-collator/.eslintrc.js similarity index 100% rename from plugins/search-backend-module-stack-overflow/.eslintrc.js rename to plugins/search-backend-module-stack-overflow-collator/.eslintrc.js diff --git a/plugins/search-backend-module-stack-overflow-collator/README.md b/plugins/search-backend-module-stack-overflow-collator/README.md new file mode 100644 index 0000000000..914746cbf9 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow-collator/README.md @@ -0,0 +1,97 @@ +# Stack Overflow Search Backend Module + +A plugin that provides stack overflow specific functionality that can be used in different ways (e.g. for search) to compose your Backstage App. + +## Getting started + +Before we begin, make sure: + +- You have created your own standalone Backstage app using @backstage/create-app and not using a fork of the backstage repository. If you haven't setup Backstage already, start [here](https://backstage.io/docs/getting-started/). + +To use any of the functionality this plugin provides, you need to start by configuring your App with the following config: + +```yaml +stackoverflow: + baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance +``` + +### Stack Overflow for Teams + +If you have a private Stack Overflow instance and/or a private Stack Overflow Team you will need to supply an API key or Personal Access Token. You can read more about how to set this up by going to [Stack Overflow's Help Page](https://stackoverflow.help/en/articles/4385859-stack-overflow-for-teams-api). + +The existing API key approach remains the default, to support the new v2.3 API and PAT authentication model you need to pass the team name and the new PAT into the existing apiAccessToken parameter to the new URL. See [15770](https://github.com/backstage/backstage/issues/15770) for more details. + +```yaml +stackoverflow: + baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance + apiKey: $STACK_OVERFLOW_API_KEY + apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN +``` + +```yaml +stackoverflow: + baseUrl: https://api.stackoverflowteams.com/2.3 # alternative: your internal stack overflow instance + teamName: $STACK_OVERFLOW_TEAM_NAME + apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN +``` + +## Areas of Responsibility + +This stack overflow backend plugin is primarily responsible for the following: + +- Provides a `StackOverflowQuestionsCollatorFactory`, which can be used in the search backend to index stack overflow questions to your Backstage Search. + +### Index Stack Overflow Questions to search + +Before you are able to start index stack overflow questions to search, you need to go through the [search getting started guide](https://backstage.io/docs/features/search/getting-started). + +When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, add the following code snippet to add the `StackOverflowQuestionsCollatorFactory`. Note that you can optionally modify the `requestParams`, otherwise it will defaults to `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as done in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). + +> Note: if your `baseUrl` is set to the external stack overflow api `https://api.stackexchange.com/2.2`, you can find optional and required parameters under the official API documentation under [`Usage of /questions GET`](https://api.stackexchange.com/docs/questions) + +```ts +indexBuilder.addCollator({ + schedule, + factory: StackOverflowQuestionsCollatorFactory.fromConfig(env.config, { + logger: env.logger, + requestParams: { + tagged: ['backstage'], + site: 'stackoverflow', + pagesize: 100, + }, + }), +}); +``` + +## New Backend System + +> DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below. + +This package exports a module that extends the search backend to also indexing the questions exposed by the [`Stack Overflow` API](https://api.stackexchange.com/docs/questions). + +### Installation + +Add the module package as a dependency: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-search-backend-module-stack-overflow-collator +``` + +Add the collator to your backend instance, along with the search plugin itself: + +```tsx +// packages/backend/src/index.ts +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); +backend.add(import('@backstage/plugin-search-backend/alpha')); +backend.add( + import( + '@backstage/plugin-search-backend-module-stack-overflow-collator/alpha' + ), +); +backend.start(); +``` + +You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `stackoverflow` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow/config.d.ts) for more details. diff --git a/plugins/search-backend-module-stack-overflow/alpha-api-report.md b/plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md similarity index 92% rename from plugins/search-backend-module-stack-overflow/alpha-api-report.md rename to plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md index 9d25cb0a7d..a77d5e9598 100644 --- a/plugins/search-backend-module-stack-overflow/alpha-api-report.md +++ b/plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/plugin-search-backend-module-stack-overflow" +## API Report File for "@backstage/plugin-search-backend-module-stack-overflow-collator" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/plugins/search-backend-module-stack-overflow/api-report.md b/plugins/search-backend-module-stack-overflow-collator/api-report.md similarity index 98% rename from plugins/search-backend-module-stack-overflow/api-report.md rename to plugins/search-backend-module-stack-overflow-collator/api-report.md index 795b30cac2..996260c4aa 100644 --- a/plugins/search-backend-module-stack-overflow/api-report.md +++ b/plugins/search-backend-module-stack-overflow-collator/api-report.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/plugin-search-backend-module-stack-overflow" +## API Report File for "@backstage/plugin-search-backend-module-stack-overflow-collator" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/plugins/search-backend-module-stack-overflow/catalog-info.yaml b/plugins/search-backend-module-stack-overflow-collator/catalog-info.yaml similarity index 79% rename from plugins/search-backend-module-stack-overflow/catalog-info.yaml rename to plugins/search-backend-module-stack-overflow-collator/catalog-info.yaml index 9075d1d591..ed73bd0eca 100644 --- a/plugins/search-backend-module-stack-overflow/catalog-info.yaml +++ b/plugins/search-backend-module-stack-overflow-collator/catalog-info.yaml @@ -1,8 +1,8 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: backstage-plugin-search-backend-module-stack-overflow - title: '@backstage/plugin-search-backend-module-stack-overflow' + name: backstage-plugin-search-backend-module-stack-overflow-collator + title: '@backstage/plugin-search-backend-module-stack-overflow-collator' description: A module for the search backend that exports stack overflow modules spec: lifecycle: experimental diff --git a/plugins/search-backend-module-stack-overflow/config.d.ts b/plugins/search-backend-module-stack-overflow-collator/config.d.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/config.d.ts rename to plugins/search-backend-module-stack-overflow-collator/config.d.ts diff --git a/plugins/search-backend-module-stack-overflow/package.json b/plugins/search-backend-module-stack-overflow-collator/package.json similarity index 99% rename from plugins/search-backend-module-stack-overflow/package.json rename to plugins/search-backend-module-stack-overflow-collator/package.json index 64ff4d32d3..6e0b484e40 100644 --- a/plugins/search-backend-module-stack-overflow/package.json +++ b/plugins/search-backend-module-stack-overflow-collator/package.json @@ -1,5 +1,5 @@ { - "name": "@backstage/plugin-search-backend-module-stack-overflow", + "name": "@backstage/plugin-search-backend-module-stack-overflow-collator", "description": "A module for the search backend that exports stack overflow modules", "version": "0.0.0", "main": "src/index.ts", diff --git a/plugins/search-backend-module-stack-overflow/src/alpha.test.ts b/plugins/search-backend-module-stack-overflow-collator/src/alpha.test.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/src/alpha.test.ts rename to plugins/search-backend-module-stack-overflow-collator/src/alpha.test.ts diff --git a/plugins/search-backend-module-stack-overflow/src/alpha.ts b/plugins/search-backend-module-stack-overflow-collator/src/alpha.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/src/alpha.ts rename to plugins/search-backend-module-stack-overflow-collator/src/alpha.ts diff --git a/plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.test.ts b/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.test.ts rename to plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts diff --git a/plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.ts b/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.ts rename to plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts diff --git a/plugins/search-backend-module-stack-overflow/src/collators/index.ts b/plugins/search-backend-module-stack-overflow-collator/src/collators/index.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/src/collators/index.ts rename to plugins/search-backend-module-stack-overflow-collator/src/collators/index.ts diff --git a/plugins/search-backend-module-stack-overflow/src/index.ts b/plugins/search-backend-module-stack-overflow-collator/src/index.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/src/index.ts rename to plugins/search-backend-module-stack-overflow-collator/src/index.ts diff --git a/plugins/search-backend-module-stack-overflow/README.md b/plugins/search-backend-module-stack-overflow/README.md deleted file mode 100644 index cba9617ea8..0000000000 --- a/plugins/search-backend-module-stack-overflow/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# search-backend-module-stack-overflow - -> DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below. - -This package exports a module that extends the search backend to also indexing the questions exposed by the [`Stack Overflow` API](https://api.stackexchange.com/docs/questions). - -## Installation - -Add the module package as a dependency: - -```bash -# From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-search-backend-module-stack-overflow -``` - -Add the collator to your backend instance, along with the search plugin itself: - -```tsx -// packages/backend/src/index.ts -import { createBackend } from '@backstage/backend-defaults'; - -const backend = createBackend(); -backend.add(import('@backstage/plugin-search-backend/alpha')); -backend.add( - import('@backstage/plugin-search-backend-module-stack-overflow/alpha'), -); -backend.start(); -``` - -You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `stackoverflow` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow/config.d.ts) for more details. diff --git a/plugins/stack-overflow-backend/api-report.md b/plugins/stack-overflow-backend/api-report.md index d113f7a0b6..14dcdf2007 100644 --- a/plugins/stack-overflow-backend/api-report.md +++ b/plugins/stack-overflow-backend/api-report.md @@ -3,9 +3,9 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { StackOverflowDocument as StackOverflowDocument_2 } from '@backstage/plugin-search-backend-module-stack-overflow'; -import { StackOverflowQuestionsCollatorFactory as StackOverflowQuestionsCollatorFactory_2 } from '@backstage/plugin-search-backend-module-stack-overflow'; -import { StackOverflowQuestionsRequestParams as StackOverflowQuestionsRequestParams_2 } from '@backstage/plugin-search-backend-module-stack-overflow'; +import { StackOverflowDocument as StackOverflowDocument_2 } from '@backstage/plugin-search-backend-module-stack-overflow-collator'; +import { StackOverflowQuestionsCollatorFactory as StackOverflowQuestionsCollatorFactory_2 } from '@backstage/plugin-search-backend-module-stack-overflow-collator'; +import { StackOverflowQuestionsRequestParams as StackOverflowQuestionsRequestParams_2 } from '@backstage/plugin-search-backend-module-stack-overflow-collator'; // @public @deprecated (undocumented) export type StackOverflowDocument = StackOverflowDocument_2; diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index 5235c33f1a..c7b65a5ef5 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -34,7 +34,7 @@ "dependencies": { "@backstage/backend-common": "workspace:^", "@backstage/config": "workspace:^", - "@backstage/plugin-search-backend-module-stack-overflow": "workspace:^", + "@backstage/plugin-search-backend-module-stack-overflow-collator": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "node-fetch": "^2.6.7", "qs": "^6.9.4", diff --git a/plugins/stack-overflow-backend/src/index.ts b/plugins/stack-overflow-backend/src/index.ts index 269748424b..e676de15e2 100644 --- a/plugins/stack-overflow-backend/src/index.ts +++ b/plugins/stack-overflow-backend/src/index.ts @@ -25,19 +25,19 @@ import { StackOverflowQuestionsRequestParams as _StackOverflowQuestionsRequestParams, StackOverflowQuestionsCollatorFactory as _StackOverflowQuestionsCollatorFactory, StackOverflowQuestionsCollatorFactoryOptions as _StackOverflowQuestionsCollatorFactoryOptions, -} from '@backstage/plugin-search-backend-module-stack-overflow'; +} from '@backstage/plugin-search-backend-module-stack-overflow-collator'; /** * @public * @deprecated - * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + * Import from `@backstage/plugin-search-backend-module-stack-overflow-collator` instead. */ export type StackOverflowDocument = _StackOverflowDocument; /** * @public * @deprecated - * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + * Import from `@backstage/plugin-search-backend-module-stack-overflow-collator` instead. */ export type StackOverflowQuestionsRequestParams = _StackOverflowQuestionsRequestParams; @@ -45,7 +45,7 @@ export type StackOverflowQuestionsRequestParams = /** * @public * @deprecated - * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + * Import from `@backstage/plugin-search-backend-module-stack-overflow-collator` instead. */ export type StackOverflowQuestionsCollatorFactoryOptions = _StackOverflowQuestionsCollatorFactory; @@ -53,7 +53,7 @@ export type StackOverflowQuestionsCollatorFactoryOptions = /** * @public * @deprecated - * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + * Import from `@backstage/plugin-search-backend-module-stack-overflow-collator` instead. */ export const StackOverflowQuestionsCollatorFactory = _StackOverflowQuestionsCollatorFactory; diff --git a/yarn.lock b/yarn.lock index 6e2db68238..1267ee2cb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8847,9 +8847,9 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-search-backend-module-stack-overflow@workspace:^, @backstage/plugin-search-backend-module-stack-overflow@workspace:plugins/search-backend-module-stack-overflow": +"@backstage/plugin-search-backend-module-stack-overflow-collator@workspace:^, @backstage/plugin-search-backend-module-stack-overflow-collator@workspace:plugins/search-backend-module-stack-overflow-collator": version: 0.0.0-use.local - resolution: "@backstage/plugin-search-backend-module-stack-overflow@workspace:plugins/search-backend-module-stack-overflow" + resolution: "@backstage/plugin-search-backend-module-stack-overflow-collator@workspace:plugins/search-backend-module-stack-overflow-collator" dependencies: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" @@ -9209,7 +9209,7 @@ __metadata: "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" - "@backstage/plugin-search-backend-module-stack-overflow": "workspace:^" + "@backstage/plugin-search-backend-module-stack-overflow-collator": "workspace:^" "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" msw: ^1.0.0 From 95dc40505ac8bd01ec8c223a0cde04151b6c6e3d Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 14 Oct 2023 12:51:32 +0200 Subject: [PATCH 28/61] docs(stack-overflow): update api reports Signed-off-by: Camila Belo --- plugins/stack-overflow/alpha-api-report.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/stack-overflow/alpha-api-report.md b/plugins/stack-overflow/alpha-api-report.md index 84d7616759..8bf621b948 100644 --- a/plugins/stack-overflow/alpha-api-report.md +++ b/plugins/stack-overflow/alpha-api-report.md @@ -3,10 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyExternalRoutes } from '@backstage/core-plugin-api'; +import { AnyRoutes } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) -const _default: BackstagePlugin; +const _default: BackstagePlugin; export default _default; // (No @packageDocumentation comment for this package) From 3b5bef78b297df7ce846b12f2ecac91e3a34036e Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 18 Oct 2023 10:51:04 +0200 Subject: [PATCH 29/61] chore(stack-overflow-backend): deprecate entire package Signed-off-by: Camila Belo --- .../README.md | 2 +- plugins/stack-overflow-backend/README.md | 65 +------------------ plugins/stack-overflow-backend/package.json | 1 + plugins/stack-overflow-backend/src/index.ts | 5 +- plugins/stack-overflow/alpha-api-report.md | 4 +- 5 files changed, 8 insertions(+), 69 deletions(-) diff --git a/plugins/search-backend-module-stack-overflow-collator/README.md b/plugins/search-backend-module-stack-overflow-collator/README.md index 914746cbf9..5fba2a3c79 100644 --- a/plugins/search-backend-module-stack-overflow-collator/README.md +++ b/plugins/search-backend-module-stack-overflow-collator/README.md @@ -94,4 +94,4 @@ backend.add( backend.start(); ``` -You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `stackoverflow` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow/config.d.ts) for more details. +You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `stackoverflow` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/config.d.ts) for more details. diff --git a/plugins/stack-overflow-backend/README.md b/plugins/stack-overflow-backend/README.md index 5de997faa6..8129d36e38 100644 --- a/plugins/stack-overflow-backend/README.md +++ b/plugins/stack-overflow-backend/README.md @@ -1,64 +1,3 @@ -# Stack Overflow +# Stack Overflow Backend -A plugin that provides stack overflow specific functionality that can be used in different ways (e.g. for search) to compose your Backstage App. - -## Getting started - -Before we begin, make sure: - -- You have created your own standalone Backstage app using @backstage/create-app and not using a fork of the backstage repository. If you haven't setup Backstage already, start [here](https://backstage.io/docs/getting-started/). - -To use any of the functionality this plugin provides, you need to start by configuring your App with the following config: - -```yaml -stackoverflow: - baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance -``` - -### Stack Overflow for Teams - -If you have a private Stack Overflow instance and/or a private Stack Overflow Team you will need to supply an API key or Personal Access Token. You can read more about how to set this up by going to [Stack Overflow's Help Page](https://stackoverflow.help/en/articles/4385859-stack-overflow-for-teams-api). - -The existing API key approach remains the default, to support the new v2.3 API and PAT authentication model you need to pass the team name and the new PAT into the existing apiAccessToken parameter to the new URL. See [15770](https://github.com/backstage/backstage/issues/15770) for more details. - -```yaml -stackoverflow: - baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance - apiKey: $STACK_OVERFLOW_API_KEY - apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN -``` - -```yaml -stackoverflow: - baseUrl: https://api.stackoverflowteams.com/2.3 # alternative: your internal stack overflow instance - teamName: $STACK_OVERFLOW_TEAM_NAME - apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN -``` - -## Areas of Responsibility - -This stack overflow backend plugin is primarily responsible for the following: - -- Provides a `StackOverflowQuestionsCollatorFactory`, which can be used in the search backend to index stack overflow questions to your Backstage Search. - -### Index Stack Overflow Questions to search - -Before you are able to start index stack overflow questions to search, you need to go through the [search getting started guide](https://backstage.io/docs/features/search/getting-started). - -When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, add the following code snippet to add the `StackOverflowQuestionsCollatorFactory`. Note that you can optionally modify the `requestParams`, otherwise it will defaults to `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as done in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). - -> Note: if your `baseUrl` is set to the external stack overflow api `https://api.stackexchange.com/2.2`, you can find optional and required parameters under the official API documentation under [`Usage of /questions GET`](https://api.stackexchange.com/docs/questions) - -```ts -indexBuilder.addCollator({ - schedule, - factory: StackOverflowQuestionsCollatorFactory.fromConfig(env.config, { - logger: env.logger, - requestParams: { - tagged: ['backstage'], - site: 'stackoverflow', - pagesize: 100, - }, - }), -}); -``` +Deprecated, consider using `@backstage/plugin-search-backend-module-stack-overflow-collator` instead. diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index c7b65a5ef5..77ebd8c69e 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -1,5 +1,6 @@ { "name": "@backstage/plugin-stack-overflow-backend", + "description": "Deprecated, consider using @backstage/plugin-search-backend-module-stack-overflow-collator instead", "version": "0.2.10", "main": "src/index.ts", "types": "src/index.ts", diff --git a/plugins/stack-overflow-backend/src/index.ts b/plugins/stack-overflow-backend/src/index.ts index e676de15e2..5ca1c8d283 100644 --- a/plugins/stack-overflow-backend/src/index.ts +++ b/plugins/stack-overflow-backend/src/index.ts @@ -15,9 +15,10 @@ */ /** - * Stack Overflow backend plugin - * * @packageDocumentation + * Stack Overflow backend plugin + * @deprecated + * Deprecated, consider using `@backstage/plugin-search-backend-module-stack-overflow-collator` instead. */ import { diff --git a/plugins/stack-overflow/alpha-api-report.md b/plugins/stack-overflow/alpha-api-report.md index 8bf621b948..167f8e1736 100644 --- a/plugins/stack-overflow/alpha-api-report.md +++ b/plugins/stack-overflow/alpha-api-report.md @@ -3,12 +3,10 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { AnyExternalRoutes } from '@backstage/core-plugin-api'; -import { AnyRoutes } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) -const _default: BackstagePlugin; +const _default: BackstagePlugin<{}, {}>; export default _default; // (No @packageDocumentation comment for this package) From d7a4e2225f7cadb61240cb44ff4ccef72f54f190 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 09:10:35 +0200 Subject: [PATCH 30/61] fix: changeset package version Signed-off-by: Camila Belo --- .changeset/chilly-terms-behave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/chilly-terms-behave.md b/.changeset/chilly-terms-behave.md index b514bc45c8..8ee1ad8375 100644 --- a/.changeset/chilly-terms-behave.md +++ b/.changeset/chilly-terms-behave.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-search-backend-module-stack-overflow-collator': patch +'@backstage/plugin-search-backend-module-stack-overflow-collator': minor --- Extract a package for the Stack Overflow new backend system plugin. From 8a7f1ba2a511973bf1f82dc408e4526c88208b5f Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 09:11:33 +0200 Subject: [PATCH 31/61] docs(stack-overflow): add note about missing hompage card extension Signed-off-by: Camila Belo --- plugins/stack-overflow/src/alpha.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/stack-overflow/src/alpha.tsx b/plugins/stack-overflow/src/alpha.tsx index 18f0a0df1c..3708271ef1 100644 --- a/plugins/stack-overflow/src/alpha.tsx +++ b/plugins/stack-overflow/src/alpha.tsx @@ -44,5 +44,6 @@ const StackOverflowSearchResultListItem = createSearchResultListItemExtension({ /** @alpha */ export default createPlugin({ id: 'stack-overflow', + // TODO: Migrate homepage cards when the declarative homepage plugin supports them extensions: [StackOverflowApi, StackOverflowSearchResultListItem], }); From e13e43a12509f72ae8322cbab1dc1f665c8c45b5 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 09:28:38 +0200 Subject: [PATCH 32/61] refactor(stack-overflow-backend): remove alpha subpath Signed-off-by: Camila Belo --- .../README.md | 4 +--- .../alpha-api-report.md | 11 ----------- .../api-report.md | 5 +++++ .../package.json | 19 +++---------------- .../src/index.ts | 1 + ...SearchStackOverflowCollatorModule.test.ts} | 6 +++--- .../SearchStackOverflowCollatorModule.ts} | 14 ++++---------- .../src/module/index.ts | 17 +++++++++++++++++ 8 files changed, 34 insertions(+), 43 deletions(-) delete mode 100644 plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md rename plugins/search-backend-module-stack-overflow-collator/src/{alpha.test.ts => module/SearchStackOverflowCollatorModule.test.ts} (90%) rename plugins/search-backend-module-stack-overflow-collator/src/{alpha.ts => module/SearchStackOverflowCollatorModule.ts} (88%) create mode 100644 plugins/search-backend-module-stack-overflow-collator/src/module/index.ts diff --git a/plugins/search-backend-module-stack-overflow-collator/README.md b/plugins/search-backend-module-stack-overflow-collator/README.md index 5fba2a3c79..7df3229eeb 100644 --- a/plugins/search-backend-module-stack-overflow-collator/README.md +++ b/plugins/search-backend-module-stack-overflow-collator/README.md @@ -87,9 +87,7 @@ import { createBackend } from '@backstage/backend-defaults'; const backend = createBackend(); backend.add(import('@backstage/plugin-search-backend/alpha')); backend.add( - import( - '@backstage/plugin-search-backend-module-stack-overflow-collator/alpha' - ), + import('@backstage/plugin-search-backend-module-stack-overflow-collator'), ); backend.start(); ``` diff --git a/plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md b/plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md deleted file mode 100644 index a77d5e9598..0000000000 --- a/plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md +++ /dev/null @@ -1,11 +0,0 @@ -## API Report File for "@backstage/plugin-search-backend-module-stack-overflow-collator" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; - -// @alpha -const _default: () => BackendFeature; -export default _default; -``` diff --git a/plugins/search-backend-module-stack-overflow-collator/api-report.md b/plugins/search-backend-module-stack-overflow-collator/api-report.md index 996260c4aa..9243e786ac 100644 --- a/plugins/search-backend-module-stack-overflow-collator/api-report.md +++ b/plugins/search-backend-module-stack-overflow-collator/api-report.md @@ -5,12 +5,17 @@ ```ts /// +import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { IndexableDocument } from '@backstage/plugin-search-common'; import { Logger } from 'winston'; import { Readable } from 'stream'; +// @public +const searchStackOverflowCollatorModule: () => BackendFeature; +export default searchStackOverflowCollatorModule; + // @public export interface StackOverflowDocument extends IndexableDocument { // (undocumented) diff --git a/plugins/search-backend-module-stack-overflow-collator/package.json b/plugins/search-backend-module-stack-overflow-collator/package.json index 6e0b484e40..14e547de8e 100644 --- a/plugins/search-backend-module-stack-overflow-collator/package.json +++ b/plugins/search-backend-module-stack-overflow-collator/package.json @@ -6,22 +6,9 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public" - }, - "exports": { - ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", - "./package.json": "./package.json" - }, - "typesVersions": { - "*": { - "alpha": [ - "src/alpha.ts" - ], - "package.json": [ - "package.json" - ] - } + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" }, "backstage": { "role": "backend-plugin-module" diff --git a/plugins/search-backend-module-stack-overflow-collator/src/index.ts b/plugins/search-backend-module-stack-overflow-collator/src/index.ts index eb1f7540de..00d57a7b27 100644 --- a/plugins/search-backend-module-stack-overflow-collator/src/index.ts +++ b/plugins/search-backend-module-stack-overflow-collator/src/index.ts @@ -20,3 +20,4 @@ */ export * from './collators'; +export { searchStackOverflowCollatorModule as default } from './module'; diff --git a/plugins/search-backend-module-stack-overflow-collator/src/alpha.test.ts b/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.test.ts similarity index 90% rename from plugins/search-backend-module-stack-overflow-collator/src/alpha.test.ts rename to plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.test.ts index 1dba1ab8c0..a785ead6eb 100644 --- a/plugins/search-backend-module-stack-overflow-collator/src/alpha.test.ts +++ b/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.test.ts @@ -16,9 +16,9 @@ import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; -import searchModuleStackOverflowCollator from './alpha'; +import { searchStackOverflowCollatorModule } from './SearchStackOverflowCollatorModule'; -describe('searchModuleStackOverflowCollator', () => { +describe('searchStackOverflowCollatorModule', () => { const schedule = { frequency: { minutes: 10 }, timeout: { minutes: 15 }, @@ -35,7 +35,7 @@ describe('searchModuleStackOverflowCollator', () => { [searchIndexRegistryExtensionPoint, extensionPointMock], ], features: [ - searchModuleStackOverflowCollator(), + searchStackOverflowCollatorModule(), mockServices.rootConfig.factory({ data: { stackoverflow: { diff --git a/plugins/search-backend-module-stack-overflow-collator/src/alpha.ts b/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.ts similarity index 88% rename from plugins/search-backend-module-stack-overflow-collator/src/alpha.ts rename to plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.ts index 85b686c9ed..3ef3d72c32 100644 --- a/plugins/search-backend-module-stack-overflow-collator/src/alpha.ts +++ b/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.ts @@ -21,20 +21,14 @@ import { createBackendModule, } from '@backstage/backend-plugin-api'; import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; -import { StackOverflowQuestionsCollatorFactory } from './collators'; - -/** - * @packageDocumentation - * A module for the search backend that exports Stack Overflow modules. - */ +import { StackOverflowQuestionsCollatorFactory } from '../collators'; /** + * @public * Search backend module for the Stack Overflow index. - * - * @alpha */ -export default createBackendModule({ - moduleId: 'stackoverflowCollator', +export const searchStackOverflowCollatorModule = createBackendModule({ + moduleId: 'stackOverflowCollator', pluginId: 'search', register(env) { env.registerInit({ diff --git a/plugins/search-backend-module-stack-overflow-collator/src/module/index.ts b/plugins/search-backend-module-stack-overflow-collator/src/module/index.ts new file mode 100644 index 0000000000..ab424398e0 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow-collator/src/module/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { searchStackOverflowCollatorModule } from './SearchStackOverflowCollatorModule'; From ed71a018b1ece87f2e73e15eeac18a8555569308 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 12 Oct 2023 22:08:03 +0200 Subject: [PATCH 33/61] feat(frontend-plugin-api): improve extension boundary Signed-off-by: Camila Belo --- packages/frontend-plugin-api/package.json | 4 +- .../src/components/ErrorBoundary.tsx | 79 +++++++++++++++++++ .../src/components/ExtensionBoundary.tsx | 24 +++++- yarn.lock | 2 + 4 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 packages/frontend-plugin-api/src/components/ErrorBoundary.tsx diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 9cc3355ec3..528d86e0d1 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@backstage/frontend-app-api": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^14.0.0", @@ -37,10 +38,11 @@ "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "dependencies": { + "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", - "@backstage/frontend-app-api": "workspace:^", "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", + "@material-ui/core": "^4.12.4", "@types/react": "^16.13.1 || ^17.0.0", "lodash": "^4.17.21", "zod": "^3.21.4", diff --git a/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx b/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000000..f9ce9c9809 --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component, PropsWithChildren } from 'react'; +import { Button } from '@material-ui/core'; +import { BackstagePlugin } from '../wiring'; +import { ErrorPanel } from '@backstage/core-components'; + +type DefaultErrorBoundaryFallbackProps = PropsWithChildren<{ + plugin?: BackstagePlugin; + error: Error; + resetError: () => void; +}>; + +const DefaultErrorBoundaryFallback = ({ + plugin, + error, + resetError, +}: DefaultErrorBoundaryFallbackProps) => { + const title = `Error in ${plugin?.id}`; + + return ( + + + + ); +}; + +type ErrorBoundaryProps = PropsWithChildren<{ plugin?: BackstagePlugin }>; +type ErrorBoundaryState = { error?: Error }; + +/** @internal */ +export class ErrorBoundary extends Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + static getDerivedStateFromError(error: Error) { + return { error }; + } + + state: ErrorBoundaryState = { error: undefined }; + + handleErrorReset = () => { + this.setState({ error: undefined }); + }; + + render() { + const { error } = this.state; + const { plugin, children } = this.props; + + if (error) { + // TODO: use a configurable error boundary fallback + return ( + + ); + } + + return children; + } +} diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx index c6a218c15f..796908c619 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx @@ -15,15 +15,35 @@ */ import React, { ReactNode } from 'react'; +import { AnalyticsContext } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '../wiring'; +import { RouteRef } from '../routing'; +import { ErrorBoundary } from './ErrorBoundary'; +import { toInternalRouteRef } from '../routing/RouteRef'; /** @public */ export interface ExtensionBoundaryProps { - children: ReactNode; + id: string; source?: BackstagePlugin; + routeRef?: RouteRef; + children: ReactNode; } /** @public */ export function ExtensionBoundary(props: ExtensionBoundaryProps) { - return <>{props.children}; + const { id, source, routeRef, children } = props; + + const attributes = { + extension: id, + pluginId: source?.id, + routeRef: routeRef + ? toInternalRouteRef(routeRef).getDescription() + : undefined, + }; + + return ( + + {children} + + ); } diff --git a/yarn.lock b/yarn.lock index 1e2f6fdb5d..8999966568 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4246,11 +4246,13 @@ __metadata: resolution: "@backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api" dependencies: "@backstage/cli": "workspace:^" + "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@backstage/version-bridge": "workspace:^" + "@material-ui/core": ^4.12.4 "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^14.0.0 "@types/react": ^16.13.1 || ^17.0.0 From b2dcb92c36e8c142f1b9bb386505b31368a3cfc7 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 13 Oct 2023 15:57:50 +0200 Subject: [PATCH 34/61] test(frontend-plugin-api): cover extension boundary component Signed-off-by: Camila Belo --- .../src/components/ExtensionBoundary.test.tsx | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx new file mode 100644 index 0000000000..150ace717c --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect } from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import { + MockAnalyticsApi, + MockConfigApi, + TestApiProvider, + renderWithEffects, +} from '@backstage/test-utils'; +import { ExtensionBoundary } from './ExtensionBoundary'; +import { + Extension, + coreExtensionData, + createExtension, + createPlugin, +} from '../wiring'; +import { analyticsApiRef, useAnalytics } from '@backstage/core-plugin-api'; +import { createApp } from '@backstage/frontend-app-api'; +import { JsonObject } from '@backstage/types'; +import { createRouteRef } from '../routing'; +import { toInternalRouteRef } from '../routing/RouteRef'; + +function renderExtensionInTestApp( + extension: Extension, + options?: { + config?: JsonObject; + }, +) { + const { config = {} } = options ?? {}; + + const app = createApp({ + features: [ + createPlugin({ + id: 'plugin', + extensions: [extension], + }), + ], + configLoader: async () => new MockConfigApi(config), + }); + + return renderWithEffects(app.createRoot()); +} + +const wrapInBoundaryExtension = (element: JSX.Element) => { + const id = 'plugin.extension'; + const routeRef = createRouteRef(); + toInternalRouteRef(routeRef).setId(id); + return createExtension({ + id, + attachTo: { id: 'core.routes', input: 'routes' }, + output: { + element: coreExtensionData.reactElement, + path: coreExtensionData.routePath, + routeRef: coreExtensionData.routeRef.optional(), + }, + factory({ bind, source }) { + bind({ + routeRef, + path: '/', + element: ( + + {element} + + ), + }); + }, + }); +}; + +describe('ExtensionBoundary', () => { + it('should render children when there is no error', async () => { + const text = 'Text Component'; + const TextComponent = () => { + return

{text}

; + }; + await renderExtensionInTestApp(wrapInBoundaryExtension()); + await waitFor(() => expect(screen.getByText(text)).toBeInTheDocument()); + }); + + it('should show app error component when an error is thrown', async () => { + const error = 'Something went wrong'; + const ErrorComponent = () => { + throw new Error(error); + }; + await renderExtensionInTestApp(wrapInBoundaryExtension()); + await waitFor(() => expect(screen.getByText(error)).toBeInTheDocument()); + }); + + it('should wrap children with analytics context', async () => { + const action = 'render'; + const subject = 'analytics'; + const analyticsApiMock = new MockAnalyticsApi(); + + const AnalyticsComponent = () => { + const analytics = useAnalytics(); + useEffect(() => { + analytics.captureEvent(action, subject); + }, [analytics]); + return null; + }; + + await renderExtensionInTestApp( + wrapInBoundaryExtension( + + + , + ), + ); + + await waitFor(() => + expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + action, + subject, + context: { + extension: 'plugin.extension', + routeRef: 'plugin.extension', + }, + }), + ); + }); +}); From 2610774e4b281d5239788fced55ead61104ef02c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 12 Oct 2023 22:08:32 +0200 Subject: [PATCH 35/61] feat(frontend-plugin-api): create extension suspense Signed-off-by: Camila Belo --- .../src/components/ExtensionSuspense.tsx | 33 +++++++++++++++++++ .../src/components/index.ts | 5 +++ 2 files changed, 38 insertions(+) create mode 100644 packages/frontend-plugin-api/src/components/ExtensionSuspense.tsx diff --git a/packages/frontend-plugin-api/src/components/ExtensionSuspense.tsx b/packages/frontend-plugin-api/src/components/ExtensionSuspense.tsx new file mode 100644 index 0000000000..e80f59f09c --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ExtensionSuspense.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactNode, Suspense } from 'react'; +import { useApp } from '@backstage/core-plugin-api'; + +/** @public */ +export interface ExtensionSuspenseProps { + children: ReactNode; +} + +/** @public */ +export function ExtensionSuspense(props: ExtensionSuspenseProps) { + const { children } = props; + + const app = useApp(); + const { Progress } = app.getComponents(); + + return }>{children}; +} diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts index 7056a59271..fecd7f36cc 100644 --- a/packages/frontend-plugin-api/src/components/index.ts +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -18,3 +18,8 @@ export { ExtensionBoundary, type ExtensionBoundaryProps, } from './ExtensionBoundary'; + +export { + ExtensionSuspense, + type ExtensionSuspenseProps, +} from './ExtensionSuspense'; From 055b7aec3da8b0b04801525d8180d47ea8978d36 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 13 Oct 2023 10:56:10 +0200 Subject: [PATCH 36/61] test(frontend-plugin-api): cover extension suspense component Signed-off-by: Camila Belo --- .../src/components/ExtensionSuspense.test.tsx | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 packages/frontend-plugin-api/src/components/ExtensionSuspense.test.tsx diff --git a/packages/frontend-plugin-api/src/components/ExtensionSuspense.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionSuspense.test.tsx new file mode 100644 index 0000000000..166c57b83b --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ExtensionSuspense.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { lazy } from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { ExtensionSuspense } from './ExtensionSuspense'; + +describe('ExtensionSuspense', () => { + it('should render the app progress component as fallback', async () => { + const LazyComponent = lazy(() => new Promise(() => {})); + + await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + expect(screen.getByTestId('progress')).toBeInTheDocument(); + }); + + it('should render the lazy loaded children component', async () => { + const LazyComponent = lazy(() => + Promise.resolve({ default: () =>
Lazy Component
}), + ); + + await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + await waitFor(() => + expect(screen.getByText('Lazy Component')).toBeInTheDocument(), + ); + }); +}); From 103b405746ada473d44cf9c01738c65010238592 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 10:05:37 +0200 Subject: [PATCH 37/61] feat(frontend-plugin-api): use extension boundary and suspense Signed-off-by: Camila Belo --- .../src/extensions/createPageExtension.tsx | 53 +++++++++++++------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index 217fb33ce5..ad7db1d222 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -14,18 +14,21 @@ * limitations under the License. */ -import React from 'react'; -import { ExtensionBoundary } from '../components'; +import React, { lazy, useEffect } from 'react'; +import { useAnalytics } from '@backstage/core-plugin-api'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker'; +import { ExtensionBoundary, ExtensionSuspense } from '../components'; import { createSchemaFromZod, PortableSchema } from '../schema'; import { coreExtensionData, createExtension, Extension, ExtensionInputValues, + AnyExtensionInputMap, } from '../wiring'; -import { AnyExtensionInputMap } from '../wiring/createExtension'; -import { Expand } from '../types'; import { RouteRef } from '../routing'; +import { Expand } from '../types'; /** * Helper for creating extensions for a routable React page component. @@ -55,6 +58,10 @@ export function createPageExtension< }) => Promise; }, ): Extension { + const { id, routeRef } = options; + + const attachTo = options.attachTo ?? { id: 'core.routes', input: 'routes' }; + const configSchema = 'configSchema' in options ? options.configSchema @@ -63,33 +70,49 @@ export function createPageExtension< ) as PortableSchema); return createExtension({ - id: options.id, - attachTo: options.attachTo ?? { id: 'core.routes', input: 'routes' }, + id, + attachTo, + configSchema, + inputs: options.inputs, disabled: options.disabled, output: { element: coreExtensionData.reactElement, path: coreExtensionData.routePath, routeRef: coreExtensionData.routeRef.optional(), }, - inputs: options.inputs, - configSchema, factory({ bind, config, inputs, source }) { - const LazyComponent = React.lazy(() => + const { path } = config; + + const PageComponent = lazy(() => options .loader({ config, inputs }) .then(element => ({ default: () => element })), ); + const ExtensionComponent = () => { + const analytics = useAnalytics(); + + // This event, never exposed to end-users of the analytics API, + // helps inform which extension metadata gets associated with a + // navigation event when the route navigated to is a gathered + // mountpoint. + useEffect(() => { + analytics.captureEvent(routableExtensionRenderedEvent, ''); + }, [analytics]); + + return ; + }; + bind({ - path: config.path, + path, + routeRef, element: ( - - - - + + + + ), - routeRef: options.routeRef, }); }, }); From 42a8ef3a4f2283de29d29f73eb3b94a6203f1d72 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 13 Oct 2023 20:58:03 +0200 Subject: [PATCH 38/61] test(frontend-plugin-api): cover create page extension Signed-off-by: Camila Belo --- .../extensions/createPageExtension.test.tsx | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx index 125bf3495d..37d97e7621 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx @@ -15,10 +15,23 @@ */ import React from 'react'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { useAnalytics } from '@backstage/core-plugin-api'; +import { waitFor } from '@testing-library/react'; import { PortableSchema } from '../schema'; -import { coreExtensionData, createExtensionInput } from '../wiring'; +import { + ExtensionInputValues, + coreExtensionData, + createExtensionInput, + createPlugin, +} from '../wiring'; import { createPageExtension } from './createPageExtension'; +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useAnalytics: jest.fn(), +})); + describe('createPageExtension', () => { it('creates the extension properly', () => { const configSchema: PortableSchema<{ path: string }> = { @@ -100,4 +113,35 @@ describe('createPageExtension', () => { factory: expect.any(Function), }); }); + + it('capture page view event in analytics', async () => { + const captureEvent = jest.fn(); + + (useAnalytics as jest.Mock).mockReturnValue({ + captureEvent, + }); + + const extension = createPageExtension({ + id: 'plugin.page', + defaultPath: '/', + loader: async () =>
Component
, + }); + + extension.factory({ + bind: (values: ExtensionInputValues) => + renderWithEffects( + wrapInTestApp(values.element as unknown as JSX.Element), + ), + source: createPlugin({ id: 'plugin ' }), + config: { path: '/' }, + inputs: {}, + }); + + await waitFor(() => + expect(captureEvent).toHaveBeenCalledWith( + '_ROUTABLE-EXTENSION-RENDERED', + '', + ), + ); + }); }); From c1e10b6175e59af190d005454096b36dc712527a Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 13 Oct 2023 09:23:51 +0200 Subject: [PATCH 39/61] docs(frontend-plugin-api): update api reports Signed-off-by: Camila Belo --- packages/frontend-plugin-api/api-report.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 3ed5965c99..b6f85e757b 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -338,6 +338,10 @@ export interface ExtensionBoundaryProps { // (undocumented) children: ReactNode; // (undocumented) + id: string; + // (undocumented) + routeRef?: RouteRef; + // (undocumented) source?: BackstagePlugin; } @@ -412,6 +416,17 @@ export interface ExtensionOverridesOptions { extensions: Extension[]; } +// @public (undocumented) +export function ExtensionSuspense( + props: ExtensionSuspenseProps, +): React_2.JSX.Element; + +// @public (undocumented) +export interface ExtensionSuspenseProps { + // (undocumented) + children: ReactNode; +} + // @public export interface ExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, From 6af88a05ff23e66d1c792e8fa6ba42fed4996c19 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 14 Oct 2023 10:38:48 +0200 Subject: [PATCH 40/61] chore(frontend-plugin-api): add changeset file Signed-off-by: Camila Belo --- .changeset/sixty-tips-argue.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sixty-tips-argue.md diff --git a/.changeset/sixty-tips-argue.md b/.changeset/sixty-tips-argue.md new file mode 100644 index 0000000000..9fc965a281 --- /dev/null +++ b/.changeset/sixty-tips-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Improve the extension boundary component and create a default extension suspense component. From b51919e3eb85f68916de6d930e079fa70ae8b55b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 13 Oct 2023 09:22:21 +0200 Subject: [PATCH 41/61] refactor(search-react): use extension boundary and suspense Signed-off-by: Camila Belo --- plugins/search-react/src/alpha.tsx | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha.tsx index 366112c95e..f28bc44b0d 100644 --- a/plugins/search-react/src/alpha.tsx +++ b/plugins/search-react/src/alpha.tsx @@ -14,18 +14,18 @@ * limitations under the License. */ -import React, { lazy, Suspense } from 'react'; +import React, { lazy } from 'react'; import { ListItemProps } from '@material-ui/core'; import { ExtensionBoundary, + ExtensionSuspense, PortableSchema, createExtension, createExtensionDataRef, createSchemaFromZod, } from '@backstage/frontend-plugin-api'; -import { Progress } from '@backstage/core-components'; import { SearchDocument, SearchResult } from '@backstage/plugin-search-common'; import { SearchResultListItemExtension } from './extensions'; @@ -87,6 +87,13 @@ export type SearchResultItemExtensionOptions< export function createSearchResultListItemExtension< TConfig extends { noTrack?: boolean }, >(options: SearchResultItemExtensionOptions) { + const id = `plugin.search.result.item.${options.id}`; + + const attachTo = options.attachTo ?? { + id: 'plugin.search.page', + input: 'items', + }; + const configSchema = 'configSchema' in options ? options.configSchema @@ -95,15 +102,16 @@ export function createSearchResultListItemExtension< noTrack: z.boolean().default(false), }), ) as PortableSchema); + return createExtension({ - id: `plugin.search.result.item.${options.id}`, - attachTo: options.attachTo ?? { id: 'plugin.search.page', input: 'items' }, + id, + attachTo, configSchema, output: { item: searchResultItemExtensionData, }, factory({ bind, config, source }) { - const LazyComponent = lazy(() => + const ExtensionComponent = lazy(() => options .component({ config }) .then(component => ({ default: component })), @@ -113,16 +121,16 @@ export function createSearchResultListItemExtension< item: { predicate: options.predicate, component: props => ( - - }> + + - + - + ), }, From e7c09c4f4b60d0107dcefe23a39ece2916b49ef3 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 14 Oct 2023 10:39:27 +0200 Subject: [PATCH 42/61] chore(search-react): add changeset file Signed-off-by: Camila Belo --- .changeset/young-days-talk.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/young-days-talk.md diff --git a/.changeset/young-days-talk.md b/.changeset/young-days-talk.md new file mode 100644 index 0000000000..b7420c7894 --- /dev/null +++ b/.changeset/young-days-talk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-react': patch +--- + +Use default extensions boundary and suspense on the alpha declarative `createSearchResultListItem` extension factory. From 11555a0da0153d52fbcd57dd94b8ff632539dbf9 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 11:32:39 +0200 Subject: [PATCH 43/61] refactor(catalog): forward plugin to extension boundary Signed-off-by: Camila Belo --- plugins/catalog/src/alpha.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx index 6eda8c3d26..27ec432d07 100644 --- a/plugins/catalog/src/alpha.tsx +++ b/plugins/catalog/src/alpha.tsx @@ -36,6 +36,7 @@ import { PortableSchema, ExtensionBoundary, createExtensionInput, + ExtensionSuspense, } from '@backstage/frontend-plugin-api'; import { AsyncEntityProvider, @@ -51,7 +52,6 @@ import { rootRouteRef, viewTechDocRouteRef, } from './routes'; -import { Progress } from '@backstage/core-components'; import { useEntityFromUrl } from './components/CatalogEntityPage/useEntityFromUrl'; /** @alpha */ @@ -97,16 +97,19 @@ export function createCatalogFilterExtension< configSchema?: PortableSchema; loader: (options: { config: TConfig }) => Promise; }) { + const id = `catalog.filter.${options.id}`; + const attachTo = { id: 'plugin.catalog.page.index', input: 'filters' }; + return createExtension({ - id: `catalog.filter.${options.id}`, - attachTo: { id: 'plugin.catalog.page.index', input: 'filters' }, + id, + attachTo, inputs: options.inputs ?? {}, configSchema: options.configSchema, output: { element: coreExtensionData.reactElement, }, factory({ bind, config, source }) { - const LazyComponent = React.lazy(() => + const ExtensionComponent = React.lazy(() => options .loader({ config }) .then(element => ({ default: () => element })), @@ -114,10 +117,10 @@ export function createCatalogFilterExtension< bind({ element: ( - - }> - - + + + + ), }); From e964c17db97ad0fdb27e9e2b2b6c19ad432e3e3a Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 11:49:47 +0200 Subject: [PATCH 44/61] chore(catalog): add changeset file Signed-off-by: Camila Belo --- .changeset/fluffy-years-shake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fluffy-years-shake.md diff --git a/.changeset/fluffy-years-shake.md b/.changeset/fluffy-years-shake.md new file mode 100644 index 0000000000..9612690d62 --- /dev/null +++ b/.changeset/fluffy-years-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Use default extensions boundary and suspense on the alpha declarative `createCatalogFilterExtension` extension factory. From 0fb1d8c7f8af8f53bc6aaf2928b78c8a836612ec Mon Sep 17 00:00:00 2001 From: Ankit Anand Date: Thu, 19 Oct 2023 16:46:23 +0530 Subject: [PATCH 45/61] removed mockdir.clear, replaced addContent with setContent Signed-off-by: Ankit Anand --- .../src/actions/fetch/rails/index.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts index 10b8466dc2..7548ddd21d 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts @@ -73,9 +73,6 @@ describe('fetch:rails', () => { createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), }; - mockDir.clear(); - mockDir.addContent({ template: {} }); - const mockReader: UrlReader = { readUrl: jest.fn(), readTree: jest.fn(), @@ -93,7 +90,7 @@ describe('fetch:rails', () => { }); beforeEach(() => { - mockDir.addContent({ + mockDir.setContent({ result: '{}', }); jest.clearAllMocks(); From c3c5c7e514f4c5145f176c575751a8023b88b006 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 19 Oct 2023 14:27:22 +0300 Subject: [PATCH 46/61] chore: add info about entity if tech docs building fails Signed-off-by: Heikki Hellgren --- .changeset/hip-mugs-camp.md | 5 +++++ .../src/service/DocsSynchronizer.test.ts | 2 +- .../techdocs-backend/src/service/DocsSynchronizer.ts | 10 ++++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 .changeset/hip-mugs-camp.md diff --git a/.changeset/hip-mugs-camp.md b/.changeset/hip-mugs-camp.md new file mode 100644 index 0000000000..3413b5ac0a --- /dev/null +++ b/.changeset/hip-mugs-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Add info about the entity when tech docs fail to build diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts index 52a3b82651..ff714eee63 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts @@ -258,7 +258,7 @@ describe('DocsSynchronizer', () => { expect(mockResponseHandler.log).toHaveBeenCalledTimes(1); expect(mockResponseHandler.log).toHaveBeenCalledWith( expect.stringMatching( - /error.*: Failed to build the docs page: Some random error/, + /error.*: Failed to build the docs page for entity component:default\/test: Some random error/, ), ); expect(mockResponseHandler.finish).toHaveBeenCalledTimes(0); diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts index 8456f64a6a..fad915b068 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts @@ -15,7 +15,11 @@ */ import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { + DEFAULT_NAMESPACE, + Entity, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError, NotFoundError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -142,7 +146,9 @@ export class DocsSynchronizer { } } catch (e) { assertError(e); - const msg = `Failed to build the docs page: ${e.message}`; + const msg = `Failed to build the docs page for entity ${stringifyEntityRef( + entity, + )}: ${e.message}`; taskLogger.error(msg); this.logger.error(msg, e); error(e); From 816493a7c50966b4e6c5491de754f764cc6ead37 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 13:55:47 +0200 Subject: [PATCH 47/61] docs: fix links to analytics-module-ga README Signed-off-by: Patrik Oldsberg --- docs/releases/v1.9.0-changelog.md | 2 +- docs/releases/v1.9.0-next.2-changelog.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/releases/v1.9.0-changelog.md b/docs/releases/v1.9.0-changelog.md index df14c3d34d..07451f6d2c 100644 --- a/docs/releases/v1.9.0-changelog.md +++ b/docs/releases/v1.9.0-changelog.md @@ -1299,7 +1299,7 @@ - d3fea4ae0a: Internal fixes to avoid implicit usage of globals - 3280711113: Updated dependency `msw` to `^0.49.0`. - 9516b0c355: Added support for sending virtual pageviews on `search` events in order to enable - Site Search functionality in GA. For more information consult [README](/plugins/analytics-module-ga/README.md#enabling-site-search) + Site Search functionality in GA. For more information consult [README](https://github.com/backstage/backstage/blob/master/plugins/analytics-module-ga/README.md#enabling-site-search) - Updated dependencies - @backstage/core-plugin-api@1.2.0 - @backstage/core-components@0.12.1 diff --git a/docs/releases/v1.9.0-next.2-changelog.md b/docs/releases/v1.9.0-next.2-changelog.md index a410da5263..2c6d495641 100644 --- a/docs/releases/v1.9.0-next.2-changelog.md +++ b/docs/releases/v1.9.0-next.2-changelog.md @@ -525,7 +525,7 @@ ### Patch Changes - 9516b0c355: Added support for sending virtual pageviews on `search` events in order to enable - Site Search functionality in GA. For more information consult [README](/plugins/analytics-module-ga/README.md#enabling-site-search) + Site Search functionality in GA. For more information consult [README](https://github.com/backstage/backstage/blob/master/plugins/analytics-module-ga/README.md#enabling-site-search) - Updated dependencies - @backstage/core-plugin-api@1.2.0-next.2 - @backstage/core-components@0.12.1-next.2 From 1b8f005aab095f83eb3652b049b1ab9192adfb2f Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 13:57:35 +0200 Subject: [PATCH 48/61] refactor(catalog): restore inline attachment Signed-off-by: Camila Belo --- plugins/catalog/src/alpha.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx index 27ec432d07..e9cb083978 100644 --- a/plugins/catalog/src/alpha.tsx +++ b/plugins/catalog/src/alpha.tsx @@ -98,11 +98,10 @@ export function createCatalogFilterExtension< loader: (options: { config: TConfig }) => Promise; }) { const id = `catalog.filter.${options.id}`; - const attachTo = { id: 'plugin.catalog.page.index', input: 'filters' }; return createExtension({ id, - attachTo, + attachTo: { id: 'plugin.catalog.page.index', input: 'filters' }, inputs: options.inputs ?? {}, configSchema: options.configSchema, output: { From cca1afc547100a9412a75c77e3ef15cf0f04d691 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 13:57:54 +0200 Subject: [PATCH 49/61] refactor(search): restore inline attachment Signed-off-by: Camila Belo --- plugins/search-react/src/alpha.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha.tsx index f28bc44b0d..322bd3e67b 100644 --- a/plugins/search-react/src/alpha.tsx +++ b/plugins/search-react/src/alpha.tsx @@ -89,11 +89,6 @@ export function createSearchResultListItemExtension< >(options: SearchResultItemExtensionOptions) { const id = `plugin.search.result.item.${options.id}`; - const attachTo = options.attachTo ?? { - id: 'plugin.search.page', - input: 'items', - }; - const configSchema = 'configSchema' in options ? options.configSchema @@ -105,7 +100,10 @@ export function createSearchResultListItemExtension< return createExtension({ id, - attachTo, + attachTo: options.attachTo ?? { + id: 'plugin.search.page', + input: 'items', + }, configSchema, output: { item: searchResultItemExtensionData, From 66af240ea692929df76d8f817e96595d648c44d6 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 14:54:35 +0200 Subject: [PATCH 50/61] refactor(frontend-plugin-api): merge boundary and suspense components Signed-off-by: Camila Belo --- packages/frontend-plugin-api/api-report.md | 13 +---- .../src/components/ErrorBoundary.tsx | 3 +- .../src/components/ExtensionBoundary.test.tsx | 6 +-- .../src/components/ExtensionBoundary.tsx | 47 ++++++++++++++----- .../src/components/index.ts | 5 -- .../src/extensions/createPageExtension.tsx | 41 ++++------------ plugins/catalog/src/alpha.tsx | 5 +- plugins/search-react/src/alpha.tsx | 17 +++---- 8 files changed, 57 insertions(+), 80 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index b6f85e757b..ac8d164fae 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -340,7 +340,7 @@ export interface ExtensionBoundaryProps { // (undocumented) id: string; // (undocumented) - routeRef?: RouteRef; + routable?: boolean; // (undocumented) source?: BackstagePlugin; } @@ -416,17 +416,6 @@ export interface ExtensionOverridesOptions { extensions: Extension[]; } -// @public (undocumented) -export function ExtensionSuspense( - props: ExtensionSuspenseProps, -): React_2.JSX.Element; - -// @public (undocumented) -export interface ExtensionSuspenseProps { - // (undocumented) - children: ReactNode; -} - // @public export interface ExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, diff --git a/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx b/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx index f9ce9c9809..1191b75a1d 100644 --- a/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx @@ -15,9 +15,10 @@ */ import React, { Component, PropsWithChildren } from 'react'; +// TODO: Dependency on MUI should be removed from core packages import { Button } from '@material-ui/core'; -import { BackstagePlugin } from '../wiring'; import { ErrorPanel } from '@backstage/core-components'; +import { BackstagePlugin } from '../wiring'; type DefaultErrorBoundaryFallbackProps = PropsWithChildren<{ plugin?: BackstagePlugin; diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx index 150ace717c..704cafda2d 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -33,7 +33,6 @@ import { analyticsApiRef, useAnalytics } from '@backstage/core-plugin-api'; import { createApp } from '@backstage/frontend-app-api'; import { JsonObject } from '@backstage/types'; import { createRouteRef } from '../routing'; -import { toInternalRouteRef } from '../routing/RouteRef'; function renderExtensionInTestApp( extension: Extension, @@ -59,7 +58,6 @@ function renderExtensionInTestApp( const wrapInBoundaryExtension = (element: JSX.Element) => { const id = 'plugin.extension'; const routeRef = createRouteRef(); - toInternalRouteRef(routeRef).setId(id); return createExtension({ id, attachTo: { id: 'core.routes', input: 'routes' }, @@ -73,7 +71,7 @@ const wrapInBoundaryExtension = (element: JSX.Element) => { routeRef, path: '/', element: ( - + {element} ), @@ -128,7 +126,7 @@ describe('ExtensionBoundary', () => { subject, context: { extension: 'plugin.extension', - routeRef: 'plugin.extension', + routeRef: 'unknown', }, }), ); diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx index 796908c619..a9ea297216 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx @@ -14,36 +14,59 @@ * limitations under the License. */ -import React, { ReactNode } from 'react'; -import { AnalyticsContext } from '@backstage/core-plugin-api'; +import React, { PropsWithChildren, ReactNode, useEffect } from 'react'; +import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '../wiring'; -import { RouteRef } from '../routing'; import { ErrorBoundary } from './ErrorBoundary'; -import { toInternalRouteRef } from '../routing/RouteRef'; +import { ExtensionSuspense } from './ExtensionSuspense'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker'; + +type RouteTrackerProps = PropsWithChildren<{ + disableTracking?: boolean; +}>; + +const RouteTracker = (props: RouteTrackerProps) => { + const { disableTracking, children } = props; + const analytics = useAnalytics(); + + // This event, never exposed to end-users of the analytics API, + // helps inform which extension metadata gets associated with a + // navigation event when the route navigated to is a gathered + // mountpoint. + useEffect(() => { + if (disableTracking) return; + analytics.captureEvent(routableExtensionRenderedEvent, ''); + }, [analytics, disableTracking]); + + return <>{children}; +}; /** @public */ export interface ExtensionBoundaryProps { id: string; source?: BackstagePlugin; - routeRef?: RouteRef; + routable?: boolean; children: ReactNode; } /** @public */ export function ExtensionBoundary(props: ExtensionBoundaryProps) { - const { id, source, routeRef, children } = props; + const { id, source, routable, children } = props; + // Skipping "routeRef" attribute in the new system, the extension "id" should provide more insight const attributes = { extension: id, pluginId: source?.id, - routeRef: routeRef - ? toInternalRouteRef(routeRef).getDescription() - : undefined, }; return ( - - {children} - + + + + {children} + + + ); } diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts index fecd7f36cc..7056a59271 100644 --- a/packages/frontend-plugin-api/src/components/index.ts +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -18,8 +18,3 @@ export { ExtensionBoundary, type ExtensionBoundaryProps, } from './ExtensionBoundary'; - -export { - ExtensionSuspense, - type ExtensionSuspenseProps, -} from './ExtensionSuspense'; diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index ad7db1d222..f05182149b 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -14,11 +14,8 @@ * limitations under the License. */ -import React, { lazy, useEffect } from 'react'; -import { useAnalytics } from '@backstage/core-plugin-api'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker'; -import { ExtensionBoundary, ExtensionSuspense } from '../components'; +import React, { lazy } from 'react'; +import { ExtensionBoundary } from '../components'; import { createSchemaFromZod, PortableSchema } from '../schema'; import { coreExtensionData, @@ -58,9 +55,7 @@ export function createPageExtension< }) => Promise; }, ): Extension { - const { id, routeRef } = options; - - const attachTo = options.attachTo ?? { id: 'core.routes', input: 'routes' }; + const { id } = options; const configSchema = 'configSchema' in options @@ -71,7 +66,7 @@ export function createPageExtension< return createExtension({ id, - attachTo, + attachTo: options.attachTo ?? { id: 'core.routes', input: 'routes' }, configSchema, inputs: options.inputs, disabled: options.disabled, @@ -81,36 +76,18 @@ export function createPageExtension< routeRef: coreExtensionData.routeRef.optional(), }, factory({ bind, config, inputs, source }) { - const { path } = config; - - const PageComponent = lazy(() => + const ExtensionComponent = lazy(() => options .loader({ config, inputs }) .then(element => ({ default: () => element })), ); - const ExtensionComponent = () => { - const analytics = useAnalytics(); - - // This event, never exposed to end-users of the analytics API, - // helps inform which extension metadata gets associated with a - // navigation event when the route navigated to is a gathered - // mountpoint. - useEffect(() => { - analytics.captureEvent(routableExtensionRenderedEvent, ''); - }, [analytics]); - - return ; - }; - bind({ - path, - routeRef, + path: config.path, + routeRef: options.routeRef, element: ( - - - - + + ), }); diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx index e9cb083978..31551c1930 100644 --- a/plugins/catalog/src/alpha.tsx +++ b/plugins/catalog/src/alpha.tsx @@ -36,7 +36,6 @@ import { PortableSchema, ExtensionBoundary, createExtensionInput, - ExtensionSuspense, } from '@backstage/frontend-plugin-api'; import { AsyncEntityProvider, @@ -117,9 +116,7 @@ export function createCatalogFilterExtension< bind({ element: ( - - - + ), }); diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha.tsx index 322bd3e67b..f937ef99cc 100644 --- a/plugins/search-react/src/alpha.tsx +++ b/plugins/search-react/src/alpha.tsx @@ -20,7 +20,6 @@ import { ListItemProps } from '@material-ui/core'; import { ExtensionBoundary, - ExtensionSuspense, PortableSchema, createExtension, createExtensionDataRef, @@ -120,15 +119,13 @@ export function createSearchResultListItemExtension< predicate: options.predicate, component: props => ( - - - - - + + + ), }, From 052998a180c8ee0b3351e26dcea849ad17d458f7 Mon Sep 17 00:00:00 2001 From: Niklas Aronsson Date: Thu, 19 Oct 2023 14:55:07 +0200 Subject: [PATCH 51/61] Use setContent instead of clear plus addContent Signed-off-by: Niklas Aronsson --- .../src/actions/fetch/cookiecutter.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index 3872b2e302..af63ca0e48 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -46,7 +46,7 @@ jest.mock( ); describe('fetch:cookiecutter', () => { - const mockDir = createMockDirectory(); + const mockDir = createMockDirectory({ mockOsTmpDir: true }); const integrations = ScmIntegrations.fromConfig( new ConfigReader({ integrations: { @@ -106,21 +106,20 @@ describe('fetch:cookiecutter', () => { output: jest.fn(), createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), }; - mockDir.clear(); - mockDir.addContent({ template: {} }); + mockDir.setContent({ template: {} }); commandExists.mockResolvedValue(null); // Mock when run container is called it creates some new files in the mock filesystem containerRunner.runContainer.mockImplementation(async () => { - mockDir.addContent({ + mockDir.setContent({ 'intermediate/testfile.json': '{}', }); }); // Mock when executeShellCommand is called it creates some new files in the mock filesystem executeShellCommand.mockImplementation(async () => { - mockDir.addContent({ + mockDir.setContent({ 'intermediate/testfile.json': '{}', }); }); From 81c8db208884271920b16682535fea161d77a7e1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 16:34:02 +0200 Subject: [PATCH 52/61] core-components: make RoutedTabs not explode when rendering without tabs Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- .changeset/dirty-ducks-behave.md | 5 +++++ .../src/components/TabbedLayout/RoutedTabs.tsx | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .changeset/dirty-ducks-behave.md diff --git a/.changeset/dirty-ducks-behave.md b/.changeset/dirty-ducks-behave.md new file mode 100644 index 0000000000..c7bea8ea0a --- /dev/null +++ b/.changeset/dirty-ducks-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Fix `RoutedTabs` so that it does not explode without tabs. diff --git a/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx b/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx index 2a0b0eae28..2c727efe1b 100644 --- a/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx +++ b/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx @@ -27,8 +27,8 @@ import { SubRoute } from './types'; export function useSelectedSubRoute(subRoutes: SubRoute[]): { index: number; - route: SubRoute; - element: JSX.Element; + route?: SubRoute; + element?: JSX.Element; } { const params = useParams(); @@ -44,7 +44,7 @@ export function useSelectedSubRoute(subRoutes: SubRoute[]): { b.path.replace(/\/\*$/, '').localeCompare(a.path.replace(/\/\*$/, '')), ); - const element = useRoutes(sortedRoutes) ?? subRoutes[0].children; + const element = useRoutes(sortedRoutes) ?? subRoutes[0]?.children; // TODO(Rugvip): Once we only support v6 stable we can always prefix // This avoids having a double / prefix for react-router v6 beta, which in turn breaks @@ -98,7 +98,7 @@ export function RoutedTabs(props: { routes: SubRoute[] }) { onChange={onTabChange} /> - + {element} From dc613f9bcfcdad42e2e5911d766ca5a7ae093cd0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 17:33:37 +0200 Subject: [PATCH 53/61] frontend-app-api: fix configuration schema Signed-off-by: Patrik Oldsberg --- .changeset/olive-paws-divide.md | 5 +++++ packages/frontend-app-api/config.d.ts | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/olive-paws-divide.md diff --git a/.changeset/olive-paws-divide.md b/.changeset/olive-paws-divide.md new file mode 100644 index 0000000000..688b6025bf --- /dev/null +++ b/.changeset/olive-paws-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Updated `app.extensions` configuration schema. diff --git a/packages/frontend-app-api/config.d.ts b/packages/frontend-app-api/config.d.ts index 9ec020fa56..28355f5376 100644 --- a/packages/frontend-app-api/config.d.ts +++ b/packages/frontend-app-api/config.d.ts @@ -34,17 +34,17 @@ export interface Config { /** * @deepVisibility frontend */ - extensions?: + extensions?: Array< | string | { [extensionId: string]: | boolean - | string | { - at?: string; - extension?: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; config?: unknown; }; - }; + } + >; }; } From 1f0b6b1e48b0002a2b6ef3515ac7927c38494f1e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 19:06:01 +0200 Subject: [PATCH 54/61] search: more reliable wait in test Signed-off-by: Patrik Oldsberg --- .../search/src/components/SearchModal/SearchModal.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index 274f9e21a1..6fce560800 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { configApiRef } from '@backstage/core-plugin-api'; @@ -203,9 +203,9 @@ describe('SearchModal', () => { expect.objectContaining({ term: 'term' }), ); - const input = screen.getByLabelText('Search'); + const input = screen.getByLabelText('Search'); await userEvent.clear(input); - await 'a tick'; + await waitFor(() => expect(input.value).toBe('')); await userEvent.type(input, 'new term{enter}'); expect(navigate).toHaveBeenCalledWith('/search?query=new term'); From 22ca64f117c29a50301ca7c56436720c2c10b1db Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Thu, 19 Oct 2023 12:02:23 -0400 Subject: [PATCH 55/61] fix(configLoader): properly resolve targets into absolute paths Signed-off-by: Phil Kuang --- .changeset/chatty-cobras-cheer.md | 5 +++++ .../config-loader/src/sources/ConfigSources.test.ts | 12 ++++++++++-- packages/config-loader/src/sources/ConfigSources.ts | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 .changeset/chatty-cobras-cheer.md diff --git a/.changeset/chatty-cobras-cheer.md b/.changeset/chatty-cobras-cheer.md new file mode 100644 index 0000000000..c8203e58c0 --- /dev/null +++ b/.changeset/chatty-cobras-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Correctly resolve config targets into absolute paths diff --git a/packages/config-loader/src/sources/ConfigSources.test.ts b/packages/config-loader/src/sources/ConfigSources.test.ts index 43feb3ca27..f095bbec42 100644 --- a/packages/config-loader/src/sources/ConfigSources.test.ts +++ b/packages/config-loader/src/sources/ConfigSources.test.ts @@ -95,6 +95,14 @@ describe('ConfigSources', () => { ), ).toEqual([{ name: 'FileConfigSource', path: '/config.yaml' }]); + expect( + mergeSources( + ConfigSources.defaultForTargets({ + targets: [{ type: 'path', target: 'config.yaml' }], + }), + ), + ).toEqual([{ name: 'FileConfigSource', path: resolvePath('config.yaml') }]); + const subFunc = async () => undefined; expect( mergeSources( @@ -172,8 +180,8 @@ describe('ConfigSources', () => { }), ), ).toEqual([ - { name: 'FileConfigSource', path: 'a.yaml' }, - { name: 'FileConfigSource', path: 'b.yaml' }, + { name: 'FileConfigSource', path: resolvePath('a.yaml') }, + { name: 'FileConfigSource', path: resolvePath('b.yaml') }, { name: 'EnvConfigSource', env: { HOME: '/' } }, ]); }); diff --git a/packages/config-loader/src/sources/ConfigSources.ts b/packages/config-loader/src/sources/ConfigSources.ts index 43123f87ea..03fc6d83d6 100644 --- a/packages/config-loader/src/sources/ConfigSources.ts +++ b/packages/config-loader/src/sources/ConfigSources.ts @@ -161,7 +161,7 @@ export class ConfigSources { } return FileConfigSource.create({ watch: options.watch, - path: arg.target, + path: resolvePath(arg.target), substitutionFunc: options.substitutionFunc, }); }); From 39cd2d7a1f9e2831b00e2ec3229e8df14af592f3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 17:02:33 +0200 Subject: [PATCH 56/61] starting point for entity page extension API Co-authored-by: Philipp Hugenroth Co-authored-by: Vincenzo Scamporlino Co-authored-by: Camila Belo Co-authored-by: Rickard Dybeck Co-authored-by: Ben Lambert Co-authored-by: Jack Palmer Co-authored-by: Elon Jefferson Signed-off-by: Patrik Oldsberg --- plugins/catalog/src/alpha.tsx | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx index 31551c1930..0753737740 100644 --- a/plugins/catalog/src/alpha.tsx +++ b/plugins/catalog/src/alpha.tsx @@ -283,3 +283,51 @@ export default createPlugin({ CatalogNavItem, ], }); + +/// IMAGINE THIS IS IN A DIFFERENT PLUGIN + +// inside the @backstage/plugin-github-pull-requests plugin +import { + createEntityCardExtension, + createEntityContentExtension, +} from '@backstage/plugin-catalog-react'; + +const githubPullRequestsPlugin = createPlugin({ + id: 'github-pull-requests', + extensions: [ + createEntityCardExtension({ + id: 'github-pull-requests', + loader: () => import('./PullRequestsCard').then(m => m.PullRequestsCard), + entityFilter: isPullRequestsAvailable, + }), + createEntityContentExtension({ + id: 'github-pull-requests', + defaultPath: 'github-pull-requests', + defaultTitle: 'GitHub Pull Requests', + loader: () => + import('./PullRequestsContent').then(m => m.PullRequestsContent), + entityFilter: isPullRequestsAvailable, + }), + ], +}); + +// /deployments +const deploymentsPlugin = createPlugin({ + id: 'github-pull-requests', + extensions: [ + createEntityCardExtension({ + id: 'github-pull-requests', + attachTo: { id: 'plugin.deployments.content', input: 'cards' }, + loader: () => import('./PullRequestsCard').then(m => m.PullRequestsCard), + entityFilter: isPullRequestsAvailable, + }), + createEntityContentExtension({ + id: 'github-pull-requests', + defaultPath: 'github-pull-requests', + defaultTitle: 'GitHub Pull Requests', + loader: () => + import('./PullRequestsContent').then(m => m.PullRequestsContent), + entityFilter: isPullRequestsAvailable, + }), + ], +}); From b34e8bf3a314537ac3fa16869072e4add05e3db5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 16:23:06 +0200 Subject: [PATCH 57/61] app-next: initial entity page implementation with cards and content Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- packages/app-next/src/App.tsx | 2 + .../app-next/src/examples/entityPages.tsx | 290 ++++++++++++++++++ plugins/catalog/src/alpha.tsx | 66 +--- 3 files changed, 293 insertions(+), 65 deletions(-) create mode 100644 packages/app-next/src/examples/entityPages.tsx diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 4e42d9b268..7ff47a3e89 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { createApp } from '@backstage/frontend-app-api'; import { pagesPlugin } from './examples/pagesPlugin'; +import { entityPagePlugins } from './examples/entityPages'; import graphiqlPlugin from '@backstage/plugin-graphiql/alpha'; import techRadarPlugin from '@backstage/plugin-tech-radar/alpha'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; @@ -120,6 +121,7 @@ const app = createApp({ techdocsPlugin, userSettingsPlugin, homePlugin, + ...entityPagePlugins, ...collectedLegacyPlugins, createExtensionOverrides({ extensions: [ diff --git a/packages/app-next/src/examples/entityPages.tsx b/packages/app-next/src/examples/entityPages.tsx new file mode 100644 index 0000000000..3b5b4cceb7 --- /dev/null +++ b/packages/app-next/src/examples/entityPages.tsx @@ -0,0 +1,290 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect } from 'react'; +import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; +import { + AnyExtensionInputMap, + Extension, + ExtensionBoundary, + ExtensionInputValues, + PortableSchema, + RouteRef, + coreExtensionData, + createExtension, + createExtensionDataRef, + createExtensionInput, + createPageExtension, + createPlugin, + createSchemaFromZod, +} from '@backstage/frontend-plugin-api'; +import { + AsyncEntityProvider, + EntityLoadingStatus, + catalogApiRef, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { Expand } from '../../../frontend-plugin-api/src/types'; +import { EntityAboutCard, EntityLayout } from '@backstage/plugin-catalog'; +import { + useApi, + errorApiRef, + useRouteRefParams, +} from '@backstage/core-plugin-api'; +import { useNavigate } from 'react-router'; +import useAsyncRetry from 'react-use/lib/useAsyncRetry'; +import Grid from '@material-ui/core/Grid'; + +export const useEntityFromUrl = (): EntityLoadingStatus => { + const { kind, namespace, name } = useRouteRefParams(entityRouteRef); + const navigate = useNavigate(); + const errorApi = useApi(errorApiRef); + const catalogApi = useApi(catalogApiRef); + + const { + value: entity, + error, + loading, + retry: refresh, + } = useAsyncRetry( + () => catalogApi.getEntityByRef({ kind, namespace, name }), + [catalogApi, kind, namespace, name], + ); + + useEffect(() => { + if (!name) { + errorApi.post(new Error('No name provided!')); + navigate('/'); + } + }, [errorApi, navigate, error, loading, entity, name]); + + return { entity, loading, error, refresh }; +}; + +export const titleExtensionDataRef = createExtensionDataRef( + 'plugin.catalog.entity.content.title', +); + +const CatalogEntityPage = createPageExtension({ + id: 'plugin.catalog.page.entity', + defaultPath: '/catalog/:namespace/:kind/:name', + routeRef: convertLegacyRouteRef(entityRouteRef), + inputs: { + contents: createExtensionInput({ + element: coreExtensionData.reactElement, + path: coreExtensionData.routePath, + routeRef: coreExtensionData.routeRef.optional(), + title: titleExtensionDataRef, + }), + }, + loader: async ({ inputs }) => { + const Component = () => { + return ( + + + {inputs.contents.map(content => ( + + {content.element} + + ))} + + + ); + }; + return ; + }, +}); + +export function createEntityCardExtension< + TConfig, + TInputs extends AnyExtensionInputMap, +>(options: { + id: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + configSchema?: PortableSchema; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; +}): Extension { + return createExtension({ + id: `entity.content.${options.id}`, + attachTo: options.attachTo ?? { + id: 'entity.content.overview', + input: 'cards', + }, + disabled: options.disabled ?? true, + output: { + element: coreExtensionData.reactElement, + }, + inputs: options.inputs, + configSchema: options.configSchema, + factory({ bind, config, inputs, source }) { + const LazyComponent = React.lazy(() => + options + .loader({ config, inputs }) + .then(element => ({ default: () => element })), + ); + + bind({ + element: ( + + + + + + ), + }); + }, + }); +} + +export function createEntityContentExtension< + TConfig extends { path: string; title: string }, + TInputs extends AnyExtensionInputMap, +>( + options: ( + | { + defaultPath: string; + defaultTitle: string; + } + | { + configSchema: PortableSchema; + } + ) & { + id: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + routeRef?: RouteRef; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; + }, +): Extension { + const configSchema = + 'configSchema' in options + ? options.configSchema + : (createSchemaFromZod(z => + z.object({ + path: z.string().default(options.defaultPath), + title: z.string().default(options.defaultTitle), + }), + ) as PortableSchema); + + return createExtension({ + id: `entity.content.${options.id}`, + attachTo: options.attachTo ?? { + id: 'plugin.catalog.page.entity', + input: 'contents', + }, + disabled: options.disabled ?? true, + output: { + element: coreExtensionData.reactElement, + path: coreExtensionData.routePath, + routeRef: coreExtensionData.routeRef.optional(), + title: titleExtensionDataRef, + }, + inputs: options.inputs, + configSchema, + factory({ bind, config, inputs, source }) { + const LazyComponent = React.lazy(() => + options + .loader({ config, inputs }) + .then(element => ({ default: () => element })), + ); + + bind({ + path: config.path, + element: ( + + + + + + ), + routeRef: options.routeRef, + title: config.title, + }); + }, + }); +} + +const entityAboutCardExtension = createEntityCardExtension({ + id: 'about', + disabled: false, + loader: async () => , + // entityFilter: isDerp, +}); + +const overviewContentExtension = createEntityContentExtension({ + id: 'overview', + defaultPath: '/', + defaultTitle: 'Overview', + disabled: false, + inputs: { + cards: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + loader: async ({ inputs }) => ( + + {inputs.cards.map(card => ( + + {card.element} + + ))} + + ), +}); + +const bonusTechdocsPlugin = createPlugin({ + id: 'techdocs-entity', + extensions: [ + createEntityContentExtension({ + id: 'techdocs', + defaultPath: 'docs', + defaultTitle: 'TechDocs', + disabled: false, + loader: () => + import('@backstage/plugin-techdocs').then(m => ( + + )), + // entityFilter: isPullRequestsAvailable, + }), + ], +}); + +export const entityPagePlugins = [ + createPlugin({ + id: 'entity-pages', + extensions: [ + CatalogEntityPage, + overviewContentExtension, + entityAboutCardExtension, + ], + }), + bonusTechdocsPlugin, + // deploymentsPlugin, +]; diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx index 0753737740..415525ab74 100644 --- a/plugins/catalog/src/alpha.tsx +++ b/plugins/catalog/src/alpha.tsx @@ -231,22 +231,6 @@ const CatalogIndexPage = createPageExtension({ }, }); -const CatalogEntityPage = createPageExtension({ - id: 'plugin.catalog.page.entity', - defaultPath: '/catalog/:namespace/:kind/:name', - routeRef: convertLegacyRouteRef(entityRouteRef), - loader: async () => { - const Component = () => { - return ( - -
🚧 Work In Progress
-
- ); - }; - return ; - }, -}); - const CatalogNavItem = createNavItemExtension({ id: 'catalog.nav.index', routeRef: convertLegacyRouteRef(rootRouteRef), @@ -279,55 +263,7 @@ export default createPlugin({ CatalogEntityProcessingStatusFilter, CatalogEntityNamespaceFilter, CatalogIndexPage, - CatalogEntityPage, + // CatalogEntityPage, CatalogNavItem, ], }); - -/// IMAGINE THIS IS IN A DIFFERENT PLUGIN - -// inside the @backstage/plugin-github-pull-requests plugin -import { - createEntityCardExtension, - createEntityContentExtension, -} from '@backstage/plugin-catalog-react'; - -const githubPullRequestsPlugin = createPlugin({ - id: 'github-pull-requests', - extensions: [ - createEntityCardExtension({ - id: 'github-pull-requests', - loader: () => import('./PullRequestsCard').then(m => m.PullRequestsCard), - entityFilter: isPullRequestsAvailable, - }), - createEntityContentExtension({ - id: 'github-pull-requests', - defaultPath: 'github-pull-requests', - defaultTitle: 'GitHub Pull Requests', - loader: () => - import('./PullRequestsContent').then(m => m.PullRequestsContent), - entityFilter: isPullRequestsAvailable, - }), - ], -}); - -// /deployments -const deploymentsPlugin = createPlugin({ - id: 'github-pull-requests', - extensions: [ - createEntityCardExtension({ - id: 'github-pull-requests', - attachTo: { id: 'plugin.deployments.content', input: 'cards' }, - loader: () => import('./PullRequestsCard').then(m => m.PullRequestsCard), - entityFilter: isPullRequestsAvailable, - }), - createEntityContentExtension({ - id: 'github-pull-requests', - defaultPath: 'github-pull-requests', - defaultTitle: 'GitHub Pull Requests', - loader: () => - import('./PullRequestsContent').then(m => m.PullRequestsContent), - entityFilter: isPullRequestsAvailable, - }), - ], -}); From c82fc7e672d73eed1ff98a9947b7fe8a98fff1ac Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 16:42:54 +0200 Subject: [PATCH 58/61] catalog: split alpha entry point into multiple modules Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- plugins/catalog/package.json | 4 +- plugins/catalog/src/alpha.tsx | 269 ------------------ .../src/alpha/builtInFilterExtensions.tsx | 121 ++++++++ .../alpha/createCatalogFilterExtension.tsx | 62 ++++ plugins/catalog/src/alpha/index.ts | 18 ++ plugins/catalog/src/alpha/plugin.tsx | 145 ++++++++++ 6 files changed, 348 insertions(+), 271 deletions(-) delete mode 100644 plugins/catalog/src/alpha.tsx create mode 100644 plugins/catalog/src/alpha/builtInFilterExtensions.tsx create mode 100644 plugins/catalog/src/alpha/createCatalogFilterExtension.tsx create mode 100644 plugins/catalog/src/alpha/index.ts create mode 100644 plugins/catalog/src/alpha/plugin.tsx diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 56ae771314..214795e73c 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -10,13 +10,13 @@ }, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.tsx", + "./alpha": "./src/alpha/index.ts", "./package.json": "./package.json" }, "typesVersions": { "*": { "alpha": [ - "src/alpha.tsx" + "src/alpha/index.ts" ], "package.json": [ "package.json" diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx deleted file mode 100644 index 415525ab74..0000000000 --- a/plugins/catalog/src/alpha.tsx +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import HomeIcon from '@material-ui/icons/Home'; -import { - createApiFactory, - discoveryApiRef, - fetchApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; -import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; -import { CatalogClient } from '@backstage/catalog-client'; -import { - createSchemaFromZod, - createApiExtension, - createPageExtension, - createPlugin, - createNavItemExtension, - createExtension, - coreExtensionData, - AnyExtensionInputMap, - PortableSchema, - ExtensionBoundary, - createExtensionInput, -} from '@backstage/frontend-plugin-api'; -import { - AsyncEntityProvider, - catalogApiRef, - entityRouteRef, - starredEntitiesApiRef, -} from '@backstage/plugin-catalog-react'; -import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; -import { DefaultStarredEntitiesApi } from './apis'; -import { - createComponentRouteRef, - createFromTemplateRouteRef, - rootRouteRef, - viewTechDocRouteRef, -} from './routes'; -import { useEntityFromUrl } from './components/CatalogEntityPage/useEntityFromUrl'; - -/** @alpha */ -export const CatalogApi = createApiExtension({ - factory: createApiFactory({ - api: catalogApiRef, - deps: { - discoveryApi: discoveryApiRef, - fetchApi: fetchApiRef, - }, - factory: ({ discoveryApi, fetchApi }) => - new CatalogClient({ discoveryApi, fetchApi }), - }), -}); - -/** @alpha */ -export const StarredEntitiesApi = createApiExtension({ - factory: createApiFactory({ - api: starredEntitiesApiRef, - deps: { storageApi: storageApiRef }, - factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }), - }), -}); - -/** @alpha */ -export const CatalogSearchResultListItemExtension = - createSearchResultListItemExtension({ - id: 'catalog', - predicate: result => result.type === 'software-catalog', - component: () => - import('./components/CatalogSearchResultListItem').then( - m => m.CatalogSearchResultListItem, - ), - }); - -/** @alpha */ -export function createCatalogFilterExtension< - TInputs extends AnyExtensionInputMap, - TConfig = never, ->(options: { - id: string; - inputs?: TInputs; - configSchema?: PortableSchema; - loader: (options: { config: TConfig }) => Promise; -}) { - const id = `catalog.filter.${options.id}`; - - return createExtension({ - id, - attachTo: { id: 'plugin.catalog.page.index', input: 'filters' }, - inputs: options.inputs ?? {}, - configSchema: options.configSchema, - output: { - element: coreExtensionData.reactElement, - }, - factory({ bind, config, source }) { - const ExtensionComponent = React.lazy(() => - options - .loader({ config }) - .then(element => ({ default: () => element })), - ); - - bind({ - element: ( - - - - ), - }); - }, - }); -} - -const CatalogEntityTagFilter = createCatalogFilterExtension({ - id: 'entity.tag', - loader: async () => { - const { EntityTagPicker } = await import('@backstage/plugin-catalog-react'); - return ; - }, -}); - -const CatalogEntityKindFilter = createCatalogFilterExtension({ - id: 'entity.kind', - configSchema: createSchemaFromZod(z => - z.object({ - initialFilter: z.string().default('component'), - }), - ), - loader: async ({ config }) => { - const { EntityKindPicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; - }, -}); - -const CatalogEntityTypeFilter = createCatalogFilterExtension({ - id: 'entity.type', - loader: async () => { - const { EntityTypePicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; - }, -}); - -const CatalogEntityOwnerFilter = createCatalogFilterExtension({ - id: 'entity.mode', - configSchema: createSchemaFromZod(z => - z.object({ - mode: z.enum(['owners-only', 'all']).optional(), - }), - ), - loader: async ({ config }) => { - const { EntityOwnerPicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; - }, -}); - -const CatalogEntityNamespaceFilter = createCatalogFilterExtension({ - id: 'entity.namespace', - loader: async () => { - const { EntityNamespacePicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; - }, -}); - -const CatalogEntityLifecycleFilter = createCatalogFilterExtension({ - id: 'entity.lifecycle', - loader: async () => { - const { EntityLifecyclePicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; - }, -}); - -const CatalogEntityProcessingStatusFilter = createCatalogFilterExtension({ - id: 'entity.processing.status', - loader: async () => { - const { EntityProcessingStatusPicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; - }, -}); - -const CatalogUserListFilter = createCatalogFilterExtension({ - id: 'user.list', - configSchema: createSchemaFromZod(z => - z.object({ - initialFilter: z.enum(['owned', 'starred', 'all']).default('owned'), - }), - ), - loader: async ({ config }) => { - const { UserListPicker } = await import('@backstage/plugin-catalog-react'); - return ; - }, -}); - -const CatalogIndexPage = createPageExtension({ - id: 'plugin.catalog.page.index', - defaultPath: '/catalog', - routeRef: convertLegacyRouteRef(rootRouteRef), - inputs: { - filters: createExtensionInput({ - element: coreExtensionData.reactElement, - }), - }, - loader: async ({ inputs }) => { - const { BaseCatalogPage } = await import('./components/CatalogPage'); - const filters = inputs.filters.map(filter => filter.element); - return {filters}} />; - }, -}); - -const CatalogNavItem = createNavItemExtension({ - id: 'catalog.nav.index', - routeRef: convertLegacyRouteRef(rootRouteRef), - title: 'Catalog', - icon: HomeIcon, -}); - -/** @alpha */ -export default createPlugin({ - id: 'catalog', - routes: { - catalogIndex: convertLegacyRouteRef(rootRouteRef), - catalogEntity: convertLegacyRouteRef(entityRouteRef), - }, - externalRoutes: { - viewTechDoc: convertLegacyRouteRef(viewTechDocRouteRef), - createComponent: convertLegacyRouteRef(createComponentRouteRef), - createFromTemplate: convertLegacyRouteRef(createFromTemplateRouteRef), - }, - extensions: [ - CatalogApi, - StarredEntitiesApi, - CatalogSearchResultListItemExtension, - CatalogEntityKindFilter, - CatalogEntityTypeFilter, - CatalogUserListFilter, - CatalogEntityOwnerFilter, - CatalogEntityLifecycleFilter, - CatalogEntityTagFilter, - CatalogEntityProcessingStatusFilter, - CatalogEntityNamespaceFilter, - CatalogIndexPage, - // CatalogEntityPage, - CatalogNavItem, - ], -}); diff --git a/plugins/catalog/src/alpha/builtInFilterExtensions.tsx b/plugins/catalog/src/alpha/builtInFilterExtensions.tsx new file mode 100644 index 0000000000..5a7575c4f5 --- /dev/null +++ b/plugins/catalog/src/alpha/builtInFilterExtensions.tsx @@ -0,0 +1,121 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { createCatalogFilterExtension } from './createCatalogFilterExtension'; +import { createSchemaFromZod } from '@backstage/frontend-plugin-api'; + +const CatalogEntityTagFilter = createCatalogFilterExtension({ + id: 'entity.tag', + loader: async () => { + const { EntityTagPicker } = await import('@backstage/plugin-catalog-react'); + return ; + }, +}); + +const CatalogEntityKindFilter = createCatalogFilterExtension({ + id: 'entity.kind', + configSchema: createSchemaFromZod(z => + z.object({ + initialFilter: z.string().default('component'), + }), + ), + loader: async ({ config }) => { + const { EntityKindPicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogEntityTypeFilter = createCatalogFilterExtension({ + id: 'entity.type', + loader: async () => { + const { EntityTypePicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogEntityOwnerFilter = createCatalogFilterExtension({ + id: 'entity.mode', + configSchema: createSchemaFromZod(z => + z.object({ + mode: z.enum(['owners-only', 'all']).optional(), + }), + ), + loader: async ({ config }) => { + const { EntityOwnerPicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogEntityNamespaceFilter = createCatalogFilterExtension({ + id: 'entity.namespace', + loader: async () => { + const { EntityNamespacePicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogEntityLifecycleFilter = createCatalogFilterExtension({ + id: 'entity.lifecycle', + loader: async () => { + const { EntityLifecyclePicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogEntityProcessingStatusFilter = createCatalogFilterExtension({ + id: 'entity.processing.status', + loader: async () => { + const { EntityProcessingStatusPicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogUserListFilter = createCatalogFilterExtension({ + id: 'user.list', + configSchema: createSchemaFromZod(z => + z.object({ + initialFilter: z.enum(['owned', 'starred', 'all']).default('owned'), + }), + ), + loader: async ({ config }) => { + const { UserListPicker } = await import('@backstage/plugin-catalog-react'); + return ; + }, +}); + +export const builtInFilterExtensions = [ + CatalogEntityTagFilter, + CatalogEntityKindFilter, + CatalogEntityTypeFilter, + CatalogEntityOwnerFilter, + CatalogEntityNamespaceFilter, + CatalogEntityLifecycleFilter, + CatalogEntityProcessingStatusFilter, + CatalogUserListFilter, +]; diff --git a/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx new file mode 100644 index 0000000000..5a6328078c --- /dev/null +++ b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { lazy } from 'react'; +import { + AnyExtensionInputMap, + ExtensionBoundary, + PortableSchema, + coreExtensionData, + createExtension, +} from '@backstage/frontend-plugin-api'; + +/** @alpha */ +export function createCatalogFilterExtension< + TInputs extends AnyExtensionInputMap, + TConfig = never, +>(options: { + id: string; + inputs?: TInputs; + configSchema?: PortableSchema; + loader: (options: { config: TConfig }) => Promise; +}) { + const id = `catalog.filter.${options.id}`; + + return createExtension({ + id, + attachTo: { id: 'plugin.catalog.page.index', input: 'filters' }, + inputs: options.inputs ?? {}, + configSchema: options.configSchema, + output: { + element: coreExtensionData.reactElement, + }, + factory({ bind, config, source }) { + const ExtensionComponent = lazy(() => + options + .loader({ config }) + .then(element => ({ default: () => element })), + ); + + bind({ + element: ( + + + + ), + }); + }, + }); +} diff --git a/plugins/catalog/src/alpha/index.ts b/plugins/catalog/src/alpha/index.ts new file mode 100644 index 0000000000..06a78ec6ea --- /dev/null +++ b/plugins/catalog/src/alpha/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default } from './plugin'; +export { createCatalogFilterExtension } from './createCatalogFilterExtension'; diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx new file mode 100644 index 0000000000..1716b85a81 --- /dev/null +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import HomeIcon from '@material-ui/icons/Home'; +import { + createApiFactory, + discoveryApiRef, + fetchApiRef, + storageApiRef, +} from '@backstage/core-plugin-api'; +import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; +import { CatalogClient } from '@backstage/catalog-client'; +import { + createApiExtension, + createPageExtension, + createPlugin, + createNavItemExtension, + coreExtensionData, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { + AsyncEntityProvider, + catalogApiRef, + entityRouteRef, + starredEntitiesApiRef, +} from '@backstage/plugin-catalog-react'; +import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; +import { DefaultStarredEntitiesApi } from '../apis'; +import { + createComponentRouteRef, + createFromTemplateRouteRef, + rootRouteRef, + viewTechDocRouteRef, +} from '../routes'; +import { builtInFilterExtensions } from './builtInFilterExtensions'; +import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; + +/** @alpha */ +export const CatalogApi = createApiExtension({ + factory: createApiFactory({ + api: catalogApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, fetchApi }) => + new CatalogClient({ discoveryApi, fetchApi }), + }), +}); + +/** @alpha */ +export const StarredEntitiesApi = createApiExtension({ + factory: createApiFactory({ + api: starredEntitiesApiRef, + deps: { storageApi: storageApiRef }, + factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }), + }), +}); + +/** @alpha */ +export const CatalogSearchResultListItemExtension = + createSearchResultListItemExtension({ + id: 'catalog', + predicate: result => result.type === 'software-catalog', + component: () => + import('../components/CatalogSearchResultListItem').then( + m => m.CatalogSearchResultListItem, + ), + }); + +const CatalogIndexPage = createPageExtension({ + id: 'plugin.catalog.page.index', + defaultPath: '/catalog', + routeRef: convertLegacyRouteRef(rootRouteRef), + inputs: { + filters: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + loader: async ({ inputs }) => { + const { BaseCatalogPage } = await import('../components/CatalogPage'); + const filters = inputs.filters.map(filter => filter.element); + return {filters}} />; + }, +}); + +const CatalogEntityPage = createPageExtension({ + id: 'plugin.catalog.page.entity', + defaultPath: '/catalog/:namespace/:kind/:name', + routeRef: convertLegacyRouteRef(entityRouteRef), + loader: async () => { + const Component = () => { + return ( + +
🚧 Work In Progress
+
+ ); + }; + return ; + }, +}); + +const CatalogNavItem = createNavItemExtension({ + id: 'catalog.nav.index', + routeRef: convertLegacyRouteRef(rootRouteRef), + title: 'Catalog', + icon: HomeIcon, +}); + +/** @alpha */ +export default createPlugin({ + id: 'catalog', + routes: { + catalogIndex: convertLegacyRouteRef(rootRouteRef), + catalogEntity: convertLegacyRouteRef(entityRouteRef), + }, + externalRoutes: { + viewTechDoc: convertLegacyRouteRef(viewTechDocRouteRef), + createComponent: convertLegacyRouteRef(createComponentRouteRef), + createFromTemplate: convertLegacyRouteRef(createFromTemplateRouteRef), + }, + extensions: [ + CatalogApi, + StarredEntitiesApi, + CatalogSearchResultListItemExtension, + CatalogIndexPage, + CatalogEntityPage, + CatalogNavItem, + ...builtInFilterExtensions, + ], +}); From 0bf6ebda889bfe17f6c444ba720883f918c28a6e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 17:04:30 +0200 Subject: [PATCH 59/61] catalog,catalog-react,techdocs: extract experimental entity page implementation from app-next Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- .changeset/mighty-crews-attack.md | 5 + .changeset/nice-apes-kneel.md | 5 + .changeset/old-apricots-taste.md | 5 + packages/app-next/app-config.yaml | 9 +- packages/app-next/src/App.tsx | 19 +- .../app-next/src/examples/entityPages.tsx | 290 ------------------ plugins/catalog-react/alpha-api-report.md | 65 ++++ plugins/catalog-react/package.json | 5 +- plugins/catalog-react/src/alpha.tsx | 158 ++++++++++ plugins/catalog/alpha-api-report.md | 11 - plugins/catalog/package.json | 4 +- .../{catalog-react => catalog}/src/alpha.ts | 4 +- plugins/catalog/src/alpha/plugin.tsx | 60 +++- plugins/techdocs/src/alpha.tsx | 14 + yarn.lock | 1 + 15 files changed, 326 insertions(+), 329 deletions(-) create mode 100644 .changeset/mighty-crews-attack.md create mode 100644 .changeset/nice-apes-kneel.md create mode 100644 .changeset/old-apricots-taste.md delete mode 100644 packages/app-next/src/examples/entityPages.tsx create mode 100644 plugins/catalog-react/src/alpha.tsx rename plugins/{catalog-react => catalog}/src/alpha.ts (85%) diff --git a/.changeset/mighty-crews-attack.md b/.changeset/mighty-crews-attack.md new file mode 100644 index 0000000000..c0b8587fe0 --- /dev/null +++ b/.changeset/mighty-crews-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Initial entity page implementation for new frontend system at `/alpha`, with an overview page enabled by default and the about card available as an optional card. diff --git a/.changeset/nice-apes-kneel.md b/.changeset/nice-apes-kneel.md new file mode 100644 index 0000000000..1ecff9d223 --- /dev/null +++ b/.changeset/nice-apes-kneel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Added entity page content for the new plugin exported via `/alpha`. diff --git a/.changeset/old-apricots-taste.md b/.changeset/old-apricots-taste.md new file mode 100644 index 0000000000..544ba11536 --- /dev/null +++ b/.changeset/old-apricots-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Added new APIs at the `/alpha` subpath for creating entity page cards and content for the new frontend system. diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 6f6081fece..0c75e1c96c 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -4,12 +4,17 @@ app: routes: bindings: plugin.pages.externalRoutes.pageX: plugin.pages.routes.pageX - # waiting for https://github.com/backstage/backstage/pull/20605 - # catalog.externalRoutes.viewTechDoc: techdocs.routes.docRoot + plugin.catalog.externalRoutes.viewTechDoc: plugin.techdocs.routes.docRoot extensions: - apis.plugin.graphiql.browse.gitlab: true + # Entity page cards + - 'entity.cards.about' + + # Entity page content + - 'entity.content.techdocs' + # scmAuthExtension: >- # createScmAuthExtension({ # id: 'apis.scmAuth.addons.ghe', diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 7ff47a3e89..1275722da4 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -17,7 +17,6 @@ import React from 'react'; import { createApp } from '@backstage/frontend-app-api'; import { pagesPlugin } from './examples/pagesPlugin'; -import { entityPagePlugins } from './examples/entityPages'; import graphiqlPlugin from '@backstage/plugin-graphiql/alpha'; import techRadarPlugin from '@backstage/plugin-tech-radar/alpha'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; @@ -30,11 +29,8 @@ import { createExtension, createApiExtension, createExtensionOverrides, - createPageExtension, } from '@backstage/frontend-plugin-api'; -import { entityRouteRef } from '@backstage/plugin-catalog-react'; import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; -import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; import { homePage } from './HomePage'; import { collectLegacyRoutes } from '@backstage/core-compat-api'; import { FlatRoutes } from '@backstage/core-app-api'; @@ -76,13 +72,6 @@ TODO: /* app.tsx */ -const entityPageExtension = createPageExtension({ - id: 'catalog:entity', - defaultPath: '/catalog/:namespace/:kind/:name', - routeRef: convertLegacyRouteRef(entityRouteRef), - loader: async () =>
Just a temporary mocked entity page
, -}); - const homePageExtension = createExtension({ id: 'myhomepage', attachTo: { id: 'home', input: 'props' }, @@ -121,15 +110,9 @@ const app = createApp({ techdocsPlugin, userSettingsPlugin, homePlugin, - ...entityPagePlugins, ...collectedLegacyPlugins, createExtensionOverrides({ - extensions: [ - entityPageExtension, - homePageExtension, - scmAuthExtension, - scmIntegrationApi, - ], + extensions: [homePageExtension, scmAuthExtension, scmIntegrationApi], }), ], /* Handled through config instead */ diff --git a/packages/app-next/src/examples/entityPages.tsx b/packages/app-next/src/examples/entityPages.tsx deleted file mode 100644 index 3b5b4cceb7..0000000000 --- a/packages/app-next/src/examples/entityPages.tsx +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useEffect } from 'react'; -import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; -import { - AnyExtensionInputMap, - Extension, - ExtensionBoundary, - ExtensionInputValues, - PortableSchema, - RouteRef, - coreExtensionData, - createExtension, - createExtensionDataRef, - createExtensionInput, - createPageExtension, - createPlugin, - createSchemaFromZod, -} from '@backstage/frontend-plugin-api'; -import { - AsyncEntityProvider, - EntityLoadingStatus, - catalogApiRef, - entityRouteRef, -} from '@backstage/plugin-catalog-react'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { Expand } from '../../../frontend-plugin-api/src/types'; -import { EntityAboutCard, EntityLayout } from '@backstage/plugin-catalog'; -import { - useApi, - errorApiRef, - useRouteRefParams, -} from '@backstage/core-plugin-api'; -import { useNavigate } from 'react-router'; -import useAsyncRetry from 'react-use/lib/useAsyncRetry'; -import Grid from '@material-ui/core/Grid'; - -export const useEntityFromUrl = (): EntityLoadingStatus => { - const { kind, namespace, name } = useRouteRefParams(entityRouteRef); - const navigate = useNavigate(); - const errorApi = useApi(errorApiRef); - const catalogApi = useApi(catalogApiRef); - - const { - value: entity, - error, - loading, - retry: refresh, - } = useAsyncRetry( - () => catalogApi.getEntityByRef({ kind, namespace, name }), - [catalogApi, kind, namespace, name], - ); - - useEffect(() => { - if (!name) { - errorApi.post(new Error('No name provided!')); - navigate('/'); - } - }, [errorApi, navigate, error, loading, entity, name]); - - return { entity, loading, error, refresh }; -}; - -export const titleExtensionDataRef = createExtensionDataRef( - 'plugin.catalog.entity.content.title', -); - -const CatalogEntityPage = createPageExtension({ - id: 'plugin.catalog.page.entity', - defaultPath: '/catalog/:namespace/:kind/:name', - routeRef: convertLegacyRouteRef(entityRouteRef), - inputs: { - contents: createExtensionInput({ - element: coreExtensionData.reactElement, - path: coreExtensionData.routePath, - routeRef: coreExtensionData.routeRef.optional(), - title: titleExtensionDataRef, - }), - }, - loader: async ({ inputs }) => { - const Component = () => { - return ( - - - {inputs.contents.map(content => ( - - {content.element} - - ))} - - - ); - }; - return ; - }, -}); - -export function createEntityCardExtension< - TConfig, - TInputs extends AnyExtensionInputMap, ->(options: { - id: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TInputs; - configSchema?: PortableSchema; - loader: (options: { - config: TConfig; - inputs: Expand>; - }) => Promise; -}): Extension { - return createExtension({ - id: `entity.content.${options.id}`, - attachTo: options.attachTo ?? { - id: 'entity.content.overview', - input: 'cards', - }, - disabled: options.disabled ?? true, - output: { - element: coreExtensionData.reactElement, - }, - inputs: options.inputs, - configSchema: options.configSchema, - factory({ bind, config, inputs, source }) { - const LazyComponent = React.lazy(() => - options - .loader({ config, inputs }) - .then(element => ({ default: () => element })), - ); - - bind({ - element: ( - - - - - - ), - }); - }, - }); -} - -export function createEntityContentExtension< - TConfig extends { path: string; title: string }, - TInputs extends AnyExtensionInputMap, ->( - options: ( - | { - defaultPath: string; - defaultTitle: string; - } - | { - configSchema: PortableSchema; - } - ) & { - id: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TInputs; - routeRef?: RouteRef; - loader: (options: { - config: TConfig; - inputs: Expand>; - }) => Promise; - }, -): Extension { - const configSchema = - 'configSchema' in options - ? options.configSchema - : (createSchemaFromZod(z => - z.object({ - path: z.string().default(options.defaultPath), - title: z.string().default(options.defaultTitle), - }), - ) as PortableSchema); - - return createExtension({ - id: `entity.content.${options.id}`, - attachTo: options.attachTo ?? { - id: 'plugin.catalog.page.entity', - input: 'contents', - }, - disabled: options.disabled ?? true, - output: { - element: coreExtensionData.reactElement, - path: coreExtensionData.routePath, - routeRef: coreExtensionData.routeRef.optional(), - title: titleExtensionDataRef, - }, - inputs: options.inputs, - configSchema, - factory({ bind, config, inputs, source }) { - const LazyComponent = React.lazy(() => - options - .loader({ config, inputs }) - .then(element => ({ default: () => element })), - ); - - bind({ - path: config.path, - element: ( - - - - - - ), - routeRef: options.routeRef, - title: config.title, - }); - }, - }); -} - -const entityAboutCardExtension = createEntityCardExtension({ - id: 'about', - disabled: false, - loader: async () => , - // entityFilter: isDerp, -}); - -const overviewContentExtension = createEntityContentExtension({ - id: 'overview', - defaultPath: '/', - defaultTitle: 'Overview', - disabled: false, - inputs: { - cards: createExtensionInput({ - element: coreExtensionData.reactElement, - }), - }, - loader: async ({ inputs }) => ( - - {inputs.cards.map(card => ( - - {card.element} - - ))} - - ), -}); - -const bonusTechdocsPlugin = createPlugin({ - id: 'techdocs-entity', - extensions: [ - createEntityContentExtension({ - id: 'techdocs', - defaultPath: 'docs', - defaultTitle: 'TechDocs', - disabled: false, - loader: () => - import('@backstage/plugin-techdocs').then(m => ( - - )), - // entityFilter: isPullRequestsAvailable, - }), - ], -}); - -export const entityPagePlugins = [ - createPlugin({ - id: 'entity-pages', - extensions: [ - CatalogEntityPage, - overviewContentExtension, - entityAboutCardExtension, - ], - }), - bonusTechdocsPlugin, - // deploymentsPlugin, -]; diff --git a/plugins/catalog-react/alpha-api-report.md b/plugins/catalog-react/alpha-api-report.md index f9b740c07c..120e162c84 100644 --- a/plugins/catalog-react/alpha-api-report.md +++ b/plugins/catalog-react/alpha-api-report.md @@ -3,8 +3,73 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + +import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { Extension } from '@backstage/frontend-plugin-api'; +import { ExtensionInputValues } from '@backstage/frontend-plugin-api'; +import { PortableSchema } from '@backstage/frontend-plugin-api'; import { ResourcePermission } from '@backstage/plugin-permission-common'; +import { RouteRef } from '@backstage/frontend-plugin-api'; + +// @alpha (undocumented) +export function createEntityCardExtension< + TConfig, + TInputs extends AnyExtensionInputMap, +>(options: { + id: string; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TInputs; + configSchema?: PortableSchema; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; +}): Extension; + +// @alpha (undocumented) +export function createEntityContentExtension< + TConfig extends { + path: string; + title: string; + }, + TInputs extends AnyExtensionInputMap, +>( + options: ( + | { + defaultPath: string; + defaultTitle: string; + } + | { + configSchema: PortableSchema; + } + ) & { + id: string; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TInputs; + routeRef?: RouteRef; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; + }, +): Extension; + +// @alpha (undocumented) +export const entityContentTitleExtensionDataRef: ConfigurableExtensionDataRef< + string, + {} +>; // @alpha export function isOwnerOf(owner: Entity, entity: Entity): boolean; diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index bb18160595..70fa5e9614 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -10,13 +10,13 @@ }, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", + "./alpha": "./src/alpha.tsx", "./package.json": "./package.json" }, "typesVersions": { "*": { "alpha": [ - "src/alpha.ts" + "src/alpha.tsx" ], "package.json": [ "package.json" @@ -51,6 +51,7 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx new file mode 100644 index 0000000000..d2eeb8f5f3 --- /dev/null +++ b/plugins/catalog-react/src/alpha.tsx @@ -0,0 +1,158 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + AnyExtensionInputMap, + Extension, + ExtensionBoundary, + ExtensionInputValues, + PortableSchema, + RouteRef, + coreExtensionData, + createExtension, + createExtensionDataRef, + createSchemaFromZod, +} from '@backstage/frontend-plugin-api'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { Expand } from '../../../packages/frontend-plugin-api/src/types'; + +export { isOwnerOf } from './utils'; +export { useEntityPermission } from './hooks/useEntityPermission'; + +/** @alpha */ +export const entityContentTitleExtensionDataRef = + createExtensionDataRef('plugin.catalog.entity.content.title'); + +/** @alpha */ +export function createEntityCardExtension< + TConfig, + TInputs extends AnyExtensionInputMap, +>(options: { + id: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + configSchema?: PortableSchema; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; +}): Extension { + const id = `entity.cards.${options.id}`; + + return createExtension({ + id, + attachTo: options.attachTo ?? { + id: 'entity.content.overview', + input: 'cards', + }, + disabled: options.disabled ?? true, + output: { + element: coreExtensionData.reactElement, + }, + inputs: options.inputs, + configSchema: options.configSchema, + factory({ bind, config, inputs, source }) { + const ExtensionComponent = React.lazy(() => + options + .loader({ config, inputs }) + .then(element => ({ default: () => element })), + ); + + bind({ + element: ( + + + + ), + }); + }, + }); +} + +/** @alpha */ +export function createEntityContentExtension< + TConfig extends { path: string; title: string }, + TInputs extends AnyExtensionInputMap, +>( + options: ( + | { + defaultPath: string; + defaultTitle: string; + } + | { + configSchema: PortableSchema; + } + ) & { + id: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + routeRef?: RouteRef; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; + }, +): Extension { + const id = `entity.content.${options.id}`; + + const configSchema = + 'configSchema' in options + ? options.configSchema + : (createSchemaFromZod(z => + z.object({ + path: z.string().default(options.defaultPath), + title: z.string().default(options.defaultTitle), + }), + ) as PortableSchema); + + return createExtension({ + id, + attachTo: options.attachTo ?? { + id: 'plugin.catalog.page.entity', + input: 'contents', + }, + disabled: options.disabled ?? true, + output: { + element: coreExtensionData.reactElement, + path: coreExtensionData.routePath, + routeRef: coreExtensionData.routeRef.optional(), + title: entityContentTitleExtensionDataRef, + }, + inputs: options.inputs, + configSchema, + factory({ bind, config, inputs, source }) { + const LazyComponent = React.lazy(() => + options + .loader({ config, inputs }) + .then(element => ({ default: () => element })), + ); + + bind({ + path: config.path, + element: ( + + + + ), + routeRef: options.routeRef, + title: config.title, + }); + }, + }); +} diff --git a/plugins/catalog/alpha-api-report.md b/plugins/catalog/alpha-api-report.md index aba33fc092..43e589bdc3 100644 --- a/plugins/catalog/alpha-api-report.md +++ b/plugins/catalog/alpha-api-report.md @@ -12,14 +12,6 @@ import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; import { PortableSchema } from '@backstage/frontend-plugin-api'; import { RouteRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) -export const CatalogApi: Extension<{}>; - -// @alpha (undocumented) -export const CatalogSearchResultListItemExtension: Extension<{ - noTrack?: boolean | undefined; -}>; - // @alpha (undocumented) export function createCatalogFilterExtension< TInputs extends AnyExtensionInputMap, @@ -62,8 +54,5 @@ const _default: BackstagePlugin< >; export default _default; -// @alpha (undocumented) -export const StarredEntitiesApi: Extension<{}>; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 214795e73c..9b879e3c50 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -10,13 +10,13 @@ }, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha/index.ts", + "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "typesVersions": { "*": { "alpha": [ - "src/alpha/index.ts" + "src/alpha.ts" ], "package.json": [ "package.json" diff --git a/plugins/catalog-react/src/alpha.ts b/plugins/catalog/src/alpha.ts similarity index 85% rename from plugins/catalog-react/src/alpha.ts rename to plugins/catalog/src/alpha.ts index e8ff21609e..e80f131817 100644 --- a/plugins/catalog-react/src/alpha.ts +++ b/plugins/catalog/src/alpha.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { isOwnerOf } from './utils'; -export { useEntityPermission } from './hooks/useEntityPermission'; +export * from './alpha/index'; +export { default } from './alpha/index'; diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index 1716b85a81..d9d2878213 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -38,6 +38,11 @@ import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; +import { + createEntityContentExtension, + createEntityCardExtension, + entityContentTitleExtensionDataRef, +} from '@backstage/plugin-catalog-react/alpha'; import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; import { DefaultStarredEntitiesApi } from '../apis'; import { @@ -48,6 +53,7 @@ import { } from '../routes'; import { builtInFilterExtensions } from './builtInFilterExtensions'; import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; +import Grid from '@material-ui/core/Grid'; /** @alpha */ export const CatalogApi = createApiExtension({ @@ -102,11 +108,30 @@ const CatalogEntityPage = createPageExtension({ id: 'plugin.catalog.page.entity', defaultPath: '/catalog/:namespace/:kind/:name', routeRef: convertLegacyRouteRef(entityRouteRef), - loader: async () => { + inputs: { + contents: createExtensionInput({ + element: coreExtensionData.reactElement, + path: coreExtensionData.routePath, + routeRef: coreExtensionData.routeRef.optional(), + title: entityContentTitleExtensionDataRef, + }), + }, + loader: async ({ inputs }) => { + const { EntityLayout } = await import('../components/EntityLayout'); const Component = () => { return ( -
🚧 Work In Progress
+ + {inputs.contents.map(content => ( + + {content.element} + + ))} +
); }; @@ -114,6 +139,35 @@ const CatalogEntityPage = createPageExtension({ }, }); +const EntityAboutCard = createEntityCardExtension({ + id: 'about', + loader: async () => + import('../components/AboutCard').then(m => ( + + )), +}); + +const OverviewEntityContent = createEntityContentExtension({ + id: 'overview', + defaultPath: '/', + defaultTitle: 'Overview', + disabled: false, + inputs: { + cards: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + loader: async ({ inputs }) => ( + + {inputs.cards.map(card => ( + + {card.element} + + ))} + + ), +}); + const CatalogNavItem = createNavItemExtension({ id: 'catalog.nav.index', routeRef: convertLegacyRouteRef(rootRouteRef), @@ -140,6 +194,8 @@ export default createPlugin({ CatalogIndexPage, CatalogEntityPage, CatalogNavItem, + OverviewEntityContent, + EntityAboutCard, ...builtInFilterExtensions, ], }); diff --git a/plugins/techdocs/src/alpha.tsx b/plugins/techdocs/src/alpha.tsx index b7ccb0b12c..ceb5f47364 100644 --- a/plugins/techdocs/src/alpha.tsx +++ b/plugins/techdocs/src/alpha.tsx @@ -42,6 +42,7 @@ import { rootDocsRouteRef, rootRouteRef, } from './routes'; +import { createEntityContentExtension } from '@backstage/plugin-catalog-react/alpha'; /** @alpha */ const techDocsStorage = createApiExtension({ @@ -141,6 +142,18 @@ const TechDocsReaderPage = createPageExtension({ )), }); +/** + * Component responsible for rendering techdocs on entity pages + * + * @alpha + */ +const TechDocsEntityContent = createEntityContentExtension({ + id: 'techdocs', + defaultPath: 'docs', + defaultTitle: 'TechDocs', + loader: () => import('./Router').then(m => ), +}); + /** @alpha */ const TechDocsNavItem = createNavItemExtension({ id: 'plugin.techdocs.nav.index', @@ -158,6 +171,7 @@ export default createPlugin({ TechDocsNavItem, TechDocsIndexPage, TechDocsReaderPage, + TechDocsEntityContent, TechDocsSearchResultListItemExtension, ], routes: { diff --git a/yarn.lock b/yarn.lock index 059c18d859..3075745d85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5837,6 +5837,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" From a9611d42518156fc568ddf023713e345be99720f Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 20 Oct 2023 09:52:47 +0200 Subject: [PATCH 60/61] fix(catalog-react): make entity content routable Signed-off-by: Camila Belo --- plugins/catalog-react/src/alpha.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx index d2eeb8f5f3..58762b1994 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { lazy } from 'react'; import { AnyExtensionInputMap, Extension, @@ -67,7 +67,7 @@ export function createEntityCardExtension< inputs: options.inputs, configSchema: options.configSchema, factory({ bind, config, inputs, source }) { - const ExtensionComponent = React.lazy(() => + const ExtensionComponent = lazy(() => options .loader({ config, inputs }) .then(element => ({ default: () => element })), @@ -137,7 +137,7 @@ export function createEntityContentExtension< inputs: options.inputs, configSchema, factory({ bind, config, inputs, source }) { - const LazyComponent = React.lazy(() => + const ExtensionComponent = lazy(() => options .loader({ config, inputs }) .then(element => ({ default: () => element })), @@ -145,13 +145,13 @@ export function createEntityContentExtension< bind({ path: config.path, + title: config.title, + routeRef: options.routeRef, element: ( - - + + ), - routeRef: options.routeRef, - title: config.title, }); }, }); From 6bf7561d3c264d8c3bc03ca8443d72e6ff82ce81 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 20 Oct 2023 11:42:19 +0200 Subject: [PATCH 61/61] cli: fix package detection breaking if package.json is not available Signed-off-by: Patrik Oldsberg --- .changeset/rich-pugs-chew.md | 5 +++ .../cli/src/lib/bundler/packageDetection.ts | 38 ++++++++++--------- 2 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 .changeset/rich-pugs-chew.md diff --git a/.changeset/rich-pugs-chew.md b/.changeset/rich-pugs-chew.md new file mode 100644 index 0000000000..6c5aa0af62 --- /dev/null +++ b/.changeset/rich-pugs-chew.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The experimental package detection will now ignore packages that don't make `package.json` available. diff --git a/packages/cli/src/lib/bundler/packageDetection.ts b/packages/cli/src/lib/bundler/packageDetection.ts index 2095fe23e7..3aa0955c06 100644 --- a/packages/cli/src/lib/bundler/packageDetection.ts +++ b/packages/cli/src/lib/bundler/packageDetection.ts @@ -77,24 +77,28 @@ async function detectPackages( return []; } - const depPackageJson: BackstagePackageJson = require(require.resolve( - `${depName}/package.json`, - { paths: [targetPath] }, - )); - if ( - ['frontend-plugin', 'frontend-plugin-module'].includes( - depPackageJson.backstage?.role ?? '', - ) - ) { - // Include alpha entry point if available. If there's no default export it will be ignored - const exp = depPackageJson.exports; - if (exp && typeof exp === 'object' && './alpha' in exp) { - return [ - { name: depName, import: depName }, - { name: depName, export: './alpha', import: `${depName}/alpha` }, - ]; + try { + const depPackageJson: BackstagePackageJson = require(require.resolve( + `${depName}/package.json`, + { paths: [targetPath] }, + )); + if ( + ['frontend-plugin', 'frontend-plugin-module'].includes( + depPackageJson.backstage?.role ?? '', + ) + ) { + // Include alpha entry point if available. If there's no default export it will be ignored + const exp = depPackageJson.exports; + if (exp && typeof exp === 'object' && './alpha' in exp) { + return [ + { name: depName, import: depName }, + { name: depName, export: './alpha', import: `${depName}/alpha` }, + ]; + } + return [{ name: depName, import: depName }]; } - return [{ name: depName, import: depName }]; + } catch { + /* ignore packages that don't make package.json available */ } return []; });