diff --git a/.changeset/itchy-rabbits-exist.md b/.changeset/itchy-rabbits-exist.md index acac771a97..6a16ac5648 100644 --- a/.changeset/itchy-rabbits-exist.md +++ b/.changeset/itchy-rabbits-exist.md @@ -2,6 +2,6 @@ '@backstage/cli': minor --- -The new backend start command that used to be enabled by setting `EXPERIMENTAL_BACKEND_START` is now the default. To revert to the old behavior, set `LEGACY_BACKEND_START` instead. +**BREAKING** The new backend start command that used to be enabled by setting `EXPERIMENTAL_BACKEND_START` is now the default. To revert to the old behavior set `LEGACY_BACKEND_START`, which is recommended if you haven't migrated to the new backend system. This new command is no longer based on Webpack, but instead uses Node.js loaders to transpile on the fly. Rather than hot reloading modules the entire backend is now restarted on change, but the SQLite database state is still maintained across restarts via a parent process. diff --git a/.changeset/modern-owls-mate.md b/.changeset/modern-owls-mate.md new file mode 100644 index 0000000000..bed33c6bbc --- /dev/null +++ b/.changeset/modern-owls-mate.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +Added experimental support for declarative integration via the `/alpha` subpath. diff --git a/.changeset/olive-islands-sneeze.md b/.changeset/olive-islands-sneeze.md new file mode 100644 index 0000000000..b44f071acc --- /dev/null +++ b/.changeset/olive-islands-sneeze.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/cli': patch +--- + +Request slightly smaller pages of data from GitHub diff --git a/.changeset/red-baboons-warn.md b/.changeset/red-baboons-warn.md new file mode 100644 index 0000000000..80b7cd3cad --- /dev/null +++ b/.changeset/red-baboons-warn.md @@ -0,0 +1,5 @@ +--- +'@backstage/dev-utils': patch +--- + +Moving development `dependencies` to `devDependencies` diff --git a/.changeset/shiny-birds-melt.md b/.changeset/shiny-birds-melt.md new file mode 100644 index 0000000000..16591b6d2f --- /dev/null +++ b/.changeset/shiny-birds-melt.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +Fixed a bug in `TranslationApi` implementation where in some cases it wouldn't notify subscribers of language changes. diff --git a/.changeset/twelve-snakes-sell.md b/.changeset/twelve-snakes-sell.md new file mode 100644 index 0000000000..7f91b2910f --- /dev/null +++ b/.changeset/twelve-snakes-sell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend': patch +--- + +Set the default length limit to search query to 100. To override it, define `search.maxTermLength` in the config file. diff --git a/docs/releases/v1.19.0-next.0-changelog.md b/docs/releases/v1.19.0-next.0-changelog.md index d1861165b9..b1c85e238e 100644 --- a/docs/releases/v1.19.0-next.0-changelog.md +++ b/docs/releases/v1.19.0-next.0-changelog.md @@ -4,7 +4,7 @@ ### Minor Changes -- 7077dbf131: The new backend start command that used to be enabled by setting `EXPERIMENTAL_BACKEND_START` is now the default. To revert to the old behavior, set `LEGACY_BACKEND_START` instead. +- 7077dbf131: **BREAKING** The new backend start command that used to be enabled by setting `EXPERIMENTAL_BACKEND_START` is now the default. To revert to the old behavior set `LEGACY_BACKEND_START`, which is recommended if you haven't migrated to the new backend system. This new command is no longer based on Webpack, but instead uses Node.js loaders to transpile on the fly. Rather than hot reloading modules the entire backend is now restarted on change, but the SQLite database state is still maintained across restarts via a parent process. diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 6159c044d2..4d72cca14f 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -25,6 +25,7 @@ import { createPageExtension, } from '@backstage/frontend-plugin-api'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; +import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; /* @@ -67,6 +68,7 @@ const app = createApp({ graphiqlPlugin, pagesPlugin, techRadarPlugin, + techdocsPlugin, userSettingsPlugin, createExtensionOverrides({ extensions: [entityPageExtension], diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index da412a58a1..a3a6b005c0 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -61,7 +61,7 @@ ### Minor Changes -- 7077dbf131: The new backend start command that used to be enabled by setting `EXPERIMENTAL_BACKEND_START` is now the default. To revert to the old behavior, set `LEGACY_BACKEND_START` instead. +- 7077dbf131: **BREAKING** The new backend start command that used to be enabled by setting `EXPERIMENTAL_BACKEND_START` is now the default. To revert to the old behavior set `LEGACY_BACKEND_START`, which is recommended if you haven't migrated to the new backend system. This new command is no longer based on Webpack, but instead uses Node.js loaders to transpile on the fly. Rather than hot reloading modules the entire backend is now restarted on change, but the SQLite database state is still maintained across restarts via a parent process. diff --git a/packages/cli/src/commands/onboard/discovery/providers/github/GithubDiscoveryProvider.ts b/packages/cli/src/commands/onboard/discovery/providers/github/GithubDiscoveryProvider.ts index d5a3dc4a77..a24756edd7 100644 --- a/packages/cli/src/commands/onboard/discovery/providers/github/GithubDiscoveryProvider.ts +++ b/packages/cli/src/commands/onboard/discovery/providers/github/GithubDiscoveryProvider.ts @@ -113,7 +113,7 @@ export class GithubDiscoveryProvider implements Provider { const query = `query repositories($org: String!, $cursor: String) { repositoryOwner(login: $org) { login - repositories(first: 100, after: $cursor) { + repositories(first: 50, after: $cursor) { nodes { name url diff --git a/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.test.ts b/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.test.ts index 224430752c..d5b812c5d7 100644 --- a/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.test.ts +++ b/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.test.ts @@ -377,8 +377,7 @@ describe('I18nextTranslationApi', () => { }, }); }); - - expect(translations).toEqual(['foo', null, 'Föö', null, 'Føø']); + expect(translations).toEqual(['foo', 'Föö', 'Føø']); }); describe('formatting', () => { diff --git a/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.ts b/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.ts index 01e75d6df2..d0ef35568f 100644 --- a/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.ts +++ b/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.ts @@ -241,7 +241,6 @@ export class I18nextTranslationApi implements TranslationApi { return new ObservableImpl>(subscriber => { let loadTicket = {}; // To check for stale loads - let lastSnapshotWasReady = false; const loadResource = () => { loadTicket = {}; @@ -250,8 +249,7 @@ export class I18nextTranslationApi implements TranslationApi { () => { if (ticket === loadTicket) { const snapshot = this.#createSnapshot(internalRef); - if (snapshot.ready || lastSnapshotWasReady) { - lastSnapshotWasReady = snapshot.ready; + if (snapshot.ready) { subscriber.next(snapshot); } } @@ -266,12 +264,9 @@ export class I18nextTranslationApi implements TranslationApi { const onChange = () => { const snapshot = this.#createSnapshot(internalRef); - if (lastSnapshotWasReady && !snapshot.ready) { - lastSnapshotWasReady = snapshot.ready; + if (snapshot.ready) { subscriber.next(snapshot); - } - - if (!snapshot.ready) { + } else { loadResource(); } }; diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index e122374004..c436bf5ad2 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -103,6 +103,7 @@ describe('tasks', () => { const mockDir = createMockDirectory(); + const origCwd = process.cwd(); const realChdir = process.chdir; // If anyone calls chdir then make it resolve within the tmpdir const mockChdir = jest.spyOn(process, 'chdir'); @@ -130,6 +131,10 @@ describe('tasks', () => { mockChdir.mockReset(); }); + afterAll(() => { + realChdir(origCwd); + }); + describe('checkAppExistsTask', () => { it('should do nothing if the directory does not exist', async () => { const dir = 'projects/'; diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index d61c21dd88..5ec49ded70 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -40,17 +40,11 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/integration-react": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", - "@backstage/test-utils": "workspace:^", "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", - "@testing-library/dom": "^9.0.0", - "@testing-library/jest-dom": "^6.0.0", - "@testing-library/react": "^12.1.3", - "@testing-library/user-event": "^14.0.0", "@types/react": "^16.13.1 || ^17.0.0", - "react-use": "^17.2.4", - "zen-observable": "^0.10.0" + "react-use": "^17.2.4" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0", @@ -58,7 +52,13 @@ "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "devDependencies": { - "@backstage/cli": "workspace:^" + "@backstage/cli": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/dom": "^9.0.0", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "zen-observable": "^0.10.0" }, "files": [ "dist" diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts index 073e6c1587..cf6b400443 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.ts @@ -462,7 +462,7 @@ export async function getOrganizationRepositories( query repositories($org: String!, $catalogPathRef: String!, $cursor: String) { repositoryOwner(login: $org) { login - repositories(first: 100, after: $cursor) { + repositories(first: 50, after: $cursor) { nodes { name catalogInfoFile: object(expression: $catalogPathRef) { diff --git a/plugins/search-backend/config.d.ts b/plugins/search-backend/config.d.ts index 3dbdf78dfe..25ae59d273 100644 --- a/plugins/search-backend/config.d.ts +++ b/plugins/search-backend/config.d.ts @@ -22,6 +22,11 @@ export interface Config { */ maxPageLimit?: number; + /** + * Sets the maximum term length for the search string. Defaults to 100. + */ + maxTermLength?: number; + /** * Options related to the search integration with the Backstage permissions system */ diff --git a/plugins/search-backend/src/service/router.test.ts b/plugins/search-backend/src/service/router.test.ts index 7ec8603307..73523f2b62 100644 --- a/plugins/search-backend/src/service/router.test.ts +++ b/plugins/search-backend/src/service/router.test.ts @@ -60,7 +60,7 @@ describe('createRouter', () => { }, config: new ConfigReader({ permissions: { enabled: false }, - search: { maxPageLimit: 200 }, + search: { maxPageLimit: 200, maxTermLength: 20 }, }), permissions: mockPermissionEvaluator, logger, @@ -162,6 +162,19 @@ describe('createRouter', () => { }); }); + it('should reject term length over configured max', async () => { + const response = await request(app).get( + `/query?term=HelloWorld1234567890!`, + ); + + expect(response.status).toEqual(400); + expect(response.body).toMatchObject({ + error: { + message: /The term length "21" is greater than "20"/i, + }, + }); + }); + it('removes backend-only properties from search documents', async () => { mockSearchEngine.query.mockResolvedValue({ results: [ diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index dca632dfaf..876cb5d103 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -63,6 +63,7 @@ export type RouterOptions = { }; const defaultMaxPageLimit = 100; +const defaultMaxTermLength = 100; const allowedLocationProtocols = ['http:', 'https:']; /** @@ -77,8 +78,19 @@ export async function createRouter( const maxPageLimit = config.getOptionalNumber('search.maxPageLimit') ?? defaultMaxPageLimit; + const maxTermLength = + config.getOptionalNumber('search.maxTermLength') ?? defaultMaxTermLength; + const requestSchema = z.object({ - term: z.string().default(''), + term: z + .string() + .refine( + term => term.length <= maxTermLength, + term => ({ + message: `The term length "${term.length}" is greater than "${maxTermLength}"`, + }), + ) + .default(''), filters: jsonObjectSchema.optional(), types: z .array(z.string().refine(type => Object.keys(types).includes(type))) diff --git a/plugins/techdocs/src/alpha.tsx b/plugins/techdocs/src/alpha.tsx index bea4b7ab34..2f991f2fcc 100644 --- a/plugins/techdocs/src/alpha.tsx +++ b/plugins/techdocs/src/alpha.tsx @@ -18,8 +18,32 @@ import React from 'react'; import { createPlugin, createSchemaFromZod, + createApiExtension, + createPageExtension, } from '@backstage/frontend-plugin-api'; import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; +import { + configApiRef, + createApiFactory, + createRouteRef, + discoveryApiRef, + fetchApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { + techdocsApiRef, + techdocsStorageApiRef, +} from '@backstage/plugin-techdocs-react'; +import { TechDocsClient, TechDocsStorageClient } from './client'; + +const rootRouteRef = createRouteRef({ + id: 'plugin.techdocs.indexPage', +}); + +const rootDocsRouteRef = createRouteRef({ + id: 'plugin.techdocs.readerPage', + params: ['namespace', 'kind', 'name'], +}); /** @alpha */ export const TechDocsSearchResultListItemExtension = @@ -44,8 +68,89 @@ export const TechDocsSearchResultListItemExtension = }, }); +/** + * Responsible for rendering the provided router element + * + * @alpha + */ +const TechDocsIndexPage = createPageExtension({ + id: 'plugin.techdocs.indexPage', + defaultPath: '/docs', + routeRef: rootRouteRef, + loader: () => + import('./home/components/TechDocsIndexPage').then(m => ( + + )), +}); + +/** + * Component responsible for composing a TechDocs reader page experience + * + * @alpha + */ +const TechDocsReaderPage = createPageExtension({ + id: 'plugin.techdocs.readerPage', + loader: () => + import('./reader/components/TechDocsReaderPage').then(m => ( + + )), + routeRef: rootDocsRouteRef, + defaultPath: '/docs/:namespace/:kind/:name/*', +}); + +/** @alpha */ +const techDocsStorage = createApiExtension({ + api: techdocsStorageApiRef, + + factory() { + return createApiFactory({ + api: techdocsStorageApiRef, + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ configApi, discoveryApi, identityApi, fetchApi }) => + new TechDocsStorageClient({ + configApi, + discoveryApi, + identityApi, + fetchApi, + }), + }); + }, +}); + +/** @alpha */ +const techDocsClient = createApiExtension({ + api: techdocsApiRef, + factory() { + return createApiFactory({ + api: techdocsApiRef, + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ configApi, discoveryApi, fetchApi }) => + new TechDocsClient({ + configApi, + discoveryApi, + fetchApi, + }), + }); + }, +}); + /** @alpha */ export default createPlugin({ id: 'techdocs', - extensions: [TechDocsSearchResultListItemExtension], + extensions: [ + TechDocsIndexPage, + TechDocsReaderPage, + techDocsClient, + techDocsStorage, + TechDocsSearchResultListItemExtension, + ], });