diff --git a/.changeset/clean-jokes-think.md b/.changeset/clean-jokes-think.md
new file mode 100644
index 0000000000..a4fc65e6fc
--- /dev/null
+++ b/.changeset/clean-jokes-think.md
@@ -0,0 +1,18 @@
+---
+'@backstage/plugin-techdocs': patch
+---
+
+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/green-ducks-tan.md b/.changeset/green-ducks-tan.md
new file mode 100644
index 0000000000..e096bfeb36
--- /dev/null
+++ b/.changeset/green-ducks-tan.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder': patch
+---
+
+Added UI Schema support for array items for example, support EntityPicker within an array field
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/.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/.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/.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/.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/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/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
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
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/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.
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',
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}
- } />
+ }>
+
+
+ }
+ />
} />
} />
{
+ return new ContainerClientIterator(this.containerName);
+ },
+ };
+ }
+
+ deleteBlob() {
+ if (this.containerName === 'delete_stale_files_error') {
+ throw new Error('Message');
+ }
+ }
}
class ContainerClientFailGetProperties extends ContainerClient {
diff --git a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts
index 18fe62af14..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,8 +109,28 @@ class Bucket {
}
file(destinationFilePath: string) {
+ if (this.bucketName === 'delete_stale_files_error') {
+ throw Error('Message');
+ }
return new GCSFile(destinationFilePath);
}
+
+ getFilesStream() {
+ const readable = new Readable();
+ 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');
+ });
+
+ 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..9b3218f6dc 100644
--- a/packages/techdocs-common/__mocks__/aws-sdk.ts
+++ b/packages/techdocs-common/__mocks__/aws-sdk.ts
@@ -104,6 +104,33 @@ export class S3 {
}),
};
}
+
+ listObjectsV2({ Bucket }) {
+ return {
+ promise: () => {
+ 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();
+ },
+ };
+ }
}
export default {
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',
+ );
});
});
diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts
index 7bdc91ffd4..02d4f7226f 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';
@@ -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,84 @@ export class AwsS3Publish implements PublisherBase {
* Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html
*/
async publish({ entity, directory }: PublishRequest): Promise {
+ // First, try to retrieve a list of all individual files currently existing
+ let existingFiles: string[] = [];
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);
+ 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}`,
+ );
+ }
- 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);
+ // 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);
- // 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 +398,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(
diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts
index e55314a71b..2e17b43b62 100644
--- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts
+++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts
@@ -63,12 +63,18 @@ const getEntityRootDir = (entity: Entity) => {
};
const logger = getVoidLogger();
-jest.spyOn(logger, 'info').mockReturnValue(logger);
-jest.spyOn(logger, 'error').mockReturnValue(logger);
+const loggerInfoSpy = jest.spyOn(logger, 'info');
+const loggerErrorSpy = jest.spyOn(logger, 'error');
-let publisher: PublisherBase;
-beforeEach(async () => {
- mockFs.restore();
+const createPublisherMock = ({
+ accountName = 'accountName',
+ containerName = 'containerName',
+}:
+ | {
+ accountName?: string;
+ containerName?: string;
+ }
+ | undefined = {}) => {
const mockConfig = new ConfigReader({
techdocs: {
requestUrl: 'http://localhost:7000',
@@ -76,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', () => {
@@ -97,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.`,
),
@@ -146,6 +143,10 @@ describe('publishing with valid credentials', () => {
});
});
+ afterEach(() => {
+ mockFs.restore();
+ });
+
it('should publish a directory', async () => {
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
@@ -156,7 +157,6 @@ describe('publishing with valid credentials', () => {
directory: entityRootDir,
}),
).toBeUndefined();
- mockFs.restore();
});
it('should fail to publish a directory', async () => {
@@ -175,37 +175,19 @@ 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),
- });
-
- mockFs.restore();
+ 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),
+ );
});
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);
@@ -225,20 +207,38 @@ 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(loggerErrorSpy).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`,
),
);
+ });
- 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 fe8fe9c2ce..f1c62f0819 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';
@@ -26,8 +27,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 +137,101 @@ export class AzureBlobStoragePublish implements PublisherBase {
* Directory structure used in the container is - entityNamespace/entityKind/entityName/index.html
*/
async publish({ entity, directory }: PublishRequest): Promise {
+ // First, try to retrieve a list of all individual files currently existing
+ const remoteFolder = getCloudPathForLocalPath(entity);
+ let existingFiles: string[] = [];
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);
+ existingFiles = await this.getAllBlobsFromContainer({
+ prefix: remoteFolder,
+ maxPageSize: BATCH_CONCURRENCY,
+ });
+ } catch (e) {
+ this.logger.error(
+ `Unable to list files for Entity ${entity.metadata.name}: ${e.message}`,
+ );
+ }
- // 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);
+ // 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);
- 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);
+ container = this.storageClient.getContainerClient(this.containerName);
+ const failedOperations: Error[] = [];
+ await bulkStorageOperation(
+ async absoluteFilePath => {
+ const relativeFilePath = path.normalize(
+ path.relative(directory, absoluteFilePath),
+ );
+ const response = await container
+ .getBlockBlobClient(
+ getCloudPathForLocalPath(entity, relativeFilePath),
+ )
+ .uploadFile(absoluteFilePath);
if (response._response.status >= 400) {
- return {
- ...response,
- error: new Error(
- `Upload failed for ${sourceFilePath} with status code ${response._response.status}`,
+ failedOperations.push(
+ new Error(
+ `Upload failed for ${absoluteFilePath} with status code ${response._response.status}`,
),
- };
+ );
}
- return {
- ...response,
- error: undefined,
- };
- });
- });
- const responses = await Promise.all(promises);
+ return response;
+ },
+ absoluteFilesToUpload,
+ { concurrencyLimit: BATCH_CONCURRENCY },
+ );
- 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 {
+ if (failedOperations.length > 0) {
throw new Error(
- failed
- .map(r => r.error?.message)
+ 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}`,
+ );
} catch (e) {
- const errorMessage = `Unable to upload file(s) to Azure Blob Storage. ${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);
+
+ 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 Azure. ${error}`;
+ this.logger.error(errorMessage);
+ }
}
private download(containerName: string, blobPath: string): Promise {
@@ -350,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;
+ }
}
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',
+ );
});
});
diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts
index cbf76cd3d7..49156e07f2 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 { File, 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,73 @@ 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, try to retrieve a list of all individual files currently existing
+ let existingFiles: string[] = [];
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);
+ 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}`,
+ );
+ }
- 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);
+ // 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);
- // 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 {
@@ -255,4 +280,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);
+ });
+ });
+ }
}
diff --git a/packages/techdocs-common/src/stages/publish/helpers.test.ts b/packages/techdocs-common/src/stages/publish/helpers.test.ts
index 27a56cd06d..b288d221d3 100644
--- a/packages/techdocs-common/src/stages/publish/helpers.test.ts
+++ b/packages/techdocs-common/src/stages/publish/helpers.test.ts
@@ -16,9 +16,13 @@
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,
+ bulkStorageOperation,
lowerCaseEntityTripletInStoragePath,
} from './helpers';
@@ -99,3 +103,118 @@ 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']));
+ });
+});
+
+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();
+ });
+});
+
+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]);
+ });
+});
diff --git a/packages/techdocs-common/src/stages/publish/helpers.ts b/packages/techdocs-common/src/stages/publish/helpers.ts
index ed681eb8ba..fda03e7415 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, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import mime from 'mime-types';
+import path from 'path';
+import createLimiter from 'p-limit';
import recursiveReadDir from 'recursive-readdir';
/**
@@ -111,3 +114,42 @@ export const lowerCaseEntityTripletInStoragePath = (
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_DEFAULT_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)));
+};
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) => {
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/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,
+ });
+ });
});
diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts
index 2dc2ad7069..dd86784275 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)) {
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,
});
}
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) => {
+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,7 +15,65 @@ 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';
+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) "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)
//
@@ -42,16 +101,58 @@ export const DocsResultListItem: ({
export const DocsTable: ({
entities,
title,
+ loading,
+ columns,
+ actions,
}: {
entities: Entity[] | undefined;
title?: string | undefined;
+ loading?: boolean | undefined;
+ columns?: TableColumn[] | undefined;
+ actions?:
+ | (
+ | Action
+ | {
+ action: (rowData: DocsTableRow) => Action;
+ position: string;
+ }
+ | ((rowData: DocsTableRow) => Action)
+ )[]
+ | 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)
//
// @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: {
+ ({
+ 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)
//
@@ -69,7 +170,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 +244,27 @@ export const TechDocsCustomHome: ({
tabsConfig: TabsConfig;
}) => JSX.Element;
+// 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 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)
+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 +372,9 @@ 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/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/package.json b/plugins/techdocs/package.json
index b853ae04f7..bb9f8a7c95 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": "*",
"dompurify": "^2.2.9",
- "eventsource": "^1.1.0",
+ "event-source-polyfill": "^1.0.25",
+ "lodash": "^4.17.21",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-lazylog": "^4.5.2",
@@ -65,10 +68,8 @@
"@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": "*",
"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/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/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx
new file mode 100644
index 0000000000..0b1d6e8db2
--- /dev/null
+++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 { MockStorageApi, renderInTestApp } from '@backstage/test-utils';
+import { screen } from '@testing-library/react';
+import React from 'react';
+import { DefaultTechDocsHome } from './DefaultTechDocsHome';
+
+import {
+ ApiProvider,
+ ApiRegistry,
+ ConfigReader,
+} from '@backstage/core-app-api';
+import {
+ ConfigApi,
+ configApiRef,
+ storageApiRef,
+} 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: () => Promise.resolve(),
+ getEntities: async () => ({
+ items: [
+ {
+ apiVersion: 'version',
+ kind: 'User',
+ metadata: {
+ name: 'owned',
+ namespace: 'default',
+ },
+ },
+ ],
+ }),
+} as Partial;
+
+describe('TechDocs Home', () => {
+ const configApi: ConfigApi = new ConfigReader({
+ organization: {
+ name: 'My Company',
+ },
+ });
+
+ const apiRegistry = ApiRegistry.from([
+ [catalogApiRef, mockCatalogApi],
+ [configApiRef, configApi],
+ [storageApiRef, MockStorageApi.create()],
+ ]);
+
+ 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();
+ });
+});
diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx
new file mode 100644
index 0000000000..ff4bf63724
--- /dev/null
+++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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 {
+ Content,
+ ContentHeader,
+ SupportButton,
+ TableColumn,
+ TableProps,
+} from '@backstage/core-components';
+import {
+ EntityListContainer,
+ FilterContainer,
+ FilteredEntityLayout,
+} from '@backstage/plugin-catalog';
+import {
+ EntityListProvider,
+ EntityOwnerPicker,
+ EntityTagPicker,
+ UserListFilterKind,
+ UserListPicker,
+} from '@backstage/plugin-catalog-react';
+import { EntityListDocsTable } from './EntityListDocsTable';
+import { TechDocsPageWrapper } from './TechDocsPageWrapper';
+import { TechDocsPicker } from './TechDocsPicker';
+import { DocsTableRow } from './types';
+
+export const DefaultTechDocsHome = ({
+ initialFilter = 'all',
+ columns,
+ actions,
+}: {
+ initialFilter?: UserListFilterKind;
+ columns?: TableColumn[];
+ actions?: TableProps['actions'];
+}) => {
+ return (
+
+
+
+
+ Discover documentation in your ecosystem.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/techdocs/src/home/components/DocsTable.tsx b/plugins/techdocs/src/home/components/DocsTable.tsx
index 031f09868f..29b63f5441 100644
--- a/plugins/techdocs/src/home/components/DocsTable.tsx
+++ b/plugins/techdocs/src/home/components/DocsTable.tsx
@@ -18,91 +18,83 @@ 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 { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model';
+import {
+ formatEntityRefTitle,
+ getEntityRelations,
+} from '@backstage/plugin-catalog-react';
import { rootDocsRouteRef } from '../../routes';
import {
- Table,
- EmptyState,
Button,
- SubvalueCell,
- Link,
+ EmptyState,
+ 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();
if (!entities) return null;
const documents = entities.map(entity => {
+ const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
+
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,
+ }),
+ ownedByRelations,
+ ownedByRelationsTitle: ownedByRelations
+ .map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
+ .join(', '),
+ },
};
});
- const columns = [
- {
- title: 'Document',
- field: 'name',
- highlight: true,
- render: (row: any): React.ReactNode => (
- {row.name}}
- subvalue={row.description}
- />
- ),
- },
- {
- title: 'Owner',
- field: 'owner',
- },
- {
- title: 'Type',
- field: 'type',
- },
- {
- title: 'Actions',
- width: '10%',
- render: (row: any) => (
-
-
- copyToClipboard(`${window.location.href}/${row.docsUrl}`)
- }
- >
-
-
-
- ),
- },
+ const defaultColumns: TableColumn[] = [
+ columnFactories.createNameColumn(),
+ columnFactories.createOwnerColumn(),
+ columnFactories.createTypeColumn(),
+ ];
+
+ 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}
+ columns={columns || defaultColumns}
+ 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..b94e7adace
--- /dev/null
+++ b/plugins/techdocs/src/home/components/EntityListDocsTable.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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,
+ TableColumn,
+ TableProps,
+ WarningPanel,
+} from '@backstage/core-components';
+import {
+ useEntityListProvider,
+ useStarredEntities,
+} from '@backstage/plugin-catalog-react';
+import { DocsTable } from './DocsTable';
+import * as actionFactories from './actions';
+import * as columnFactories from './columns';
+import { DocsTableRow } from './types';
+
+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 defaultActions = [
+ actionFactories.createCopyDocsUrlAction(copyToClipboard),
+ actionFactories.createStarEntityAction(
+ isStarredEntity,
+ toggleStarredEntity,
+ ),
+ ];
+
+ if (error) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+EntityListDocsTable.columns = columnFactories;
+EntityListDocsTable.actions = actionFactories;
diff --git a/plugins/techdocs/src/home/components/TechDocsHome.test.tsx b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx
similarity index 94%
rename from plugins/techdocs/src/home/components/TechDocsHome.test.tsx
rename to plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx
index f90b35e181..c9a6871906 100644
--- a/plugins/techdocs/src/home/components/TechDocsHome.test.tsx
+++ b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx
@@ -18,7 +18,7 @@ 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 { TechDocsHome } from './TechDocsHome';
+import { LegacyTechDocsHome } from './LegacyTechDocsHome';
import {
ApiProvider,
@@ -51,7 +51,7 @@ const mockCatalogApi = {
}),
} as Partial;
-describe('TechDocs Home', () => {
+describe('Legacy TechDocs Home', () => {
const configApi: ConfigApi = new ConfigReader({
organization: {
name: 'My Company',
@@ -66,7 +66,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/LegacyTechDocsHome.tsx
similarity index 97%
rename from plugins/techdocs/src/home/components/TechDocsHome.tsx
rename to plugins/techdocs/src/home/components/LegacyTechDocsHome.tsx
index 6445713875..34ea416026 100644
--- a/plugins/techdocs/src/home/components/TechDocsHome.tsx
+++ b/plugins/techdocs/src/home/components/LegacyTechDocsHome.tsx
@@ -17,7 +17,7 @@
import React from 'react';
import { PanelType, TechDocsCustomHome } from './TechDocsCustomHome';
-export const TechDocsHome = () => {
+export const LegacyTechDocsHome = () => {
const tabsConfig = [
{
label: 'Overview',
diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx
index e2623fe81f..9975db6435 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 { TechDocsPageWrapper } from './TechDocsPageWrapper';
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/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/TechDocsPageWrapper.tsx b/plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx
new file mode 100644
index 0000000000..453cfc22cc
--- /dev/null
+++ b/plugins/techdocs/src/home/components/TechDocsPageWrapper.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 TechDocsPageWrapper = ({ 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..b737e99d14
--- /dev/null
+++ b/plugins/techdocs/src/home/components/actions.tsx
@@ -0,0 +1,54 @@
+/*
+ * 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 {
+ favoriteEntityIcon,
+ favoriteEntityTooltip,
+} from '@backstage/plugin-catalog-react';
+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.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
new file mode 100644
index 0000000000..53dd0fd909
--- /dev/null
+++ b/plugins/techdocs/src/home/components/index.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 { 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/home/components/types.ts b/plugins/techdocs/src/home/components/types.ts
new file mode 100644
index 0000000000..6fd29f1716
--- /dev/null
+++ b/plugins/techdocs/src/home/components/types.ts
@@ -0,0 +1,26 @@
+/*
+ * 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, 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 66502a4585..4ebf29180c 100644
--- a/plugins/techdocs/src/index.ts
+++ b/plugins/techdocs/src/index.ts
@@ -18,13 +18,20 @@ 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 { DocsTableRow, PanelType } from './home/components';
+export {
+ EntityListDocsTable,
+ DefaultTechDocsHome,
+ TechDocsPageWrapper,
+ TechDocsPicker,
+} from './home/components';
export * from './components/DocsResultListItem';
export {
DocsCardGrid,
DocsTable,
EntityTechdocsContent,
TechDocsCustomHome,
+ TechDocsIndexPage,
TechdocsPage,
techdocsPlugin as plugin,
techdocsPlugin,
diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts
index 39544500bd..18b386f88c 100644
--- a/plugins/techdocs/src/plugin.ts
+++ b/plugins/techdocs/src/plugin.ts
@@ -113,6 +113,16 @@ export const TechDocsCustomHome = techdocsPlugin.provide(
}),
);
+export const TechDocsIndexPage = techdocsPlugin.provide(
+ createRoutableExtension({
+ component: () =>
+ import('./home/components/TechDocsIndexPage').then(
+ m => m.TechDocsIndexPage,
+ ),
+ mountPoint: rootRouteRef,
+ }),
+);
+
export const TechDocsReaderPage = techdocsPlugin.provide(
createRoutableExtension({
component: () =>
diff --git a/yarn.lock b/yarn.lock
index 49a7a58059..6568aed665 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"
@@ -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,34 +5467,34 @@
react-syntax-highlighter "^13.5.3"
regenerator-runtime "^0.13.7"
-"@storybook/addons@6.3.6", "@storybook/addons@^6.1.11":
- version "6.3.6"
- resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.6.tgz#330fd722bdae8abefeb029583e89e51e62c20b60"
- integrity sha512-tVV0vqaEEN9Md4bgScwfrnZYkN8iKZarpkIOFheLev+PHjSp8lgWMK5SNWDlbBYqfQfzrz9xbs+F07bMjfx9jQ==
+"@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.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"
+ "@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"
- integrity sha512-F5VuR1FrEwD51OO/EDDAZXNfF5XmJedYHJLwwCB4az2ZMrzG45TxGRmiEohrSTO6wAHGkAvjlEoX5jWOCqQ4pw==
+"@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.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"
"@storybook/csf" "0.0.1"
- "@storybook/router" "6.3.6"
+ "@storybook/router" "6.3.7"
"@storybook/semver" "^7.3.2"
- "@storybook/theming" "6.3.6"
+ "@storybook/theming" "6.3.7"
"@types/reach__router" "^1.3.7"
core-js "^3.8.2"
fast-deep-equal "^3.1.3"
@@ -5508,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"
@@ -5534,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"
@@ -5584,38 +5584,38 @@
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==
+"@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"
- 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"
@@ -5630,23 +5630,23 @@
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==
+"@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"
- integrity sha512-aZkmtAY8b+LFXG6dVp6cTS6zGJuxkHRHcesRSWRQPxtgitaz1G58clRHxbKPRokfjPHNgYA3snogyeqxSA7YNQ==
+"@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.6"
+ "@storybook/client-logger" "6.3.7"
"@storybook/csf" "0.0.1"
- "@storybook/theming" "6.3.6"
+ "@storybook/theming" "6.3.7"
"@types/color-convert" "^2.0.0"
"@types/overlayscrollbars" "^1.12.0"
"@types/react-syntax-highlighter" "11.0.5"
@@ -5668,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"
@@ -5691,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"
@@ -5717,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"
@@ -5745,24 +5745,24 @@
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==
+"@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"
- 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"
@@ -5791,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"
@@ -5826,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"
@@ -5869,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"
@@ -5894,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"
@@ -5922,13 +5922,13 @@
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==
+"@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.6"
+ "@storybook/client-logger" "6.3.7"
"@types/reach__router" "^1.3.7"
core-js "^3.8.2"
fast-deep-equal "^3.1.3"
@@ -5946,13 +5946,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"
@@ -5962,15 +5962,15 @@
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==
+"@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.6"
+ "@storybook/client-logger" "6.3.7"
core-js "^3.8.2"
deep-object-diff "^1.1.0"
emotion-theming "^10.0.27"
@@ -5980,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"
@@ -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"
@@ -6831,27 +6826,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"
@@ -13219,6 +13204,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 +13269,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"