diff --git a/.changeset/eleven-tables-tease.md b/.changeset/eleven-tables-tease.md new file mode 100644 index 0000000000..3f95fbb9d2 --- /dev/null +++ b/.changeset/eleven-tables-tease.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': patch +--- + +Modifying import functionality to register existing catalog-info.yaml if one exists in given GitHub repository diff --git a/.changeset/fair-geckos-collect.md b/.changeset/fair-geckos-collect.md new file mode 100644 index 0000000000..204041b195 --- /dev/null +++ b/.changeset/fair-geckos-collect.md @@ -0,0 +1,16 @@ +--- +'@backstage/create-app': patch +--- + +Due to a package name change from `@kyma-project/asyncapi-react` to +`@asyncapi/react-component` the jest configuration in the root `package.json` +has to be updated: + +```diff + "jest": { + "transformModules": [ +- "@kyma-project/asyncapi-react ++ "@asyncapi/react-component" + ] + } +``` diff --git a/.changeset/friendly-flies-fly.md b/.changeset/friendly-flies-fly.md new file mode 100644 index 0000000000..cfccf621fe --- /dev/null +++ b/.changeset/friendly-flies-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Change AWS Account type from Component to Resource diff --git a/.changeset/purple-olives-destroy.md b/.changeset/purple-olives-destroy.md new file mode 100644 index 0000000000..3578f5fdf5 --- /dev/null +++ b/.changeset/purple-olives-destroy.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +URL Reader: Use API response headers for archive filename in readTree. Fixes bug for users with hosted Bitbucket. diff --git a/.changeset/rich-geckos-lie.md b/.changeset/rich-geckos-lie.md new file mode 100644 index 0000000000..61ff7600f6 --- /dev/null +++ b/.changeset/rich-geckos-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Update `@asyncapi/react-component` to 0.18.2 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 11a756a597..9a5ef09554 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,6 +6,7 @@ * @backstage/maintainers /docs/features/techdocs @backstage/techdocs-core +/docs/features/search @backstage/techdocs-core /plugins/cost-insights @backstage/silver-lining /plugins/cloudbuild @trivago/ebarrios /plugins/search @backstage/techdocs-core diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml new file mode 100644 index 0000000000..f3fe2bbc5f --- /dev/null +++ b/.github/workflows/fossa.yml @@ -0,0 +1,44 @@ +name: FOSSA +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + # We use this to modify the generated .fossa.yml + - name: Install yq + run: sudo snap install yq + + - name: Install Fossa + run: "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash" + + - name: Configure Fossa + # The --option flag for fossa init does not work yet, see https://github.com/fossas/fossa-cli/issues/614 + run: | + fossa init + yq eval -i '.analyze.modules[].options.strategy = "yarn-list"' .fossa.yml + + # This deletes entries for template and example packages found within packages and plugins + # Seems like yq has a bug that causes only a subset of all matches to be deleted each run + yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml + yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml + yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml + yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml + yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml + + - name: Show config + run: cat .fossa.yml + + - name: Fossa Analyze + env: + # FOSSA Push-Only API Token + FOSSA_API_KEY: 9ee7e8893660832a7387dcc32377fb61 + run: fossa analyze --branch "$GITHUB_REF" diff --git a/app-config.yaml b/app-config.yaml index 741991877e..71c3310713 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -185,33 +185,46 @@ catalog: # groupFilter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified') locations: + # Add a location here to ingest it, for example from an URL: + # + # - type: url + # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-components.yaml + # + # For local development you can use a file location instead: + # + # - type: file + # target: ../catalog-model/examples/all-components.yaml + # + # File locations are relative to the current working directory of the + # backend, for example packages/backend/. + # Backstage example components - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-components.yaml + - type: file + target: ../catalog-model/examples/all-components.yaml # Example component for github-actions - - type: url - target: https://github.com/backstage/backstage/blob/master/plugins/github-actions/examples/sample.yaml + - type: file + target: ../../plugins/github-actions/examples/sample.yaml # Example component for TechDocs - - type: url - target: https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml + - type: file + target: ../../plugins/techdocs-backend/examples/documented-component/catalog-info.yaml # Backstage example APIs - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-apis.yaml + - type: file + target: ../catalog-model/examples/all-apis.yaml # Backstage example resources - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-resources.yaml + - type: file + target: ../catalog-model/examples/all-resources.yaml # Backstage example systems - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-systems.yaml + - type: file + target: ../catalog-model/examples/all-systems.yaml # Backstage example domains - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-domains.yaml + - type: file + target: ../catalog-model/examples/all-domains.yaml # Backstage example templates - - type: url - target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/all-templates.yaml + - type: file + target: ../../plugins/scaffolder-backend/sample-templates/all-templates.yaml # Backstage example groups and users - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml + - type: file + target: ../catalog-model/examples/acme-corp.yaml scaffolder: github: diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md index 51dcf042da..eecb4ad87a 100644 --- a/docs/architecture-decisions/adr006-avoid-react-fc.md +++ b/docs/architecture-decisions/adr006-avoid-react-fc.md @@ -43,6 +43,15 @@ const GoodComponent = ({ text, children }: GoodProps) => ( {children} ); + +/* Or as a shorthand, if no specifc child type is required */ +type GoodProps = PropsWithChildren<{ text: string }>; +const GoodComponent = ({ text, children }: GoodProps) => ( +
+
{text}
+ {children} +
+); ``` ## Consequences diff --git a/package.json b/package.json index aaa68e67de..758a8ba884 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ }, "jest": { "transformModules": [ - "@kyma-project/asyncapi-react" + "@asyncapi/react-component" ] } } diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index 59e437607f..578db2ac92 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -76,6 +76,8 @@ export class AzureUrlReader implements UrlReader { url: string, options?: ReadTreeOptions, ): Promise { + // TODO: Support filepath based reading tree feature like other providers + // Get latest commit SHA const commitsAzureResponse = await fetch( diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 3542e822e1..9661368b5e 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -95,6 +95,10 @@ describe('BitbucketUrlReader', () => { res( ctx.status(200), ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock-12ab34cd56ef.zip', + ), ctx.body(repoBuffer), ), ), @@ -114,6 +118,10 @@ describe('BitbucketUrlReader', () => { res( ctx.status(200), ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock.zip', + ), ctx.body(privateBitbucketRepoBuffer), ), ), diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 869870f248..e9727e04cf 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -101,17 +101,13 @@ export class BitbucketUrlReader implements UrlReader { url: string, options?: ReadTreeOptions, ): Promise { - const { name: repoName, owner: project, resource, filepath } = parseGitUrl( - url, - ); + const { filepath } = parseGitUrl(url); const lastCommitShortHash = await this.getLastCommitShortHash(url); if (options?.etag && options.etag === lastCommitShortHash) { throw new NotModifiedError(); } - const isHosted = resource === 'bitbucket.org'; - const downloadUrl = await getBitbucketDownloadUrl(url, this.config); const archiveBitbucketResponse = await fetch( downloadUrl, @@ -125,14 +121,31 @@ export class BitbucketUrlReader implements UrlReader { throw new Error(message); } - let folderPath = `${project}-${repoName}`; - if (isHosted) { - folderPath = `${project}-${repoName}-${lastCommitShortHash}`; + // Get the filename of archive from the header of the response + const contentDispositionHeader = archiveBitbucketResponse.headers.get( + 'content-disposition', + ) as string; + if (!contentDispositionHeader) { + throw new Error( + `Failed to read tree from ${url}. ` + + 'Bitbucket API response for downloading archive does not contain content-disposition header ', + ); + } + const fileNameRegEx = new RegExp( + /^attachment; filename=(?.*).zip$/, + ); + const archiveFileName = contentDispositionHeader.match(fileNameRegEx) + ?.groups?.fileName; + if (!archiveFileName) { + throw new Error( + `Failed to read tree from ${url}. Bitbucket API response for downloading archive has an unexpected ` + + `format of content-disposition header ${contentDispositionHeader} `, + ); } return await this.treeResponseFactory.fromZipArchive({ stream: (archiveBitbucketResponse.body as unknown) as Readable, - path: `${folderPath}/${filepath}`, + path: `${archiveFileName}/${filepath}`, etag: lastCommitShortHash, filter: options?.filter, }); diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index 0b7d56480f..080e8b1d5b 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -165,6 +165,10 @@ describe('GithubUrlReader', () => { res( ctx.status(200), ctx.set('Content-Type', 'application/x-gzip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock-etag123.tar.gz', + ), ctx.body(repoBuffer), ), ), @@ -178,6 +182,10 @@ describe('GithubUrlReader', () => { res( ctx.status(200), ctx.set('Content-Type', 'application/x-gzip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock-etag123.tar.gz', + ), ctx.body(repoBuffer), ), ), @@ -244,6 +252,10 @@ describe('GithubUrlReader', () => { return res( ctx.status(200), ctx.set('Content-Type', 'application/x-gzip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock-etag123.tar.gz', + ), ctx.body(repoBuffer), ); }, diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index b5b4e9c1a7..6c7cefe2ef 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -166,18 +166,31 @@ export class GithubUrlReader implements UrlReader { throw new Error(message); } - // Note that repoResponseJson.full_name must be used over full_name because the path - // is case sensitive and full_name may not be inq the correct case. - // TODO(OrkoHunter): The directory name inside the tarball should be retrieved from the tar - // instead of being constructed here. Same goes for GitLab, Bitbucket and Azure. - const extractedDirName = `${repoResponseJson.full_name.replace( - '/', - '-', - )}-${commitSha.substr(0, 7)}`; + // Get the filename of archive from the header of the response + const contentDispositionHeader = archive.headers.get( + 'content-disposition', + ) as string; + if (!contentDispositionHeader) { + throw new Error( + `Failed to read tree from ${url}. ` + + 'GitHub API response for downloading archive does not contain content-disposition header ', + ); + } + const fileNameRegEx = new RegExp( + /^attachment; filename=(?.*).tar.gz$/, + ); + const archiveFileName = contentDispositionHeader.match(fileNameRegEx) + ?.groups?.fileName; + if (!archiveFileName) { + throw new Error( + `Failed to read tree from ${url}. GitHub API response for downloading archive has an unexpected ` + + `format of content-disposition header ${contentDispositionHeader} `, + ); + } // The path includes the name of the directory inside the tarball and a sub path // if requested in readTree. - const path = `${extractedDirName}/${filepath}`; + const path = `${archiveFileName}/${filepath}`; return await this.deps.treeResponseFactory.fromTarArchive({ // TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 2e66794397..c0736f769d 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -176,6 +176,10 @@ describe('GitlabUrlReader', () => { res( ctx.status(200), ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename="mock-main-sha123abc.zip"', + ), ctx.body(archiveBuffer), ), ), @@ -225,6 +229,10 @@ describe('GitlabUrlReader', () => { res( ctx.status(200), ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename="mock-main-sha123abc.zip"', + ), ctx.body(archiveBuffer), ), ), @@ -254,6 +262,10 @@ describe('GitlabUrlReader', () => { res( ctx.status(200), ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename="mock-main-sha123abc.zip"', + ), ctx.body(archiveBuffer), ), ), diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 72db901ac1..654f4f9a85 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -78,7 +78,7 @@ export class GitlabUrlReader implements UrlReader { url: string, options?: ReadTreeOptions, ): Promise { - const { name: repoName, ref, full_name, filepath } = parseGitUrl(url); + const { ref, full_name, filepath } = parseGitUrl(url); // Use GitLab API to get the default branch // encodeURIComponent is required for GitLab API @@ -140,9 +140,29 @@ export class GitlabUrlReader implements UrlReader { throw new Error(message); } - const path = filepath - ? `${repoName}-${branch}-${commitSha}/${filepath}/` - : ''; + // Get the filename of archive from the header of the response + const contentDispositionHeader = archiveGitLabResponse.headers.get( + 'content-disposition', + ) as string; + if (!contentDispositionHeader) { + throw new Error( + `Failed to read tree from ${url}. ` + + 'GitLab API response for downloading archive does not contain content-disposition header ', + ); + } + const fileNameRegEx = new RegExp( + /^attachment; filename="(?.*).zip"$/, + ); + const archiveFileName = contentDispositionHeader.match(fileNameRegEx) + ?.groups?.fileName; + if (!archiveFileName) { + throw new Error( + `Failed to read tree from ${url}. GitLab API response for downloading archive has an unexpected ` + + `format of content-disposition header ${contentDispositionHeader} `, + ); + } + + const path = filepath ? `${archiveFileName}/${filepath}/` : ''; return await this.treeResponseFactory.fromZipArchive({ stream: (archiveGitLabResponse.body as unknown) as Readable, diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 1353e33072..3fa350fa7c 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -45,7 +45,7 @@ }, "jest": { "transformModules": [ - "@kyma-project/asyncapi-react" + "@asyncapi/react-component" ] } } diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 39e202eef8..571b4a3067 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -29,11 +29,11 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@asyncapi/react-component": "^0.18.2", "@backstage/catalog-model": "^0.6.0", "@backstage/core": "^0.4.4", "@backstage/plugin-catalog": "^0.2.9", "@backstage/theme": "^0.2.2", - "@kyma-project/asyncapi-react": "^0.14.2", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx index 86f524d099..a908a91b14 100644 --- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import AsyncApi from '@kyma-project/asyncapi-react'; +import AsyncApi from '@asyncapi/react-component'; +import '@asyncapi/react-component/lib/styles/fiori.css'; +import { fade, makeStyles } from '@material-ui/core/styles'; import React from 'react'; -import { makeStyles, fade } from '@material-ui/core/styles'; -import '@kyma-project/asyncapi-react/lib/styles/fiori.css'; const useStyles = makeStyles(theme => ({ root: { diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts index 5044a16aea..6eb7a45c63 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts @@ -45,7 +45,7 @@ describe('AwsOrganizationCloudAccountProcessor', () => { location, entity: { apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', + kind: 'Resource', metadata: { annotations: { 'amazonaws.com/arn': @@ -58,7 +58,6 @@ describe('AwsOrganizationCloudAccountProcessor', () => { }, spec: { type: 'cloud-account', - lifecycle: 'unknown', owner: 'unknown', }, }, @@ -91,7 +90,7 @@ describe('AwsOrganizationCloudAccountProcessor', () => { location, entity: { apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', + kind: 'Resource', metadata: { annotations: { 'amazonaws.com/arn': @@ -104,7 +103,6 @@ describe('AwsOrganizationCloudAccountProcessor', () => { }, spec: { type: 'cloud-account', - lifecycle: 'unknown', owner: 'unknown', }, }, diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts index 298fcb82ec..40516f88f7 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - ComponentEntityV1alpha1, - LocationSpec, -} from '@backstage/catalog-model'; +import { LocationSpec, ResourceEntityV1alpha1 } from '@backstage/catalog-model'; import { Account, Organizations, @@ -82,13 +79,13 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { return awsAccounts; } - mapAccountToComponent(account: Account): ComponentEntityV1alpha1 { + mapAccountToComponent(account: Account): ResourceEntityV1alpha1 { const { accountId, organizationId } = this.extractInformationFromArn( account.Arn as string, ); return { apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', + kind: 'Resource', metadata: { annotations: { [ACCOUNTID_ANNOTATION]: accountId, @@ -100,7 +97,6 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { }, spec: { type: 'cloud-account', - lifecycle: 'unknown', owner: 'unknown', }, }; @@ -129,7 +125,7 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { } return true; }) - .forEach((entity: ComponentEntityV1alpha1) => { + .forEach((entity: ResourceEntityV1alpha1) => { emit(results.entity(location, entity)); }); diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 5e601ad896..c4e8d918d1 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -32,8 +32,8 @@ "dependencies": { "@backstage/catalog-model": "^0.6.1", "@backstage/core": "^0.4.4", - "@backstage/plugin-catalog": "^0.2.11", "@backstage/integration": "^0.2.0", + "@backstage/plugin-catalog": "^0.2.11", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts index 99526a3eaa..5abb0e3e53 100644 --- a/plugins/catalog-import/src/api/CatalogImportApi.ts +++ b/plugins/catalog-import/src/api/CatalogImportApi.ts @@ -30,6 +30,11 @@ export interface CatalogImportApi { fileContent: string; githubIntegrationConfig: GitHubIntegrationConfig; }): Promise<{ link: string; location: string }>; + checkForExistingCatalogInfo(options: { + owner: string; + repo: string; + githubIntegrationConfig: GitHubIntegrationConfig; + }): Promise<{ exists: boolean; url?: string }>; createRepositoryLocation(options: { location: string }): Promise; generateEntityDefinitions(options: { repo: string; diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts new file mode 100644 index 0000000000..0e0cf4b323 --- /dev/null +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { CatalogImportClient } from './CatalogImportClient'; + +jest.mock('@octokit/rest', () => ({ + Octokit: jest.fn().mockImplementation(() => { + return { + repos: { + get: () => + Promise.resolve({ + data: { + default_branch: 'main', + }, + }), + }, + search: { + code: () => + Promise.resolve({ + data: { + total_count: 2, + items: [ + { path: 'simple/path/catalog-info.yaml' }, + { path: 'co/mple/x/path/catalog-info.yaml' }, + { path: 'catalog-info.yaml' }, + ], + }, + }), + }, + }; + }), +})); + +describe('CatalogImportClient', () => { + describe('checkForExistingCatalogInfo', () => { + const cic = new CatalogImportClient({ + discoveryApi: { getBaseUrl: () => Promise.resolve('base') }, + githubAuthApi: { getAccessToken: (_, __) => Promise.resolve('token') }, + configApi: {} as any, + }); + it('should return the closest-to-root catalog-info from multiple responses', async () => { + const respo = await cic.checkForExistingCatalogInfo({ + owner: 'test-user', + repo: 'rest-repo', + githubIntegrationConfig: { host: 'https://github.com' }, + }); + expect(respo.exists).toBe(true); + expect(respo.url).toBe('blob/main/catalog-info.yaml'); + }); + }); +}); diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index a578f37a96..d1509c2105 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -90,6 +90,52 @@ export class CatalogImportClient implements CatalogImportApi { } } + async checkForExistingCatalogInfo({ + owner, + repo, + githubIntegrationConfig, + }: { + owner: string; + repo: string; + githubIntegrationConfig: GitHubIntegrationConfig; + }): Promise<{ exists: boolean; url?: string }> { + const token = await this.githubAuthApi.getAccessToken(['repo']); + const octo = new Octokit({ + auth: token, + baseUrl: githubIntegrationConfig.apiBaseUrl, + }); + const catalogFileName = 'catalog-info.yaml'; + const query = `repo:${owner}/${repo}+filename:${catalogFileName}`; + + const searchResult = await octo.search.code({ q: query }).catch(e => { + throw new Error( + formatHttpErrorMessage( + "Couldn't search repository for metadata file.", + e, + ), + ); + }); + const exists = searchResult.data.total_count > 0; + if (exists) { + const repoInformation = await octo.repos.get({ owner, repo }).catch(e => { + throw new Error(formatHttpErrorMessage("Couldn't fetch repo data", e)); + }); + const defaultBranch = repoInformation.data.default_branch; + + // Github search sorts returned values with 'best match' using 'multiple factors to boost the most relevant item', + // aka magic. + // Sorting to use the shortest item, closest to the repository root. + const catalogInfoItem = searchResult.data.items + .map(it => it.path) + .sort((a, b) => a.length - b.length)[0]; + return { + url: `blob/${defaultBranch}/${catalogInfoItem}`, + exists, + }; + } + return { exists }; + } + async submitPrToRepo({ owner, repo, diff --git a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx new file mode 100644 index 0000000000..91a5208bee --- /dev/null +++ b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { RegisterComponentForm } from './ImportComponentForm'; +import { + ApiProvider, + ApiRegistry, + DiscoveryApi, + errorApiRef, +} from '@backstage/core'; +import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; +import { catalogImportApiRef, CatalogImportClient } from '../api'; +import { fireEvent, waitFor, screen } from '@testing-library/react'; + +describe('', () => { + let apis: ApiRegistry; + + const mockErrorApi: jest.Mocked = { + post: jest.fn(), + error$: jest.fn(), + }; + + beforeEach(() => { + apis = ApiRegistry.from([ + [catalogApiRef, new CatalogClient({ discoveryApi: {} as DiscoveryApi })], + [ + catalogImportApiRef, + new CatalogImportClient({ + discoveryApi: { getBaseUrl: () => Promise.resolve('base') }, + githubAuthApi: { + getAccessToken: (_, __) => Promise.resolve('token'), + }, + configApi: {} as any, + }), + ], + [errorApiRef, mockErrorApi], + ]); + }); + + async function renderSUT( + nextStep: () => void = () => {}, + saveConfig: () => void = () => {}, + ) { + return await renderInTestApp( + + + , + ); + } + + it('Renders without exploding', async () => { + await renderSUT(); + expect( + screen.getByPlaceholderText('https://github.com/backstage/backstage'), + ).toBeInTheDocument(); + }); + + it('Should have basic URL validation for input', async () => { + await renderSUT(); + await waitFor(() => { + fireEvent.input( + screen.getByPlaceholderText('https://github.com/backstage/backstage'), + { target: { value: 'not a url' } }, + ); + }); + await waitFor(() => { + fireEvent.click(screen.getByText('Next')); + }); + expect(screen.getByText('Must start with https://.')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog-import/src/components/ImportComponentForm.tsx b/plugins/catalog-import/src/components/ImportComponentForm.tsx index 7434f07b10..c233be5a8c 100644 --- a/plugins/catalog-import/src/components/ImportComponentForm.tsx +++ b/plugins/catalog-import/src/components/ImportComponentForm.tsx @@ -64,27 +64,45 @@ export const RegisterComponentForm = ({ const isMounted = useMountedState(); const errorApi = useApi(errorApiRef); - const { generateEntityDefinitions } = useGithubRepos(); + const { + generateEntityDefinitions, + checkForExistingCatalogInfo, + } = useGithubRepos(); const onSubmit = async (formData: Record) => { const { componentLocation: target } = formData; - try { - if (!isMounted()) return; - const type = urlType(target); + async function saveCatalogFileConfig(target: string) { + const data = await catalogApi.addLocation({ target }); + saveConfig({ + type: 'file', + location: data.location.target, + config: data.entities, + }); + } - if (type === 'tree') { + async function trySaveRepositoryConfig(target: string) { + const existingCatalog = await checkForExistingCatalogInfo(target); + if (existingCatalog.exists) { + const targetUrl = target.endsWith('/') + ? `${target}${existingCatalog.url}` + : `${target}/${existingCatalog.url}`; + await saveCatalogFileConfig(targetUrl); + } else { saveConfig({ - type, + type: 'tree', location: target, config: await generateEntityDefinitions(target), }); + } + } + + try { + if (!isMounted()) return; + const type = urlType(target); + if (type === 'tree') { + await trySaveRepositoryConfig(target); } else { - const data = await catalogApi.addLocation({ target }); - saveConfig({ - type, - location: data.location.target, - config: data.entities, - }); + await saveCatalogFileConfig(target); } nextStep(); } catch (e) { diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx new file mode 100644 index 0000000000..932f9aafa0 --- /dev/null +++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx @@ -0,0 +1,267 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { msw, renderInTestApp } from '@backstage/test-utils'; +import { ImportComponentPage } from './ImportComponentPage'; +import { + ApiProvider, + ApiRegistry, + configApiRef, + errorApiRef, +} from '@backstage/core'; +import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; +import { catalogImportApiRef, CatalogImportClient } from '../api'; + +import { fireEvent, screen, waitFor } from '@testing-library/react'; + +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +let codeSearchMockResponse: () => Promise<{ + data: { + total_count: number; + items: Array<{ path: string }>; + }; +}>; + +let findGithubConfigMockResponse = () => ({ + host: 'test.localhost', + owner: 'someuser', +}); + +jest.mock('@backstage/integration', () => ({ + readGitHubIntegrationConfigs: () => ({ + find: findGithubConfigMockResponse, + }), +})); + +jest.mock('@octokit/rest', () => ({ + Octokit: jest.fn().mockImplementation(() => { + return { + repos: { + get: () => + Promise.resolve({ + data: { + default_branch: 'main', + }, + }), + }, + search: { + code: codeSearchMockResponse, + }, + }; + }), +})); + +const OUR_GITHUB_TEST_REPO = 'https://github.com/someuser/somerepo'; +describe('', () => { + const server = setupServer(); + msw.setupDefaultHandlers(server); + + beforeEach(() => { + server.use( + rest.post('https://backend.localhost/locations', (_, res, ctx) => { + return res( + ctx.status(201), + ctx.json(require('../mocks/locations-POST-response.json')), + ); + }), + rest.post('https://backend.localhost/analyze-location', (_, res, ctx) => { + return res( + ctx.json(require('../mocks/analyze-location-POST-response.json')), + ); + }), + ); + }); + beforeAll(() => server.listen()); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + let apis: ApiRegistry; + + const mockErrorApi: jest.Mocked = { + post: jest.fn(), + error$: jest.fn(), + }; + + beforeEach(() => { + const getBaseUrl = () => Promise.resolve('https://backend.localhost'); + apis = ApiRegistry.from([ + [ + catalogApiRef, + new CatalogClient({ + discoveryApi: { getBaseUrl }, + }), + ], + [ + catalogImportApiRef, + new CatalogImportClient({ + discoveryApi: { getBaseUrl }, + githubAuthApi: { + getAccessToken: (_, __) => Promise.resolve('token'), + }, + configApi: {} as any, + }), + ], + [ + configApiRef, + { + getOptional: () => 'Title', + getOptionalConfigArray: () => [], + has: () => true, + getConfig: () => ({ + has: () => true, + }), + }, + ], + [errorApiRef, mockErrorApi], + ]); + }); + + async function renderSUT() { + return await renderInTestApp( + + + , + ); + } + + it('Should not explode on non-Github URLs', async () => { + findGithubConfigMockResponse = () => undefined!!; + await renderSUT(); + await waitFor(() => { + fireEvent.input( + screen.getByPlaceholderText('https://github.com/backstage/backstage'), + { + target: { + value: 'https://test-git-provider.localhost/someuser/somerepo', + }, + }, + ); + }); + + fireEvent.click(screen.getByText('Next')); + await waitFor(() => { + const firstStepInput = screen.queryByPlaceholderText( + 'https://github.com/backstage/backstage', + ); + expect(firstStepInput).not.toBeInTheDocument(); + }); + }); + + it('Should offer direct file import from non-Github URLs', async () => { + findGithubConfigMockResponse = () => undefined!!; + await renderSUT(); + await waitFor(() => { + fireEvent.input( + screen.getByPlaceholderText('https://github.com/backstage/backstage'), + { + target: { + value: + 'https://test-git-provider.localhost/someuser/somerepo/catalog-info.yaml', + }, + }, + ); + }); + + fireEvent.click(screen.getByText('Next')); + await waitFor(() => { + const firstStepInput = screen.queryByPlaceholderText( + 'https://github.com/backstage/backstage', + ); + expect(firstStepInput).not.toBeInTheDocument(); + }); + expect( + screen.getByText( + 'https://test-git-provider.localhost/someuser/somerepo/catalog-info.yaml', + ), + ).toBeInTheDocument(); + }); + + it('Should use found yaml file directly and not create a pull request if GitHub api returns one', async () => { + findGithubConfigMockResponse = () => ({ + host: 'test.localhost', + owner: 'someuser', + }); + codeSearchMockResponse = () => + Promise.resolve({ + data: { + total_count: 3, + items: [ + { path: 'simple/path/catalog-info.yaml' }, + { path: 'co/mple/x/path/catalog-info.yaml' }, + { path: 'catalog-info.yaml' }, + ], + }, + }); + await renderSUT(); + await waitFor(() => { + fireEvent.input( + screen.getByPlaceholderText('https://github.com/backstage/backstage'), + { target: { value: OUR_GITHUB_TEST_REPO } }, + ); + }); + + fireEvent.click(screen.getByText('Next')); + await waitFor(() => { + expect( + screen.getByText( + 'https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml', + ), + ).toBeInTheDocument(); + + const pullReqText = screen.queryByText('pull request'); + expect(pullReqText).not.toBeInTheDocument(); + }); + }); + + it('Should indicate a pull request creation when no yaml file found in the repo', async () => { + findGithubConfigMockResponse = () => ({ + host: 'test.localhost', + owner: 'someuser', + }); + codeSearchMockResponse = () => + Promise.resolve({ + data: { + total_count: 0, + items: [], + }, + }); + const { container } = await renderSUT(); + await waitFor(() => { + fireEvent.input( + screen.getByPlaceholderText('https://github.com/backstage/backstage'), + { target: { value: OUR_GITHUB_TEST_REPO } }, + ); + }); + + fireEvent.click(screen.getByText('Next')); + await waitFor(() => { + expect(screen.getByText(OUR_GITHUB_TEST_REPO)).toBeInTheDocument(); + }); + const textNode = container + .querySelector('a[href="https://github.com/someuser/somerepo"]') + ?.closest('p'); + expect(textNode?.innerHTML).toContain( + 'Following config object will be submitted in a pull request to the repository', + ); + expect( + screen.queryByText( + 'https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml', + ), + ).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx index 7e0d0ff92e..e284e95456 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.tsx @@ -109,10 +109,11 @@ export const ImportComponentPage = ({ GitHub Repo - If you already have code in a GitHub repository, enter the - full URL to your repo and a new pull request with a sample - Backstage metadata Entity File ( - catalog-info.yaml) will be opened for you. + If you already have code in a GitHub repository without + Backstage metadata file set up for it, enter the full URL to + your repo and a new pull request with a sample Backstage + metadata Entity File (catalog-info.yaml) will + be opened for you. )} diff --git a/plugins/catalog-import/src/mocks/analyze-location-POST-response.json b/plugins/catalog-import/src/mocks/analyze-location-POST-response.json new file mode 100644 index 0000000000..eaadf1054a --- /dev/null +++ b/plugins/catalog-import/src/mocks/analyze-location-POST-response.json @@ -0,0 +1,22 @@ +{ + "existingEntityFiles": [], + "generateEntities": [ + { + "entity": { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Component", + "metadata": { + "name": "somerepo", + "annotations": { + "github.com/project-slug": "someuser/somerepo" + } + }, + "spec": { + "type": "other", + "lifecycle": "unknown" + } + }, + "fields": [] + } + ] +} diff --git a/plugins/catalog-import/src/mocks/locations-POST-response.json b/plugins/catalog-import/src/mocks/locations-POST-response.json new file mode 100644 index 0000000000..c44a0f4d61 --- /dev/null +++ b/plugins/catalog-import/src/mocks/locations-POST-response.json @@ -0,0 +1,39 @@ +{ + "location": { + "id": "d4a64359-a709-4c91-a9de-0905a033bf22", + "type": "url", + "target": "https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml" + }, + "entities": [ + { + "metadata": { + "namespace": "default", + "annotations": { + "backstage.io/managed-by-location": "url:https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml", + "github.com/project-slug": "someusername/somerepo" + }, + "name": "somerepo", + "uid": "e992d5ee-7c70-4316-90cf-325f1a0a5146", + "etag": "YWE2M2Q5MzgtNjdkNi00N2QwLWJkZjYtNDM0MTMzMDI4Y2I0", + "generation": 1 + }, + "apiVersion": "backstage.io/v1alpha1", + "kind": "Component", + "spec": { + "type": "other", + "lifecycle": "unknown", + "owner": "unknown" + }, + "relations": [ + { + "target": { + "kind": "group", + "namespace": "default", + "name": "unknown" + }, + "type": "ownedBy" + } + ] + } + ] +} diff --git a/plugins/catalog-import/src/setupTests.ts b/plugins/catalog-import/src/setupTests.ts index 825bcd4115..aea2220869 100644 --- a/plugins/catalog-import/src/setupTests.ts +++ b/plugins/catalog-import/src/setupTests.ts @@ -15,3 +15,4 @@ */ import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/catalog-import/src/util/useGithubRepos.ts b/plugins/catalog-import/src/util/useGithubRepos.ts index 89448a1be1..a2d299b9f4 100644 --- a/plugins/catalog-import/src/util/useGithubRepos.ts +++ b/plugins/catalog-import/src/util/useGithubRepos.ts @@ -22,18 +22,21 @@ import parseGitUrl from 'git-url-parse'; // TODO: (O5ten) Refactor into a core API instead of direct usage like this // https://github.com/backstage/backstage/pull/3613#issuecomment-7408929430 -import { readGitHubIntegrationConfigs } from '@backstage/integration'; +import { + GitHubIntegrationConfig, + readGitHubIntegrationConfigs, +} from '@backstage/integration'; export function useGithubRepos() { const api = useApi(catalogImportApiRef); const config = useApi(configApiRef); - const submitPrToRepo = async (selectedRepo: ConfigSpec) => { + const getGithubIntegrationConfig = (location: string) => { const { name: repoName, owner: ownerName, resource: hostname, - } = parseGitUrl(selectedRepo.location); + } = parseGitUrl(location); const configs = readGitHubIntegrationConfigs( config.getOptionalConfigArray('integrations.github') ?? [], @@ -41,9 +44,22 @@ export function useGithubRepos() { const githubIntegrationConfig = configs.find(v => v.host === hostname); if (!githubIntegrationConfig) { throw new Error( - `Unable to locate github-integration for repo-location: ${selectedRepo.location}`, + `Unable to locate github-integration for repo-location, ${location}`, ); } + return { + repoName, + ownerName, + githubIntegrationConfig, + }; + }; + + const submitPrToRepo = async (selectedRepo: ConfigSpec) => { + const { + repoName, + ownerName, + githubIntegrationConfig, + } = getGithubIntegrationConfig(selectedRepo.location); const submitPRResponse = await api .submitPrToRepo({ owner: ownerName, @@ -54,7 +70,7 @@ export function useGithubRepos() { githubIntegrationConfig, }) .catch(e => { - throw new Error(`Failed to submit PR to repo:\n${e.message}`); + throw new Error(`Failed to submit PR to repo, ${e.message}`); }); await api @@ -62,14 +78,41 @@ export function useGithubRepos() { location: submitPRResponse.location, }) .catch(e => { - throw new Error(`Failed to create repository location:\n${e.message}`); + throw new Error(`Failed to create repository location, ${e.message}`); }); return submitPRResponse; }; + const checkForExistingCatalogInfo = async ( + location: string, + ): Promise<{ exists: boolean; url?: string }> => { + let githubConfig: { + repoName: string; + ownerName: string; + githubIntegrationConfig: GitHubIntegrationConfig; + }; + try { + githubConfig = getGithubIntegrationConfig(location); + } catch (e) { + return { exists: false }; + } + return await api + .checkForExistingCatalogInfo({ + owner: githubConfig.ownerName, + repo: githubConfig.repoName, + githubIntegrationConfig: githubConfig.githubIntegrationConfig, + }) + .catch(e => { + throw new Error( + `Failed to inspect repository for existing catalog-info.yaml, ${e.message}`, + ); + }); + }; + return { submitPrToRepo, + checkForExistingCatalogInfo, generateEntityDefinitions: (repo: string) => api.generateEntityDefinitions({ repo }), addLocation: (location: string) => diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md index 5e0a701caf..154fcf913d 100644 --- a/plugins/jenkins/README.md +++ b/plugins/jenkins/README.md @@ -84,6 +84,5 @@ YWRtaW46MTFlYzI1NmU0Mzg1MDFjM2Y1Yzc2Yjc1MWE3ZTQ3YWY4Mw== is the base64 of user a ## Limitations -- Only works with projects that use the Git SCM -- It requires jobs to be organised into folders -- No pagination support currently - don't run this on a Jenkins with lots of builds +- Only works with organization folder jobs backed by GitHub +- No pagination support currently, limited to 50 jobs - don't run this on a Jenkins with lots of builds diff --git a/plugins/kafka/src/assets/screenshot-1.png b/plugins/kafka/src/assets/screenshot-1.png index 0afae0b5d3..f10a8918cc 100644 Binary files a/plugins/kafka/src/assets/screenshot-1.png and b/plugins/kafka/src/assets/screenshot-1.png differ diff --git a/plugins/kubernetes/src/assets/emptystate.svg b/plugins/kubernetes/src/assets/emptystate.svg index f01a74f374..fa7f19123e 100644 --- a/plugins/kubernetes/src/assets/emptystate.svg +++ b/plugins/kubernetes/src/assets/emptystate.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 5b5e2a2c44..ff91348489 100644 --- a/yarn.lock +++ b/yarn.lock @@ -68,10 +68,10 @@ dependencies: "@openapi-contrib/openapi-schema-to-json-schema" "^3.0.0" -"@asyncapi/parser@^1.0.1": - version "1.1.0" - resolved "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.1.0.tgz#b366c85a6322e182e040d60f36400a0b5c26393c" - integrity sha512-0+NeTiW5sPNYaFf4P2VCcy7Z7MLMP7H969cgOp3gvVTKroI5idkYnWo/C16tKAxs+2oOQ9WnPolsNgc+jb/qtg== +"@asyncapi/parser@^1.3.2": + version "1.3.2" + resolved "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.3.2.tgz#6ff22680ff0e9aaf732121040e01e1f5836a305d" + integrity sha512-YDGdxoVA3+gQ3jvWDp+J3IV5XsvAe+nSHSytPg9m5hfJ48R+mBPMnO7Bnh//gR17LjbmyebPj27h4Ceyaa4d3Q== dependencies: "@apidevtools/json-schema-ref-parser" "^9.0.6" "@asyncapi/specs" "^2.7.5" @@ -82,6 +82,21 @@ node-fetch "^2.6.0" tiny-merge-patch "^0.1.2" +"@asyncapi/react-component@^0.18.2": + version "0.18.2" + resolved "https://registry.npmjs.org/@asyncapi/react-component/-/react-component-0.18.2.tgz#74882b4c2a5ecd00c156898f38d0a5c9e8c4384d" + integrity sha512-vsD3acSM0hTxxSsTJoU79kpI9FXVA64OqNCu8xrawfaU4wdsSQWWn0y7SD8pUfwe4gohwmms0xNcAaMzYIhGLw== + dependencies: + "@asyncapi/avro-schema-parser" "^0.2.0" + "@asyncapi/openapi-schema-parser" "^2.0.0" + "@asyncapi/parser" "^1.3.2" + constate "^1.2.0" + dompurify "^2.1.1" + markdown-it "^11.0.1" + merge "^2.1.0" + openapi-sampler "^1.0.0-beta.15" + react-use "^12.2.0" + "@asyncapi/specs@^2.7.5": version "2.7.5" resolved "https://registry.npmjs.org/@asyncapi/specs/-/specs-2.7.5.tgz#3a516d198fc41a1103695bd889fdd4fbbebe7f5d" @@ -3770,21 +3785,6 @@ dependencies: stream "^0.0.2" -"@kyma-project/asyncapi-react@^0.14.2": - version "0.14.2" - resolved "https://registry.npmjs.org/@kyma-project/asyncapi-react/-/asyncapi-react-0.14.2.tgz#6d99ef878f0481b05db0f3be7c94f4534176675f" - integrity sha512-lf3zcIuCTaVEIkCpF7Vwulrucgdfp1zXUIvkqjYPteOJjOuT6BAvnYwgvFUYibitmD/ADkvK8ybIeKdNJcyaPw== - dependencies: - "@asyncapi/avro-schema-parser" "^0.2.0" - "@asyncapi/openapi-schema-parser" "^2.0.0" - "@asyncapi/parser" "^1.0.1" - constate "^1.2.0" - dompurify "^2.1.1" - markdown-it "^11.0.1" - merge "^1.2.1" - openapi-sampler "^1.0.0-beta.15" - react-use "^12.2.0" - "@lerna/add@3.21.0": version "3.21.0" resolved "https://registry.npmjs.org/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b" @@ -18348,10 +18348,10 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== -merge@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" - integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== +merge@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/merge/-/merge-2.1.0.tgz#91fff62458ba2eca378dd395fa85f1690bf87f60" + integrity sha512-TcuhVDV+e6X457MQAm7xIb19rWhZuEDEho7RrwxMpQ/3GhD5sDlnP188gjQQuweXHy9igdke5oUtVOXX1X8Sxg== methods@^1.0.0, methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: version "1.1.2"