From 110bebc37efbb11ffe801a2c1347d84345e55c24 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Fri, 30 Jul 2021 11:38:31 +0200 Subject: [PATCH 01/34] Fix MaterialUI errors & warnings Signed-off-by: Philipp Hugenroth --- packages/core-components/src/layout/Content/Content.tsx | 7 ++++++- packages/core-components/src/layout/Sidebar/Items.tsx | 2 +- plugins/shortcuts/src/AddShortcut.tsx | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/core-components/src/layout/Content/Content.tsx b/packages/core-components/src/layout/Content/Content.tsx index cb6a841cf3..323565159a 100644 --- a/packages/core-components/src/layout/Content/Content.tsx +++ b/packages/core-components/src/layout/Content/Content.tsx @@ -24,7 +24,12 @@ const useStyles = makeStyles((theme: Theme) => ({ minWidth: 0, paddingTop: theme.spacing(3), paddingBottom: theme.spacing(3), - ...theme.mixins.gutters({}), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + [theme.breakpoints.up('sm')]: { + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3), + }, }, stretch: { display: 'flex', diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index d675b73a2c..b930c455b5 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -167,7 +167,7 @@ export const SidebarItem = forwardRef((props, ref) => { diff --git a/plugins/shortcuts/src/AddShortcut.tsx b/plugins/shortcuts/src/AddShortcut.tsx index 4429dbaeb0..87c2750206 100644 --- a/plugins/shortcuts/src/AddShortcut.tsx +++ b/plugins/shortcuts/src/AddShortcut.tsx @@ -86,7 +86,7 @@ export const AddShortcut = ({ onClose, anchorEl, api }: Props) => { Date: Thu, 15 Jul 2021 11:45:43 +0200 Subject: [PATCH 02/34] Remove stale TechDocs artifacts after publishing new version for GCS Co-authored-by: Camila Belo Co-authored-by: Eric Peterson Signed-off-by: Otto Sichert --- .../src/stages/publish/googleStorage.ts | 100 +++++++++++------- .../src/stages/publish/helpers.ts | 40 +++++++ 2 files changed, 100 insertions(+), 40 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts index 9def24722a..349d6ead97 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts @@ -15,18 +15,19 @@ */ import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { - FileExistsResponse, - Storage, - UploadResponse, -} from '@google-cloud/storage'; +import { FileExistsResponse, Storage } from '@google-cloud/storage'; import express from 'express'; import JSON5 from 'json5'; -import createLimiter from 'p-limit'; import path from 'path'; import { Readable } from 'stream'; import { Logger } from 'winston'; -import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers'; +import { + bulkStorageOperation, + getCloudPathForLocalPath, + getFileTreeRecursively, + getHeadersForFileExtension, + getStaleFiles, +} from './helpers'; import { MigrateWriteStream } from './migrations'; import { PublisherBase, @@ -114,49 +115,68 @@ export class GoogleGCSPublish implements PublisherBase { * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html */ async publish({ entity, directory }: PublishRequest): Promise { + const bucket = this.storageClient.bucket(this.bucketName); + + // First, retrieve a list of all individual files in currently existing + const remoteFolder = getCloudPathForLocalPath(entity); + const existingFiles = ( + await bucket.getFiles({ prefix: remoteFolder }) + )[0].map(file => file.name); + + // Then, merge new files into the same folder + let absoluteFilesToUpload; try { - // Note: GCS manages creation of parent directories if they do not exist. - // So collecting path of only the files is good enough. - const allFilesToUpload = await getFileTreeRecursively(directory); + // Remove the absolute path prefix of the source directory + // Path of all files to upload, relative to the root of the source directory + // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png'] + absoluteFilesToUpload = await getFileTreeRecursively(directory); - const limiter = createLimiter(10); - const uploadPromises: Array> = []; - allFilesToUpload.forEach(sourceFilePath => { - // Remove the absolute path prefix of the source directory - // Path of all files to upload, relative to the root of the source directory - // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png'] - const relativeFilePath = path.relative(directory, sourceFilePath); - - // Convert destination file path to a POSIX path for uploading. - // GCS expects / as path separator and relativeFilePath will contain \\ on Windows. - // https://cloud.google.com/storage/docs/gsutil/addlhelp/HowSubdirectoriesWork - const relativeFilePathPosix = relativeFilePath - .split(path.sep) - .join(path.posix.sep); - - // The / delimiter is intentional since it represents the cloud storage and not the local file system. - const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; - const destination = `${entityRootDir}/${relativeFilePathPosix}`; // GCS Bucket file relative path - - // Rate limit the concurrent execution of file uploads to batches of 10 (per publish) - const uploadFile = limiter(() => - this.storageClient.bucket(this.bucketName).upload(sourceFilePath, { - destination, - }), - ); - uploadPromises.push(uploadFile); - }); - - await Promise.all(uploadPromises); + await bulkStorageOperation( + async absoluteFilePath => { + const relativeFilePath = path.relative(directory, absoluteFilePath); + return await bucket.upload(absoluteFilePath, { + destination: getCloudPathForLocalPath(entity, relativeFilePath), + }); + }, + absoluteFilesToUpload, + { concurrencyLimit: 10 }, + ); this.logger.info( - `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${allFilesToUpload.length}`, + `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${absoluteFilesToUpload.length}`, ); } catch (e) { const errorMessage = `Unable to upload file(s) to Google Cloud Storage. ${e}`; this.logger.error(errorMessage); throw new Error(errorMessage); } + + // Last, try to remove the files that were *only* present previously + try { + const relativeFilesToUpload = absoluteFilesToUpload.map( + absoluteFilePath => + getCloudPathForLocalPath( + entity, + path.relative(directory, absoluteFilePath), + ), + ); + const staleFiles = getStaleFiles(relativeFilesToUpload, existingFiles); + + await bulkStorageOperation( + async relativeFilePath => { + return await bucket.file(relativeFilePath).delete(); + }, + staleFiles, + { concurrencyLimit: 10 }, + ); + + this.logger.info( + `Successfully deleted stale files for Entity ${entity.metadata.name}. Total number of files: ${staleFiles.length}`, + ); + } catch (error) { + const errorMessage = `Unable to delete file(s) from Google Cloud Storage. ${error}`; + this.logger.error(errorMessage); + } } fetchTechDocsMetadata(entityName: EntityName): Promise { diff --git a/packages/techdocs-common/src/stages/publish/helpers.ts b/packages/techdocs-common/src/stages/publish/helpers.ts index ed681eb8ba..1f77d33971 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.ts @@ -13,7 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; import mime from 'mime-types'; +import path from 'path'; +import createLimiter from 'p-limit'; import recursiveReadDir from 'recursive-readdir'; /** @@ -110,4 +113,41 @@ export const lowerCaseEntityTripletInStoragePath = ( const lowerKind = kind.toLowerCase(); const lowerName = name.toLowerCase(); return [lowerNamespace, lowerKind, lowerName, ...parts].join('/'); +} + +// Only returns the files that existed previously and are not present anymore. +export const getStaleFiles = ( + newFiles: string[], + oldFiles: string[], +): string[] => { + const staleFiles = new Set(oldFiles); + newFiles.forEach(newFile => { + staleFiles.delete(newFile); + }); + return Array.from(staleFiles); +}; + +// Compose actual filename on remote bucket including entity information +export const getCloudPathForLocalPath = ( + entity: Entity, + localPath = '', +): string => { + // Convert destination file path to a POSIX path for uploading. + // GCS expects / as path separator and relativeFilePath will contain \\ on Windows. + // https://cloud.google.com/storage/docs/gsutil/addlhelp/HowSubdirectoriesWork + const relativeFilePathPosix = localPath.split(path.sep).join(path.posix.sep); + + // The / delimiter is intentional since it represents the cloud storage and not the local file system. + const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; + return `${entityRootDir}/${relativeFilePathPosix}`; // GCS Bucket file relative path +}; + +// Perform rate limited generic operations by passing a function and a list of arguments +export const bulkStorageOperation = async ( + operation: (arg: T) => Promise, + args: T[], + { concurrencyLimit } = { concurrencyLimit: 25 }, +) => { + const limiter = createLimiter(concurrencyLimit); + await Promise.all(args.map(arg => limiter(operation, arg))); }; From 06b507283b940dd9ebeef2f43c11a43255d7ef58 Mon Sep 17 00:00:00 2001 From: Otto Sichert Date: Wed, 21 Jul 2021 14:53:31 +0200 Subject: [PATCH 03/34] WIP: Remove stale TechDOcs files from AWS Signed-off-by: Otto Sichert --- .../src/stages/publish/awsS3.ts | 98 ++++++++++++------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 7bdc91ffd4..7306320fd2 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -26,8 +26,11 @@ import path from 'path'; import { Readable } from 'stream'; import { Logger } from 'winston'; import { + bulkStorageOperation, + getCloudPathForLocalPath, getFileTreeRecursively, getHeadersForFileExtension, + getStaleFiles, lowerCaseEntityTripletInStoragePath, } from './helpers'; import { @@ -175,54 +178,77 @@ export class AwsS3Publish implements PublisherBase { * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html */ async publish({ entity, directory }: PublishRequest): Promise { + // First, retrieve a list of all individual files in currently existing + const remoteFolder = getCloudPathForLocalPath(entity); + const existingFiles = await this.getAllObjectsFromBucket({ + prefix: remoteFolder, + }); + + // Then, merge new files into the same folder + let absoluteFilesToUpload; try { - // Note: S3 manages creation of parent directories if they do not exist. - // So collecting path of only the files is good enough. - const allFilesToUpload = await getFileTreeRecursively(directory); + // Remove the absolute path prefix of the source directory + // Path of all files to upload, relative to the root of the source directory + // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png'] + absoluteFilesToUpload = await getFileTreeRecursively(directory); - const limiter = createLimiter(10); - const uploadPromises: Array> = []; - for (const filePath of allFilesToUpload) { - // Remove the absolute path prefix of the source directory - // Path of all files to upload, relative to the root of the source directory - // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png'] - const relativeFilePath = path.relative(directory, filePath); - - // Convert destination file path to a POSIX path for uploading. - // S3 expects / as path separator and relativeFilePath will contain \\ on Windows. - // https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html - const relativeFilePathPosix = relativeFilePath - .split(path.sep) - .join(path.posix.sep); - - // The / delimiter is intentional since it represents the cloud storage and not the local file system. - const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; - const destination = `${entityRootDir}/${relativeFilePathPosix}`; // S3 Bucket file relative path - - // Rate limit the concurrent execution of file uploads to batches of 10 (per publish) - const uploadFile = limiter(() => { - const fileStream = fs.createReadStream(filePath); + await bulkStorageOperation( + async absoluteFilePath => { + const relativeFilePath = path.relative(directory, absoluteFilePath); + const fileStream = fs.createReadStream(absoluteFilePath); const params = { Bucket: this.bucketName, - Key: destination, + Key: getCloudPathForLocalPath(entity, relativeFilePath), Body: fileStream, }; return this.storageClient.upload(params).promise(); - }); - uploadPromises.push(uploadFile); - } - await Promise.all(uploadPromises); - this.logger.info( - `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${allFilesToUpload.length}`, + }, + absoluteFilesToUpload, + { concurrencyLimit: 10 }, + ); + + this.logger.info( + `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${absoluteFilesToUpload.length}`, ); - return; } catch (e) { const errorMessage = `Unable to upload file(s) to AWS S3. ${e}`; this.logger.error(errorMessage); throw new Error(errorMessage); } + + // Last, try to remove the files that were *only* present previously + try { + const relativeFilesToUpload = absoluteFilesToUpload.map( + absoluteFilePath => + getCloudPathForLocalPath( + entity, + path.relative(directory, absoluteFilePath), + ), + ); + const staleFiles = getStaleFiles(relativeFilesToUpload, existingFiles); + + await bulkStorageOperation( + async relativeFilePath => { + return await this.storageClient + .deleteObject({ + Bucket: this.bucketName, + Key: relativeFilePath, + }) + .promise(); + }, + staleFiles, + { concurrencyLimit: 10 }, + ); + + this.logger.info( + `Successfully deleted stale files for Entity ${entity.metadata.name}. Total number of files: ${staleFiles.length}`, + ); + } catch (error) { + const errorMessage = `Unable to delete file(s) from AWS S3. ${error}`; + this.logger.error(errorMessage); + } } async fetchTechDocsMetadata( @@ -365,17 +391,19 @@ export class AwsS3Publish implements PublisherBase { /** * Returns a list of all object keys from the configured bucket. */ - protected async getAllObjectsFromBucket(): Promise { + protected async getAllObjectsFromBucket( + { prefix } = { prefix: '' }, + ): Promise { const objects: string[] = []; let nextContinuation: string | undefined; let allObjects: ListObjectsV2Output; - // Iterate through every file in the root of the publisher. do { allObjects = await this.storageClient .listObjectsV2({ Bucket: this.bucketName, ContinuationToken: nextContinuation, + ...(prefix ? { Prefix: prefix } : {}), }) .promise(); objects.push( From 064f8d24876a96fba2489c77451b2bb32cf6fd50 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 23 Jul 2021 20:28:52 +0200 Subject: [PATCH 04/34] feat(techdocs): delete stale files when publishing to Azure Signed-off-by: Camila Belo --- .../src/stages/publish/azureBlobStorage.ts | 139 ++++++++++-------- .../src/stages/publish/helpers.ts | 11 +- 2 files changed, 85 insertions(+), 65 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index fe8fe9c2ce..5dc868ba2b 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -26,8 +26,11 @@ import limiterFactory from 'p-limit'; import { default as path, default as platformPath } from 'path'; import { Logger } from 'winston'; import { + bulkStorageOperation, + getCloudPathForLocalPath, getFileTreeRecursively, getHeadersForFileExtension, + getStaleFiles, lowerCaseEntityTripletInStoragePath, } from './helpers'; import { @@ -133,75 +136,85 @@ export class AzureBlobStoragePublish implements PublisherBase { * Directory structure used in the container is - entityNamespace/entityKind/entityName/index.html */ async publish({ entity, directory }: PublishRequest): Promise { + // First, retrieve a list of all individual files in currently existing + const remoteFolder = getCloudPathForLocalPath(entity); + const existingFiles: string[] = []; + const container = this.storageClient.getContainerClient(this.containerName); try { - // Note: Azure Blob Storage manages creation of parent directories if they do not exist. - // So collecting path of only the files is good enough. - const allFilesToUpload = await getFileTreeRecursively(directory); - - // Bound the number of concurrent batches. We want a bit of concurrency for - // performance reasons, but not so much that we starve the connection pool - // or start thrashing. - const limiter = limiterFactory(BATCH_CONCURRENCY); - - const promises = allFilesToUpload.map(sourceFilePath => { - // Remove the absolute path prefix of the source directory - // Path of all files to upload, relative to the root of the source directory - // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png'] - const relativeFilePath = path.normalize( - path.relative(directory, sourceFilePath), - ); - - // Convert destination file path to a POSIX path for uploading. - // Azure Blob Storage expects / as path separator and relativeFilePath will contain \\ on Windows. - // https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#blob-names - const relativeFilePathPosix = relativeFilePath - .split(path.sep) - .join(path.posix.sep); - - // The / delimiter is intentional since it represents the cloud storage and not the local file system. - const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; - const destination = `${entityRootDir}/${relativeFilePathPosix}`; // Azure Blob Storage Container file relative path - return limiter(async () => { - const response = await this.storageClient - .getContainerClient(this.containerName) - .getBlockBlobClient(destination) - .uploadFile(sourceFilePath); - - if (response._response.status >= 400) { - return { - ...response, - error: new Error( - `Upload failed for ${sourceFilePath} with status code ${response._response.status}`, - ), - }; - } - return { - ...response, - error: undefined, - }; - }); - }); - - const responses = await Promise.all(promises); - - const failed = responses.filter(r => r.error); - if (failed.length === 0) { - this.logger.info( - `Successfully uploaded the ${responses.length} generated file(s) for Entity ${entity.metadata.name}. Total number of files: ${allFilesToUpload.length}`, - ); - } else { - throw new Error( - failed - .map(r => r.error?.message) - .filter(Boolean) - .join(' '), - ); + for await (const blob of container.listBlobsFlat()) { + existingFiles.push(blob.name); } } catch (e) { - const errorMessage = `Unable to upload file(s) to Azure Blob Storage. ${e}`; + const errorMessage = `Unable to list file(s) to Azure. ${e}`; this.logger.error(errorMessage); throw new Error(errorMessage); } + + // Then, merge new files into the same folder + let absoluteFilesToUpload; + try { + // Remove the absolute path prefix of the source directory + // Path of all files to upload, relative to the root of the source directory + // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png'] + absoluteFilesToUpload = await getFileTreeRecursively(directory); + + await bulkStorageOperation( + async absoluteFilePath => { + // const relativeFilePath = path.relative(directory, absoluteFilePath); + const relativeFilePath = path.normalize( + path.relative(directory, absoluteFilePath), + ); + return await this.storageClient + .getContainerClient(this.containerName) + .getBlockBlobClient( + getCloudPathForLocalPath(entity, relativeFilePath), + ) + .uploadFile(absoluteFilePath); + }, + absoluteFilesToUpload, + { concurrencyLimit: BATCH_CONCURRENCY }, + ); + + this.logger.info( + `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${absoluteFilesToUpload.length}`, + ); + } catch (e) { + const errorMessage = `Unable to upload file(s) to Azure. ${e}`; + this.logger.error(errorMessage); + throw new Error(errorMessage); + } + + // Last, try to remove the files that were *only* present previously + try { + const relativeFilesToUpload = absoluteFilesToUpload.map( + absoluteFilePath => + getCloudPathForLocalPath( + entity, + path.relative(directory, absoluteFilePath), + ), + ); + + const staleFiles = getStaleFiles( + relativeFilesToUpload, + existingFiles, + remoteFolder, + ); + + await bulkStorageOperation( + async relativeFilePath => { + return await container.deleteBlob(relativeFilePath); + }, + staleFiles, + { concurrencyLimit: BATCH_CONCURRENCY }, + ); + + this.logger.info( + `Successfully deleted stale files for Entity ${entity.metadata.name}. Total number of files: ${staleFiles.length}`, + ); + } catch (error) { + const errorMessage = `Unable to delete file(s) from AWS S3. ${error}`; + this.logger.error(errorMessage); + } } private download(containerName: string, blobPath: string): Promise { diff --git a/packages/techdocs-common/src/stages/publish/helpers.ts b/packages/techdocs-common/src/stages/publish/helpers.ts index 1f77d33971..2aabcb3259 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.ts @@ -113,14 +113,21 @@ export const lowerCaseEntityTripletInStoragePath = ( const lowerKind = kind.toLowerCase(); const lowerName = name.toLowerCase(); return [lowerNamespace, lowerKind, lowerName, ...parts].join('/'); -} +}; // Only returns the files that existed previously and are not present anymore. export const getStaleFiles = ( newFiles: string[], oldFiles: string[], + remoteFolder?: string, ): string[] => { - const staleFiles = new Set(oldFiles); + let filteredFiles = [...oldFiles]; + if (remoteFolder) { + filteredFiles = filteredFiles.filter(filePath => + filePath.match(remoteFolder), + ); + } + const staleFiles = new Set(filteredFiles); newFiles.forEach(newFile => { staleFiles.delete(newFile); }); From fedf69a396809146197255bb2327cebdf47d5b52 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 23 Jul 2021 20:39:20 +0200 Subject: [PATCH 05/34] fix(techdocs): remove unused imported module Signed-off-by: Camila Belo --- packages/techdocs-common/src/stages/publish/awsS3.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 7306320fd2..3ba5ffe4e3 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -16,7 +16,7 @@ import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import aws, { Credentials } from 'aws-sdk'; -import { ListObjectsV2Output, ManagedUpload } from 'aws-sdk/clients/s3'; +import { ListObjectsV2Output } from 'aws-sdk/clients/s3'; import { CredentialsOptions } from 'aws-sdk/lib/credentials'; import express from 'express'; import fs from 'fs-extra'; From 6f9e155973077d9958d21c68250f2249f1f1532c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 27 Jul 2021 14:48:06 +0200 Subject: [PATCH 06/34] fix(techdocs): make azure tests pass Co-authored-by: Eric Peterson Signed-off-by: Camila Belo --- .../__mocks__/@azure/storage-blob.ts | 4 +++ .../stages/publish/azureBlobStorage.test.ts | 23 +++++++--------- .../src/stages/publish/azureBlobStorage.ts | 27 ++++++++++++++++--- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/packages/techdocs-common/__mocks__/@azure/storage-blob.ts b/packages/techdocs-common/__mocks__/@azure/storage-blob.ts index 73c5873832..56574dedf7 100644 --- a/packages/techdocs-common/__mocks__/@azure/storage-blob.ts +++ b/packages/techdocs-common/__mocks__/@azure/storage-blob.ts @@ -125,6 +125,10 @@ export class ContainerClient { getBlockBlobClient(blobName: string) { return new BlockBlobClient(blobName); } + + listBlobsFlat() { + return []; + } } class ContainerClientFailGetProperties extends ContainerClient { diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts index e55314a71b..8c31153201 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts @@ -64,10 +64,11 @@ const getEntityRootDir = (entity: Entity) => { const logger = getVoidLogger(); jest.spyOn(logger, 'info').mockReturnValue(logger); -jest.spyOn(logger, 'error').mockReturnValue(logger); +const loggerSpy = jest.spyOn(logger, 'error').mockReturnValue(logger); let publisher: PublisherBase; beforeEach(async () => { + loggerSpy.mockClear(); mockFs.restore(); const mockConfig = new ConfigReader({ techdocs: { @@ -175,14 +176,12 @@ describe('publishing with valid credentials', () => { directory: wrongPathToGeneratedDirectory, }); - await expect(fails).rejects.toMatchObject({ - message: expect.stringContaining( - `Unable to upload file(s) to Azure Blob Storage. Error: Failed to read template directory: ENOENT, no such file or directory`, - ), - }); - await expect(fails).rejects.toMatchObject({ - message: expect.stringContaining(wrongPathToGeneratedDirectory), - }); + await expect(fails).rejects.toThrow( + /Unable to upload file\(s\) to Azure. Error: Failed to read template directory: ENOENT, no such file or directory/, + ); + await expect(fails).rejects.toThrow( + new RegExp(wrongPathToGeneratedDirectory), + ); mockFs.restore(); }); @@ -225,13 +224,11 @@ describe('publishing with valid credentials', () => { error = e; } - expect(error.message).toContain( - `Unable to upload file(s) to Azure Blob Storage.`, - ); + expect(error.message).toContain(`Unable to upload file(s) to Azure`); expect(logger.error).toHaveBeenCalledWith( expect.stringContaining( - `Unable to upload file(s) to Azure Blob Storage. Error: Upload failed for ${path.join( + `Unable to upload file(s) to Azure. Error: Upload failed for ${path.join( entityRootDir, 'index.html', )} with status code 500`, diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index 5dc868ba2b..c7b86ed39e 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -16,6 +16,7 @@ import { DefaultAzureCredential } from '@azure/identity'; import { BlobServiceClient, + ContainerClient, StorageSharedKeyCredential, } from '@azure/storage-blob'; import { Entity, EntityName } from '@backstage/catalog-model'; @@ -139,8 +140,9 @@ export class AzureBlobStoragePublish implements PublisherBase { // First, retrieve a list of all individual files in currently existing const remoteFolder = getCloudPathForLocalPath(entity); const existingFiles: string[] = []; - const container = this.storageClient.getContainerClient(this.containerName); + let container: ContainerClient; try { + container = this.storageClient.getContainerClient(this.containerName); for await (const blob of container.listBlobsFlat()) { existingFiles.push(blob.name); } @@ -158,23 +160,42 @@ export class AzureBlobStoragePublish implements PublisherBase { // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png'] absoluteFilesToUpload = await getFileTreeRecursively(directory); + const failedOperations: Error[] = []; await bulkStorageOperation( async absoluteFilePath => { // const relativeFilePath = path.relative(directory, absoluteFilePath); const relativeFilePath = path.normalize( path.relative(directory, absoluteFilePath), ); - return await this.storageClient - .getContainerClient(this.containerName) + const response = await container .getBlockBlobClient( getCloudPathForLocalPath(entity, relativeFilePath), ) .uploadFile(absoluteFilePath); + + if (response._response.status >= 400) { + failedOperations.push( + new Error( + `Upload failed for ${absoluteFilePath} with status code ${response._response.status}`, + ), + ); + } + + return response; }, absoluteFilesToUpload, { concurrencyLimit: BATCH_CONCURRENCY }, ); + if (failedOperations.length > 0) { + throw new Error( + failedOperations + .map(r => r.message) + .filter(Boolean) + .join(' '), + ); + } + this.logger.info( `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${absoluteFilesToUpload.length}`, ); From caf519f69cc0139b34598244aaf01c8e321d689e Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 28 Jul 2021 11:46:33 +0200 Subject: [PATCH 07/34] fix(techdocs): make aws and gcs tests pass Co-authored-by: Eric Peterson Signed-off-by: Camila Belo --- .../__mocks__/@google-cloud/storage.ts | 11 +++++++ packages/techdocs-common/__mocks__/aws-sdk.ts | 8 +++++ .../src/stages/publish/googleStorage.ts | 31 ++++++++++++++++--- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts index 18fe62af14..ae2bce7a8f 100644 --- a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts +++ b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts @@ -107,6 +107,17 @@ class Bucket { file(destinationFilePath: string) { return new GCSFile(destinationFilePath); } + + getFilesStream() { + const readable = new Readable(); + readable._read = () => {}; + + process.nextTick(() => { + readable.emit('end'); + }); + + return readable; + } } export class Storage { diff --git a/packages/techdocs-common/__mocks__/aws-sdk.ts b/packages/techdocs-common/__mocks__/aws-sdk.ts index 2826a8d8bb..ec0273e277 100644 --- a/packages/techdocs-common/__mocks__/aws-sdk.ts +++ b/packages/techdocs-common/__mocks__/aws-sdk.ts @@ -104,6 +104,14 @@ export class S3 { }), }; } + + listObjectsV2() { + return { + promise: () => { + return Promise.resolve([]); + }, + }; + } } export default { diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts index 349d6ead97..24f84f0136 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts @@ -15,7 +15,7 @@ */ import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { FileExistsResponse, Storage } from '@google-cloud/storage'; +import { File, FileExistsResponse, Storage } from '@google-cloud/storage'; import express from 'express'; import JSON5 from 'json5'; import path from 'path'; @@ -119,9 +119,7 @@ export class GoogleGCSPublish implements PublisherBase { // First, retrieve a list of all individual files in currently existing const remoteFolder = getCloudPathForLocalPath(entity); - const existingFiles = ( - await bucket.getFiles({ prefix: remoteFolder }) - )[0].map(file => file.name); + const existingFiles = await this.getFilesForFolder(remoteFolder); // Then, merge new files into the same folder let absoluteFilesToUpload; @@ -276,4 +274,29 @@ export class GoogleGCSPublish implements PublisherBase { }); }); } + + private getFilesForFolder(folder: string): Promise { + const fileMetadataStream: Readable = this.storageClient + .bucket(this.bucketName) + .getFilesStream({ prefix: folder }); + + return new Promise((resolve, reject) => { + const files: string[] = []; + + fileMetadataStream.on('error', error => { + // push file to file array + reject(error); + }); + + fileMetadataStream.on('data', (file: File) => { + // push file to file array + files.push(file.name); + }); + + fileMetadataStream.on('end', () => { + // resolve promise + resolve(files); + }); + }); + } } From da705a74e0dd7aec23827fecbcc8efc4a907b582 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 2 Aug 2021 17:17:29 +0200 Subject: [PATCH 08/34] test(techdocs-common): cover stale files deletion for GCS Signed-off-by: Camila Belo --- .../__mocks__/@google-cloud/storage.ts | 13 +++++ .../src/stages/publish/googleStorage.test.ts | 58 +++++++++++-------- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts index ae2bce7a8f..f5616236cf 100644 --- a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts +++ b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts @@ -77,6 +77,10 @@ class GCSFile { }); return readable; } + + delete() { + return Promise.resolve(); + } } class Bucket { @@ -105,6 +109,9 @@ class Bucket { } file(destinationFilePath: string) { + if (this.bucketName === 'delete_stale_files_error') { + throw Error('Message'); + } return new GCSFile(destinationFilePath); } @@ -113,6 +120,12 @@ class Bucket { readable._read = () => {}; process.nextTick(() => { + if ( + this.bucketName === 'delete_stale_files_success' || + this.bucketName === 'delete_stale_files_error' + ) { + readable.emit('data', { name: 'stale-file.png' }); + } readable.emit('end'); }); diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts index 8a00fc9708..65cbf3c5ac 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts @@ -63,12 +63,10 @@ const getEntityRootDir = (entity: Entity) => { }; const logger = getVoidLogger(); -jest.spyOn(logger, 'info').mockReturnValue(logger); +const loggerInfoSpy = jest.spyOn(logger, 'info').mockReturnValue(logger); +const loggerErrorSpy = jest.spyOn(logger, 'error').mockReturnValue(logger); -let publisher: PublisherBase; - -beforeEach(async () => { - mockFs.restore(); +const createPublisherMock = (bucketName: string) => { const mockConfig = new ConfigReader({ techdocs: { requestUrl: 'http://localhost:7000', @@ -76,13 +74,21 @@ beforeEach(async () => { type: 'googleGcs', googleGcs: { credentials: '{}', - bucketName: 'bucketName', + bucketName, }, }, }, }); + return GoogleGCSPublish.fromConfig(mockConfig, logger); +}; - publisher = await GoogleGCSPublish.fromConfig(mockConfig, logger); +let publisher: PublisherBase; + +beforeEach(async () => { + loggerInfoSpy.mockClear(); + loggerErrorSpy.mockClear(); + mockFs.restore(); + publisher = createPublisherMock('bucketName'); }); describe('GoogleGCSPublish', () => { @@ -94,20 +100,7 @@ describe('GoogleGCSPublish', () => { }); it('should reject incorrect config', async () => { - const mockConfig = new ConfigReader({ - techdocs: { - requestUrl: 'http://localhost:7000', - publisher: { - type: 'googleGcs', - googleGcs: { - credentials: '{}', - bucketName: 'errorBucket', - }, - }, - }, - }); - - const errorPublisher = GoogleGCSPublish.fromConfig(mockConfig, logger); + const errorPublisher = createPublisherMock('errorBucket'); expect(await errorPublisher.getReadiness()).toEqual({ isAvailable: false, @@ -145,7 +138,6 @@ describe('GoogleGCSPublish', () => { directory: entityRootDir, }), ).toBeUndefined(); - mockFs.restore(); }); it('should fail to publish a directory', async () => { @@ -181,8 +173,28 @@ describe('GoogleGCSPublish', () => { await expect(fails).rejects.toMatchObject({ message: expect.stringContaining(wrongPathToGeneratedDirectory), }); + }); - mockFs.restore(); + it('should delete stale files after upload', async () => { + const entity = createMockEntity(); + const directory = getEntityRootDir(entity); + const bucketName = 'delete_stale_files_success'; + publisher = createPublisherMock(bucketName); + await publisher.publish({ entity, directory }); + expect(loggerInfoSpy).toHaveBeenLastCalledWith( + `Successfully deleted stale files for Entity ${entity.metadata.name}. Total number of files: 1`, + ); + }); + + it('should log error when the stale files deletion fails', async () => { + const entity = createMockEntity(); + const directory = getEntityRootDir(entity); + const bucketName = 'delete_stale_files_error'; + publisher = createPublisherMock(bucketName); + await publisher.publish({ entity, directory }); + expect(loggerErrorSpy).toHaveBeenLastCalledWith( + 'Unable to delete file(s) from Google Cloud Storage. Error: Message', + ); }); }); From 5386bcc989e2a45db39064d41161965d70703384 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 2 Aug 2021 19:41:36 +0200 Subject: [PATCH 09/34] test(techdocs-common): cover stale files deletion for AWS Signed-off-by: Camila Belo --- packages/techdocs-common/__mocks__/aws-sdk.ts | 23 ++++++- .../src/stages/publish/awsS3.test.ts | 61 +++++++++++-------- 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/packages/techdocs-common/__mocks__/aws-sdk.ts b/packages/techdocs-common/__mocks__/aws-sdk.ts index ec0273e277..9b3218f6dc 100644 --- a/packages/techdocs-common/__mocks__/aws-sdk.ts +++ b/packages/techdocs-common/__mocks__/aws-sdk.ts @@ -105,10 +105,29 @@ export class S3 { }; } - listObjectsV2() { + listObjectsV2({ Bucket }) { return { promise: () => { - return Promise.resolve([]); + if ( + Bucket === 'delete_stale_files_success' || + Bucket === 'delete_stale_files_error' + ) { + return Promise.resolve({ + Contents: [{ Key: 'stale_file.png' }], + }); + } + return Promise.resolve({}); + }, + }; + } + + deleteObject({ Bucket }) { + return { + promise: () => { + if (Bucket === 'delete_stale_files_error') { + throw new Error('Message'); + } + return Promise.resolve(); }, }; } diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts index 81f06716b1..b94518c857 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -63,11 +63,10 @@ const getEntityRootDir = (entity: Entity) => { }; const logger = getVoidLogger(); +const loggerInfoSpy = jest.spyOn(logger, 'info'); +const loggerErrorSpy = jest.spyOn(logger, 'error'); -let publisher: PublisherBase; - -beforeEach(() => { - mockFs.restore(); +const createPublisherMock = (bucketName: string) => { const mockConfig = new ConfigReader({ techdocs: { requestUrl: 'http://localhost:7000', @@ -78,13 +77,22 @@ beforeEach(() => { accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', }, - bucketName: 'bucketName', + bucketName, }, }, }, }); - publisher = AwsS3Publish.fromConfig(mockConfig, logger); + return AwsS3Publish.fromConfig(mockConfig, logger); +}; + +let publisher: PublisherBase; + +beforeEach(() => { + loggerInfoSpy.mockClear(); + loggerErrorSpy.mockClear(); + mockFs.restore(); + publisher = createPublisherMock('bucketName'); }); describe('AwsS3Publish', () => { @@ -96,24 +104,7 @@ describe('AwsS3Publish', () => { }); it('should reject incorrect config', async () => { - const mockConfig = new ConfigReader({ - techdocs: { - requestUrl: 'http://localhost:7000', - publisher: { - type: 'awsS3', - awsS3: { - credentials: { - accessKeyId: 'accessKeyId', - secretAccessKey: 'secretAccessKey', - }, - // this bucket name will throw an error - bucketName: 'errorBucket', - }, - }, - }, - }); - - const errorPublisher = AwsS3Publish.fromConfig(mockConfig, logger); + const errorPublisher = createPublisherMock('errorBucket'); expect(await errorPublisher.getReadiness()).toEqual({ isAvailable: false, @@ -188,8 +179,28 @@ describe('AwsS3Publish', () => { await expect(fails).rejects.toMatchObject({ message: expect.stringContaining(wrongPathToGeneratedDirectory), }); + }); - mockFs.restore(); + it('should delete stale files after upload', async () => { + const entity = createMockEntity(); + const directory = getEntityRootDir(entity); + const bucketName = 'delete_stale_files_success'; + publisher = createPublisherMock(bucketName); + await publisher.publish({ entity, directory }); + expect(loggerInfoSpy).toHaveBeenLastCalledWith( + `Successfully deleted stale files for Entity ${entity.metadata.name}. Total number of files: 1`, + ); + }); + + it('should log error when the stale files deletion fails', async () => { + const entity = createMockEntity(); + const directory = getEntityRootDir(entity); + const bucketName = 'delete_stale_files_error'; + publisher = createPublisherMock(bucketName); + await publisher.publish({ entity, directory }); + expect(loggerErrorSpy).toHaveBeenLastCalledWith( + 'Unable to delete file(s) from AWS S3. Error: Message', + ); }); }); From 9cf37164b1802687a7aa468645da5c70ab5f8a9a Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 2 Aug 2021 22:15:36 +0200 Subject: [PATCH 10/34] test(techdocs-common): cover stale files deletion for Azure Signed-off-by: Camila Belo --- .../__mocks__/@azure/storage-blob.ts | 14 ++- .../stages/publish/azureBlobStorage.test.ts | 103 +++++++++--------- .../src/stages/publish/azureBlobStorage.ts | 13 +-- .../src/stages/publish/helpers.ts | 9 +- 4 files changed, 72 insertions(+), 67 deletions(-) diff --git a/packages/techdocs-common/__mocks__/@azure/storage-blob.ts b/packages/techdocs-common/__mocks__/@azure/storage-blob.ts index 56574dedf7..f7303ce1cb 100644 --- a/packages/techdocs-common/__mocks__/@azure/storage-blob.ts +++ b/packages/techdocs-common/__mocks__/@azure/storage-blob.ts @@ -126,9 +126,21 @@ export class ContainerClient { return new BlockBlobClient(blobName); } - listBlobsFlat() { + listBlobsFlat({ prefix }: { prefix: string }) { + if ( + this.containerName === 'delete_stale_files_success' || + this.containerName === 'delete_stale_files_error' + ) { + return [{ name: `${prefix}stale_file.png` }]; + } return []; } + + deleteBlob() { + if (this.containerName === 'delete_stale_files_error') { + throw new Error('Message'); + } + } } class ContainerClientFailGetProperties extends ContainerClient { diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts index 8c31153201..2e17b43b62 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts @@ -63,13 +63,18 @@ const getEntityRootDir = (entity: Entity) => { }; const logger = getVoidLogger(); -jest.spyOn(logger, 'info').mockReturnValue(logger); -const loggerSpy = jest.spyOn(logger, 'error').mockReturnValue(logger); +const loggerInfoSpy = jest.spyOn(logger, 'info'); +const loggerErrorSpy = jest.spyOn(logger, 'error'); -let publisher: PublisherBase; -beforeEach(async () => { - loggerSpy.mockClear(); - mockFs.restore(); +const createPublisherMock = ({ + accountName = 'accountName', + containerName = 'containerName', +}: + | { + accountName?: string; + containerName?: string; + } + | undefined = {}) => { const mockConfig = new ConfigReader({ techdocs: { requestUrl: 'http://localhost:7000', @@ -77,16 +82,24 @@ beforeEach(async () => { type: 'azureBlobStorage', azureBlobStorage: { credentials: { - accountName: 'accountName', + accountName, accountKey: 'accountKey', }, - containerName: 'containerName', + containerName, }, }, }, }); - publisher = AzureBlobStoragePublish.fromConfig(mockConfig, logger); + return AzureBlobStoragePublish.fromConfig(mockConfig, logger); +}; + +let publisher: PublisherBase; +beforeEach(async () => { + loggerInfoSpy.mockClear(); + loggerErrorSpy.mockClear(); + mockFs.restore(); + publisher = createPublisherMock(); }); describe('publishing with valid credentials', () => { @@ -98,32 +111,15 @@ describe('publishing with valid credentials', () => { }); it('should reject incorrect config', async () => { - const mockConfig = new ConfigReader({ - techdocs: { - requestUrl: 'http://localhost:7000', - publisher: { - type: 'azureBlobStorage', - azureBlobStorage: { - credentials: { - accountName: 'accountName', - accountKey: 'accountKey', - }, - containerName: 'bad_container', - }, - }, - }, + const errorPublisher = createPublisherMock({ + containerName: 'bad_container', }); - const errorPublisher = await AzureBlobStoragePublish.fromConfig( - mockConfig, - logger, - ); - expect(await errorPublisher.getReadiness()).toEqual({ isAvailable: false, }); - expect(logger.error).toHaveBeenCalledWith( + expect(loggerErrorSpy).toHaveBeenCalledWith( expect.stringContaining( `Could not retrieve metadata about the Azure Blob Storage container bad_container.`, ), @@ -147,6 +143,10 @@ describe('publishing with valid credentials', () => { }); }); + afterEach(() => { + mockFs.restore(); + }); + it('should publish a directory', async () => { const entity = createMockEntity(); const entityRootDir = getEntityRootDir(entity); @@ -157,7 +157,6 @@ describe('publishing with valid credentials', () => { directory: entityRootDir, }), ).toBeUndefined(); - mockFs.restore(); }); it('should fail to publish a directory', async () => { @@ -182,29 +181,13 @@ describe('publishing with valid credentials', () => { await expect(fails).rejects.toThrow( new RegExp(wrongPathToGeneratedDirectory), ); - - mockFs.restore(); }); it('reports an error when bad account credentials', async () => { - const mockConfig = new ConfigReader({ - techdocs: { - requestUrl: 'http://localhost:7000', - publisher: { - type: 'azureBlobStorage', - azureBlobStorage: { - credentials: { - accountName: 'failupload', - accountKey: 'accountKey', - }, - containerName: 'containerName', - }, - }, - }, + publisher = createPublisherMock({ + accountName: 'failupload', }); - publisher = await AzureBlobStoragePublish.fromConfig(mockConfig, logger); - const entity = createMockEntity(); const entityRootDir = getEntityRootDir(entity); @@ -226,7 +209,7 @@ describe('publishing with valid credentials', () => { expect(error.message).toContain(`Unable to upload file(s) to Azure`); - expect(logger.error).toHaveBeenCalledWith( + expect(loggerErrorSpy).toHaveBeenCalledWith( expect.stringContaining( `Unable to upload file(s) to Azure. Error: Upload failed for ${path.join( entityRootDir, @@ -234,8 +217,28 @@ describe('publishing with valid credentials', () => { )} with status code 500`, ), ); + }); - mockFs.restore(); + it('should delete stale files after upload', async () => { + const entity = createMockEntity(); + const directory = getEntityRootDir(entity); + const containerName = 'delete_stale_files_success'; + publisher = createPublisherMock({ containerName }); + await publisher.publish({ entity, directory }); + expect(loggerInfoSpy).toHaveBeenLastCalledWith( + `Successfully deleted stale files for Entity ${entity.metadata.name}. Total number of files: 1`, + ); + }); + + it('should log error when the stale files deletion fails', async () => { + const entity = createMockEntity(); + const directory = getEntityRootDir(entity); + const containerName = 'delete_stale_files_error'; + publisher = createPublisherMock({ containerName }); + await publisher.publish({ entity, directory }); + expect(loggerErrorSpy).toHaveBeenLastCalledWith( + 'Unable to delete file(s) from Azure. Error: Message', + ); }); }); diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index c7b86ed39e..f49b317ecf 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -143,7 +143,9 @@ export class AzureBlobStoragePublish implements PublisherBase { let container: ContainerClient; try { container = this.storageClient.getContainerClient(this.containerName); - for await (const blob of container.listBlobsFlat()) { + for await (const blob of container.listBlobsFlat({ + prefix: remoteFolder, + })) { existingFiles.push(blob.name); } } catch (e) { @@ -163,7 +165,6 @@ export class AzureBlobStoragePublish implements PublisherBase { const failedOperations: Error[] = []; await bulkStorageOperation( async absoluteFilePath => { - // const relativeFilePath = path.relative(directory, absoluteFilePath); const relativeFilePath = path.normalize( path.relative(directory, absoluteFilePath), ); @@ -215,11 +216,7 @@ export class AzureBlobStoragePublish implements PublisherBase { ), ); - const staleFiles = getStaleFiles( - relativeFilesToUpload, - existingFiles, - remoteFolder, - ); + const staleFiles = getStaleFiles(relativeFilesToUpload, existingFiles); await bulkStorageOperation( async relativeFilePath => { @@ -233,7 +230,7 @@ export class AzureBlobStoragePublish implements PublisherBase { `Successfully deleted stale files for Entity ${entity.metadata.name}. Total number of files: ${staleFiles.length}`, ); } catch (error) { - const errorMessage = `Unable to delete file(s) from AWS S3. ${error}`; + const errorMessage = `Unable to delete file(s) from Azure. ${error}`; this.logger.error(errorMessage); } } diff --git a/packages/techdocs-common/src/stages/publish/helpers.ts b/packages/techdocs-common/src/stages/publish/helpers.ts index 2aabcb3259..6f6eb7dfa5 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.ts @@ -119,15 +119,8 @@ export const lowerCaseEntityTripletInStoragePath = ( export const getStaleFiles = ( newFiles: string[], oldFiles: string[], - remoteFolder?: string, ): string[] => { - let filteredFiles = [...oldFiles]; - if (remoteFolder) { - filteredFiles = filteredFiles.filter(filePath => - filePath.match(remoteFolder), - ); - } - const staleFiles = new Set(filteredFiles); + const staleFiles = new Set(oldFiles); newFiles.forEach(newFile => { staleFiles.delete(newFile); }); From ab9b315e2a748658d628dac0bdb31aefb1390661 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 3 Aug 2021 09:33:58 +0200 Subject: [PATCH 11/34] test(techdocs-common): cover getStaleFiles helper Signed-off-by: Camila Belo --- .../src/stages/publish/helpers.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/techdocs-common/src/stages/publish/helpers.test.ts b/packages/techdocs-common/src/stages/publish/helpers.test.ts index 27a56cd06d..63b4e0aeec 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.test.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.test.ts @@ -17,6 +17,7 @@ import mockFs from 'mock-fs'; import * as os from 'os'; import * as path from 'path'; import { + getStaleFiles, getFileTreeRecursively, getHeadersForFileExtension, lowerCaseEntityTripletInStoragePath, @@ -99,3 +100,27 @@ describe('lowerCaseEntityTripletInStoragePath', () => { ).toThrowError(error); }); }); + +describe('getStaleFiles', () => { + const defaultFiles = [ + 'default/Component/backstage/index.html', + 'default/Component/backstage/techdocs_metadata.json', + 'default/Component/backstage/assests/javascripts/bundle.7f4f3c92.min.js', + 'default/Component/backstage/assets/stylesheets/main.fe0cca5b.min.css', + ]; + + it('should return empty array if there is no stale file', () => { + const oldFiles = [...defaultFiles]; + const newFiles = [...defaultFiles]; + const staleFiles = getStaleFiles(newFiles, oldFiles); + expect(staleFiles).toHaveLength(0); + }); + + it('should return all stale files when they exists', () => { + const oldFiles = [...defaultFiles, 'stale_file.png']; + const newFiles = [...defaultFiles]; + const staleFiles = getStaleFiles(newFiles, oldFiles); + expect(staleFiles).toHaveLength(1); + expect(staleFiles).toEqual(expect.arrayContaining(['stale_file.png'])); + }); +}); From 7eef84a0404e43aebf440749fd25f8ec171e205e Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 3 Aug 2021 10:02:30 +0200 Subject: [PATCH 12/34] test(techdocs-common): cover getCloudPathForLocalPath helper Signed-off-by: Camila Belo --- .../src/stages/publish/helpers.test.ts | 44 +++++++++++++++++++ .../src/stages/publish/helpers.ts | 6 ++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/helpers.test.ts b/packages/techdocs-common/src/stages/publish/helpers.test.ts index 63b4e0aeec..54761cf311 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.test.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.test.ts @@ -16,9 +16,11 @@ import mockFs from 'mock-fs'; import * as os from 'os'; import * as path from 'path'; +import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { getStaleFiles, getFileTreeRecursively, + getCloudPathForLocalPath, getHeadersForFileExtension, lowerCaseEntityTripletInStoragePath, } from './helpers'; @@ -124,3 +126,45 @@ describe('getStaleFiles', () => { expect(staleFiles).toEqual(expect.arrayContaining(['stale_file.png'])); }); }); + +describe('getCloudPathForLocalPath', () => { + const entity: Entity = { + apiVersion: 'version', + metadata: { namespace: 'default', name: 'backstage' }, + kind: 'Component', + }; + + it('should compose a remote bucket path including entity information', () => { + const remoteBucket = getCloudPathForLocalPath(entity); + expect(remoteBucket).toBe( + `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}/`, + ); + }); + + it('should compose a remote filename including entity information', () => { + const localPath = 'index.html'; + const remoteBucket = getCloudPathForLocalPath(entity, localPath); + expect(remoteBucket).toBe( + `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}/${localPath}`, + ); + }); + + it('should use the default namespace when it is undefined', () => { + const localPath = 'index.html'; + const { + kind, + metadata: { name }, + } = entity; + const remoteBucket = getCloudPathForLocalPath( + { kind, metadata: { name } } as Entity, + localPath, + ); + expect(remoteBucket).toBe( + `${ENTITY_DEFAULT_NAMESPACE}/${entity.kind}/${entity.metadata.name}/${localPath}`, + ); + }); + + it('should throw error when entity is invalid', () => { + expect(() => getCloudPathForLocalPath({} as Entity)).toThrow(); + }); +}); diff --git a/packages/techdocs-common/src/stages/publish/helpers.ts b/packages/techdocs-common/src/stages/publish/helpers.ts index 6f6eb7dfa5..fda03e7415 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import mime from 'mime-types'; import path from 'path'; import createLimiter from 'p-limit'; @@ -138,7 +138,9 @@ export const getCloudPathForLocalPath = ( const relativeFilePathPosix = localPath.split(path.sep).join(path.posix.sep); // The / delimiter is intentional since it represents the cloud storage and not the local file system. - const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; + const entityRootDir = `${ + entity.metadata?.namespace ?? ENTITY_DEFAULT_NAMESPACE + }/${entity.kind}/${entity.metadata.name}`; return `${entityRootDir}/${relativeFilePathPosix}`; // GCS Bucket file relative path }; From 5486071aa8867653df75d158c15aed51c8967015 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 3 Aug 2021 11:52:19 +0200 Subject: [PATCH 13/34] test(techdocs-common): cover bulkStorageOperation helper Signed-off-by: Camila Belo --- .../src/stages/publish/helpers.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/techdocs-common/src/stages/publish/helpers.test.ts b/packages/techdocs-common/src/stages/publish/helpers.test.ts index 54761cf311..b288d221d3 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.test.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.test.ts @@ -22,6 +22,7 @@ import { getFileTreeRecursively, getCloudPathForLocalPath, getHeadersForFileExtension, + bulkStorageOperation, lowerCaseEntityTripletInStoragePath, } from './helpers'; @@ -168,3 +169,52 @@ describe('getCloudPathForLocalPath', () => { expect(() => getCloudPathForLocalPath({} as Entity)).toThrow(); }); }); + +describe('bulkStorageOperation', () => { + const length = 26; + const args = Array.from({ length }); + const createConcurrentRequestCounter = ( + callback: (count: number) => void, + ) => { + let count = 0; + return () => + new Promise(resolve => { + callback(++count); + setTimeout(() => { + count--; + resolve(null); + }, 100); + }); + }; + + it('should take care of rate limit by default', async () => { + const operation = createConcurrentRequestCounter((count: number) => { + expect(count <= 25).toBeTruthy(); + }); + await bulkStorageOperation(operation, args); + }); + + it('should accept the number of concurrency limit', async () => { + const concurrencyLimit = 10; + const operation = createConcurrentRequestCounter((count: number) => { + expect(count <= concurrencyLimit).toBeTruthy(); + }); + await bulkStorageOperation(operation, args, { concurrencyLimit }); + }); + + it('should wait for all promises be resolved', async () => { + const callback = jest.fn(); + const operation = createConcurrentRequestCounter(callback); + await bulkStorageOperation(operation, args); + expect(callback).toHaveBeenCalledTimes(length); + }); + + it('should call operation with the correct argument', async () => { + const files = ['file1.txt', 'file2.txt']; + const fn = jest.fn(); + await bulkStorageOperation(fn, files); + expect(fn).toHaveBeenCalledTimes(2); + expect(fn).toHaveBeenNthCalledWith(1, files[0]); + expect(fn).toHaveBeenNthCalledWith(2, files[1]); + }); +}); From 822240373ea0da6f016a5c9e7b5174438f78f1a0 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 3 Aug 2021 14:26:02 +0200 Subject: [PATCH 14/34] refactor(techdocs-common): use continuation token for listing blob files Co-authored-by: Eric Peterson Signed-off-by: Camila Belo --- .../src/stages/publish/azureBlobStorage.ts | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index f49b317ecf..4fbe071d1c 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -139,15 +139,12 @@ export class AzureBlobStoragePublish implements PublisherBase { async publish({ entity, directory }: PublishRequest): Promise { // First, retrieve a list of all individual files in currently existing const remoteFolder = getCloudPathForLocalPath(entity); - const existingFiles: string[] = []; - let container: ContainerClient; + let existingFiles: string[]; try { - container = this.storageClient.getContainerClient(this.containerName); - for await (const blob of container.listBlobsFlat({ + existingFiles = await this.getAllBlobsFromContainer({ prefix: remoteFolder, - })) { - existingFiles.push(blob.name); - } + maxPageSize: BATCH_CONCURRENCY, + }); } catch (e) { const errorMessage = `Unable to list file(s) to Azure. ${e}`; this.logger.error(errorMessage); @@ -156,12 +153,14 @@ export class AzureBlobStoragePublish implements PublisherBase { // Then, merge new files into the same folder let absoluteFilesToUpload; + let container: ContainerClient; try { // Remove the absolute path prefix of the source directory // Path of all files to upload, relative to the root of the source directory // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png'] absoluteFilesToUpload = await getFileTreeRecursively(directory); + container = this.storageClient.getContainerClient(this.containerName); const failedOperations: Error[] = []; await bulkStorageOperation( async absoluteFilePath => { @@ -381,4 +380,30 @@ export class AzureBlobStoragePublish implements PublisherBase { await Promise.all(promises); } + + protected async getAllBlobsFromContainer({ + prefix, + maxPageSize, + }: { + prefix: string; + maxPageSize: number; + }): Promise { + const blobs: string[] = []; + const container = this.storageClient.getContainerClient(this.containerName); + + let iterator = container.listBlobsFlat({ prefix }).byPage({ maxPageSize }); + let response = (await iterator.next()).value; + + do { + for (const blob of response.segment.blobItems) { + blobs.push(blob.name); + } + iterator = container + .listBlobsFlat({ prefix }) + .byPage({ continuationToken: response.continuationToken, maxPageSize }); + response = (await iterator.next()).value; + } while (response && response.continuationToken); + + return blobs; + } } From 11117811aa5897ea26309a5e867541dfbe1b8832 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 3 Aug 2021 14:53:53 +0200 Subject: [PATCH 15/34] test(techdocs-common): fix by page mock for azure Co-authored-by: Eric Peterson Signed-off-by: Camila Belo --- .../__mocks__/@azure/storage-blob.ts | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/packages/techdocs-common/__mocks__/@azure/storage-blob.ts b/packages/techdocs-common/__mocks__/@azure/storage-blob.ts index f7303ce1cb..7279fc31eb 100644 --- a/packages/techdocs-common/__mocks__/@azure/storage-blob.ts +++ b/packages/techdocs-common/__mocks__/@azure/storage-blob.ts @@ -102,6 +102,36 @@ class BlockBlobClientFailUpload extends BlockBlobClient { } } +class ContainerClientIterator { + private containerName: string; + + constructor(containerName) { + this.containerName = containerName; + } + + async next() { + if ( + this.containerName === 'delete_stale_files_success' || + this.containerName === 'delete_stale_files_error' + ) { + return { + value: { + segment: { + blobItems: [{ name: `stale_file.png` }], + }, + }, + }; + } + return { + value: { + segment: { + blobItems: [], + }, + }, + }; + } +} + export class ContainerClient { private readonly containerName; @@ -126,14 +156,12 @@ export class ContainerClient { return new BlockBlobClient(blobName); } - listBlobsFlat({ prefix }: { prefix: string }) { - if ( - this.containerName === 'delete_stale_files_success' || - this.containerName === 'delete_stale_files_error' - ) { - return [{ name: `${prefix}stale_file.png` }]; - } - return []; + listBlobsFlat() { + return { + byPage: () => { + return new ContainerClientIterator(this.containerName); + }, + }; } deleteBlob() { From bc405be6edada0002b371a1691eae66043274026 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 3 Aug 2021 16:09:25 +0200 Subject: [PATCH 16/34] Changeset and docs updates. Co-authored-by: Camila Loiola Signed-off-by: Eric Peterson --- .changeset/techdocs-stale-breakfast-king.md | 19 +++++++++++++++++++ docs/features/techdocs/using-cloud-storage.md | 8 +++++++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .changeset/techdocs-stale-breakfast-king.md diff --git a/.changeset/techdocs-stale-breakfast-king.md b/.changeset/techdocs-stale-breakfast-king.md new file mode 100644 index 0000000000..cdbdb4d0ad --- /dev/null +++ b/.changeset/techdocs-stale-breakfast-king.md @@ -0,0 +1,19 @@ +--- +'@backstage/techdocs-common': patch +--- + +Stale TechDocs content (files that had previously been published but which have +since been removed) is now removed from storage at publish-time. This is now +supported by the following publishers: + +- Google GCS +- AWS S3 +- Azure Blob Storage + +You may need to apply a greater level of permissions (e.g. the ability to +delete objects in your storage provider) to any credentials/accounts used by +the TechDocs CLI or TechDocs backend in order for this change to take effect. + +For more details, see [#6132][issue-ref]. + +[issue-ref]: https://github.com/backstage/backstage/issues/6132 diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 159137c879..6d221bd9ad 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -61,7 +61,7 @@ If you do not prefer (3a) and optionally like to use a service account, you can follow these steps. Create a new Service Account and a key associated with it. In roles of the -service account, use "Storage Admin". +service account, use "Storage Object Admin". If you want to create a custom role, make sure to include both `get` and `create` permissions for both "Objects" and "Buckets". See @@ -143,6 +143,8 @@ permissions to: - `s3:ListBucket` to retrieve bucket metadata - `s3:PutObject` to upload files to the bucket +- `s3:DeleteObject` and `s3:DeleteObjectVersion` to delete stale content during + re-publishing To _read_ TechDocs from the S3 bucket the IAM policy needs to have at a minimum permissions to: @@ -345,6 +347,10 @@ techdocs: accountKey: ${TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY} ``` +In either case, the account or credentials used to access your container and all +TechDocs objects underneath it should have the `Storage Blog Data Owner` role +applied, in order to read, write, and delete objects as needed. + **4. That's it!** Your Backstage app is now ready to use Azure Blob Storage for TechDocs, to store From 51bbece433482db15fc963e77485451bb60f25a2 Mon Sep 17 00:00:00 2001 From: Jos Craw Date: Sat, 7 Aug 2021 00:38:30 +1200 Subject: [PATCH 17/34] feat: added support for ui schema on array items Signed-off-by: Jos Craw --- .../scaffolder/src/components/MultistepJsonForm/schema.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts index ee2dbce406..71a60fdd48 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts +++ b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts @@ -26,7 +26,7 @@ function extractUiSchema(schema: JsonObject, uiSchema: JsonObject) { return; } - const { properties, anyOf, oneOf, allOf, dependencies } = schema; + const { properties, items, anyOf, oneOf, allOf, dependencies } = schema; for (const propName in schema) { if (!schema.hasOwnProperty(propName)) { @@ -55,6 +55,12 @@ function extractUiSchema(schema: JsonObject, uiSchema: JsonObject) { } } + if (isObject(items)) { + const innerUiSchema = {}; + uiSchema.items = innerUiSchema; + extractUiSchema(items, innerUiSchema); + } + if (Array.isArray(anyOf)) { for (const schemaNode of anyOf) { if (!isObject(schemaNode)) { From 67f80c74f6683dc8b51981c37e373d4bb9c188a5 Mon Sep 17 00:00:00 2001 From: Jos Craw Date: Sat, 7 Aug 2021 00:45:12 +1200 Subject: [PATCH 18/34] added changeset Signed-off-by: Jos Craw --- .changeset/perfect-colts-crash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/perfect-colts-crash.md diff --git a/.changeset/perfect-colts-crash.md b/.changeset/perfect-colts-crash.md new file mode 100644 index 0000000000..78c07003b1 --- /dev/null +++ b/.changeset/perfect-colts-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Added UI Schema support for array items eg. support EntityPicker within an array field From cfe17f43a435a92a9041303691fa0b510d8baedb Mon Sep 17 00:00:00 2001 From: Jos Craw Date: Sat, 7 Aug 2021 01:02:09 +1200 Subject: [PATCH 19/34] chore: added tests Signed-off-by: Jos Craw --- .../MultistepJsonForm/schema.test.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts b/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts index b51d01f719..ef6b582021 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts +++ b/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts @@ -346,4 +346,68 @@ describe('transformSchemaToProps', () => { uiSchema: expectedUiSchema, }); }); + + it('transforms schema with array items', () => { + const inputSchema = { + type: 'object', + properties: { + person: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + address: { + type: 'string', + 'ui:widget': 'textarea', + }, + }, + }, + }, + accountNumber: { + type: 'number', + }, + }, + }; + const expectedSchema = { + type: 'object', + properties: { + person: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + address: { + type: 'string', + }, + }, + }, + }, + accountNumber: { + type: 'number', + }, + }, + }; + const expectedUiSchema = { + accountNumber: {}, + person: { + items: { + name: {}, + address: { + 'ui:widget': 'textarea', + }, + }, + }, + }; + + expect(transformSchemaToProps(inputSchema)).toEqual({ + schema: expectedSchema, + uiSchema: expectedUiSchema, + }); + }); }); From 7894421f14dc4031c7ac2896fb9e7182ec0f7125 Mon Sep 17 00:00:00 2001 From: Jos Craw Date: Sat, 7 Aug 2021 01:07:56 +1200 Subject: [PATCH 20/34] chore: changed minor version to patch Signed-off-by: Jos Craw --- .changeset/{perfect-colts-crash.md => green-ducks-tan.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .changeset/{perfect-colts-crash.md => green-ducks-tan.md} (71%) diff --git a/.changeset/perfect-colts-crash.md b/.changeset/green-ducks-tan.md similarity index 71% rename from .changeset/perfect-colts-crash.md rename to .changeset/green-ducks-tan.md index 78c07003b1..9c8620e739 100644 --- a/.changeset/perfect-colts-crash.md +++ b/.changeset/green-ducks-tan.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder': patch --- Added UI Schema support for array items eg. support EntityPicker within an array field From 95a9308bb31f8f260aeb3b0cb5cf979cf24f8074 Mon Sep 17 00:00:00 2001 From: Jos Craw Date: Sat, 7 Aug 2021 01:11:50 +1200 Subject: [PATCH 21/34] chore: changed eg. to support markdown linter Signed-off-by: Jos Craw --- .changeset/green-ducks-tan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/green-ducks-tan.md b/.changeset/green-ducks-tan.md index 9c8620e739..e096bfeb36 100644 --- a/.changeset/green-ducks-tan.md +++ b/.changeset/green-ducks-tan.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder': patch --- -Added UI Schema support for array items eg. support EntityPicker within an array field +Added UI Schema support for array items for example, support EntityPicker within an array field From 76872096bf6b2e06ae1385b54d3238dc5c83e446 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Thu, 5 Aug 2021 12:29:34 -0400 Subject: [PATCH 22/34] fix(elasticSearchEngine): optionally read auth config Signed-off-by: Phil Kuang --- .changeset/quick-fans-smash.md | 5 ++ docs/features/search/search-engines.md | 19 ++----- .../config.d.ts | 49 ++++++++++--------- .../src/engines/ElasticSearchSearchEngine.ts | 26 ++++++---- 4 files changed, 52 insertions(+), 47 deletions(-) create mode 100644 .changeset/quick-fans-smash.md diff --git a/.changeset/quick-fans-smash.md b/.changeset/quick-fans-smash.md new file mode 100644 index 0000000000..b394b2af23 --- /dev/null +++ b/.changeset/quick-fans-smash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +--- + +Fix to allow optionally reading `auth` parameter for custom hosted ElasticSearch instances. Also remove `bearer` auth config since it's currently unsupported. diff --git a/docs/features/search/search-engines.md b/docs/features/search/search-engines.md index a2e4831824..69a223b29e 100644 --- a/docs/features/search/search-engines.md +++ b/docs/features/search/search-engines.md @@ -130,12 +130,9 @@ Other ElasticSearch instances can be connected to by using standard ElasticSearch authentication methods and exposed URL, provided that the cluster supports that. The configuration options needed are the URL to the node and authentication information. Authentication can be handled by either providing -username/password or an API key or a bearer token. In case both -username/password combination and one of the tokens are provided, token takes -precedence. For more information how to create an API key, see -[Elastic documentation on API keys](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html), -and how to create a bearer token see -[Elastic documentation on tokens.](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-service-token.html) +username/password or an API key. For more information how to create an API key, +see +[Elastic documentation on API keys](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html). #### Configuration examples @@ -150,16 +147,6 @@ search: password: changeme ``` -##### With bearer token - -```yaml -search: - elasticsearch: - node: http://localhost:9200 - auth: - bearer: token -``` - ##### With API key ```yaml diff --git a/plugins/search-backend-module-elasticsearch/config.d.ts b/plugins/search-backend-module-elasticsearch/config.d.ts index 2ea18763fa..31b139794b 100644 --- a/plugins/search-backend-module-elasticsearch/config.d.ts +++ b/plugins/search-backend-module-elasticsearch/config.d.ts @@ -75,30 +75,35 @@ export interface Config { * Authentication credentials for ElasticSearch * If both ApiKey/Bearer token and username+password is provided, tokens take precedence */ - auth?: { - username?: string; + auth?: + | { + username: string; - /** - * @visibility secret - */ - password?: string; + /** + * @visibility secret + */ + password: string; + } + | { + /** + * Base64 Encoded API key to be used to connect to the cluster. + * See: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html + * + * @visibility secret + */ + apiKey: string; + }; + /* TODO(kuangp): unsupported until @elastic/elasticsearch@7.14 is released + | { - /** - * Base64 Encoded API key to be used to connect to the cluster. - * See: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html - * - * @visibility secret - */ - apiKey?: string; - - /** - * Bearer authentication token to connect to the cluster. - * See: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-service-token.html - * - * @visibility secret - */ - bearer?: string; - }; + /** + * Bearer authentication token to connect to the cluster. + * See: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-service-token.html + * + * @visibility secret + * + bearer: string; + };*/ }; }; } diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 09f2838abf..d1e831b2ca 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -35,13 +35,6 @@ export type ConcreteElasticSearchQuery = { elasticSearchQuery: Object; }; -type ElasticConfigAuth = { - username: string; - password: string; - apiKey: string; - bearer: string; -}; - type ElasticSearchQueryTranslator = ( query: SearchQuery, ) => ConcreteElasticSearchQuery; @@ -105,11 +98,15 @@ export class ElasticSearchSearchEngine implements SearchEngine { if (config.getOptionalString('provider') === 'elastic') { logger.info('Initializing Elastic.co ElasticSearch search engine.'); + const authConfig = config.getConfig('auth'); return new Client({ cloud: { id: config.getString('cloudId'), }, - auth: config.get('auth'), + auth: { + username: authConfig.getString('username'), + password: authConfig.getString('password'), + }, }); } if (config.getOptionalString('provider') === 'aws') { @@ -122,9 +119,20 @@ export class ElasticSearchSearchEngine implements SearchEngine { }); } logger.info('Initializing ElasticSearch search engine.'); + const authConfig = config.getOptionalConfig('auth'); + const auth = + authConfig && + (authConfig.has('apiKey') + ? { + apiKey: authConfig.getString('apiKey'), + } + : { + username: authConfig.getString('username'), + password: authConfig.getString('password'), + }); return new Client({ node: config.getString('node'), - auth: config.get('auth'), + auth, }); } From c2d6c8054db8de6fe33847b9869307cadf22b54f Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 9 Aug 2021 14:38:33 +0200 Subject: [PATCH 23/34] Publish should not be blocked on inability to list Signed-off-by: Eric Peterson --- .../techdocs-common/src/stages/publish/awsS3.ts | 17 ++++++++++++----- .../src/stages/publish/azureBlobStorage.ts | 12 ++++++------ .../src/stages/publish/googleStorage.ts | 13 ++++++++++--- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 3ba5ffe4e3..02d4f7226f 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -178,11 +178,18 @@ export class AwsS3Publish implements PublisherBase { * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html */ async publish({ entity, directory }: PublishRequest): Promise { - // First, retrieve a list of all individual files in currently existing - const remoteFolder = getCloudPathForLocalPath(entity); - const existingFiles = await this.getAllObjectsFromBucket({ - prefix: remoteFolder, - }); + // First, try to retrieve a list of all individual files currently existing + let existingFiles: string[] = []; + try { + const remoteFolder = getCloudPathForLocalPath(entity); + existingFiles = await this.getAllObjectsFromBucket({ + prefix: remoteFolder, + }); + } catch (e) { + this.logger.error( + `Unable to list files for Entity ${entity.metadata.name}: ${e.message}`, + ); + } // Then, merge new files into the same folder let absoluteFilesToUpload; diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index 4fbe071d1c..f1c62f0819 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -137,18 +137,18 @@ export class AzureBlobStoragePublish implements PublisherBase { * Directory structure used in the container is - entityNamespace/entityKind/entityName/index.html */ async publish({ entity, directory }: PublishRequest): Promise { - // First, retrieve a list of all individual files in currently existing + // First, try to retrieve a list of all individual files currently existing const remoteFolder = getCloudPathForLocalPath(entity); - let existingFiles: string[]; + let existingFiles: string[] = []; try { existingFiles = await this.getAllBlobsFromContainer({ prefix: remoteFolder, maxPageSize: BATCH_CONCURRENCY, }); } catch (e) { - const errorMessage = `Unable to list file(s) to Azure. ${e}`; - this.logger.error(errorMessage); - throw new Error(errorMessage); + this.logger.error( + `Unable to list files for Entity ${entity.metadata.name}: ${e.message}`, + ); } // Then, merge new files into the same folder @@ -395,7 +395,7 @@ export class AzureBlobStoragePublish implements PublisherBase { let response = (await iterator.next()).value; do { - for (const blob of response.segment.blobItems) { + for (const blob of response?.segment?.blobItems ?? []) { blobs.push(blob.name); } iterator = container diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts index 24f84f0136..9d8566f0b3 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts @@ -117,9 +117,16 @@ export class GoogleGCSPublish implements PublisherBase { async publish({ entity, directory }: PublishRequest): Promise { const bucket = this.storageClient.bucket(this.bucketName); - // First, retrieve a list of all individual files in currently existing - const remoteFolder = getCloudPathForLocalPath(entity); - const existingFiles = await this.getFilesForFolder(remoteFolder); + // First, try to retrieve a list of all individual files currently existing + let existingFiles: string[] = []; + try { + const remoteFolder = getCloudPathForLocalPath(entity); + existingFiles = await this.getFilesForFolder(remoteFolder); + } catch (e) { + this.logger.error( + `Unable to list files for Entity ${entity.metadata.name}: ${e.message}`, + ); + } // Then, merge new files into the same folder let absoluteFilesToUpload; From 135e6ba84e0662fa77394a1d1a2a2a7f89891724 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 9 Aug 2021 15:51:56 +0200 Subject: [PATCH 24/34] Adjust colors for Tabs in dark mode Signed-off-by: Philipp Hugenroth --- .../core-components/src/layout/HeaderTabs/HeaderTabs.tsx | 8 +++++++- .../core-components/src/layout/TabbedCard/TabbedCard.tsx | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx index 1b300df5a2..20742114c9 100644 --- a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx +++ b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx @@ -36,6 +36,12 @@ const useStyles = makeStyles(theme => ({ selected: { color: theme.palette.text.primary, }, + tabRoot: { + '&:hover': { + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + }, + }, })); export type Tab = { @@ -89,7 +95,7 @@ export const HeaderTabs = ({ key={tab.id} value={index} className={styles.defaultTab} - classes={{ selected: styles.selected }} + classes={{ selected: styles.selected, root: styles.tabRoot }} /> ))} diff --git a/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx b/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx index 41b39984a5..c9f2a477ef 100644 --- a/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx +++ b/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx @@ -98,6 +98,7 @@ const TabbedCard = ({ {title && } ({ margin: theme.spacing(0, 2, 0, 0), padding: theme.spacing(0.5, 0, 0.5, 0), textTransform: 'none', + '&:hover': { + opacity: 1, + backgroundColor: 'transparent', + color: theme.palette.text.primary, + }, }, selected: { fontWeight: 'bold', From c9bab6d5963c2a7857d88f2d343b372cd75866fb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 9 Aug 2021 16:55:32 +0200 Subject: [PATCH 25/34] auth-backend: read and encode the entire state object in encodeState and readState Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/src/lib/oauth/helpers.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts index df84d421bd..ead9acae27 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -29,18 +29,14 @@ export const readState = (stateString: string): OAuthState => { ) { throw Error(`Invalid state passed via request`); } - return { - nonce: state.nonce, - env: state.env, - }; + + return state as OAuthState; }; export const encodeState = (state: OAuthState): string => { - const searchParams = new URLSearchParams(); - searchParams.append('nonce', state.nonce); - searchParams.append('env', state.env); + const stateString = new URLSearchParams(state).toString(); - return Buffer.from(searchParams.toString(), 'utf-8').toString('hex'); + return Buffer.from(stateString, 'utf-8').toString('hex'); }; export const verifyNonce = (req: express.Request, providerId: string) => { From 40ea43e57eeb3a8d0575b3261bbec61bc5505cec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Aug 2021 04:13:16 +0000 Subject: [PATCH 26/34] chore(deps-dev): bump @types/json-schema-merge-allof from 0.6.0 to 0.6.1 Bumps [@types/json-schema-merge-allof](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/json-schema-merge-allof) from 0.6.0 to 0.6.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/json-schema-merge-allof) --- updated-dependencies: - dependency-name: "@types/json-schema-merge-allof" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index 49a7a58059..ef19134536 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6831,27 +6831,17 @@ recast "^0.20.3" "@types/json-schema-merge-allof@^0.6.0": - version "0.6.0" - resolved "https://registry.npmjs.org/@types/json-schema-merge-allof/-/json-schema-merge-allof-0.6.0.tgz#0f587d8a3bcb41a55ef2e91d3ba96430c9bc1813" - integrity sha512-v6iCEk4Sxy1twlCTtrRxMqSHX0vuLJ7Ql4hiIUZRMOswxtlUeybIfYaZIj7pX747RBczG2YtBm4Fn6sqKzeY2Q== + version "0.6.1" + resolved "https://registry.npmjs.org/@types/json-schema-merge-allof/-/json-schema-merge-allof-0.6.1.tgz#c476c2aeed04d8a14882ff872de9ddeca5608e0b" + integrity sha512-tBVtkCCbA1oF8vQ2cp2yuGLp0T2f0AZ2dAic64ZftoWQnKqrTYY/+PuiqPKX1XaxoR43ll/EkYcHnJbdbHUS2g== dependencies: "@types/json-schema" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7": - version "7.0.7" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" - integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== - -"@types/json-schema@^7.0.4": +"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8": version "7.0.9" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== -"@types/json-schema@^7.0.8": - version "7.0.8" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818" - integrity sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg== - "@types/json-stable-stringify@^1.0.32": version "1.0.32" resolved "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.32.tgz#121f6917c4389db3923640b2e68de5fa64dda88e" From edf5faf1c2324275680f6b5ab72d9bcec61649a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Aug 2021 04:16:29 +0000 Subject: [PATCH 27/34] chore(deps-dev): bump @storybook/addon-storysource from 6.3.6 to 6.3.7 Bumps [@storybook/addon-storysource](https://github.com/storybookjs/storybook/tree/HEAD/addons/storysource) from 6.3.6 to 6.3.7. - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v6.3.7/addons/storysource) --- updated-dependencies: - dependency-name: "@storybook/addon-storysource" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 163 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 146 insertions(+), 17 deletions(-) diff --git a/yarn.lock b/yarn.lock index 49a7a58059..132ba2c16d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5448,17 +5448,17 @@ ts-dedent "^2.0.0" "@storybook/addon-storysource@^6.1.11": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/addon-storysource/-/addon-storysource-6.3.6.tgz#3ecacd39690675ffcba0d40ca235090fc2bf5574" - integrity sha512-BNNuy/6Vb7NzeCDbt+bCL1dC/XQOalzKHW6FplO0pR0eMSi6EmJJnU9V+5rFAUtG5zxGeCx2h7TrrFecouM2BA== + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/addon-storysource/-/addon-storysource-6.3.7.tgz#7818f2402e19453747274d2c1e8e0a99f24fc2ab" + integrity sha512-cb1F47aD/c5TQDMmYwuKz608YeLWYSYQXGtCdijzjznYVSMJ4eAFnd38AiF2Ax1EM79BEMqzqH1SrveibAaQlA== dependencies: - "@storybook/addons" "6.3.6" - "@storybook/api" "6.3.6" - "@storybook/client-logger" "6.3.6" - "@storybook/components" "6.3.6" - "@storybook/router" "6.3.6" - "@storybook/source-loader" "6.3.6" - "@storybook/theming" "6.3.6" + "@storybook/addons" "6.3.7" + "@storybook/api" "6.3.7" + "@storybook/client-logger" "6.3.7" + "@storybook/components" "6.3.7" + "@storybook/router" "6.3.7" + "@storybook/source-loader" "6.3.7" + "@storybook/theming" "6.3.7" core-js "^3.8.2" estraverse "^5.2.0" loader-utils "^2.0.0" @@ -5467,7 +5467,7 @@ react-syntax-highlighter "^13.5.3" regenerator-runtime "^0.13.7" -"@storybook/addons@6.3.6", "@storybook/addons@^6.1.11": +"@storybook/addons@6.3.6": version "6.3.6" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.6.tgz#330fd722bdae8abefeb029583e89e51e62c20b60" integrity sha512-tVV0vqaEEN9Md4bgScwfrnZYkN8iKZarpkIOFheLev+PHjSp8lgWMK5SNWDlbBYqfQfzrz9xbs+F07bMjfx9jQ== @@ -5482,6 +5482,21 @@ global "^4.4.0" regenerator-runtime "^0.13.7" +"@storybook/addons@6.3.7", "@storybook/addons@^6.1.11": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.7.tgz#7c6b8d11b65f67b1884f6140437fe996dc39537a" + integrity sha512-9stVjTcc52bqqh7YQex/LpSjJ4e2Czm4/ZYDjIiNy0p4OZEx+yLhL5mZzMWh2NQd6vv+pHASBSxf2IeaR5511A== + dependencies: + "@storybook/api" "6.3.7" + "@storybook/channels" "6.3.7" + "@storybook/client-logger" "6.3.7" + "@storybook/core-events" "6.3.7" + "@storybook/router" "6.3.7" + "@storybook/theming" "6.3.7" + core-js "^3.8.2" + global "^4.4.0" + regenerator-runtime "^0.13.7" + "@storybook/api@6.3.6": version "6.3.6" resolved "https://registry.npmjs.org/@storybook/api/-/api-6.3.6.tgz#b110688ae0a970c9443d47b87616a09456f3708e" @@ -5508,6 +5523,32 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/api@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/api/-/api-6.3.7.tgz#88b8a51422cd0739c91bde0b1d65fb6d8a8485d0" + integrity sha512-57al8mxmE9agXZGo8syRQ8UhvGnDC9zkuwkBPXchESYYVkm3Mc54RTvdAOYDiy85VS4JxiGOywHayCaRwgUddQ== + dependencies: + "@reach/router" "^1.3.4" + "@storybook/channels" "6.3.7" + "@storybook/client-logger" "6.3.7" + "@storybook/core-events" "6.3.7" + "@storybook/csf" "0.0.1" + "@storybook/router" "6.3.7" + "@storybook/semver" "^7.3.2" + "@storybook/theming" "6.3.7" + "@types/reach__router" "^1.3.7" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + lodash "^4.17.20" + memoizerific "^1.11.3" + qs "^6.10.0" + regenerator-runtime "^0.13.7" + store2 "^2.12.0" + telejson "^5.3.2" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/builder-webpack4@6.3.6": version "6.3.6" resolved "https://registry.npmjs.org/@storybook/builder-webpack4/-/builder-webpack4-6.3.6.tgz#fe444abfc178e005ea077e2bcfd6ae7509522908" @@ -5606,6 +5647,15 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/channels@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.3.7.tgz#85ed5925522b802d959810f78d37aacde7fea66e" + integrity sha512-aErXO+SRO8MPp2wOkT2n9d0fby+8yM35tq1tI633B4eQsM74EykbXPv7EamrYPqp1AI4BdiloyEpr0hmr2zlvg== + dependencies: + core-js "^3.8.2" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/client-api@6.3.6": version "6.3.6" resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.6.tgz#4826ce366ae109f608da6ade24b29efeb9b7f7dd" @@ -5638,6 +5688,14 @@ core-js "^3.8.2" global "^4.4.0" +"@storybook/client-logger@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.3.7.tgz#ff17b7494e7e9e23089b0d5c5364c371c726bdd1" + integrity sha512-BQRErHE3nIEuUJN/3S3dO1LzxAknOgrFeZLd4UVcH/fvjtS1F4EkhcbH+jNyUWvcWGv66PZYN0oFPEn/g3Savg== + dependencies: + core-js "^3.8.2" + global "^4.4.0" + "@storybook/components@6.3.6": version "6.3.6" resolved "https://registry.npmjs.org/@storybook/components/-/components-6.3.6.tgz#bc2fa1dbe59f42b5b2aeb9f84424072835d4ce8b" @@ -5668,6 +5726,36 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/components@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/components/-/components-6.3.7.tgz#42b1ca6d24e388e02eab82aa9ed3365db2266ecc" + integrity sha512-O7LIg9Z18G0AJqXX7Shcj0uHqwXlSA5UkHgaz9A7mqqqJNl6m6FwwTWcxR1acUfYVNkO+czgpqZHNrOF6rky1A== + dependencies: + "@popperjs/core" "^2.6.0" + "@storybook/client-logger" "6.3.7" + "@storybook/csf" "0.0.1" + "@storybook/theming" "6.3.7" + "@types/color-convert" "^2.0.0" + "@types/overlayscrollbars" "^1.12.0" + "@types/react-syntax-highlighter" "11.0.5" + color-convert "^2.0.1" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + lodash "^4.17.20" + markdown-to-jsx "^7.1.3" + memoizerific "^1.11.3" + overlayscrollbars "^1.13.1" + polished "^4.0.5" + prop-types "^15.7.2" + react-colorful "^5.1.2" + react-popper-tooltip "^3.1.1" + react-syntax-highlighter "^13.5.3" + react-textarea-autosize "^8.3.0" + regenerator-runtime "^0.13.7" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/core-client@6.3.6": version "6.3.6" resolved "https://registry.npmjs.org/@storybook/core-client/-/core-client-6.3.6.tgz#7def721aa15d4faaff574780d30b92055db7261c" @@ -5752,6 +5840,13 @@ dependencies: core-js "^3.8.2" +"@storybook/core-events@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.3.7.tgz#c5bc7cae7dc295de73b6b9f671ecbe582582e9bd" + integrity sha512-l5Hlhe+C/dqxjobemZ6DWBhTOhQoFF3F1Y4kjFGE7pGZl/mas4M72I5I/FUcYCmbk2fbLfZX8hzKkUqS1hdyLA== + dependencies: + core-js "^3.8.2" + "@storybook/core-server@6.3.6": version "6.3.6" resolved "https://registry.npmjs.org/@storybook/core-server/-/core-server-6.3.6.tgz#43c1415573c3b72ec6b9ae48d68e1bb446722f7c" @@ -5938,6 +6033,22 @@ qs "^6.10.0" ts-dedent "^2.0.0" +"@storybook/router@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/router/-/router-6.3.7.tgz#1714a99a58a7b9f08b6fcfe2b678dad6ca896736" + integrity sha512-6tthN8op7H0NoYoE1SkQFJKC42pC4tGaoPn7kEql8XGeUJnxPtVFjrnywlTrRnKuxZKIvbilQBAwDml97XH23Q== + dependencies: + "@reach/router" "^1.3.4" + "@storybook/client-logger" "6.3.7" + "@types/reach__router" "^1.3.7" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + lodash "^4.17.20" + memoizerific "^1.11.3" + qs "^6.10.0" + ts-dedent "^2.0.0" + "@storybook/semver@^7.3.2": version "7.3.2" resolved "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz#f3b9c44a1c9a0b933c04e66d0048fcf2fa10dac0" @@ -5946,13 +6057,13 @@ core-js "^3.6.5" find-up "^4.1.0" -"@storybook/source-loader@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-6.3.6.tgz#2d3d01919baad7a40f67d1150c74e41dea5f1d4c" - integrity sha512-om3iS3a+D287FzBrbXB/IXB6Z5Ql2yc4dFKTy6FPe5v4N3U0p5puWOKUYWWbTX1JbcpRj0IXXo7952G68tcC1g== +"@storybook/source-loader@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-6.3.7.tgz#cc348305df3c2d8d716c0bab7830c9f537b859ff" + integrity sha512-0xQTq90jwx7W7MJn/idEBCGPOyxi/3No5j+5YdfJsSGJRuMFH3jt6pGgdeZ4XA01cmmIX6bZ+fB9al6yAzs91w== dependencies: - "@storybook/addons" "6.3.6" - "@storybook/client-logger" "6.3.6" + "@storybook/addons" "6.3.7" + "@storybook/client-logger" "6.3.7" "@storybook/csf" "0.0.1" core-js "^3.8.2" estraverse "^5.2.0" @@ -5980,6 +6091,24 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" +"@storybook/theming@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.3.7.tgz#6daf9a21b26ed607f3c28a82acd90c0248e76d8b" + integrity sha512-GXBdw18JJd5jLLcRonAZWvGGdwEXByxF1IFNDSOYCcpHWsMgTk4XoLjceu9MpXET04pVX51LbVPLCvMdggoohg== + dependencies: + "@emotion/core" "^10.1.1" + "@emotion/is-prop-valid" "^0.8.6" + "@emotion/styled" "^10.0.27" + "@storybook/client-logger" "6.3.7" + core-js "^3.8.2" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.27" + global "^4.4.0" + memoizerific "^1.11.3" + polished "^4.0.5" + resolve-from "^5.0.0" + ts-dedent "^2.0.0" + "@storybook/ui@6.3.6": version "6.3.6" resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.3.6.tgz#a9ed8265e34bb8ef9f0dd08f40170b3dcf8a8931" From 8a3e465910a3f41450a1a1bf6aafd418462c53e5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 9 Aug 2021 19:12:43 +0200 Subject: [PATCH 28/34] techdocs: switch to xhr-based event source polyfill Signed-off-by: Patrik Oldsberg --- .changeset/pink-pugs-relate.md | 5 ++++ plugins/techdocs/package.json | 3 +-- plugins/techdocs/src/client.test.ts | 25 ++++++++--------- plugins/techdocs/src/client.ts | 5 ++-- .../techdocs/src/event-source-polyfill.d.ts | 27 +++++++++++++++++++ yarn.lock | 17 ++++-------- 6 files changed, 54 insertions(+), 28 deletions(-) create mode 100644 .changeset/pink-pugs-relate.md create mode 100644 plugins/techdocs/src/event-source-polyfill.d.ts diff --git a/.changeset/pink-pugs-relate.md b/.changeset/pink-pugs-relate.md new file mode 100644 index 0000000000..e9de00901e --- /dev/null +++ b/.changeset/pink-pugs-relate.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Switch `EventSource` implementation with header support from a Node.js API-based one to an XHR-based one. diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index b853ae04f7..7051109c2e 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -45,7 +45,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "@material-ui/styles": "^4.10.0", "dompurify": "^2.2.9", - "eventsource": "^1.1.0", + "event-source-polyfill": "^1.0.25", "react": "^16.13.1", "react-dom": "^16.13.1", "react-lazylog": "^4.5.2", @@ -65,7 +65,6 @@ "@testing-library/react-hooks": "^3.4.2", "@testing-library/user-event": "^13.1.8", "@types/dompurify": "^2.2.2", - "@types/eventsource": "^1.1.5", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/react": "*", diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index aa57c97c65..7624f250d9 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -18,13 +18,14 @@ import { Config } from '@backstage/config'; import { UrlPatternDiscovery } from '@backstage/core-app-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; -import EventSource from 'eventsource'; +import { EventSourcePolyfill } from 'event-source-polyfill'; import { TechDocsStorageClient } from './client'; -const MockedEventSource: jest.MockedClass = - EventSource as any; +const MockedEventSource = EventSourcePolyfill as jest.MockedClass< + typeof EventSourcePolyfill +>; -jest.mock('eventsource'); +jest.mock('event-source-polyfill'); const mockEntity = { kind: 'Component', @@ -82,7 +83,7 @@ describe('TechDocsStorageClient', () => { MockedEventSource.prototype.addEventListener.mockImplementation( (type, fn) => { - if (type === 'finish') { + if (type === 'finish' && typeof fn === 'function') { fn({ data: '{"updated": false}' } as any); } }, @@ -106,7 +107,7 @@ describe('TechDocsStorageClient', () => { MockedEventSource.prototype.addEventListener.mockImplementation( (type, fn) => { - if (type === 'finish') { + if (type === 'finish' && typeof fn === 'function') { fn({ data: '{"updated": false}' } as any); } }, @@ -132,7 +133,7 @@ describe('TechDocsStorageClient', () => { MockedEventSource.prototype.addEventListener.mockImplementation( (type, fn) => { - if (type === 'finish') { + if (type === 'finish' && typeof fn === 'function') { fn({ data: '{"updated": false}' } as any); } }, @@ -153,7 +154,7 @@ describe('TechDocsStorageClient', () => { MockedEventSource.prototype.addEventListener.mockImplementation( (type, fn) => { - if (type === 'finish') { + if (type === 'finish' && typeof fn === 'function') { fn({ data: '{"updated": true}' } as any); } }, @@ -174,11 +175,11 @@ describe('TechDocsStorageClient', () => { MockedEventSource.prototype.addEventListener.mockImplementation( (type, fn) => { - if (type === 'log') { + if (type === 'log' && typeof fn === 'function') { fn({ data: '"A log message"' } as any); } - if (type === 'finish') { + if (type === 'finish' && typeof fn === 'function') { fn({ data: '{"updated": false}' } as any); } }, @@ -210,7 +211,7 @@ describe('TechDocsStorageClient', () => { const instance = MockedEventSource.mock .instances[0] as jest.Mocked; - instance.onerror({ + instance.onerror?.({ status: 404, message: 'Some not found warning', } as any); @@ -236,7 +237,7 @@ describe('TechDocsStorageClient', () => { const instance = MockedEventSource.mock .instances[0] as jest.Mocked; - instance.onerror({ + instance.onerror?.({ type: 'error', data: 'Some other error', } as any); diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index f867ce4011..8a3ad59c03 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -18,7 +18,7 @@ import { EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; import { NotFoundError, ResponseError } from '@backstage/errors'; -import EventSource from 'eventsource'; +import { EventSourcePolyfill } from 'event-source-polyfill'; import { SyncResult, TechDocsApi, TechDocsStorageApi } from './api'; import { TechDocsEntityMetadata, TechDocsMetadata } from './types'; @@ -214,7 +214,8 @@ export class TechDocsStorageClient implements TechDocsStorageApi { const token = await this.identityApi.getIdToken(); return new Promise((resolve, reject) => { - const source = new EventSource(url, { + // Polyfill is used to add support for custom headers and auth + const source = new EventSourcePolyfill(url, { withCredentials: true, headers: token ? { Authorization: `Bearer ${token}` } : {}, }); diff --git a/plugins/techdocs/src/event-source-polyfill.d.ts b/plugins/techdocs/src/event-source-polyfill.d.ts new file mode 100644 index 0000000000..8e298bde38 --- /dev/null +++ b/plugins/techdocs/src/event-source-polyfill.d.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +declare module 'event-source-polyfill' { + export class EventSourcePolyfill extends EventSource { + constructor( + url: string, + options?: { + withCredentials?: boolean; + headers?: HeadersInit; + }, + ); + } +} diff --git a/yarn.lock b/yarn.lock index 49a7a58059..fc89cd4476 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6574,11 +6574,6 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== -"@types/eventsource@^1.1.5": - version "1.1.5" - resolved "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.5.tgz#408e9b45efb176c8bea672ab58c81e7ab00d24bc" - integrity sha512-BA9q9uC2PAMkUS7DunHTxWZZaVpeNzDG8lkBxcKwzKJClfDQ4Z59/Csx7HSH/SIqFN2JWh0tAKAM6k/wRR0OZg== - "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18", "@types/express-serve-static-core@^4.17.21": version "4.17.24" resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz#ea41f93bf7e0d59cd5a76665068ed6aab6815c07" @@ -13219,6 +13214,11 @@ event-emitter@^0.3.5: d "1" es5-ext "~0.10.14" +event-source-polyfill@^1.0.25: + version "1.0.25" + resolved "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.25.tgz#d8bb7f99cb6f8119c2baf086d9f6ee0514b6d9c8" + integrity sha512-hQxu6sN1Eq4JjoI7ITdQeGGUN193A2ra83qC0Ltm9I2UJVAten3OFVN6k5RX4YWeCS0BoC8xg/5czOCIHVosQg== + event-stream@=3.3.4: version "3.3.4" resolved "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" @@ -13279,13 +13279,6 @@ eventsource@^1.0.5: dependencies: original "^1.0.0" -eventsource@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf" - integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg== - dependencies: - original "^1.0.0" - evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" From 3af7110f3d4ee11a3f6413e039c968e49beccd1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 10 Aug 2021 13:48:26 +0200 Subject: [PATCH 29/34] add docs for the orphan annotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../well-known-annotations.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index c0c347ac17..c3af6caf2d 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -57,6 +57,30 @@ if the original location delegates to another location. A common case is, that a location is registered as `bootstrap:bootstrap` which means that it is part of the `app-config.yaml` of a Backstage installation. +### backstage.io/orphan + +This annotation is either absent, or present with the exact _string_ value +`"true"`. It should never be added manually. Instead, the catalog itself injects +the annotation as part of its processing loops, on entities that are found to +have no registered locations or config locations that keep them "active" / +"alive". + +For example, suppose that the user first registers a location URL pointing to a +`Location` kind entity, which in turn refers to two `Component` kind entities in +two other files nearby. The end result is that the catalog contains those three +entities. Now suppose that the user edits the original `Location` entity to only +refer to the first of the `Component` kind entities. This will intentionally +_not_ lead to the other `Component` entity to be removed from the catalog (for +safety reasons). Instead, it gains this orphan marker annotation, to make it +clear that user action is required to completely remove it, if desired. + +```yaml +# Example: +metadata: + annotations: + backstage.io/orphan: 'true' +``` + ### backstage.io/techdocs-ref ```yaml From 2c5484324eacda4b2cb825a0d2be7869ea7d177a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 10 Aug 2021 14:33:11 +0200 Subject: [PATCH 30/34] yarn.lock: bump all storybook deps Signed-off-by: Patrik Oldsberg --- yarn.lock | 385 ++++++++++++++++++------------------------------------ 1 file changed, 128 insertions(+), 257 deletions(-) diff --git a/yarn.lock b/yarn.lock index 132ba2c16d..3f842d57d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5385,18 +5385,18 @@ integrity sha512-VYOdo8P7lIScAkl02nB9KpUAuOYMManryBIBuKJkAw5D3aVtLobfmdIKvdV6MqEmGMEQPbn7w/UpnjJYhUH+IA== "@storybook/addon-a11y@^6.3.4": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-6.3.6.tgz#6d9dbdaa067a8f6ad15cef64f2103568457d2aae" - integrity sha512-IYVDFEGgKORdm7NPZPqltOvu29R9LaZwGBvfzbS3GUc3I9XLbT/cxkZN68Zzmjn2GtP/X1v4uLqp57OsPb4Cdg== + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-6.3.7.tgz#a802455f2d932eda07314e3d44a96c94bbd22b3d" + integrity sha512-Z5Lhxm8r5CkPW9FYf6zmAk9c7IhUeUQZxKZeEWGZdOvcjQ32rtg4IYvO2SHgWNrEKBdxxFm3pMiyK3wylQLfsQ== dependencies: - "@storybook/addons" "6.3.6" - "@storybook/api" "6.3.6" - "@storybook/channels" "6.3.6" - "@storybook/client-api" "6.3.6" - "@storybook/client-logger" "6.3.6" - "@storybook/components" "6.3.6" - "@storybook/core-events" "6.3.6" - "@storybook/theming" "6.3.6" + "@storybook/addons" "6.3.7" + "@storybook/api" "6.3.7" + "@storybook/channels" "6.3.7" + "@storybook/client-api" "6.3.7" + "@storybook/client-logger" "6.3.7" + "@storybook/components" "6.3.7" + "@storybook/core-events" "6.3.7" + "@storybook/theming" "6.3.7" axe-core "^4.2.0" core-js "^3.8.2" global "^4.4.0" @@ -5407,16 +5407,16 @@ util-deprecate "^1.0.2" "@storybook/addon-actions@^6.1.11": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.3.6.tgz#691d61d6aca9c4b3edba50c531cbe4d4139ed451" - integrity sha512-1MBqCbFiupGEDyIXqFkzF4iR8AduuB7qSNduqtsFauvIkrG5bnlbg5JC7WjnixkCaaWlufgbpasEHioXO9EXGw== + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.3.7.tgz#b25434972bef351aceb3f7ec6fd66e210f256aac" + integrity sha512-CEAmztbVt47Gw1o6Iw0VP20tuvISCEKk9CS/rCjHtb4ubby6+j/bkp3pkEUQIbyLdHiLWFMz0ZJdyA/U6T6jCw== dependencies: - "@storybook/addons" "6.3.6" - "@storybook/api" "6.3.6" - "@storybook/client-api" "6.3.6" - "@storybook/components" "6.3.6" - "@storybook/core-events" "6.3.6" - "@storybook/theming" "6.3.6" + "@storybook/addons" "6.3.7" + "@storybook/api" "6.3.7" + "@storybook/client-api" "6.3.7" + "@storybook/components" "6.3.7" + "@storybook/core-events" "6.3.7" + "@storybook/theming" "6.3.7" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" @@ -5430,15 +5430,15 @@ uuid-browser "^3.1.0" "@storybook/addon-links@^6.1.11": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-6.3.6.tgz#dc410d5b4a0d222b6b8d0ef03da7a4c16919c092" - integrity sha512-PaeAJTjwtPlhrLZlaSQ1YIFA8V0C1yI0dc351lPbTiE7fJ7DwTE03K6xIF/jEdTo+xzhi2PM1Fgvi/SsSecI8w== + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-6.3.7.tgz#f273abba6d056794a4aa920b2fa9639136e6747f" + integrity sha512-/8Gq18o1DejP3Om0ZOJRkMzW5FoHqoAmR0pFx4DipmNu5lYy7V3krLw4jW4qja1MuQkZ53MGh08FJOoAc2RZvQ== dependencies: - "@storybook/addons" "6.3.6" - "@storybook/client-logger" "6.3.6" - "@storybook/core-events" "6.3.6" + "@storybook/addons" "6.3.7" + "@storybook/client-logger" "6.3.7" + "@storybook/core-events" "6.3.7" "@storybook/csf" "0.0.1" - "@storybook/router" "6.3.6" + "@storybook/router" "6.3.7" "@types/qs" "^6.9.5" core-js "^3.8.2" global "^4.4.0" @@ -5467,21 +5467,6 @@ react-syntax-highlighter "^13.5.3" regenerator-runtime "^0.13.7" -"@storybook/addons@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.6.tgz#330fd722bdae8abefeb029583e89e51e62c20b60" - integrity sha512-tVV0vqaEEN9Md4bgScwfrnZYkN8iKZarpkIOFheLev+PHjSp8lgWMK5SNWDlbBYqfQfzrz9xbs+F07bMjfx9jQ== - dependencies: - "@storybook/api" "6.3.6" - "@storybook/channels" "6.3.6" - "@storybook/client-logger" "6.3.6" - "@storybook/core-events" "6.3.6" - "@storybook/router" "6.3.6" - "@storybook/theming" "6.3.6" - core-js "^3.8.2" - global "^4.4.0" - regenerator-runtime "^0.13.7" - "@storybook/addons@6.3.7", "@storybook/addons@^6.1.11": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.7.tgz#7c6b8d11b65f67b1884f6140437fe996dc39537a" @@ -5497,32 +5482,6 @@ global "^4.4.0" regenerator-runtime "^0.13.7" -"@storybook/api@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/api/-/api-6.3.6.tgz#b110688ae0a970c9443d47b87616a09456f3708e" - integrity sha512-F5VuR1FrEwD51OO/EDDAZXNfF5XmJedYHJLwwCB4az2ZMrzG45TxGRmiEohrSTO6wAHGkAvjlEoX5jWOCqQ4pw== - dependencies: - "@reach/router" "^1.3.4" - "@storybook/channels" "6.3.6" - "@storybook/client-logger" "6.3.6" - "@storybook/core-events" "6.3.6" - "@storybook/csf" "0.0.1" - "@storybook/router" "6.3.6" - "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.3.6" - "@types/reach__router" "^1.3.7" - core-js "^3.8.2" - fast-deep-equal "^3.1.3" - global "^4.4.0" - lodash "^4.17.20" - memoizerific "^1.11.3" - qs "^6.10.0" - regenerator-runtime "^0.13.7" - store2 "^2.12.0" - telejson "^5.3.2" - ts-dedent "^2.0.0" - util-deprecate "^1.0.2" - "@storybook/api@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/api/-/api-6.3.7.tgz#88b8a51422cd0739c91bde0b1d65fb6d8a8485d0" @@ -5549,10 +5508,10 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/builder-webpack4@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/builder-webpack4/-/builder-webpack4-6.3.6.tgz#fe444abfc178e005ea077e2bcfd6ae7509522908" - integrity sha512-LhTPQQowS2t6BRnyfusWZLbhjjf54/HiQyovJTTDnqrCiO6QoCMbVnp79LeO1aSkpQCKoeqOZ7TzH87fCytnZA== +"@storybook/builder-webpack4@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/builder-webpack4/-/builder-webpack4-6.3.7.tgz#1cc1a1184043be3f6ef840d0b43ba91a803105e2" + integrity sha512-M5envblMzAUrNqP1+ouKiL8iSIW/90+kBRU2QeWlZoZl1ib+fiFoKk06cgbaC70Bx1lU8nOnI/VBvB5pLhXLaw== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -5575,20 +5534,20 @@ "@babel/preset-env" "^7.12.11" "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" - "@storybook/addons" "6.3.6" - "@storybook/api" "6.3.6" - "@storybook/channel-postmessage" "6.3.6" - "@storybook/channels" "6.3.6" - "@storybook/client-api" "6.3.6" - "@storybook/client-logger" "6.3.6" - "@storybook/components" "6.3.6" - "@storybook/core-common" "6.3.6" - "@storybook/core-events" "6.3.6" - "@storybook/node-logger" "6.3.6" - "@storybook/router" "6.3.6" + "@storybook/addons" "6.3.7" + "@storybook/api" "6.3.7" + "@storybook/channel-postmessage" "6.3.7" + "@storybook/channels" "6.3.7" + "@storybook/client-api" "6.3.7" + "@storybook/client-logger" "6.3.7" + "@storybook/components" "6.3.7" + "@storybook/core-common" "6.3.7" + "@storybook/core-events" "6.3.7" + "@storybook/node-logger" "6.3.7" + "@storybook/router" "6.3.7" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.3.6" - "@storybook/ui" "6.3.6" + "@storybook/theming" "6.3.7" + "@storybook/ui" "6.3.7" "@types/node" "^14.0.10" "@types/webpack" "^4.41.26" autoprefixer "^9.8.6" @@ -5625,28 +5584,19 @@ webpack-hot-middleware "^2.25.0" webpack-virtual-modules "^0.2.2" -"@storybook/channel-postmessage@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.3.6.tgz#f29c3678161462428e78c9cfed2da11ffca4acb0" - integrity sha512-GK7hXnaa+1pxEeMpREDzAZ3+2+k1KN1lbrZf+V7Kc1JZv1/Ji/vxk8AgxwiuzPAMx5J0yh/FduPscIPZ87Pibw== +"@storybook/channel-postmessage@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.3.7.tgz#bd4edf84a29aa2cd4a22d26115c60194d289a840" + integrity sha512-Cmw8HRkeSF1yUFLfEIUIkUICyCXX8x5Ol/5QPbiW9HPE2hbZtYROCcg4bmWqdq59N0Tp9FQNSn+9ZygPgqQtNw== dependencies: - "@storybook/channels" "6.3.6" - "@storybook/client-logger" "6.3.6" - "@storybook/core-events" "6.3.6" + "@storybook/channels" "6.3.7" + "@storybook/client-logger" "6.3.7" + "@storybook/core-events" "6.3.7" core-js "^3.8.2" global "^4.4.0" qs "^6.10.0" telejson "^5.3.2" -"@storybook/channels@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.3.6.tgz#a258764ed78fd836ff90489ae74ac055312bf056" - integrity sha512-gCIQVr+dS/tg3AyCxIvkOXMVAs08BCIHXsaa2+XzmacnJBSP+CEHtI6IZ8WEv7tzZuXOiKLVg+wugeIh4j2I4g== - dependencies: - core-js "^3.8.2" - ts-dedent "^2.0.0" - util-deprecate "^1.0.2" - "@storybook/channels@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.3.7.tgz#85ed5925522b802d959810f78d37aacde7fea66e" @@ -5656,16 +5606,16 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-api@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.6.tgz#4826ce366ae109f608da6ade24b29efeb9b7f7dd" - integrity sha512-Q/bWuH691L6k7xkiKtBmZo8C+ijgmQ+vc2Fz8pzIRZuMV8ROL74qhrS4BMKV4LhiYm4f8todtWfaQPBjawZMIA== +"@storybook/client-api@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.7.tgz#cb1dca05467d777bd09aadbbdd1dd22ca537ce14" + integrity sha512-8wOH19cMIwIIYhZy5O5Wl8JT1QOL5kNuamp9GPmg5ff4DtnG+/uUslskRvsnKyjPvl+WbIlZtBVWBiawVdd/yQ== dependencies: - "@storybook/addons" "6.3.6" - "@storybook/channel-postmessage" "6.3.6" - "@storybook/channels" "6.3.6" - "@storybook/client-logger" "6.3.6" - "@storybook/core-events" "6.3.6" + "@storybook/addons" "6.3.7" + "@storybook/channel-postmessage" "6.3.7" + "@storybook/channels" "6.3.7" + "@storybook/client-logger" "6.3.7" + "@storybook/core-events" "6.3.7" "@storybook/csf" "0.0.1" "@types/qs" "^6.9.5" "@types/webpack-env" "^1.16.0" @@ -5680,14 +5630,6 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-logger@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.3.6.tgz#020ba518ab8286194ce103a6ff91767042e296c0" - integrity sha512-qpXQ52ylxPm7l3+WAteV42NmqWA+L1FaJhMOvm2gwl3PxRd2cNXn2BwEhw++eA6qmJH/7mfOKXG+K+QQwOTpRA== - dependencies: - core-js "^3.8.2" - global "^4.4.0" - "@storybook/client-logger@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.3.7.tgz#ff17b7494e7e9e23089b0d5c5364c371c726bdd1" @@ -5696,36 +5638,6 @@ core-js "^3.8.2" global "^4.4.0" -"@storybook/components@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/components/-/components-6.3.6.tgz#bc2fa1dbe59f42b5b2aeb9f84424072835d4ce8b" - integrity sha512-aZkmtAY8b+LFXG6dVp6cTS6zGJuxkHRHcesRSWRQPxtgitaz1G58clRHxbKPRokfjPHNgYA3snogyeqxSA7YNQ== - dependencies: - "@popperjs/core" "^2.6.0" - "@storybook/client-logger" "6.3.6" - "@storybook/csf" "0.0.1" - "@storybook/theming" "6.3.6" - "@types/color-convert" "^2.0.0" - "@types/overlayscrollbars" "^1.12.0" - "@types/react-syntax-highlighter" "11.0.5" - color-convert "^2.0.1" - core-js "^3.8.2" - fast-deep-equal "^3.1.3" - global "^4.4.0" - lodash "^4.17.20" - markdown-to-jsx "^7.1.3" - memoizerific "^1.11.3" - overlayscrollbars "^1.13.1" - polished "^4.0.5" - prop-types "^15.7.2" - react-colorful "^5.1.2" - react-popper-tooltip "^3.1.1" - react-syntax-highlighter "^13.5.3" - react-textarea-autosize "^8.3.0" - regenerator-runtime "^0.13.7" - ts-dedent "^2.0.0" - util-deprecate "^1.0.2" - "@storybook/components@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/components/-/components-6.3.7.tgz#42b1ca6d24e388e02eab82aa9ed3365db2266ecc" @@ -5756,18 +5668,18 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/core-client@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/core-client/-/core-client-6.3.6.tgz#7def721aa15d4faaff574780d30b92055db7261c" - integrity sha512-Bq86flEdXdMNbdHrGMNQ6OT1tcBQU8ym56d+nG46Ctjf5GN+Dl+rPtRWuu7cIZs10KgqJH+86DXp+tvpQIDidg== +"@storybook/core-client@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/core-client/-/core-client-6.3.7.tgz#cfb75952e0e1d32f2aca92bca2786334ab589c40" + integrity sha512-M/4A65yV+Y4lsCQXX4BtQO/i3BcMPrU5FkDG8qJd3dkcx2fUlFvGWqQPkcTZE+MPVvMEGl/AsEZSADzah9+dAg== dependencies: - "@storybook/addons" "6.3.6" - "@storybook/channel-postmessage" "6.3.6" - "@storybook/client-api" "6.3.6" - "@storybook/client-logger" "6.3.6" - "@storybook/core-events" "6.3.6" + "@storybook/addons" "6.3.7" + "@storybook/channel-postmessage" "6.3.7" + "@storybook/client-api" "6.3.7" + "@storybook/client-logger" "6.3.7" + "@storybook/core-events" "6.3.7" "@storybook/csf" "0.0.1" - "@storybook/ui" "6.3.6" + "@storybook/ui" "6.3.7" airbnb-js-shims "^2.2.1" ansi-to-html "^0.6.11" core-js "^3.8.2" @@ -5779,10 +5691,10 @@ unfetch "^4.2.0" util-deprecate "^1.0.2" -"@storybook/core-common@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/core-common/-/core-common-6.3.6.tgz#da8eed703b609968e15177446f0f1609d1d6d0d0" - integrity sha512-nHolFOmTPymI50j180bCtcf1UJZ2eOnYaECRtHvVrCUod5KFF7wh2EHrgWoKqrKrsn84UOY/LkX2C2WkbYtWRg== +"@storybook/core-common@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/core-common/-/core-common-6.3.7.tgz#9eedf3ff16aff870950e3372ab71ef846fa3ac52" + integrity sha512-exLoqRPPsAefwyjbsQBLNFrlPCcv69Q/pclqmIm7FqAPR7f3CKP1rqsHY0PnemizTL/+cLX5S7mY90gI6wpNug== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -5805,7 +5717,7 @@ "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" "@babel/register" "^7.12.1" - "@storybook/node-logger" "6.3.6" + "@storybook/node-logger" "6.3.7" "@storybook/semver" "^7.3.2" "@types/glob-base" "^0.3.0" "@types/micromatch" "^4.0.1" @@ -5833,13 +5745,6 @@ util-deprecate "^1.0.2" webpack "4" -"@storybook/core-events@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.3.6.tgz#c4a09e2c703170995604d63e46e45adc3c9cd759" - integrity sha512-Ut1dz96bJ939oSn5t1ckPXd3WcFejK96Sb3+R/z23vEHUWGBFtygGyw8r/SX/WNDVzGmQU8c+mzJJTZwCBJz8A== - dependencies: - core-js "^3.8.2" - "@storybook/core-events@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.3.7.tgz#c5bc7cae7dc295de73b6b9f671ecbe582582e9bd" @@ -5847,17 +5752,17 @@ dependencies: core-js "^3.8.2" -"@storybook/core-server@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/core-server/-/core-server-6.3.6.tgz#43c1415573c3b72ec6b9ae48d68e1bb446722f7c" - integrity sha512-47ZcfxYn7t891oAMG98iH1BQIgQT9Yk/2BBNVCWY43Ong+ME1xJ6j4C/jkRUOseP7URlfLUQsUYKAYJNVijDvg== +"@storybook/core-server@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/core-server/-/core-server-6.3.7.tgz#6f29ad720aafe4a97247b5e306eac4174d0931f2" + integrity sha512-m5OPD/rmZA7KFewkXzXD46/i1ngUoFO4LWOiAY/wR6RQGjYXGMhSa5UYFF6MNwSbiGS5YieHkR5crB1HP47AhQ== dependencies: - "@storybook/builder-webpack4" "6.3.6" - "@storybook/core-client" "6.3.6" - "@storybook/core-common" "6.3.6" - "@storybook/csf-tools" "6.3.6" - "@storybook/manager-webpack4" "6.3.6" - "@storybook/node-logger" "6.3.6" + "@storybook/builder-webpack4" "6.3.7" + "@storybook/core-client" "6.3.7" + "@storybook/core-common" "6.3.7" + "@storybook/csf-tools" "6.3.7" + "@storybook/manager-webpack4" "6.3.7" + "@storybook/node-logger" "6.3.7" "@storybook/semver" "^7.3.2" "@types/node" "^14.0.10" "@types/node-fetch" "^2.5.7" @@ -5886,18 +5791,18 @@ util-deprecate "^1.0.2" webpack "4" -"@storybook/core@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/core/-/core-6.3.6.tgz#604419d346433103675901b3736bfa1ed9bc534f" - integrity sha512-y71VvVEbqCpG28fDBnfNg3RnUPnicwFYq9yuoFVRF0LYcJCy5cYhkIfW3JG8mN2m0P+LzH80mt2Rj6xlSXrkdQ== +"@storybook/core@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/core/-/core-6.3.7.tgz#482228a270abc3e23fed10c7bc4df674da22ca19" + integrity sha512-YTVLPXqgyBg7TALNxQ+cd+GtCm/NFjxr/qQ1mss1T9GCMR0IjE0d0trgOVHHLAO8jCVlK8DeuqZCCgZFTXulRw== dependencies: - "@storybook/core-client" "6.3.6" - "@storybook/core-server" "6.3.6" + "@storybook/core-client" "6.3.7" + "@storybook/core-server" "6.3.7" -"@storybook/csf-tools@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-6.3.6.tgz#603d9e832f946998b75ff8368fe862375d6cb52c" - integrity sha512-MQevelkEUVNCSjKMXLNc/G8q/BB5babPnSeI0IcJq4k+kLUSHtviimLNpPowMgGJBPx/y9VihH8N7vdJUWVj9w== +"@storybook/csf-tools@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-6.3.7.tgz#505514d211f8698c47ddb15662442098b4b00156" + integrity sha512-A7yGsrYwh+vwVpmG8dHpEimX021RbZd9VzoREcILH56u8atssdh/rseljyWlRes3Sr4QgtLvDB7ggoJ+XDZH7w== dependencies: "@babel/generator" "^7.12.11" "@babel/parser" "^7.12.11" @@ -5921,20 +5826,20 @@ dependencies: lodash "^4.17.15" -"@storybook/manager-webpack4@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/manager-webpack4/-/manager-webpack4-6.3.6.tgz#a5334aa7ae1e048bca8f4daf868925d7054fb715" - integrity sha512-qh/jV4b6mFRpRFfhk1JSyO2gKRz8PLPvDt2AD52/bTAtNRzypKoiWqyZNR2CJ9hgNQtDrk2CO3eKPrcdKYGizQ== +"@storybook/manager-webpack4@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/manager-webpack4/-/manager-webpack4-6.3.7.tgz#9ca604dea38d3c47eb38bf485ca6107861280aa8" + integrity sha512-cwUdO3oklEtx6y+ZOl2zHvflICK85emiXBQGgRcCsnwWQRBZOMh+tCgOSZj4jmISVpT52RtT9woG4jKe15KBig== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-transform-template-literals" "^7.12.1" "@babel/preset-react" "^7.12.10" - "@storybook/addons" "6.3.6" - "@storybook/core-client" "6.3.6" - "@storybook/core-common" "6.3.6" - "@storybook/node-logger" "6.3.6" - "@storybook/theming" "6.3.6" - "@storybook/ui" "6.3.6" + "@storybook/addons" "6.3.7" + "@storybook/core-client" "6.3.7" + "@storybook/core-common" "6.3.7" + "@storybook/node-logger" "6.3.7" + "@storybook/theming" "6.3.7" + "@storybook/ui" "6.3.7" "@types/node" "^14.0.10" "@types/webpack" "^4.41.26" babel-loader "^8.2.2" @@ -5964,10 +5869,10 @@ webpack-dev-middleware "^3.7.3" webpack-virtual-modules "^0.2.2" -"@storybook/node-logger@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.6.tgz#10356608593440a8e3acf2aababef40333a3401b" - integrity sha512-XMDkMN7nVRojjiezrURlkI57+nz3OoH4UBV6qJZICKclxtdKAy0wwOlUSYEUq+axcJ4nvdfzPPoDfGoj37SW7A== +"@storybook/node-logger@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.7.tgz#492469ea4749de8d984af144976961589a1ac382" + integrity sha512-YXHCblruRe6HcNefDOpuXJoaybHnnSryIVP9Z+gDv6OgLAMkyxccTIaQL9dbc/eI4ywgzAz4kD8t1RfVwXNVXw== dependencies: "@types/npmlog" "^4.1.2" chalk "^4.1.0" @@ -5989,17 +5894,17 @@ tslib "^2.0.0" "@storybook/react@^6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/react/-/react-6.3.6.tgz#593bc0743ad22ed5e6e072e6157c20c704864fc3" - integrity sha512-2c30XTe7WzKnvgHBGOp1dzBVlhcbc3oEX0SIeVE9ZYkLvRPF+J1jG948a26iqOCOgRAW13Bele37mG1gbl4tiw== + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/react/-/react-6.3.7.tgz#b15259aeb4cdeef99cc7f09d21db42e3ecd7a01a" + integrity sha512-4S0iCQIzgi6PdAtV2sYw4uL57yIwbMInNFSux9CxPnVdlxOxCJ+U8IgTxD4tjkTvR4boYSEvEle46ns/bwg5iQ== dependencies: "@babel/preset-flow" "^7.12.1" "@babel/preset-react" "^7.12.10" "@pmmmwh/react-refresh-webpack-plugin" "^0.4.3" - "@storybook/addons" "6.3.6" - "@storybook/core" "6.3.6" - "@storybook/core-common" "6.3.6" - "@storybook/node-logger" "6.3.6" + "@storybook/addons" "6.3.7" + "@storybook/core" "6.3.7" + "@storybook/core-common" "6.3.7" + "@storybook/node-logger" "6.3.7" "@storybook/react-docgen-typescript-plugin" "1.0.2-canary.253f8c1.0" "@storybook/semver" "^7.3.2" "@types/webpack-env" "^1.16.0" @@ -6017,22 +5922,6 @@ ts-dedent "^2.0.0" webpack "4" -"@storybook/router@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/router/-/router-6.3.6.tgz#cea20d64bae17397dc9e1689a656b80a98674c34" - integrity sha512-fQ1n7cm7lPFav7I+fStQciSVMlNdU+yLY6Fue252rpV5Q68bMTjwKpjO9P2/Y3CCj4QD3dPqwEkn4s0qUn5tNA== - dependencies: - "@reach/router" "^1.3.4" - "@storybook/client-logger" "6.3.6" - "@types/reach__router" "^1.3.7" - core-js "^3.8.2" - fast-deep-equal "^3.1.3" - global "^4.4.0" - lodash "^4.17.20" - memoizerific "^1.11.3" - qs "^6.10.0" - ts-dedent "^2.0.0" - "@storybook/router@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/router/-/router-6.3.7.tgz#1714a99a58a7b9f08b6fcfe2b678dad6ca896736" @@ -6073,24 +5962,6 @@ prettier "~2.2.1" regenerator-runtime "^0.13.7" -"@storybook/theming@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.3.6.tgz#75624f6d4e01530b87afca3eab9996a16c0370ab" - integrity sha512-mPrQrMUREajNEWxzgR8t0YIZsI9avPv25VNA08fANnwVsc887p4OL5eCTL2dFIlD34YDzAwiyRKYoLj2vDW4nw== - dependencies: - "@emotion/core" "^10.1.1" - "@emotion/is-prop-valid" "^0.8.6" - "@emotion/styled" "^10.0.27" - "@storybook/client-logger" "6.3.6" - core-js "^3.8.2" - deep-object-diff "^1.1.0" - emotion-theming "^10.0.27" - global "^4.4.0" - memoizerific "^1.11.3" - polished "^4.0.5" - resolve-from "^5.0.0" - ts-dedent "^2.0.0" - "@storybook/theming@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.3.7.tgz#6daf9a21b26ed607f3c28a82acd90c0248e76d8b" @@ -6109,21 +5980,21 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" -"@storybook/ui@6.3.6": - version "6.3.6" - resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.3.6.tgz#a9ed8265e34bb8ef9f0dd08f40170b3dcf8a8931" - integrity sha512-S9FjISUiAmbBR7d6ubUEcELQdffDfRxerloxkXs5Ou7n8fEPqpgQB01Hw5MLRUwTEpxPzHn+xtIGYritAGxt/Q== +"@storybook/ui@6.3.7": + version "6.3.7" + resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.3.7.tgz#d0caea50640670da3189bbbb67c43da30c90455a" + integrity sha512-PBeRO8qtwAbtHvxUgNtz/ChUR6qnN+R37dMaIs3Y96jbks1fS2K9Mt7W5s1HnUbWbg2KsZMv9D4VYPBasY+Isw== dependencies: "@emotion/core" "^10.1.1" - "@storybook/addons" "6.3.6" - "@storybook/api" "6.3.6" - "@storybook/channels" "6.3.6" - "@storybook/client-logger" "6.3.6" - "@storybook/components" "6.3.6" - "@storybook/core-events" "6.3.6" - "@storybook/router" "6.3.6" + "@storybook/addons" "6.3.7" + "@storybook/api" "6.3.7" + "@storybook/channels" "6.3.7" + "@storybook/client-logger" "6.3.7" + "@storybook/components" "6.3.7" + "@storybook/core-events" "6.3.7" + "@storybook/router" "6.3.7" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.3.6" + "@storybook/theming" "6.3.7" "@types/markdown-to-jsx" "^6.11.3" copy-to-clipboard "^3.3.1" core-js "^3.8.2" From a440d3b3893679c1c202e4893484b842427d9bbd Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Tue, 13 Jul 2021 17:16:51 -0400 Subject: [PATCH 31/34] refactor(techdocs): rework TechDocsHome to use EntityListProvider Signed-off-by: Phil Kuang --- .changeset/clean-jokes-think.md | 5 ++ .changeset/spicy-crews-applaud.md | 6 ++ plugins/catalog-react/api-report.md | 28 ++++++- .../FavoriteEntity/FavoriteEntity.tsx} | 14 ++-- .../src/components/FavoriteEntity/index.ts | 21 +++++ plugins/catalog-react/src/components/index.ts | 1 + plugins/catalog-react/src/types.ts | 5 +- plugins/catalog-react/src/utils/filters.ts | 4 +- .../components/CatalogTable/CatalogTable.tsx | 10 +-- .../components/EntityLayout/EntityLayout.tsx | 4 +- .../EntityPageLayout/EntityPageLayout.tsx | 4 +- plugins/techdocs/api-report.md | 38 ++++++++- plugins/techdocs/package.json | 3 + .../src/home/components/DocsTable.tsx | 70 ++++++++--------- .../home/components/EntityListDocsTable.tsx | 70 +++++++++++++++++ .../home/components/TechDocsCustomHome.tsx | 25 ++---- .../src/home/components/TechDocsHome.test.tsx | 14 ++-- .../src/home/components/TechDocsHome.tsx | 78 ++++++++++--------- .../home/components/TechDocsHomeLayout.tsx | 41 ++++++++++ .../src/home/components/TechDocsPicker.tsx | 47 +++++++++++ .../techdocs/src/home/components/actions.tsx | 32 ++++++++ plugins/techdocs/src/home/components/index.ts | 20 +++++ plugins/techdocs/src/home/components/types.ts | 24 ++++++ plugins/techdocs/src/index.ts | 7 +- 24 files changed, 451 insertions(+), 120 deletions(-) create mode 100644 .changeset/clean-jokes-think.md create mode 100644 .changeset/spicy-crews-applaud.md rename plugins/{catalog/src/components/FavouriteEntity/FavouriteEntity.tsx => catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx} (80%) create mode 100644 plugins/catalog-react/src/components/FavoriteEntity/index.ts create mode 100644 plugins/techdocs/src/home/components/EntityListDocsTable.tsx create mode 100644 plugins/techdocs/src/home/components/TechDocsHomeLayout.tsx create mode 100644 plugins/techdocs/src/home/components/TechDocsPicker.tsx create mode 100644 plugins/techdocs/src/home/components/actions.tsx create mode 100644 plugins/techdocs/src/home/components/index.ts create mode 100644 plugins/techdocs/src/home/components/types.ts diff --git a/.changeset/clean-jokes-think.md b/.changeset/clean-jokes-think.md new file mode 100644 index 0000000000..4a2c6a2c13 --- /dev/null +++ b/.changeset/clean-jokes-think.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +Rework `TechDocsHome` to use `EntityListProvider` with support for starring docs and filtering on owned, starred, owner, and tags. diff --git a/.changeset/spicy-crews-applaud.md b/.changeset/spicy-crews-applaud.md new file mode 100644 index 0000000000..d2707516d2 --- /dev/null +++ b/.changeset/spicy-crews-applaud.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-react': patch +--- + +Move and rename `FavoriteEntity` component to `catalog-react` diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 8471a80fe5..57bb5360c2 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -10,9 +10,11 @@ import { AsyncState } from 'react-use/lib/useAsync'; import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { CatalogApi } from '@backstage/catalog-client'; import { ComponentEntity } from '@backstage/catalog-model'; +import { ComponentProps } from 'react'; import { Context } from 'react'; import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; +import { IconButton } from '@material-ui/core'; import { LinkProps } from '@backstage/core-components'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; @@ -116,7 +118,10 @@ export const EntityContext: Context; // // @public (undocumented) export type EntityFilter = { - getCatalogFilters?: () => Record; + getCatalogFilters?: () => Record< + string, + string | symbol | (string | symbol)[] + >; filterEntity?: (entity: Entity) => boolean; toQueryValue?: () => string | string[]; }; @@ -618,6 +623,25 @@ export class EntityTypeFilter implements EntityFilter { // @public (undocumented) export const EntityTypePicker: () => JSX.Element | null; +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "FavoriteEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export const FavoriteEntity: (props: Props_2) => JSX.Element; + +// Warning: (ae-missing-release-tag) "favoriteEntityIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const favoriteEntityIcon: (isStarred: boolean) => JSX.Element; + +// Warning: (ae-missing-release-tag) "favoriteEntityTooltip" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const favoriteEntityTooltip: ( + isStarred: boolean, +) => 'Remove from favorites' | 'Add to favorites'; + // Warning: (ae-missing-release-tag) "formatEntityRefTitle" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -677,7 +701,7 @@ export const MockEntityListContextProvider: ({ // @public (undocumented) export function reduceCatalogFilters( filters: EntityFilter[], -): Record; +): Record; // Warning: (ae-missing-release-tag) "reduceEntityFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx similarity index 80% rename from plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx rename to plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx index 108697541b..95e617ab7f 100644 --- a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx +++ b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx @@ -15,7 +15,7 @@ */ import React, { ComponentProps } from 'react'; -import { useStarredEntities } from '@backstage/plugin-catalog-react'; +import { useStarredEntities } from '../../hooks/useStarredEntities'; import { IconButton, Tooltip, withStyles } from '@material-ui/core'; import StarBorder from '@material-ui/icons/StarBorder'; import Star from '@material-ui/icons/Star'; @@ -29,17 +29,17 @@ const YellowStar = withStyles({ }, })(Star); -export const favouriteEntityTooltip = (isStarred: boolean) => +export const favoriteEntityTooltip = (isStarred: boolean) => isStarred ? 'Remove from favorites' : 'Add to favorites'; -export const favouriteEntityIcon = (isStarred: boolean) => +export const favoriteEntityIcon = (isStarred: boolean) => isStarred ? : ; /** - * IconButton for showing if a current entity is starred and adding/removing it from the favourite entities + * IconButton for showing if a current entity is starred and adding/removing it from the favorite entities * @param props MaterialUI IconButton props extended by required `entity` prop */ -export const FavouriteEntity = (props: Props) => { +export const FavoriteEntity = (props: Props) => { const { toggleStarredEntity, isStarredEntity } = useStarredEntities(); const isStarred = isStarredEntity(props.entity); return ( @@ -48,8 +48,8 @@ export const FavouriteEntity = (props: Props) => { {...props} onClick={() => toggleStarredEntity(props.entity)} > - - {favouriteEntityIcon(isStarred)} + + {favoriteEntityIcon(isStarred)} ); diff --git a/plugins/catalog-react/src/components/FavoriteEntity/index.ts b/plugins/catalog-react/src/components/FavoriteEntity/index.ts new file mode 100644 index 0000000000..f731ca7483 --- /dev/null +++ b/plugins/catalog-react/src/components/FavoriteEntity/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + favoriteEntityTooltip, + favoriteEntityIcon, + FavoriteEntity, +} from './FavoriteEntity'; diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts index dd7c4bebc2..2664af8ef9 100644 --- a/plugins/catalog-react/src/components/index.ts +++ b/plugins/catalog-react/src/components/index.ts @@ -22,4 +22,5 @@ export * from './EntitySearchBar'; export * from './EntityTable'; export * from './EntityTagPicker'; export * from './EntityTypePicker'; +export * from './FavoriteEntity'; export * from './UserListPicker'; diff --git a/plugins/catalog-react/src/types.ts b/plugins/catalog-react/src/types.ts index 4ac7644ee1..c5612fdf3f 100644 --- a/plugins/catalog-react/src/types.ts +++ b/plugins/catalog-react/src/types.ts @@ -23,7 +23,10 @@ export type EntityFilter = { * { field: 'kind', values: ['component'] } * { field: 'metadata.name', values: ['component-1', 'component-2'] } */ - getCatalogFilters?: () => Record; + getCatalogFilters?: () => Record< + string, + string | symbol | (string | symbol)[] + >; /** * Filter entities on the frontend after a catalog-backend request. This function will be called diff --git a/plugins/catalog-react/src/utils/filters.ts b/plugins/catalog-react/src/utils/filters.ts index b3683beea0..109e864c52 100644 --- a/plugins/catalog-react/src/utils/filters.ts +++ b/plugins/catalog-react/src/utils/filters.ts @@ -19,13 +19,13 @@ import { EntityFilter } from '../types'; export function reduceCatalogFilters( filters: EntityFilter[], -): Record { +): Record { return filters.reduce((compoundFilter, filter) => { return { ...compoundFilter, ...(filter.getCatalogFilters ? filter.getCatalogFilters() : {}), }; - }, {} as Record); + }, {} as Record); } export function reduceEntityFilters( diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 86e95b48e0..84c2afa0f6 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -15,6 +15,8 @@ */ import { RELATION_OWNED_BY, RELATION_PART_OF } from '@backstage/catalog-model'; import { + favoriteEntityIcon, + favoriteEntityTooltip, formatEntityRefTitle, getEntityMetadataEditUrl, getEntityMetadataViewUrl, @@ -26,10 +28,6 @@ import Edit from '@material-ui/icons/Edit'; import OpenInNew from '@material-ui/icons/OpenInNew'; import { capitalize } from 'lodash'; import React from 'react'; -import { - favouriteEntityIcon, - favouriteEntityTooltip, -} from '../FavouriteEntity/FavouriteEntity'; import * as columnFactories from './columns'; import { EntityRow } from './types'; import { @@ -105,8 +103,8 @@ export const CatalogTable = ({ columns, actions }: CatalogTableProps) => { const isStarred = isStarredEntity(entity); return { cellStyle: { paddingLeft: '1em' }, - icon: () => favouriteEntityIcon(isStarred), - tooltip: favouriteEntityTooltip(isStarred), + icon: () => favoriteEntityIcon(isStarred), + tooltip: favoriteEntityTooltip(isStarred), onClick: () => toggleStarredEntity(entity), }; }, diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index f431027097..a139fbaa1e 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -35,6 +35,7 @@ import { import { EntityContext, EntityRefLinks, + FavoriteEntity, getEntityRelations, useEntityCompoundName, } from '@backstage/plugin-catalog-react'; @@ -43,7 +44,6 @@ import { Alert } from '@material-ui/lab'; import React, { useContext, useState } from 'react'; import { useNavigate } from 'react-router'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; -import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; type SubRoute = { @@ -79,7 +79,7 @@ const EntityLayoutTitle = ({ > {title} - {entity && } + {entity && } ); }; diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx index 4abac4e01b..088a9f2c53 100644 --- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx @@ -21,6 +21,7 @@ import { import { EntityContext, EntityRefLinks, + FavoriteEntity, getEntityRelations, useEntityCompoundName, } from '@backstage/plugin-catalog-react'; @@ -28,7 +29,6 @@ import { Box } from '@material-ui/core'; import React, { useContext, useState } from 'react'; import { useNavigate } from 'react-router'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; -import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; import { Tabbed } from './Tabbed'; @@ -54,7 +54,7 @@ const EntityPageTitle = ({ }) => ( {title} - {entity && } + {entity && } ); diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index 8364263f42..95d78475fb 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -5,6 +5,7 @@ ```ts /// +import { Action } from '@material-table/core'; import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Config } from '@backstage/config'; @@ -14,6 +15,7 @@ import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; import { LocationSpec } from '@backstage/catalog-model'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // Warning: (ae-missing-release-tag) "DocsCardGrid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -42,16 +44,34 @@ export const DocsResultListItem: ({ export const DocsTable: ({ entities, title, + loading, + actions, }: { entities: Entity[] | undefined; title?: string | undefined; + loading?: boolean | undefined; + actions?: + | ( + | Action + | { + action: (rowData: DocsTableRow) => Action; + position: string; + } + | ((rowData: DocsTableRow) => Action) + )[] + | undefined; }) => JSX.Element | null; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EmbeddedDocsRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EmbeddedDocsRouter: (_props: Props) => JSX.Element; +export const EmbeddedDocsRouter: (_props: Props_2) => JSX.Element; + +// Warning: (ae-missing-release-tag) "EntityListDocsTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EntityListDocsTable: () => JSX.Element; // Warning: (ae-missing-release-tag) "EntityTechdocsContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -69,7 +89,7 @@ export type PanelType = 'DocsCardGrid' | 'DocsTable'; // Warning: (ae-missing-release-tag) "Reader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Reader: ({ entityId, onReady }: Props_2) => JSX.Element; +export const Reader: ({ entityId, onReady }: Props_3) => JSX.Element; // Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -143,11 +163,22 @@ export const TechDocsCustomHome: ({ tabsConfig: TabsConfig; }) => JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "TechDocsHomeLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const TechDocsHomeLayout: ({ children }: Props) => JSX.Element; + // Warning: (ae-missing-release-tag) "TechdocsPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const TechdocsPage: () => JSX.Element; +// Warning: (ae-missing-release-tag) "TechDocsPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const TechDocsPicker: () => null; + // Warning: (ae-missing-release-tag) "techdocsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -255,7 +286,8 @@ export class TechDocsStorageClient implements TechDocsStorageApi { // Warnings were encountered during analysis: // -// src/plugin.d.ts:18:5 - (ae-forgotten-export) The symbol "TabsConfig" needs to be exported by the entry point index.d.ts +// src/plugin.d.ts:17:5 - (ae-forgotten-export) The symbol "DocsTableRow" needs to be exported by the entry point index.d.ts +// src/plugin.d.ts:23:5 - (ae-forgotten-export) The symbol "TabsConfig" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 7051109c2e..9e15dd7aff 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -38,14 +38,17 @@ "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.9", "@backstage/integration-react": "^0.1.6", + "@backstage/plugin-catalog": "^0.6.10", "@backstage/plugin-catalog-react": "^0.4.1", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@material-ui/styles": "^4.10.0", + "@types/react": "^16.9", "dompurify": "^2.2.9", "event-source-polyfill": "^1.0.25", + "lodash": "^4.17.21", "react": "^16.13.1", "react-dom": "^16.13.1", "react-lazylog": "^4.5.2", diff --git a/plugins/techdocs/src/home/components/DocsTable.tsx b/plugins/techdocs/src/home/components/DocsTable.tsx index 031f09868f..68e6405265 100644 --- a/plugins/techdocs/src/home/components/DocsTable.tsx +++ b/plugins/techdocs/src/home/components/DocsTable.tsx @@ -18,24 +18,29 @@ import React from 'react'; import { useCopyToClipboard } from 'react-use'; import { generatePath } from 'react-router-dom'; -import { IconButton, Tooltip } from '@material-ui/core'; -import ShareIcon from '@material-ui/icons/Share'; import { Entity } from '@backstage/catalog-model'; import { rootDocsRouteRef } from '../../routes'; import { - Table, - EmptyState, Button, - SubvalueCell, + EmptyState, Link, + SubvalueCell, + Table, + TableProps, } from '@backstage/core-components'; +import * as actionFactories from './actions'; +import { DocsTableRow } from './types'; export const DocsTable = ({ entities, title, + loading, + actions, }: { entities: Entity[] | undefined; title?: string | undefined; + loading?: boolean | undefined; + actions?: TableProps['actions']; }) => { const [, copyToClipboard] = useCopyToClipboard(); @@ -43,15 +48,14 @@ export const DocsTable = ({ const documents = entities.map(entity => { return { - name: entity.metadata.name, - description: entity.metadata.description, - owner: entity?.spec?.owner, - type: entity?.spec?.type, - docsUrl: generatePath(rootDocsRouteRef.path, { - namespace: entity.metadata.namespace ?? 'default', - kind: entity.kind, - name: entity.metadata.name, - }), + entity, + resolved: { + docsUrl: generatePath(rootDocsRouteRef.path, { + namespace: entity.metadata.namespace ?? 'default', + kind: entity.kind, + name: entity.metadata.name, + }), + }, }; }); @@ -60,49 +64,43 @@ export const DocsTable = ({ title: 'Document', field: 'name', highlight: true, - render: (row: any): React.ReactNode => ( + render: (row: DocsTableRow): React.ReactNode => ( {row.name}} - subvalue={row.description} + value={ + {row.entity.metadata.name} + } + subvalue={row.entity.metadata.description} /> ), }, { title: 'Owner', - field: 'owner', + field: 'entity.spec.owner', }, { title: 'Type', - field: 'type', - }, - { - title: 'Actions', - width: '10%', - render: (row: any) => ( - - - copyToClipboard(`${window.location.href}/${row.docsUrl}`) - } - > - - - - ), + field: 'entity.spec.type', }, ]; + const defaultActions: TableProps['actions'] = [ + actionFactories.createCopyDocsUrlAction(copyToClipboard), + ]; + return ( <> - {documents && documents.length > 0 ? ( - 0) ? ( + + isLoading={loading} options={{ paging: true, pageSize: 20, search: true, + actionsColumnIndex: -1, }} data={documents} columns={columns} + actions={actions || defaultActions} title={ title ? `${title} (${documents.length})` diff --git a/plugins/techdocs/src/home/components/EntityListDocsTable.tsx b/plugins/techdocs/src/home/components/EntityListDocsTable.tsx new file mode 100644 index 0000000000..11e49fa2d4 --- /dev/null +++ b/plugins/techdocs/src/home/components/EntityListDocsTable.tsx @@ -0,0 +1,70 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { useCopyToClipboard } from 'react-use'; +import { capitalize } from 'lodash'; +import { CodeSnippet, WarningPanel } from '@backstage/core-components'; +import { + favoriteEntityIcon, + favoriteEntityTooltip, + useEntityListProvider, + useStarredEntities, +} from '@backstage/plugin-catalog-react'; +import * as actionFactories from './actions'; +import { DocsTable } from './DocsTable'; +import { DocsTableRow } from './types'; + +export const EntityListDocsTable = () => { + const { loading, error, entities, filters } = useEntityListProvider(); + const { isStarredEntity, toggleStarredEntity } = useStarredEntities(); + const [, copyToClipboard] = useCopyToClipboard(); + + const title = capitalize(filters.user?.value ?? 'all'); + + const actions = [ + actionFactories.createCopyDocsUrlAction(copyToClipboard), + ({ entity }: DocsTableRow) => { + const isStarred = isStarredEntity(entity); + return { + cellStyle: { paddingLeft: '1em' }, + icon: () => favoriteEntityIcon(isStarred), + tooltip: favoriteEntityTooltip(isStarred), + onClick: () => toggleStarredEntity(entity), + }; + }, + ]; + + if (error) { + return ( + + + + ); + } + + return ( + + ); +}; diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx index e2623fe81f..7f93f6a306 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx @@ -28,20 +28,19 @@ import { import { Entity } from '@backstage/catalog-model'; import { DocsTable } from './DocsTable'; import { DocsCardGrid } from './DocsCardGrid'; +import { TechDocsHomeLayout } from './TechDocsHomeLayout'; import { CodeSnippet, Content, - Header, HeaderTabs, - Page, Progress, WarningPanel, SupportButton, ContentHeader, } from '@backstage/core-components'; -import { ConfigApi, configApiRef, useApi } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; const panels = { DocsTable: DocsTable, @@ -122,7 +121,6 @@ export const TechDocsCustomHome = ({ }) => { const [selectedTab, setSelectedTab] = useState(0); const catalogApi: CatalogApi = useApi(catalogApiRef); - const configApi: ConfigApi = useApi(configApiRef); const { value: entities, @@ -147,27 +145,21 @@ export const TechDocsCustomHome = ({ }); }); - const generatedSubtitle = `Documentation available in ${ - configApi.getOptionalString('organization.name') ?? 'Backstage' - }`; - const currentTabConfig = tabsConfig[selectedTab]; if (loading) { return ( - -
+ - + ); } if (error) { return ( - -
+ - + ); } return ( - -
+ setSelectedTab(index)} @@ -201,6 +192,6 @@ export const TechDocsCustomHome = ({ /> ))} - + ); }; diff --git a/plugins/techdocs/src/home/components/TechDocsHome.test.tsx b/plugins/techdocs/src/home/components/TechDocsHome.test.tsx index f90b35e181..9289884f1c 100644 --- a/plugins/techdocs/src/home/components/TechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/TechDocsHome.test.tsx @@ -15,7 +15,7 @@ */ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; import { TechDocsHome } from './TechDocsHome'; @@ -25,7 +25,11 @@ import { ApiRegistry, ConfigReader, } from '@backstage/core-app-api'; -import { ConfigApi, configApiRef } from '@backstage/core-plugin-api'; +import { + ConfigApi, + configApiRef, + storageApiRef, +} from '@backstage/core-plugin-api'; jest.mock('@backstage/plugin-catalog-react', () => { const actual = jest.requireActual('@backstage/plugin-catalog-react'); @@ -36,7 +40,7 @@ jest.mock('@backstage/plugin-catalog-react', () => { }); const mockCatalogApi = { - getEntityByName: jest.fn(), + getEntityByName: () => Promise.resolve(), getEntities: async () => ({ items: [ { @@ -61,6 +65,7 @@ describe('TechDocs Home', () => { const apiRegistry = ApiRegistry.from([ [catalogApiRef, mockCatalogApi], [configApiRef, configApi], + [storageApiRef, MockStorageApi.create()], ]); it('should render a TechDocs home page', async () => { @@ -75,8 +80,5 @@ describe('TechDocs Home', () => { expect( await screen.findByText(/Documentation available in My Company/i), ).toBeInTheDocument(); - - // Explore Content - expect(await screen.findByTestId('docs-explore')).toBeDefined(); }); }); diff --git a/plugins/techdocs/src/home/components/TechDocsHome.tsx b/plugins/techdocs/src/home/components/TechDocsHome.tsx index 6445713875..ef69bcdc44 100644 --- a/plugins/techdocs/src/home/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/home/components/TechDocsHome.tsx @@ -15,41 +15,49 @@ */ import React from 'react'; -import { PanelType, TechDocsCustomHome } from './TechDocsCustomHome'; +import { + Content, + ContentHeader, + SupportButton, +} from '@backstage/core-components'; +import { + EntityListContainer, + FilterContainer, + FilteredEntityLayout, +} from '@backstage/plugin-catalog'; +import { + EntityListProvider, + EntityOwnerPicker, + EntityTagPicker, + UserListPicker, +} from '@backstage/plugin-catalog-react'; +import { EntityListDocsTable } from './EntityListDocsTable'; +import { TechDocsHomeLayout } from './TechDocsHomeLayout'; +import { TechDocsPicker } from './TechDocsPicker'; export const TechDocsHome = () => { - const tabsConfig = [ - { - label: 'Overview', - panels: [ - { - title: 'Overview', - description: - 'Explore your internal technical ecosystem through documentation.', - panelType: 'DocsCardGrid' as PanelType, - filterPredicate: () => true, - }, - // uncomment this if you would like to have a secondary panel with owned documents - // { - // title: 'Owned', - // description: 'Explore your owned internal documentation.', - // panelType: 'DocsCardGrid' as PanelType, - // filterPredicate: 'ownedByUser', - // }, - ], - }, - { - label: 'Owned Documents', - panels: [ - { - title: 'Owned documents', - description: 'Access your documentation.', - panelType: 'DocsTable' as PanelType, - // ownedByUser filters out entities owned by signed in user - filterPredicate: 'ownedByUser', - }, - ], - }, - ]; - return ; + return ( + + + + + Discover documentation in your ecosystem. + + + + + + + + + + + + + + + + + + ); }; diff --git a/plugins/techdocs/src/home/components/TechDocsHomeLayout.tsx b/plugins/techdocs/src/home/components/TechDocsHomeLayout.tsx new file mode 100644 index 0000000000..e6717fbecc --- /dev/null +++ b/plugins/techdocs/src/home/components/TechDocsHomeLayout.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; + +import { PageWithHeader } from '@backstage/core-components'; +import { useApi, configApiRef } from '@backstage/core-plugin-api'; + +type Props = { + children?: React.ReactNode; +}; + +export const TechDocsHomeLayout = ({ children }: Props) => { + const configApi = useApi(configApiRef); + const generatedSubtitle = `Documentation available in ${ + configApi.getOptionalString('organization.name') ?? 'Backstage' + }`; + + return ( + + {children} + + ); +}; diff --git a/plugins/techdocs/src/home/components/TechDocsPicker.tsx b/plugins/techdocs/src/home/components/TechDocsPicker.tsx new file mode 100644 index 0000000000..9ed21da368 --- /dev/null +++ b/plugins/techdocs/src/home/components/TechDocsPicker.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useEffect } from 'react'; +import { + CATALOG_FILTER_EXISTS, + DefaultEntityFilters, + EntityFilter, + useEntityListProvider, +} from '@backstage/plugin-catalog-react'; + +class TechDocsFilter implements EntityFilter { + getCatalogFilters(): Record { + return { + 'metadata.annotations.backstage.io/techdocs-ref': CATALOG_FILTER_EXISTS, + }; + } +} + +type CustomFilters = DefaultEntityFilters & { + techdocs?: TechDocsFilter; +}; + +export const TechDocsPicker = () => { + const { updateFilters } = useEntityListProvider(); + + useEffect(() => { + updateFilters({ + techdocs: new TechDocsFilter(), + }); + }, [updateFilters]); + + return null; +}; diff --git a/plugins/techdocs/src/home/components/actions.tsx b/plugins/techdocs/src/home/components/actions.tsx new file mode 100644 index 0000000000..e26415b049 --- /dev/null +++ b/plugins/techdocs/src/home/components/actions.tsx @@ -0,0 +1,32 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import ShareIcon from '@material-ui/icons/Share'; +import { DocsTableRow } from './types'; + +export function createCopyDocsUrlAction(copyToClipboard: Function) { + return (row: DocsTableRow) => { + return { + icon: () => , + tooltip: 'Click to copy documentation link to clipboard', + onClick: () => + copyToClipboard( + `${window.location.href.split('/?')[0]}/${row.resolved.docsUrl}`, + ), + }; + }; +} diff --git a/plugins/techdocs/src/home/components/index.ts b/plugins/techdocs/src/home/components/index.ts new file mode 100644 index 0000000000..392aa2e82f --- /dev/null +++ b/plugins/techdocs/src/home/components/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { EntityListDocsTable } from './EntityListDocsTable'; +export { TechDocsHomeLayout } from './TechDocsHomeLayout'; +export { TechDocsPicker } from './TechDocsPicker'; +export type { PanelType } from './TechDocsCustomHome'; diff --git a/plugins/techdocs/src/home/components/types.ts b/plugins/techdocs/src/home/components/types.ts new file mode 100644 index 0000000000..324cb59f96 --- /dev/null +++ b/plugins/techdocs/src/home/components/types.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; + +export type DocsTableRow = { + entity: Entity; + resolved: { + docsUrl: string; + }; +}; diff --git a/plugins/techdocs/src/index.ts b/plugins/techdocs/src/index.ts index 66502a4585..9a93e2371c 100644 --- a/plugins/techdocs/src/index.ts +++ b/plugins/techdocs/src/index.ts @@ -18,7 +18,12 @@ export * from './api'; export { techdocsApiRef, techdocsStorageApiRef } from './api'; export type { TechDocsApi, TechDocsStorageApi } from './api'; export { TechDocsClient, TechDocsStorageClient } from './client'; -export type { PanelType } from './home/components/TechDocsCustomHome'; +export type { PanelType } from './home/components'; +export { + EntityListDocsTable, + TechDocsHomeLayout, + TechDocsPicker, +} from './home/components'; export * from './components/DocsResultListItem'; export { DocsCardGrid, From 10ef115554e5b2c29a4fe357cb9325cfd0295761 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Wed, 4 Aug 2021 10:01:00 -0400 Subject: [PATCH 32/34] refactor(EntityListDocsTable): support custom columns and actions Signed-off-by: Phil Kuang --- plugins/techdocs/api-report.md | 101 +++++++++++++++++- .../src/home/components/DocsTable.tsx | 46 ++++---- .../home/components/EntityListDocsTable.tsx | 41 ++++--- .../src/home/components/TechDocsHome.tsx | 18 +++- .../techdocs/src/home/components/actions.tsx | 24 ++++- .../techdocs/src/home/components/columns.tsx | 56 ++++++++++ plugins/techdocs/src/home/components/index.ts | 1 + plugins/techdocs/src/home/components/types.ts | 4 +- plugins/techdocs/src/index.ts | 3 +- plugins/techdocs/src/plugin.ts | 8 ++ 10 files changed, 251 insertions(+), 51 deletions(-) create mode 100644 plugins/techdocs/src/home/components/columns.tsx diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index 95d78475fb..edae3d9585 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -17,6 +17,54 @@ import { IdentityApi } from '@backstage/core-plugin-api'; import { LocationSpec } from '@backstage/catalog-model'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; +import { TableColumn } from '@backstage/core-components'; +import { TableProps } from '@backstage/core-components'; +import { UserListFilterKind } from '@backstage/plugin-catalog-react'; + +// Warning: (ae-missing-release-tag) "createCopyDocsUrlAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +function createCopyDocsUrlAction( + copyToClipboard: Function, +): ( + row: DocsTableRow, +) => { + icon: () => JSX.Element; + tooltip: string; + onClick: () => any; +}; + +// Warning: (ae-missing-release-tag) "createNameColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +function createNameColumn(): TableColumn; + +// Warning: (ae-missing-release-tag) "createOwnerColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +function createOwnerColumn(): TableColumn; + +// Warning: (ae-missing-release-tag) "createStarEntityAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +function createStarEntityAction( + isStarredEntity: Function, + toggleStarredEntity: Function, +): ({ + entity, +}: DocsTableRow) => { + cellStyle: { + paddingLeft: string; + }; + icon: () => JSX.Element; + tooltip: string; + onClick: () => any; +}; + +// Warning: (ae-missing-release-tag) "createTypeColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +function createTypeColumn(): TableColumn; // Warning: (ae-missing-release-tag) "DocsCardGrid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -45,11 +93,13 @@ export const DocsTable: ({ entities, title, loading, + columns, actions, }: { entities: Entity[] | undefined; title?: string | undefined; loading?: boolean | undefined; + columns?: TableColumn[] | undefined; actions?: | ( | Action @@ -62,6 +112,18 @@ export const DocsTable: ({ | undefined; }) => JSX.Element | null; +// Warning: (ae-missing-release-tag) "DocsTableRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type DocsTableRow = { + entity: Entity; + resolved: { + docsUrl: string; + ownedByRelationsTitle: string; + ownedByRelations: EntityName[]; + }; +}; + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EmbeddedDocsRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -71,7 +133,17 @@ export const EmbeddedDocsRouter: (_props: Props_2) => JSX.Element; // Warning: (ae-missing-release-tag) "EntityListDocsTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityListDocsTable: () => JSX.Element; +export const EntityListDocsTable: { + ({ + columns, + actions, + }: { + columns?: TableColumn[] | undefined; + actions?: TableProps['actions']; + }): JSX.Element; + columns: typeof columnFactories; + actions: typeof actionFactories; +}; // Warning: (ae-missing-release-tag) "EntityTechdocsContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -163,6 +235,28 @@ export const TechDocsCustomHome: ({ tabsConfig: TabsConfig; }) => JSX.Element; +// Warning: (ae-missing-release-tag) "TechDocsHome" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const TechDocsHome: ({ + initialFilter, + columns, + actions, +}: { + initialFilter?: UserListFilterKind | undefined; + columns?: TableColumn[] | undefined; + actions?: + | ( + | Action + | { + action: (rowData: DocsTableRow) => Action; + position: string; + } + | ((rowData: DocsTableRow) => Action) + )[] + | undefined; +}) => JSX.Element; + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "TechDocsHomeLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -286,8 +380,9 @@ export class TechDocsStorageClient implements TechDocsStorageApi { // Warnings were encountered during analysis: // -// src/plugin.d.ts:17:5 - (ae-forgotten-export) The symbol "DocsTableRow" needs to be exported by the entry point index.d.ts -// src/plugin.d.ts:23:5 - (ae-forgotten-export) The symbol "TabsConfig" needs to be exported by the entry point index.d.ts +// src/home/components/EntityListDocsTable.d.ts:11:5 - (ae-forgotten-export) The symbol "columnFactories" needs to be exported by the entry point index.d.ts +// src/home/components/EntityListDocsTable.d.ts:12:5 - (ae-forgotten-export) The symbol "actionFactories" needs to be exported by the entry point index.d.ts +// src/plugin.d.ts:24:5 - (ae-forgotten-export) The symbol "TabsConfig" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/techdocs/src/home/components/DocsTable.tsx b/plugins/techdocs/src/home/components/DocsTable.tsx index 68e6405265..29b63f5441 100644 --- a/plugins/techdocs/src/home/components/DocsTable.tsx +++ b/plugins/techdocs/src/home/components/DocsTable.tsx @@ -18,28 +18,34 @@ import React from 'react'; import { useCopyToClipboard } from 'react-use'; import { generatePath } from 'react-router-dom'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { + formatEntityRefTitle, + getEntityRelations, +} from '@backstage/plugin-catalog-react'; import { rootDocsRouteRef } from '../../routes'; import { Button, EmptyState, - Link, - SubvalueCell, Table, + TableColumn, TableProps, } from '@backstage/core-components'; import * as actionFactories from './actions'; +import * as columnFactories from './columns'; import { DocsTableRow } from './types'; export const DocsTable = ({ entities, title, loading, + columns, actions, }: { entities: Entity[] | undefined; title?: string | undefined; loading?: boolean | undefined; + columns?: TableColumn[]; actions?: TableProps['actions']; }) => { const [, copyToClipboard] = useCopyToClipboard(); @@ -47,6 +53,8 @@ export const DocsTable = ({ if (!entities) return null; const documents = entities.map(entity => { + const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); + return { entity, resolved: { @@ -55,32 +63,18 @@ export const DocsTable = ({ kind: entity.kind, name: entity.metadata.name, }), + ownedByRelations, + ownedByRelationsTitle: ownedByRelations + .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) + .join(', '), }, }; }); - const columns = [ - { - title: 'Document', - field: 'name', - highlight: true, - render: (row: DocsTableRow): React.ReactNode => ( - {row.entity.metadata.name} - } - subvalue={row.entity.metadata.description} - /> - ), - }, - { - title: 'Owner', - field: 'entity.spec.owner', - }, - { - title: 'Type', - field: 'entity.spec.type', - }, + const defaultColumns: TableColumn[] = [ + columnFactories.createNameColumn(), + columnFactories.createOwnerColumn(), + columnFactories.createTypeColumn(), ]; const defaultActions: TableProps['actions'] = [ @@ -99,7 +93,7 @@ export const DocsTable = ({ actionsColumnIndex: -1, }} data={documents} - columns={columns} + columns={columns || defaultColumns} actions={actions || defaultActions} title={ title diff --git a/plugins/techdocs/src/home/components/EntityListDocsTable.tsx b/plugins/techdocs/src/home/components/EntityListDocsTable.tsx index 11e49fa2d4..b94e7adace 100644 --- a/plugins/techdocs/src/home/components/EntityListDocsTable.tsx +++ b/plugins/techdocs/src/home/components/EntityListDocsTable.tsx @@ -17,35 +17,40 @@ import React from 'react'; import { useCopyToClipboard } from 'react-use'; import { capitalize } from 'lodash'; -import { CodeSnippet, WarningPanel } from '@backstage/core-components'; import { - favoriteEntityIcon, - favoriteEntityTooltip, + CodeSnippet, + TableColumn, + TableProps, + WarningPanel, +} from '@backstage/core-components'; +import { useEntityListProvider, useStarredEntities, } from '@backstage/plugin-catalog-react'; -import * as actionFactories from './actions'; import { DocsTable } from './DocsTable'; +import * as actionFactories from './actions'; +import * as columnFactories from './columns'; import { DocsTableRow } from './types'; -export const EntityListDocsTable = () => { +export const EntityListDocsTable = ({ + columns, + actions, +}: { + columns?: TableColumn[]; + actions?: TableProps['actions']; +}) => { const { loading, error, entities, filters } = useEntityListProvider(); const { isStarredEntity, toggleStarredEntity } = useStarredEntities(); const [, copyToClipboard] = useCopyToClipboard(); const title = capitalize(filters.user?.value ?? 'all'); - const actions = [ + const defaultActions = [ actionFactories.createCopyDocsUrlAction(copyToClipboard), - ({ entity }: DocsTableRow) => { - const isStarred = isStarredEntity(entity); - return { - cellStyle: { paddingLeft: '1em' }, - icon: () => favoriteEntityIcon(isStarred), - tooltip: favoriteEntityTooltip(isStarred), - onClick: () => toggleStarredEntity(entity), - }; - }, + actionFactories.createStarEntityAction( + isStarredEntity, + toggleStarredEntity, + ), ]; if (error) { @@ -64,7 +69,11 @@ export const EntityListDocsTable = () => { title={title} entities={entities} loading={loading} - actions={actions} + actions={actions || defaultActions} + columns={columns} /> ); }; + +EntityListDocsTable.columns = columnFactories; +EntityListDocsTable.actions = actionFactories; diff --git a/plugins/techdocs/src/home/components/TechDocsHome.tsx b/plugins/techdocs/src/home/components/TechDocsHome.tsx index ef69bcdc44..0aa1b87282 100644 --- a/plugins/techdocs/src/home/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/home/components/TechDocsHome.tsx @@ -19,6 +19,8 @@ import { Content, ContentHeader, SupportButton, + TableColumn, + TableProps, } from '@backstage/core-components'; import { EntityListContainer, @@ -29,13 +31,23 @@ import { EntityListProvider, EntityOwnerPicker, EntityTagPicker, + UserListFilterKind, UserListPicker, } from '@backstage/plugin-catalog-react'; import { EntityListDocsTable } from './EntityListDocsTable'; import { TechDocsHomeLayout } from './TechDocsHomeLayout'; import { TechDocsPicker } from './TechDocsPicker'; +import { DocsTableRow } from './types'; -export const TechDocsHome = () => { +export const TechDocsHome = ({ + initialFilter = 'all', + columns, + actions, +}: { + initialFilter?: UserListFilterKind; + columns?: TableColumn[]; + actions?: TableProps['actions']; +}) => { return ( @@ -48,12 +60,12 @@ export const TechDocsHome = () => { - + - + diff --git a/plugins/techdocs/src/home/components/actions.tsx b/plugins/techdocs/src/home/components/actions.tsx index e26415b049..b737e99d14 100644 --- a/plugins/techdocs/src/home/components/actions.tsx +++ b/plugins/techdocs/src/home/components/actions.tsx @@ -16,6 +16,10 @@ import React from 'react'; import ShareIcon from '@material-ui/icons/Share'; +import { + favoriteEntityIcon, + favoriteEntityTooltip, +} from '@backstage/plugin-catalog-react'; import { DocsTableRow } from './types'; export function createCopyDocsUrlAction(copyToClipboard: Function) { @@ -25,8 +29,26 @@ export function createCopyDocsUrlAction(copyToClipboard: Function) { tooltip: 'Click to copy documentation link to clipboard', onClick: () => copyToClipboard( - `${window.location.href.split('/?')[0]}/${row.resolved.docsUrl}`, + `${window.location.origin}${window.location.pathname.replace( + /\/?$/, + '/', + )}${row.resolved.docsUrl}`, ), }; }; } + +export function createStarEntityAction( + isStarredEntity: Function, + toggleStarredEntity: Function, +) { + return ({ entity }: DocsTableRow) => { + const isStarred = isStarredEntity(entity); + return { + cellStyle: { paddingLeft: '1em' }, + icon: () => favoriteEntityIcon(isStarred), + tooltip: favoriteEntityTooltip(isStarred), + onClick: () => toggleStarredEntity(entity), + }; + }; +} diff --git a/plugins/techdocs/src/home/components/columns.tsx b/plugins/techdocs/src/home/components/columns.tsx new file mode 100644 index 0000000000..5cdcc66c37 --- /dev/null +++ b/plugins/techdocs/src/home/components/columns.tsx @@ -0,0 +1,56 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Link, SubvalueCell, TableColumn } from '@backstage/core-components'; +import { EntityRefLinks } from '@backstage/plugin-catalog-react'; +import { DocsTableRow } from './types'; + +export function createNameColumn(): TableColumn { + return { + title: 'Document', + field: 'entity.metadata.name', + highlight: true, + render: (row: DocsTableRow) => ( + {row.entity.metadata.name} + } + subvalue={row.entity.metadata.description} + /> + ), + }; +} + +export function createOwnerColumn(): TableColumn { + return { + title: 'Owner', + field: 'resolved.ownedByRelationsTitle', + render: ({ resolved }) => ( + + ), + }; +} + +export function createTypeColumn(): TableColumn { + return { + title: 'Type', + field: 'entity.spec.type', + }; +} diff --git a/plugins/techdocs/src/home/components/index.ts b/plugins/techdocs/src/home/components/index.ts index 392aa2e82f..a56b80f9a1 100644 --- a/plugins/techdocs/src/home/components/index.ts +++ b/plugins/techdocs/src/home/components/index.ts @@ -18,3 +18,4 @@ export { EntityListDocsTable } from './EntityListDocsTable'; export { TechDocsHomeLayout } from './TechDocsHomeLayout'; export { TechDocsPicker } from './TechDocsPicker'; export type { PanelType } from './TechDocsCustomHome'; +export type { DocsTableRow } from './types'; diff --git a/plugins/techdocs/src/home/components/types.ts b/plugins/techdocs/src/home/components/types.ts index 324cb59f96..6fd29f1716 100644 --- a/plugins/techdocs/src/home/components/types.ts +++ b/plugins/techdocs/src/home/components/types.ts @@ -14,11 +14,13 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { Entity, EntityName } from '@backstage/catalog-model'; export type DocsTableRow = { entity: Entity; resolved: { docsUrl: string; + ownedByRelationsTitle: string; + ownedByRelations: EntityName[]; }; }; diff --git a/plugins/techdocs/src/index.ts b/plugins/techdocs/src/index.ts index 9a93e2371c..7c3bf607b4 100644 --- a/plugins/techdocs/src/index.ts +++ b/plugins/techdocs/src/index.ts @@ -18,7 +18,7 @@ export * from './api'; export { techdocsApiRef, techdocsStorageApiRef } from './api'; export type { TechDocsApi, TechDocsStorageApi } from './api'; export { TechDocsClient, TechDocsStorageClient } from './client'; -export type { PanelType } from './home/components'; +export type { DocsTableRow, PanelType } from './home/components'; export { EntityListDocsTable, TechDocsHomeLayout, @@ -30,6 +30,7 @@ export { DocsTable, EntityTechdocsContent, TechDocsCustomHome, + TechDocsHome, TechdocsPage, techdocsPlugin as plugin, techdocsPlugin, diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index 39544500bd..2e031176e4 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -113,6 +113,14 @@ export const TechDocsCustomHome = techdocsPlugin.provide( }), ); +export const TechDocsHome = techdocsPlugin.provide( + createRoutableExtension({ + component: () => + import('./home/components/TechDocsHome').then(m => m.TechDocsHome), + mountPoint: rootRouteRef, + }), +); + export const TechDocsReaderPage = techdocsPlugin.provide( createRoutableExtension({ component: () => From 80582cbec49fc9d2613724799eb08bfaa2e348dc Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Thu, 5 Aug 2021 16:37:23 -0400 Subject: [PATCH 33/34] feat(techdocs): implement composable home page Signed-off-by: Phil Kuang --- .changeset/clean-jokes-think.md | 17 +++- .changeset/plenty-carpets-jog.md | 18 ++++ packages/app/src/App.tsx | 14 +++- .../default-app/packages/app/src/App.tsx | 14 +++- plugins/techdocs/api-report.md | 54 ++++++------ plugins/techdocs/package.json | 3 +- plugins/techdocs/src/Router.tsx | 11 ++- ....test.tsx => DefaultTechDocsHome.test.tsx} | 4 +- ...chDocsHome.tsx => DefaultTechDocsHome.tsx} | 8 +- .../components/LegacyTechDocsHome.test.tsx | 82 +++++++++++++++++++ .../home/components/LegacyTechDocsHome.tsx | 55 +++++++++++++ .../home/components/TechDocsCustomHome.tsx | 14 ++-- .../components/TechDocsIndexPage.test.tsx | 44 ++++++++++ .../src/home/components/TechDocsIndexPage.tsx | 25 ++++++ ...HomeLayout.tsx => TechDocsPageWrapper.tsx} | 2 +- plugins/techdocs/src/home/components/index.ts | 3 +- plugins/techdocs/src/index.ts | 5 +- plugins/techdocs/src/plugin.ts | 6 +- 18 files changed, 317 insertions(+), 62 deletions(-) create mode 100644 .changeset/plenty-carpets-jog.md rename plugins/techdocs/src/home/components/{TechDocsHome.test.tsx => DefaultTechDocsHome.test.tsx} (95%) rename plugins/techdocs/src/home/components/{TechDocsHome.tsx => DefaultTechDocsHome.tsx} (93%) create mode 100644 plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx create mode 100644 plugins/techdocs/src/home/components/LegacyTechDocsHome.tsx create mode 100644 plugins/techdocs/src/home/components/TechDocsIndexPage.test.tsx create mode 100644 plugins/techdocs/src/home/components/TechDocsIndexPage.tsx rename plugins/techdocs/src/home/components/{TechDocsHomeLayout.tsx => TechDocsPageWrapper.tsx} (94%) diff --git a/.changeset/clean-jokes-think.md b/.changeset/clean-jokes-think.md index 4a2c6a2c13..a4fc65e6fc 100644 --- a/.changeset/clean-jokes-think.md +++ b/.changeset/clean-jokes-think.md @@ -1,5 +1,18 @@ --- -'@backstage/plugin-techdocs': minor +'@backstage/plugin-techdocs': patch --- -Rework `TechDocsHome` to use `EntityListProvider` with support for starring docs and filtering on owned, starred, owner, and tags. +Expose a new composable `TechDocsIndexPage` and a `DefaultTechDocsHome` with support for starring docs and filtering on owned, starred, owner, and tags. + +You can migrate to the new UI view by making the following changes in your `App.tsx`: + +```diff +- } /> ++ }> ++ ++ ++ } ++ /> +``` diff --git a/.changeset/plenty-carpets-jog.md b/.changeset/plenty-carpets-jog.md new file mode 100644 index 0000000000..f4586072a7 --- /dev/null +++ b/.changeset/plenty-carpets-jog.md @@ -0,0 +1,18 @@ +--- +'@backstage/create-app': patch +--- + +Use new composable `TechDocsIndexPage` and `DefaultTechDocsHome` + +Make the following changes to your `App.tsx` to migrate existing apps: + +```diff +- } /> ++ }> ++ ++ ++ } ++ /> +``` diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 10fb743304..1132368a91 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -52,7 +52,11 @@ import { } from '@backstage/plugin-scaffolder'; import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; -import { TechdocsPage } from '@backstage/plugin-techdocs'; +import { + DefaultTechDocsHome, + TechDocsIndexPage, + TechDocsReaderPage, +} from '@backstage/plugin-techdocs'; import { UserSettingsPage } from '@backstage/plugin-user-settings'; import AlarmIcon from '@material-ui/icons/Alarm'; import React from 'react'; @@ -116,7 +120,13 @@ const routes = ( {entityPage} } /> - } /> + }> + + + } + /> }> diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 9b77cf6a95..57491c9222 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -13,7 +13,11 @@ import { import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder'; import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; -import { TechdocsPage } from '@backstage/plugin-techdocs'; +import { + DefaultTechDocsHome, + TechDocsIndexPage, + TechDocsReaderPage, +} from '@backstage/plugin-techdocs'; import { UserSettingsPage } from '@backstage/plugin-user-settings'; import { apis } from './apis'; import { entityPage } from './components/catalog/EntityPage'; @@ -50,7 +54,13 @@ const routes = ( > {entityPage} - } /> + }> + + + } + /> } /> } /> { icon: () => JSX.Element; @@ -50,9 +48,7 @@ function createOwnerColumn(): TableColumn; function createStarEntityAction( isStarredEntity: Function, toggleStarredEntity: Function, -): ({ - entity, -}: DocsTableRow) => { +): ({ entity }: DocsTableRow) => { cellStyle: { paddingLeft: string; }; @@ -66,6 +62,19 @@ function createStarEntityAction( // @public (undocumented) function createTypeColumn(): TableColumn; +// Warning: (ae-missing-release-tag) "DefaultTechDocsHome" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const DefaultTechDocsHome: ({ + initialFilter, + columns, + actions, +}: { + initialFilter?: UserListFilterKind | undefined; + columns?: TableColumn[] | undefined; + actions?: TableProps['actions']; +}) => JSX.Element; + // Warning: (ae-missing-release-tag) "DocsCardGrid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -235,39 +244,22 @@ export const TechDocsCustomHome: ({ tabsConfig: TabsConfig; }) => JSX.Element; -// Warning: (ae-missing-release-tag) "TechDocsHome" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "TechDocsIndexPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const TechDocsHome: ({ - initialFilter, - columns, - actions, -}: { - initialFilter?: UserListFilterKind | undefined; - columns?: TableColumn[] | undefined; - actions?: - | ( - | Action - | { - action: (rowData: DocsTableRow) => Action; - position: string; - } - | ((rowData: DocsTableRow) => Action) - )[] - | undefined; -}) => JSX.Element; - -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "TechDocsHomeLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const TechDocsHomeLayout: ({ children }: Props) => JSX.Element; +export const TechDocsIndexPage: () => JSX.Element; // Warning: (ae-missing-release-tag) "TechdocsPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const TechdocsPage: () => JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "TechDocsPageWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const TechDocsPageWrapper: ({ children }: Props) => JSX.Element; + // Warning: (ae-missing-release-tag) "TechDocsPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 9e15dd7aff..bb9f8a7c95 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -45,7 +45,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@material-ui/styles": "^4.10.0", - "@types/react": "^16.9", + "@types/react": "*", "dompurify": "^2.2.9", "event-source-polyfill": "^1.0.25", "lodash": "^4.17.21", @@ -70,7 +70,6 @@ "@types/dompurify": "^2.2.2", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/react": "*", "canvas": "^2.6.1", "cross-fetch": "^3.0.6", "msw": "^0.29.0" diff --git a/plugins/techdocs/src/Router.tsx b/plugins/techdocs/src/Router.tsx index 6218f7520d..3fb109e790 100644 --- a/plugins/techdocs/src/Router.tsx +++ b/plugins/techdocs/src/Router.tsx @@ -23,8 +23,8 @@ import { rootDocsRouteRef, rootCatalogDocsRouteRef, } from './routes'; -import { TechDocsHome } from './home/components/TechDocsHome'; -import { TechDocsPage } from './reader/components/TechDocsPage'; +import { TechDocsIndexPage } from './home/components/TechDocsIndexPage'; +import { TechDocsPage as TechDocsReaderPage } from './reader/components/TechDocsPage'; import { EntityPageDocs } from './EntityPageDocs'; import { MissingAnnotationEmptyState } from '@backstage/core-components'; @@ -33,8 +33,11 @@ const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref'; export const Router = () => { return ( - } /> - } /> + } /> + } + /> ); }; diff --git a/plugins/techdocs/src/home/components/TechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx similarity index 95% rename from plugins/techdocs/src/home/components/TechDocsHome.test.tsx rename to plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx index 9289884f1c..0b1d6e8db2 100644 --- a/plugins/techdocs/src/home/components/TechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx @@ -18,7 +18,7 @@ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; -import { TechDocsHome } from './TechDocsHome'; +import { DefaultTechDocsHome } from './DefaultTechDocsHome'; import { ApiProvider, @@ -71,7 +71,7 @@ describe('TechDocs Home', () => { it('should render a TechDocs home page', async () => { await renderInTestApp( - + , ); diff --git a/plugins/techdocs/src/home/components/TechDocsHome.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx similarity index 93% rename from plugins/techdocs/src/home/components/TechDocsHome.tsx rename to plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx index 0aa1b87282..ff4bf63724 100644 --- a/plugins/techdocs/src/home/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx @@ -35,11 +35,11 @@ import { UserListPicker, } from '@backstage/plugin-catalog-react'; import { EntityListDocsTable } from './EntityListDocsTable'; -import { TechDocsHomeLayout } from './TechDocsHomeLayout'; +import { TechDocsPageWrapper } from './TechDocsPageWrapper'; import { TechDocsPicker } from './TechDocsPicker'; import { DocsTableRow } from './types'; -export const TechDocsHome = ({ +export const DefaultTechDocsHome = ({ initialFilter = 'all', columns, actions, @@ -49,7 +49,7 @@ export const TechDocsHome = ({ actions?: TableProps['actions']; }) => { return ( - + @@ -70,6 +70,6 @@ export const TechDocsHome = ({ - + ); }; diff --git a/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx new file mode 100644 index 0000000000..c9a6871906 --- /dev/null +++ b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { LegacyTechDocsHome } from './LegacyTechDocsHome'; + +import { + ApiProvider, + ApiRegistry, + ConfigReader, +} from '@backstage/core-app-api'; +import { ConfigApi, configApiRef } from '@backstage/core-plugin-api'; + +jest.mock('@backstage/plugin-catalog-react', () => { + const actual = jest.requireActual('@backstage/plugin-catalog-react'); + return { + ...actual, + useOwnUser: () => 'test-user', + }; +}); + +const mockCatalogApi = { + getEntityByName: jest.fn(), + getEntities: async () => ({ + items: [ + { + apiVersion: 'version', + kind: 'User', + metadata: { + name: 'owned', + namespace: 'default', + }, + }, + ], + }), +} as Partial; + +describe('Legacy TechDocs Home', () => { + const configApi: ConfigApi = new ConfigReader({ + organization: { + name: 'My Company', + }, + }); + + const apiRegistry = ApiRegistry.from([ + [catalogApiRef, mockCatalogApi], + [configApiRef, configApi], + ]); + + it('should render a TechDocs home page', async () => { + await renderInTestApp( + + + , + ); + + // Header + expect(await screen.findByText('Documentation')).toBeInTheDocument(); + expect( + await screen.findByText(/Documentation available in My Company/i), + ).toBeInTheDocument(); + + // Explore Content + expect(await screen.findByTestId('docs-explore')).toBeDefined(); + }); +}); diff --git a/plugins/techdocs/src/home/components/LegacyTechDocsHome.tsx b/plugins/techdocs/src/home/components/LegacyTechDocsHome.tsx new file mode 100644 index 0000000000..34ea416026 --- /dev/null +++ b/plugins/techdocs/src/home/components/LegacyTechDocsHome.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { PanelType, TechDocsCustomHome } from './TechDocsCustomHome'; + +export const LegacyTechDocsHome = () => { + const tabsConfig = [ + { + label: 'Overview', + panels: [ + { + title: 'Overview', + description: + 'Explore your internal technical ecosystem through documentation.', + panelType: 'DocsCardGrid' as PanelType, + filterPredicate: () => true, + }, + // uncomment this if you would like to have a secondary panel with owned documents + // { + // title: 'Owned', + // description: 'Explore your owned internal documentation.', + // panelType: 'DocsCardGrid' as PanelType, + // filterPredicate: 'ownedByUser', + // }, + ], + }, + { + label: 'Owned Documents', + panels: [ + { + title: 'Owned documents', + description: 'Access your documentation.', + panelType: 'DocsTable' as PanelType, + // ownedByUser filters out entities owned by signed in user + filterPredicate: 'ownedByUser', + }, + ], + }, + ]; + return ; +}; diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx index 7f93f6a306..9975db6435 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx @@ -28,7 +28,7 @@ import { import { Entity } from '@backstage/catalog-model'; import { DocsTable } from './DocsTable'; import { DocsCardGrid } from './DocsCardGrid'; -import { TechDocsHomeLayout } from './TechDocsHomeLayout'; +import { TechDocsPageWrapper } from './TechDocsPageWrapper'; import { CodeSnippet, @@ -149,17 +149,17 @@ export const TechDocsCustomHome = ({ if (loading) { return ( - + - + ); } if (error) { return ( - + - + ); } return ( - + setSelectedTab(index)} @@ -192,6 +192,6 @@ export const TechDocsCustomHome = ({ /> ))} - + ); }; diff --git a/plugins/techdocs/src/home/components/TechDocsIndexPage.test.tsx b/plugins/techdocs/src/home/components/TechDocsIndexPage.test.tsx new file mode 100644 index 0000000000..fbeca7cfd4 --- /dev/null +++ b/plugins/techdocs/src/home/components/TechDocsIndexPage.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { useOutlet } from 'react-router'; +import { TechDocsIndexPage } from './TechDocsIndexPage'; + +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), + useOutlet: jest.fn().mockReturnValue('Route Children'), +})); + +jest.mock('./LegacyTechDocsHome', () => ({ + LegacyTechDocsHome: jest.fn().mockReturnValue('LegacyTechDocsHomeMock'), +})); + +describe('TechDocsIndexPage', () => { + it('renders provided router element', async () => { + const { getByText } = await renderInTestApp(); + + expect(getByText('Route Children')).toBeInTheDocument(); + }); + + it('renders legacy TechDocs home when no router children are provided', async () => { + (useOutlet as jest.Mock).mockReturnValueOnce(null); + const { getByText } = await renderInTestApp(); + + expect(getByText('LegacyTechDocsHomeMock')).toBeInTheDocument(); + }); +}); diff --git a/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx b/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx new file mode 100644 index 0000000000..ff694d3570 --- /dev/null +++ b/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx @@ -0,0 +1,25 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { useOutlet } from 'react-router'; +import { LegacyTechDocsHome } from './LegacyTechDocsHome'; + +export const TechDocsIndexPage = () => { + const outlet = useOutlet(); + + return outlet || ; +}; diff --git a/plugins/techdocs/src/home/components/TechDocsHomeLayout.tsx b/plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx similarity index 94% rename from plugins/techdocs/src/home/components/TechDocsHomeLayout.tsx rename to plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx index e6717fbecc..453cfc22cc 100644 --- a/plugins/techdocs/src/home/components/TechDocsHomeLayout.tsx +++ b/plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx @@ -23,7 +23,7 @@ type Props = { children?: React.ReactNode; }; -export const TechDocsHomeLayout = ({ children }: Props) => { +export const TechDocsPageWrapper = ({ children }: Props) => { const configApi = useApi(configApiRef); const generatedSubtitle = `Documentation available in ${ configApi.getOptionalString('organization.name') ?? 'Backstage' diff --git a/plugins/techdocs/src/home/components/index.ts b/plugins/techdocs/src/home/components/index.ts index a56b80f9a1..53dd0fd909 100644 --- a/plugins/techdocs/src/home/components/index.ts +++ b/plugins/techdocs/src/home/components/index.ts @@ -15,7 +15,8 @@ */ export { EntityListDocsTable } from './EntityListDocsTable'; -export { TechDocsHomeLayout } from './TechDocsHomeLayout'; +export { DefaultTechDocsHome } from './DefaultTechDocsHome'; +export { TechDocsPageWrapper } from './TechDocsPageWrapper'; export { TechDocsPicker } from './TechDocsPicker'; export type { PanelType } from './TechDocsCustomHome'; export type { DocsTableRow } from './types'; diff --git a/plugins/techdocs/src/index.ts b/plugins/techdocs/src/index.ts index 7c3bf607b4..4ebf29180c 100644 --- a/plugins/techdocs/src/index.ts +++ b/plugins/techdocs/src/index.ts @@ -21,7 +21,8 @@ export { TechDocsClient, TechDocsStorageClient } from './client'; export type { DocsTableRow, PanelType } from './home/components'; export { EntityListDocsTable, - TechDocsHomeLayout, + DefaultTechDocsHome, + TechDocsPageWrapper, TechDocsPicker, } from './home/components'; export * from './components/DocsResultListItem'; @@ -30,7 +31,7 @@ export { DocsTable, EntityTechdocsContent, TechDocsCustomHome, - TechDocsHome, + TechDocsIndexPage, TechdocsPage, techdocsPlugin as plugin, techdocsPlugin, diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index 2e031176e4..18b386f88c 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -113,10 +113,12 @@ export const TechDocsCustomHome = techdocsPlugin.provide( }), ); -export const TechDocsHome = techdocsPlugin.provide( +export const TechDocsIndexPage = techdocsPlugin.provide( createRoutableExtension({ component: () => - import('./home/components/TechDocsHome').then(m => m.TechDocsHome), + import('./home/components/TechDocsIndexPage').then( + m => m.TechDocsIndexPage, + ), mountPoint: rootRouteRef, }), ); From 8f1a12fb7a1da48f34e8deec170e53db6b9ad32e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 10 Aug 2021 16:26:34 +0200 Subject: [PATCH 34/34] cli: fix frontend serving without a public folder Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/bundler/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 060b482479..2f8e3125c1 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -52,7 +52,7 @@ export async function serveBundle(options: ServeOptions) { }, static: { publicPath: config.output?.publicPath as string, - directory: paths.targetPublic, + directory: paths.targetPublic ?? '/', }, historyApiFallback: { // Paths with dots should still use the history fallback.