Merge branch 'master' into mobile-sidebar
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Fix bug with setting owner in RepoUrlPicker causing validation failure
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Use ellipsis style for overflowed text in sidebar menu
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-api-docs': patch
|
||||
---
|
||||
|
||||
Update AsyncAPI component to 1.0.0-x releases
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/techdocs-common': minor
|
||||
'@backstage/plugin-techdocs-backend': minor
|
||||
---
|
||||
|
||||
Added the ability for the TechDocs Backend to (optionally) leverage a cache
|
||||
store to improve performance when reading files from a cloud storage provider.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
TechDocs Backend may now (optionally) leverage a cache store to improve
|
||||
performance when reading content from a cloud storage provider.
|
||||
|
||||
To apply this change to an existing app, pass the cache manager from the plugin
|
||||
environment to the `createRouter` function in your backend:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/techdocs.ts
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
discovery,
|
||||
reader,
|
||||
+ cache,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
|
||||
// ...
|
||||
|
||||
return await createRouter({
|
||||
preparers,
|
||||
generators,
|
||||
publisher,
|
||||
logger,
|
||||
config,
|
||||
discovery,
|
||||
+ cache,
|
||||
});
|
||||
```
|
||||
|
||||
If your `PluginEnvironment` does not include a cache manager, be sure you've
|
||||
applied [the cache management change][cm-change] to your backend as well.
|
||||
|
||||
[Additional configuration][td-rec-arch] is required if you wish to enable
|
||||
caching in TechDocs.
|
||||
|
||||
[cm-change]: https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#patch-changes-6
|
||||
[td-rec-arch]: https://backstage.io/docs/features/techdocs/architecture#recommended-deployment
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Bump @spotify/prettier-config
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-xcmetrics': patch
|
||||
---
|
||||
|
||||
Handle a case where XCode data from backend (before 0.0.8) could be missing
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Removed the `scaffolder.github.visibility` configuration that is no longer used from the default app template.
|
||||
@@ -73,3 +73,5 @@
|
||||
| [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 |
|
||||
| [LogMeIn](https://www.logmein.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. |
|
||||
| [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates<br/> Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc |
|
||||
| [Mosaico](https://www.mosaico.com.br/) | [Wédney Yuri](https://github.com/wedneyyuri),[@tino.milton](https://github.com/miltonjacomini) | A centralized service catalog of our documentation for our service engineers. |
|
||||
| [Mox Bank](https://www.mox.com/) | [Nick Laqua](https://github.com/nick-laqua-dragon), [Gauthier Roebroeck](https://github.com/gauthier-roebroeck-mox) | "Single pane of glass" developer portal for providing a best-in-class developer experience to our product teams and making Mox the best tech environment in Hongkong 🥰🚀 |
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 64 KiB |
@@ -18,17 +18,31 @@ The next step is to add
|
||||
[add templates](http://backstage.io/docs/features/software-templates/adding-templates)
|
||||
to your Backstage app.
|
||||
|
||||
### GitHub
|
||||
### Publishing defaults
|
||||
|
||||
For GitHub, you can configure who can see the new repositories that are created
|
||||
by specifying `visibility` option. Valid options are `public`, `private` and
|
||||
`internal`. The `internal` option is for GitHub Enterprise clients, which means
|
||||
public within the enterprise.
|
||||
Software templates can define _publish_ actions, such as `publish:github`, to
|
||||
create new repositories or submit pull / merge requests to existing
|
||||
repositories. You can configure the author and commit message through the
|
||||
`scaffolder` configuration in `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
scaffolder:
|
||||
github:
|
||||
visibility: public # or 'internal' or 'private'
|
||||
defaultAuthor:
|
||||
name: M.C. Hammer # Defaults to `Scaffolder`
|
||||
email: hammer@donthurtem.com # Defaults to `scaffolder@backstage.io`
|
||||
defaultCommitMessage: "U can't touch this" # Defaults to 'Initial commit'
|
||||
```
|
||||
|
||||
To configure who can see the new repositories created from software templates,
|
||||
add the `repoVisibility` key within a software template:
|
||||
|
||||
```yaml
|
||||
- id: publish
|
||||
name: Publish
|
||||
action: publish:github
|
||||
input:
|
||||
repoUrl: '{{ parameters.repoUrl }}'
|
||||
repoVisibility: public # or 'internal' or 'private'
|
||||
```
|
||||
|
||||
### Disabling Docker in Docker situation (Optional)
|
||||
|
||||
@@ -40,7 +40,7 @@ storage system (e.g. AWS S3, GCS or Azure Blob Storage). Read more in
|
||||
|
||||
## Recommended deployment
|
||||
|
||||
This is how we recommend deploying TechDocs in production environment.
|
||||
This is how we recommend deploying TechDocs in a production environment.
|
||||
|
||||
<img data-zoomable src="../../assets/techdocs/architecture-recommended.drawio.svg" alt="TechDocs Architecture diagram" />
|
||||
|
||||
@@ -58,12 +58,12 @@ Similar to how it is done in the Basic setup, the TechDocs Reader requests
|
||||
your configured storage solution for the necessary files and returns them to
|
||||
TechDocs Reader.
|
||||
|
||||
Note about caching: We have noticed internally that some storage providers can
|
||||
be quite slow, which is why we are recommending a cache that sits between the
|
||||
TechDocs Reader and the Storage.
|
||||
|
||||
_Feel free to suggest better ideas to us in #docs-like-code channel in Discord
|
||||
or via a GitHub issue._
|
||||
Depending on your chosen cloud storage provider and its real-world proximity to
|
||||
your backend server, there may be a comparably high amount of latency when
|
||||
loading TechDocs sites using this deployment approach. If you encounter this,
|
||||
you can optionally configure the `techdocs-backend` to cache responses in a
|
||||
cache store
|
||||
[supported by Backstage](../../overview/architecture-overview.md#cache).
|
||||
|
||||
### Security consideration
|
||||
|
||||
|
||||
@@ -135,6 +135,22 @@ techdocs:
|
||||
# the old, case-sensitive entity triplet behavior.
|
||||
legacyUseCaseSensitiveTripletPaths: false
|
||||
|
||||
# techdocs.cache is optional, and is only recommended when you've configured
|
||||
# an external techdocs.publisher.type above. Also requires backend.cache to
|
||||
# be configured with a valid cache store.
|
||||
cache:
|
||||
# Represents the number of milliseconds a statically built asset should
|
||||
# stay cached. Cache invalidation is handled automatically by the frontend,
|
||||
# which compares the build times in cached metadata vs. canonical storage,
|
||||
# allowing long TTLs (e.g. 1 month/year)
|
||||
ttl: 3600000
|
||||
|
||||
# (Optional) The time (in milliseconds) that the TechDocs backend will wait
|
||||
# for a cache service to respond before continuing on as though the cached
|
||||
# object was not found (e.g. when the cache sercice is unavailable). The
|
||||
# default value is 1000
|
||||
readTimeout: 500
|
||||
|
||||
# (Optional and Legacy) TechDocs makes API calls to techdocs-backend using this URL. e.g. get docs of an entity, get metadata, etc.
|
||||
# You don't have to specify this anymore.
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@spotify/prettier-config": "^12.0.0",
|
||||
"docusaurus": "^2.0.0-alpha.70",
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "^2.5.0",
|
||||
"prettier": "^2.5.1",
|
||||
"yarn-lock-check": "^1.0.5"
|
||||
},
|
||||
"prettier": "@spotify/prettier-config"
|
||||
|
||||
+4
-4
@@ -5190,10 +5190,10 @@ prepend-http@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
||||
|
||||
prettier@^2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.npmjs.org/prettier/-/prettier-2.5.0.tgz#a6370e2d4594e093270419d9cc47f7670488f893"
|
||||
integrity sha512-FM/zAKgWTxj40rH03VxzIPdXmj39SwSjwG0heUcNFwI+EMZJnY93yAiKXM3dObIKAM5TA88werc8T/EwhB45eg==
|
||||
prettier@^2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
|
||||
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
|
||||
|
||||
prismjs@^1.22.0:
|
||||
version "1.25.0"
|
||||
|
||||
@@ -29,6 +29,7 @@ export default async function createPlugin({
|
||||
config,
|
||||
discovery,
|
||||
reader,
|
||||
cache,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
// Preparers are responsible for fetching source files for documentation.
|
||||
const preparers = await Preparers.fromConfig(config, {
|
||||
@@ -64,5 +65,6 @@ export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
discovery,
|
||||
cache,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ const SidebarItemWithSubmenu = ({
|
||||
{itemIcon}
|
||||
</div>
|
||||
{text && (
|
||||
<Typography variant="subtitle2" className={classes.text}>
|
||||
<Typography variant="subtitle2" className={classes.label}>
|
||||
{text}
|
||||
</Typography>
|
||||
)}
|
||||
@@ -467,7 +467,7 @@ export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
|
||||
{itemIcon}
|
||||
</div>
|
||||
{text && (
|
||||
<Typography variant="subtitle2" className={classes.text}>
|
||||
<Typography variant="subtitle2" className={classes.label}>
|
||||
{text}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -22,4 +22,4 @@ yarn backstage-create-app
|
||||
## Documentation
|
||||
|
||||
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
|
||||
- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md)
|
||||
- [Backstage Documentation](https://backstage.io/docs/)
|
||||
|
||||
@@ -65,7 +65,7 @@ export default async (cmd: Command, version: string): Promise<void> => {
|
||||
const templateDir = paths.resolveOwn('templates/default-app');
|
||||
const tempDir = resolvePath(os.tmpdir(), answers.name);
|
||||
|
||||
// Use `--path` argument as applicaiton directory when specified, otherwise
|
||||
// Use `--path` argument as application directory when specified, otherwise
|
||||
// create a directory using `answers.name`
|
||||
const appDir = cmd.path
|
||||
? resolvePath(paths.targetDir, cmd.path)
|
||||
|
||||
@@ -77,9 +77,7 @@ auth:
|
||||
providers: {}
|
||||
|
||||
scaffolder:
|
||||
github:
|
||||
token: ${GITHUB_TOKEN}
|
||||
visibility: public # or 'internal' or 'private'
|
||||
# see https://backstage.io/docs/features/software-templates/configuration for software template options
|
||||
|
||||
catalog:
|
||||
rules:
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^{{version '@backstage/cli'}}",
|
||||
"@spotify/prettier-config": "^11.0.0",
|
||||
"@spotify/prettier-config": "^12.0.0",
|
||||
"concurrently": "^6.0.0",
|
||||
"lerna": "^4.0.0",
|
||||
"prettier": "^2.3.2"
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"@backstage/plugin-tech-radar": "^{{version '@backstage/plugin-tech-radar'}}",
|
||||
"@backstage/plugin-techdocs": "^{{version '@backstage/plugin-techdocs'}}",
|
||||
"@backstage/plugin-user-settings": "^{{version '@backstage/plugin-user-settings'}}",
|
||||
"@backstage/test-utils": "^{{version '@backstage/test-utils'}}",
|
||||
"@backstage/theme": "^{{version '@backstage/theme'}}",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
@@ -34,6 +33,7 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/test-utils": "^{{version '@backstage/test-utils'}}",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
@@ -48,11 +48,11 @@
|
||||
"scripts": {
|
||||
"start": "backstage-cli app:serve",
|
||||
"build": "backstage-cli app:build",
|
||||
"test": "backstage-cli test",
|
||||
"lint": "backstage-cli lint",
|
||||
"clean": "backstage-cli clean",
|
||||
"test": "backstage-cli test",
|
||||
"test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev",
|
||||
"test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run",
|
||||
"lint": "backstage-cli lint",
|
||||
"cy:dev": "cypress open",
|
||||
"cy:run": "cypress run"
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ export default async function createPlugin({
|
||||
config,
|
||||
discovery,
|
||||
reader,
|
||||
cache,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
// Preparers are responsible for fetching source files for documentation.
|
||||
const preparers = await Preparers.fromConfig(config, {
|
||||
@@ -49,5 +50,6 @@ export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
discovery,
|
||||
cache,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -287,6 +287,8 @@ export type TechDocsMetadata = {
|
||||
site_name: string;
|
||||
site_description: string;
|
||||
etag: string;
|
||||
build_timestamp: number;
|
||||
files?: string[];
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "transformDirLocation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
||||
@@ -23,7 +23,7 @@ import os from 'os';
|
||||
import path, { resolve as resolvePath } from 'path';
|
||||
import { ParsedLocationAnnotation } from '../../helpers';
|
||||
import {
|
||||
addBuildTimestampMetadata,
|
||||
createOrUpdateMetadata,
|
||||
getGeneratorKey,
|
||||
getMkdocsYml,
|
||||
getRepoUrlFromLocationAnnotation,
|
||||
@@ -369,13 +369,15 @@ describe('helpers', () => {
|
||||
});
|
||||
|
||||
describe('addBuildTimestampMetadata', () => {
|
||||
const mockFiles = {
|
||||
'invalid_techdocs_metadata.json': 'dsds',
|
||||
'techdocs_metadata.json': '{"site_name": "Tech Docs"}',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs.restore();
|
||||
mockFs({
|
||||
[rootDir]: {
|
||||
'invalid_techdocs_metadata.json': 'dsds',
|
||||
'techdocs_metadata.json': '{"site_name": "Tech Docs"}',
|
||||
},
|
||||
[rootDir]: mockFiles,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -385,7 +387,7 @@ describe('helpers', () => {
|
||||
|
||||
it('should create the file if it does not exist', async () => {
|
||||
const filePath = path.join(rootDir, 'wrong_techdocs_metadata.json');
|
||||
await addBuildTimestampMetadata(filePath, mockLogger);
|
||||
await createOrUpdateMetadata(filePath, mockLogger);
|
||||
|
||||
// Check if the file exists
|
||||
await expect(
|
||||
@@ -397,18 +399,28 @@ describe('helpers', () => {
|
||||
const filePath = path.join(rootDir, 'invalid_techdocs_metadata.json');
|
||||
|
||||
await expect(
|
||||
addBuildTimestampMetadata(filePath, mockLogger),
|
||||
createOrUpdateMetadata(filePath, mockLogger),
|
||||
).rejects.toThrowError('Unexpected token d in JSON at position 0');
|
||||
});
|
||||
|
||||
it('should add build timestamp to the metadata json', async () => {
|
||||
const filePath = path.join(rootDir, 'techdocs_metadata.json');
|
||||
|
||||
await addBuildTimestampMetadata(filePath, mockLogger);
|
||||
await createOrUpdateMetadata(filePath, mockLogger);
|
||||
|
||||
const json = await fs.readJson(filePath);
|
||||
expect(json.build_timestamp).toBeLessThanOrEqual(Date.now());
|
||||
});
|
||||
|
||||
it('should add list of files to the metadata json', async () => {
|
||||
const filePath = path.join(rootDir, 'techdocs_metadata.json');
|
||||
|
||||
await createOrUpdateMetadata(filePath, mockLogger);
|
||||
|
||||
const json = await fs.readJson(filePath);
|
||||
expect(json.files[0]).toEqual(Object.keys(mockFiles)[0]);
|
||||
expect(json.files[1]).toEqual(Object.keys(mockFiles)[1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('storeEtagMetadata', () => {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { PassThrough, Writable } from 'stream';
|
||||
import { Logger } from 'winston';
|
||||
import { ParsedLocationAnnotation } from '../../helpers';
|
||||
import { SupportedGeneratorKey } from './types';
|
||||
import { getFileTreeRecursively } from '../publish/helpers';
|
||||
|
||||
// TODO: Implement proper support for more generators.
|
||||
export function getGeneratorKey(entity: Entity): SupportedGeneratorKey {
|
||||
@@ -345,14 +346,20 @@ export const patchIndexPreBuild = async ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the techdocs_metadata.json to add a new build timestamp metadata. Create the .json file if it doesn't exist.
|
||||
* Create or update the techdocs_metadata.json. Values initialized/updated are:
|
||||
* - The build_timestamp (now)
|
||||
* - The list of files generated
|
||||
*
|
||||
* @param {string} techdocsMetadataPath File path to techdocs_metadata.json
|
||||
*/
|
||||
export const addBuildTimestampMetadata = async (
|
||||
export const createOrUpdateMetadata = async (
|
||||
techdocsMetadataPath: string,
|
||||
logger: Logger,
|
||||
): Promise<void> => {
|
||||
const techdocsMetadataDir = techdocsMetadataPath
|
||||
.split(path.sep)
|
||||
.slice(0, -1)
|
||||
.join(path.sep);
|
||||
// check if file exists, create if it does not.
|
||||
try {
|
||||
await fs.access(techdocsMetadataPath, fs.constants.F_OK);
|
||||
@@ -372,6 +379,19 @@ export const addBuildTimestampMetadata = async (
|
||||
}
|
||||
|
||||
json.build_timestamp = Date.now();
|
||||
|
||||
// Get and write generated files to the metadata JSON. Each file string is in
|
||||
// a form appropriate for invalidating the associated object from cache.
|
||||
try {
|
||||
json.files = (await getFileTreeRecursively(techdocsMetadataDir)).map(file =>
|
||||
file.replace(`${techdocsMetadataDir}${path.sep}`, ''),
|
||||
);
|
||||
} catch (err) {
|
||||
assertError(err);
|
||||
json.files = [];
|
||||
logger.warn(`Unable to add files list to metadata: ${err.message}`);
|
||||
}
|
||||
|
||||
await fs.writeJson(techdocsMetadataPath, json);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import {
|
||||
addBuildTimestampMetadata,
|
||||
createOrUpdateMetadata,
|
||||
getMkdocsYml,
|
||||
patchIndexPreBuild,
|
||||
patchMkdocsYmlPreBuild,
|
||||
@@ -164,9 +164,9 @@ export class TechdocsGenerator implements GeneratorBase {
|
||||
* Post Generate steps
|
||||
*/
|
||||
|
||||
// Add build timestamp to techdocs_metadata.json
|
||||
// Add build timestamp and files to techdocs_metadata.json
|
||||
// Creates techdocs_metadata.json if file does not exist.
|
||||
await addBuildTimestampMetadata(
|
||||
await createOrUpdateMetadata(
|
||||
path.join(outputDir, 'techdocs_metadata.json'),
|
||||
childLogger,
|
||||
);
|
||||
|
||||
@@ -95,6 +95,7 @@ describe('AwsS3Publish', () => {
|
||||
site_name: 'backstage',
|
||||
site_description: 'site_content',
|
||||
etag: 'etag',
|
||||
build_timestamp: 612741599,
|
||||
};
|
||||
|
||||
const directory = getEntityRootDir(entity);
|
||||
@@ -149,21 +150,39 @@ describe('AwsS3Publish', () => {
|
||||
describe('publish', () => {
|
||||
it('should publish a directory', async () => {
|
||||
const publisher = createPublisherFromConfig();
|
||||
expect(await publisher.publish({ entity, directory })).toBeUndefined();
|
||||
expect(await publisher.publish({ entity, directory })).toMatchObject({
|
||||
objects: expect.arrayContaining([
|
||||
'default/component/backstage/404.html',
|
||||
`default/component/backstage/index.html`,
|
||||
`default/component/backstage/assets/main.css`,
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should publish a directory as well when legacy casing is used', async () => {
|
||||
const publisher = createPublisherFromConfig({
|
||||
legacyUseCaseSensitiveTripletPaths: true,
|
||||
});
|
||||
expect(await publisher.publish({ entity, directory })).toBeUndefined();
|
||||
expect(await publisher.publish({ entity, directory })).toMatchObject({
|
||||
objects: expect.arrayContaining([
|
||||
'default/Component/backstage/404.html',
|
||||
`default/Component/backstage/index.html`,
|
||||
`default/Component/backstage/assets/main.css`,
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should publish a directory when root path is specified', async () => {
|
||||
const publisher = createPublisherFromConfig({
|
||||
bucketRootPath: 'backstage-data/techdocs',
|
||||
});
|
||||
expect(await publisher.publish({ entity, directory })).toBeUndefined();
|
||||
expect(await publisher.publish({ entity, directory })).toMatchObject({
|
||||
objects: expect.arrayContaining([
|
||||
'backstage-data/techdocs/default/component/backstage/404.html',
|
||||
`backstage-data/techdocs/default/component/backstage/index.html`,
|
||||
`backstage-data/techdocs/default/component/backstage/assets/main.css`,
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should publish a directory when root path is specified and legacy casing is used', async () => {
|
||||
@@ -171,14 +190,26 @@ describe('AwsS3Publish', () => {
|
||||
bucketRootPath: 'backstage-data/techdocs',
|
||||
legacyUseCaseSensitiveTripletPaths: true,
|
||||
});
|
||||
expect(await publisher.publish({ entity, directory })).toBeUndefined();
|
||||
expect(await publisher.publish({ entity, directory })).toMatchObject({
|
||||
objects: expect.arrayContaining([
|
||||
'backstage-data/techdocs/default/Component/backstage/404.html',
|
||||
`backstage-data/techdocs/default/Component/backstage/index.html`,
|
||||
`backstage-data/techdocs/default/Component/backstage/assets/main.css`,
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should publish a directory when sse is specified', async () => {
|
||||
const publisher = createPublisherFromConfig({
|
||||
sse: 'aws:kms',
|
||||
});
|
||||
expect(await publisher.publish({ entity, directory })).toBeUndefined();
|
||||
expect(await publisher.publish({ entity, directory })).toMatchObject({
|
||||
objects: expect.arrayContaining([
|
||||
'default/component/backstage/404.html',
|
||||
'default/component/backstage/index.html',
|
||||
'default/component/backstage/assets/main.css',
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to publish a directory', async () => {
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
import {
|
||||
PublisherBase,
|
||||
PublishRequest,
|
||||
PublishResponse,
|
||||
ReadinessResponse,
|
||||
TechDocsMetadata,
|
||||
} from './types';
|
||||
@@ -214,7 +215,11 @@ export class AwsS3Publish implements PublisherBase {
|
||||
* Upload all the files from the generated `directory` to the S3 bucket.
|
||||
* Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html
|
||||
*/
|
||||
async publish({ entity, directory }: PublishRequest): Promise<void> {
|
||||
async publish({
|
||||
entity,
|
||||
directory,
|
||||
}: PublishRequest): Promise<PublishResponse> {
|
||||
const objects: string[] = [];
|
||||
const useLegacyPathCasing = this.legacyPathCasing;
|
||||
const bucketRootPath = this.bucketRootPath;
|
||||
const sse = this.sse;
|
||||
@@ -263,6 +268,7 @@ export class AwsS3Publish implements PublisherBase {
|
||||
...(sse && { ServerSideEncryption: sse }),
|
||||
} as aws.S3.PutObjectRequest;
|
||||
|
||||
objects.push(params.Key);
|
||||
return this.storageClient.upload(params).promise();
|
||||
},
|
||||
absoluteFilesToUpload,
|
||||
@@ -311,6 +317,7 @@ export class AwsS3Publish implements PublisherBase {
|
||||
const errorMessage = `Unable to delete file(s) from AWS S3. ${error}`;
|
||||
this.logger.error(errorMessage);
|
||||
}
|
||||
return { objects };
|
||||
}
|
||||
|
||||
async fetchTechDocsMetadata(
|
||||
|
||||
@@ -89,6 +89,7 @@ describe('AzureBlobStoragePublish', () => {
|
||||
site_name: 'backstage',
|
||||
site_description: 'site_content',
|
||||
etag: 'etag',
|
||||
build_timestamp: 612741599,
|
||||
};
|
||||
|
||||
const directory = getEntityRootDir(entity);
|
||||
@@ -154,14 +155,26 @@ describe('AzureBlobStoragePublish', () => {
|
||||
describe('publish', () => {
|
||||
it('should publish a directory', async () => {
|
||||
const publisher = createPublisherFromConfig();
|
||||
expect(await publisher.publish({ entity, directory })).toBeUndefined();
|
||||
expect(await publisher.publish({ entity, directory })).toMatchObject({
|
||||
objects: expect.arrayContaining([
|
||||
'default/component/backstage/404.html',
|
||||
`default/component/backstage/index.html`,
|
||||
`default/component/backstage/assets/main.css`,
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should publish a directory as well when legacy casing is used', async () => {
|
||||
const publisher = createPublisherFromConfig({
|
||||
legacyUseCaseSensitiveTripletPaths: true,
|
||||
});
|
||||
expect(await publisher.publish({ entity, directory })).toBeUndefined();
|
||||
expect(await publisher.publish({ entity, directory })).toMatchObject({
|
||||
objects: expect.arrayContaining([
|
||||
'default/Component/backstage/404.html',
|
||||
`default/Component/backstage/index.html`,
|
||||
`default/Component/backstage/assets/main.css`,
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to publish a directory', async () => {
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
import {
|
||||
PublisherBase,
|
||||
PublishRequest,
|
||||
PublishResponse,
|
||||
ReadinessResponse,
|
||||
TechDocsMetadata,
|
||||
} from './types';
|
||||
@@ -156,7 +157,11 @@ export class AzureBlobStoragePublish implements PublisherBase {
|
||||
* Upload all the files from the generated `directory` to the Azure Blob Storage container.
|
||||
* Directory structure used in the container is - entityNamespace/entityKind/entityName/index.html
|
||||
*/
|
||||
async publish({ entity, directory }: PublishRequest): Promise<void> {
|
||||
async publish({
|
||||
entity,
|
||||
directory,
|
||||
}: PublishRequest): Promise<PublishResponse> {
|
||||
const objects: string[] = [];
|
||||
const useLegacyPathCasing = this.legacyPathCasing;
|
||||
|
||||
// First, try to retrieve a list of all individual files currently existing
|
||||
@@ -194,14 +199,14 @@ export class AzureBlobStoragePublish implements PublisherBase {
|
||||
const relativeFilePath = path.normalize(
|
||||
path.relative(directory, absoluteFilePath),
|
||||
);
|
||||
const remotePath = getCloudPathForLocalPath(
|
||||
entity,
|
||||
relativeFilePath,
|
||||
useLegacyPathCasing,
|
||||
);
|
||||
objects.push(remotePath);
|
||||
const response = await container
|
||||
.getBlockBlobClient(
|
||||
getCloudPathForLocalPath(
|
||||
entity,
|
||||
relativeFilePath,
|
||||
useLegacyPathCasing,
|
||||
),
|
||||
)
|
||||
.getBlockBlobClient(remotePath)
|
||||
.uploadFile(absoluteFilePath);
|
||||
|
||||
if (response._response.status >= 400) {
|
||||
@@ -264,6 +269,8 @@ export class AzureBlobStoragePublish implements PublisherBase {
|
||||
const errorMessage = `Unable to delete file(s) from Azure. ${error}`;
|
||||
this.logger.error(errorMessage);
|
||||
}
|
||||
|
||||
return { objects };
|
||||
}
|
||||
|
||||
private download(containerName: string, blobPath: string): Promise<Buffer> {
|
||||
|
||||
@@ -88,6 +88,7 @@ describe('GoogleGCSPublish', () => {
|
||||
site_name: 'backstage',
|
||||
site_description: 'site_content',
|
||||
etag: 'etag',
|
||||
build_timestamp: 612741599,
|
||||
};
|
||||
|
||||
const directory = getEntityRootDir(entity);
|
||||
@@ -142,21 +143,39 @@ describe('GoogleGCSPublish', () => {
|
||||
describe('publish', () => {
|
||||
it('should publish a directory', async () => {
|
||||
const publisher = createPublisherFromConfig();
|
||||
expect(await publisher.publish({ entity, directory })).toBeUndefined();
|
||||
expect(await publisher.publish({ entity, directory })).toMatchObject({
|
||||
objects: expect.arrayContaining([
|
||||
'default/component/backstage/404.html',
|
||||
`default/component/backstage/index.html`,
|
||||
`default/component/backstage/assets/main.css`,
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should publish a directory as well when legacy casing is used', async () => {
|
||||
const publisher = createPublisherFromConfig({
|
||||
legacyUseCaseSensitiveTripletPaths: true,
|
||||
});
|
||||
expect(await publisher.publish({ entity, directory })).toBeUndefined();
|
||||
expect(await publisher.publish({ entity, directory })).toMatchObject({
|
||||
objects: expect.arrayContaining([
|
||||
'default/Component/backstage/404.html',
|
||||
`default/Component/backstage/index.html`,
|
||||
`default/Component/backstage/assets/main.css`,
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should publish a directory when root path is specified', async () => {
|
||||
const publisher = createPublisherFromConfig({
|
||||
bucketRootPath: 'backstage-data/techdocs',
|
||||
});
|
||||
expect(await publisher.publish({ entity, directory })).toBeUndefined();
|
||||
expect(await publisher.publish({ entity, directory })).toMatchObject({
|
||||
objects: expect.arrayContaining([
|
||||
'backstage-data/techdocs/default/component/backstage/404.html',
|
||||
`backstage-data/techdocs/default/component/backstage/index.html`,
|
||||
`backstage-data/techdocs/default/component/backstage/assets/main.css`,
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should publish a directory when root path is specified and legacy casing is used', async () => {
|
||||
@@ -164,7 +183,13 @@ describe('GoogleGCSPublish', () => {
|
||||
bucketRootPath: 'backstage-data/techdocs',
|
||||
legacyUseCaseSensitiveTripletPaths: true,
|
||||
});
|
||||
expect(await publisher.publish({ entity, directory })).toBeUndefined();
|
||||
expect(await publisher.publish({ entity, directory })).toMatchObject({
|
||||
objects: expect.arrayContaining([
|
||||
'backstage-data/techdocs/default/Component/backstage/404.html',
|
||||
`backstage-data/techdocs/default/Component/backstage/index.html`,
|
||||
`backstage-data/techdocs/default/Component/backstage/assets/main.css`,
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to publish a directory', async () => {
|
||||
|
||||
@@ -36,6 +36,7 @@ import { MigrateWriteStream } from './migrations';
|
||||
import {
|
||||
PublisherBase,
|
||||
PublishRequest,
|
||||
PublishResponse,
|
||||
ReadinessResponse,
|
||||
TechDocsMetadata,
|
||||
} from './types';
|
||||
@@ -145,7 +146,11 @@ export class GoogleGCSPublish implements PublisherBase {
|
||||
* Upload all the files from the generated `directory` to the GCS bucket.
|
||||
* Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html
|
||||
*/
|
||||
async publish({ entity, directory }: PublishRequest): Promise<void> {
|
||||
async publish({
|
||||
entity,
|
||||
directory,
|
||||
}: PublishRequest): Promise<PublishResponse> {
|
||||
const objects: string[] = [];
|
||||
const useLegacyPathCasing = this.legacyPathCasing;
|
||||
const bucket = this.storageClient.bucket(this.bucketName);
|
||||
const bucketRootPath = this.bucketRootPath;
|
||||
@@ -178,14 +183,14 @@ export class GoogleGCSPublish implements PublisherBase {
|
||||
await bulkStorageOperation(
|
||||
async absoluteFilePath => {
|
||||
const relativeFilePath = path.relative(directory, absoluteFilePath);
|
||||
return await bucket.upload(absoluteFilePath, {
|
||||
destination: getCloudPathForLocalPath(
|
||||
entity,
|
||||
relativeFilePath,
|
||||
useLegacyPathCasing,
|
||||
bucketRootPath,
|
||||
),
|
||||
});
|
||||
const destination = getCloudPathForLocalPath(
|
||||
entity,
|
||||
relativeFilePath,
|
||||
useLegacyPathCasing,
|
||||
bucketRootPath,
|
||||
);
|
||||
objects.push(destination);
|
||||
return await bucket.upload(absoluteFilePath, { destination });
|
||||
},
|
||||
absoluteFilesToUpload,
|
||||
{ concurrencyLimit: 10 },
|
||||
@@ -228,6 +233,8 @@ export class GoogleGCSPublish implements PublisherBase {
|
||||
const errorMessage = `Unable to delete file(s) from Google Cloud Storage. ${error}`;
|
||||
this.logger.error(errorMessage);
|
||||
}
|
||||
|
||||
return { objects };
|
||||
}
|
||||
|
||||
fetchTechDocsMetadata(entityName: EntityName): Promise<TechDocsMetadata> {
|
||||
|
||||
@@ -98,7 +98,10 @@ export class LocalPublish implements PublisherBase {
|
||||
};
|
||||
}
|
||||
|
||||
publish({ entity, directory }: PublishRequest): Promise<PublishResponse> {
|
||||
async publish({
|
||||
entity,
|
||||
directory,
|
||||
}: PublishRequest): Promise<PublishResponse> {
|
||||
const entityNamespace = entity.metadata.namespace ?? 'default';
|
||||
|
||||
const publishDir = this.staticEntityPathJoin(
|
||||
@@ -112,27 +115,30 @@ export class LocalPublish implements PublisherBase {
|
||||
fs.mkdirSync(publishDir, { recursive: true });
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.copy(directory, publishDir, err => {
|
||||
if (err) {
|
||||
this.logger.debug(
|
||||
`Failed to copy docs from ${directory} to ${publishDir}`,
|
||||
);
|
||||
reject(err);
|
||||
}
|
||||
this.logger.info(`Published site stored at ${publishDir}`);
|
||||
this.discovery
|
||||
.getBaseUrl('techdocs')
|
||||
.then(techdocsApiUrl => {
|
||||
resolve({
|
||||
remoteUrl: `${techdocsApiUrl}/static/docs/${entity.metadata.name}`,
|
||||
});
|
||||
})
|
||||
.catch(reason => {
|
||||
reject(reason);
|
||||
});
|
||||
});
|
||||
});
|
||||
try {
|
||||
await fs.copy(directory, publishDir);
|
||||
this.logger.info(`Published site stored at ${publishDir}`);
|
||||
} catch (error) {
|
||||
this.logger.debug(
|
||||
`Failed to copy docs from ${directory} to ${publishDir}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Generate publish response.
|
||||
const techdocsApiUrl = await this.discovery.getBaseUrl('techdocs');
|
||||
const publishedFilePaths = (await getFileTreeRecursively(publishDir)).map(
|
||||
abs => {
|
||||
return abs.split(`${staticDocsDir}/`)[1];
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
remoteUrl: `${techdocsApiUrl}/static/docs/${encodeURIComponent(
|
||||
entity.metadata.name,
|
||||
)}`,
|
||||
objects: publishedFilePaths,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTechDocsMetadata(
|
||||
|
||||
@@ -170,7 +170,13 @@ describe('OpenStackSwiftPublish', () => {
|
||||
entity,
|
||||
directory: entityRootDir,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
).toMatchObject({
|
||||
objects: expect.arrayContaining([
|
||||
'test-namespace/TestKind/test-component-name/404.html',
|
||||
`test-namespace/TestKind/test-component-name/index.html`,
|
||||
`test-namespace/TestKind/test-component-name/assets/main.css`,
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to publish a directory', async () => {
|
||||
@@ -241,7 +247,7 @@ describe('OpenStackSwiftPublish', () => {
|
||||
mockFs({
|
||||
[entityRootDir]: {
|
||||
'techdocs_metadata.json':
|
||||
'{"site_name": "backstage", "site_description": "site_content", "etag": "etag"}',
|
||||
'{"site_name": "backstage", "site_description": "site_content", "etag": "etag", "build_timestamp": 612741599}',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -249,6 +255,7 @@ describe('OpenStackSwiftPublish', () => {
|
||||
site_name: 'backstage',
|
||||
site_description: 'site_content',
|
||||
etag: 'etag',
|
||||
build_timestamp: 612741599,
|
||||
};
|
||||
expect(
|
||||
await publisher.fetchTechDocsMetadata(entityNameMock),
|
||||
@@ -263,7 +270,7 @@ describe('OpenStackSwiftPublish', () => {
|
||||
|
||||
mockFs({
|
||||
[entityRootDir]: {
|
||||
'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content', 'etag': 'etag'}`,
|
||||
'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content', 'etag': 'etag', 'build_timestamp': 612741599}`,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -271,6 +278,7 @@ describe('OpenStackSwiftPublish', () => {
|
||||
site_name: 'backstage',
|
||||
site_description: 'site_content',
|
||||
etag: 'etag',
|
||||
build_timestamp: 612741599,
|
||||
};
|
||||
expect(
|
||||
await publisher.fetchTechDocsMetadata(entityNameMock),
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import {
|
||||
PublisherBase,
|
||||
PublishRequest,
|
||||
PublishResponse,
|
||||
ReadinessResponse,
|
||||
TechDocsMetadata,
|
||||
} from './types';
|
||||
@@ -139,8 +140,13 @@ export class OpenStackSwiftPublish implements PublisherBase {
|
||||
* Upload all the files from the generated `directory` to the OpenStack Swift container.
|
||||
* Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html
|
||||
*/
|
||||
async publish({ entity, directory }: PublishRequest): Promise<void> {
|
||||
async publish({
|
||||
entity,
|
||||
directory,
|
||||
}: PublishRequest): Promise<PublishResponse> {
|
||||
try {
|
||||
const objects: string[] = [];
|
||||
|
||||
// Note: OpenStack Swift 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);
|
||||
@@ -161,6 +167,7 @@ export class OpenStackSwiftPublish implements PublisherBase {
|
||||
// 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}`; // Swift container file relative path
|
||||
objects.push(destination);
|
||||
|
||||
// Rate limit the concurrent execution of file uploads to batches of 10 (per publish)
|
||||
const uploadFile = limiter(async () => {
|
||||
@@ -178,7 +185,7 @@ export class OpenStackSwiftPublish implements PublisherBase {
|
||||
this.logger.info(
|
||||
`Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${allFilesToUpload.length}`,
|
||||
);
|
||||
return;
|
||||
return { objects };
|
||||
} catch (e) {
|
||||
const errorMessage = `Unable to upload file(s) to OpenStack Swift. ${e}`;
|
||||
this.logger.error(errorMessage);
|
||||
|
||||
@@ -32,9 +32,21 @@ export type PublishRequest = {
|
||||
directory: string;
|
||||
};
|
||||
|
||||
/* `remoteUrl` is the URL which serves files from the local publisher's static directory. */
|
||||
/**
|
||||
* Response containing metadata about where files were published and what may
|
||||
* have been published or updated.
|
||||
*/
|
||||
export type PublishResponse = {
|
||||
/**
|
||||
* The URL which serves files from the local publisher's static directory.
|
||||
*/
|
||||
remoteUrl?: string;
|
||||
/**
|
||||
* The list of objects (specifically their paths) that were published.
|
||||
* Objects do not have a preceding slash, and match how one would load the
|
||||
* object over the `/static/docs/*` TechDocs Backend Plugin endpoint.
|
||||
*/
|
||||
objects?: string[];
|
||||
} | void;
|
||||
|
||||
/**
|
||||
@@ -53,6 +65,8 @@ export type TechDocsMetadata = {
|
||||
site_name: string;
|
||||
site_description: string;
|
||||
etag: string;
|
||||
build_timestamp: number;
|
||||
files?: string[];
|
||||
};
|
||||
|
||||
export type MigrateRequest = {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@asyncapi/react-component": "^0.23.0",
|
||||
"@asyncapi/react-component": "^1.0.0-next.25",
|
||||
"@backstage/catalog-model": "^0.9.7",
|
||||
"@backstage/core-components": "^0.7.6",
|
||||
"@backstage/core-plugin-api": "^0.2.2",
|
||||
|
||||
+11
-10
@@ -18,6 +18,15 @@ import { renderInTestApp } from '@backstage/test-utils';
|
||||
import React from 'react';
|
||||
import { AsyncApiDefinition } from './AsyncApiDefinition';
|
||||
|
||||
jest.mock('use-resize-observer', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('<AsyncApiDefinition />', () => {
|
||||
it('renders asyncapi spec', async () => {
|
||||
const definition = `
|
||||
@@ -45,15 +54,7 @@ components:
|
||||
|
||||
expect(getByText(/Account Service/i)).toBeInTheDocument();
|
||||
expect(getByText(/user\/signedup/i)).toBeInTheDocument();
|
||||
expect(getByText(/UserSignedUp/i)).toBeInTheDocument();
|
||||
expect(getAllByText(/displayName/i)).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('renders error if definition is missing', async () => {
|
||||
const { getByText } = await renderInTestApp(
|
||||
<AsyncApiDefinition definition="" />,
|
||||
);
|
||||
expect(getByText(/Error/i)).toBeInTheDocument();
|
||||
expect(getByText(/Document can't be null or falsey/i)).toBeInTheDocument();
|
||||
expect(getAllByText(/UserSignedUp/i)).toHaveLength(2);
|
||||
expect(getAllByText(/displayName/i)).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,23 +15,72 @@
|
||||
*/
|
||||
|
||||
import AsyncApi from '@asyncapi/react-component';
|
||||
import '@asyncapi/react-component/lib/styles/fiori.css';
|
||||
import { alpha, makeStyles } from '@material-ui/core/styles';
|
||||
import '@asyncapi/react-component/styles/default.css';
|
||||
import { makeStyles, alpha, darken } from '@material-ui/core/styles';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import React from 'react';
|
||||
import { useTheme } from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
const useStyles = makeStyles((theme: BackstageTheme) => ({
|
||||
root: {
|
||||
'& .asyncapi': {
|
||||
'font-family': 'inherit',
|
||||
fontFamily: 'inherit',
|
||||
'& .bg-white': {
|
||||
background: 'none',
|
||||
},
|
||||
'& h2': {
|
||||
...theme.typography.h6,
|
||||
'& .text-4xl': {
|
||||
...theme.typography.h3,
|
||||
},
|
||||
'& .text-teal': {
|
||||
color: theme.palette.primary.main,
|
||||
' & h2': {
|
||||
...theme.typography.h4,
|
||||
},
|
||||
'& .border': {
|
||||
borderColor: alpha(theme.palette.border, 0.1),
|
||||
},
|
||||
'& .min-w-min': {
|
||||
minWidth: 'fit-content',
|
||||
},
|
||||
'& .examples': {
|
||||
padding: '1rem',
|
||||
},
|
||||
'& .bg-teal-500': {
|
||||
backgroundColor: theme.palette.status.ok,
|
||||
},
|
||||
'& .bg-blue-500': {
|
||||
backgroundColor: theme.palette.info.main,
|
||||
},
|
||||
'& .bg-blue-400': {
|
||||
backgroundColor: theme.palette.info.light,
|
||||
},
|
||||
'& .bg-indigo-400': {
|
||||
backgroundColor: theme.palette.warning.main,
|
||||
},
|
||||
'& .text-teal-50': {
|
||||
color: theme.palette.status.ok,
|
||||
},
|
||||
'& .text-red-600': {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
'& .text-orange-600': {
|
||||
color: theme.palette.warning.main,
|
||||
},
|
||||
'& .text-teal-500': {
|
||||
color: theme.palette.status.ok,
|
||||
},
|
||||
'& .text-blue-500': {
|
||||
color: theme.palette.info.main,
|
||||
},
|
||||
'& .-rotate-90': {
|
||||
'--tw-rotate': '0deg',
|
||||
},
|
||||
'& button': {
|
||||
...theme.typography.button,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
'& a': {
|
||||
color: theme.palette.link,
|
||||
},
|
||||
'& a.no-underline': {
|
||||
...theme.typography.button,
|
||||
background: 'none',
|
||||
boxSizing: 'border-box',
|
||||
@@ -48,84 +97,47 @@ const useStyles = makeStyles(theme => ({
|
||||
border: `1px solid ${alpha(theme.palette.primary.main, 0.5)}`,
|
||||
'&:hover': {
|
||||
textDecoration: 'none',
|
||||
'&.Mui-disabled': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
backgroundColor: alpha(
|
||||
theme.palette.primary.main,
|
||||
theme.palette.action.hoverOpacity,
|
||||
),
|
||||
// Reset on touch devices, it doesn't add specificity
|
||||
'@media (hover: none)': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
color: theme.palette.action.disabled,
|
||||
},
|
||||
},
|
||||
'& .asyncapi__collapse-button:hover': {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
'& button.asyncapi__toggle-button': {
|
||||
'min-width': 'inherit',
|
||||
},
|
||||
'& .asyncapi__info-list li': {
|
||||
'border-color': theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
color: theme.palette.text.primary,
|
||||
'border-color': theme.palette.primary.main,
|
||||
'background-color': theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
'& .asyncapi__info-list li a': {
|
||||
color: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
'& li.no-underline': {
|
||||
'& a': {
|
||||
textDecoration: 'none',
|
||||
color: theme.palette.getContrastText(theme.palette.primary.main),
|
||||
},
|
||||
},
|
||||
'& .asyncapi__enum': {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
dark: {
|
||||
'& svg': {
|
||||
fill: theme.palette.text.primary,
|
||||
},
|
||||
'& .asyncapi__info, .asyncapi__channel, .asyncapi__channels > div, .asyncapi__schema, .asyncapi__channel-operations-list .asyncapi__messages-list-item .asyncapi__message, .asyncapi__message, .asyncapi__server, .asyncapi__servers > div, .asyncapi__messages > div, .asyncapi__schemas > div':
|
||||
{
|
||||
'background-color': 'inherit',
|
||||
},
|
||||
'& .asyncapi__channel-parameters-header, .asyncapi__channel-operations-header, .asyncapi__channel-operation-oneOf-subscribe-header, .asyncapi__channel-operation-oneOf-publish-header, .asyncapi__channel-operation-message-header, .asyncapi__message-header, .asyncapi__message-header-title, .asyncapi__message-header-title > h3, .asyncapi__bindings, .asyncapi__bindings-header, .asyncapi__bindings-header > h4':
|
||||
{
|
||||
'background-color': 'inherit',
|
||||
'& .prose': {
|
||||
color: theme.palette.text.secondary,
|
||||
'& h3': {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'& .asyncapi__additional-properties-notice': {
|
||||
color: theme.palette.text.hint,
|
||||
},
|
||||
'& .asyncapi__code, .asyncapi__code-pre': {
|
||||
background: theme.palette.background.default,
|
||||
'& .bg-gray-100, .bg-gray-200': {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
'& .asyncapi__schema-example-header-title': {
|
||||
color: theme.palette.text.secondary,
|
||||
'& .text-gray-600': {
|
||||
color: theme.palette.grey['50'],
|
||||
},
|
||||
'& .asyncapi__message-headers-header, .asyncapi__message-payload-header, .asyncapi__server-variables-header, .asyncapi__server-security-header':
|
||||
{
|
||||
'background-color': 'inherit',
|
||||
color: theme.palette.text.secondary,
|
||||
'& .text-gray-700': {
|
||||
color: theme.palette.grey['100'],
|
||||
},
|
||||
'& .panel--right': {
|
||||
background: darken(theme.palette.navigation.background, 0.1),
|
||||
},
|
||||
'& .examples': {
|
||||
backgroundColor: darken(theme.palette.navigation.background, 0.1),
|
||||
'& pre': {
|
||||
backgroundColor: darken(theme.palette.background.default, 0.2),
|
||||
},
|
||||
'& .asyncapi__table-header': {
|
||||
background: theme.palette.background.default,
|
||||
},
|
||||
'& .asyncapi__table-body': {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'& .asyncapi__server-security-flow': {
|
||||
background: theme.palette.background.default,
|
||||
border: 'none',
|
||||
},
|
||||
'& .asyncapi__server-security-flows-list a': {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
'& .asyncapi__table-row--nested': {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -134,11 +146,15 @@ type Props = {
|
||||
definition: string;
|
||||
};
|
||||
|
||||
export const AsyncApiDefinition = ({ definition }: Props) => {
|
||||
export const AsyncApiDefinition = ({ definition }: Props): JSX.Element => {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
const classNames = `${classes.root} ${
|
||||
theme.palette.type === 'dark' ? classes.dark : ''
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classNames}>
|
||||
<AsyncApi schema={definition} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -27,7 +27,7 @@ import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { Progress } from '@backstage/core-components';
|
||||
|
||||
function splitFormData(url: string | undefined, allowedOwners: string[]) {
|
||||
function splitFormData(url: string | undefined, allowedOwners?: string[]) {
|
||||
let host = undefined;
|
||||
let owner = undefined;
|
||||
let repo = undefined;
|
||||
@@ -39,10 +39,7 @@ function splitFormData(url: string | undefined, allowedOwners: string[]) {
|
||||
if (url) {
|
||||
const parsed = new URL(`https://${url}`);
|
||||
host = parsed.host;
|
||||
owner =
|
||||
parsed.searchParams.get('owner') || allowedOwners
|
||||
? allowedOwners[0]
|
||||
: undefined;
|
||||
owner = parsed.searchParams.get('owner') || allowedOwners?.[0];
|
||||
repo = parsed.searchParams.get('repo') || undefined;
|
||||
// This is azure dev ops specific. not used for any other provider.
|
||||
organization = parsed.searchParams.get('organization') || undefined;
|
||||
|
||||
@@ -10,6 +10,7 @@ import express from 'express';
|
||||
import { GeneratorBuilder } from '@backstage/techdocs-common';
|
||||
import { Knex } from 'knex';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
import { PluginCacheManager } from '@backstage/backend-common';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { PreparerBuilder } from '@backstage/techdocs-common';
|
||||
import { PublisherBase } from '@backstage/techdocs-common';
|
||||
|
||||
Vendored
+26
@@ -226,6 +226,32 @@ export interface Config {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @example http://localhost:7007/api/techdocs
|
||||
* Techdocs cache information
|
||||
*/
|
||||
cache?: {
|
||||
/**
|
||||
* The cache time-to-live for TechDocs sites (in milliseconds). Set this
|
||||
* to a non-zero value to cache TechDocs sites and assets as they are
|
||||
* read from storage.
|
||||
*
|
||||
* Note: you must also configure `backend.cache` appropriately as well,
|
||||
* and to pass a PluginCacheManager instance to TechDocs Backend's
|
||||
* createRouter method in your backend.
|
||||
*/
|
||||
ttl: number;
|
||||
|
||||
/**
|
||||
* The time (in milliseconds) that the TechDocs backend will wait for
|
||||
* a cache service to respond before continuing on as though the cached
|
||||
* object was not found (e.g. when the cache sercice is unavailable).
|
||||
*
|
||||
* Defaults to 1000 milliseconds.
|
||||
*/
|
||||
readTimeout?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* @example http://localhost:7007/api/techdocs
|
||||
* @visibility frontend
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"@backstage/search-common": "^0.2.1",
|
||||
"@backstage/techdocs-common": "^0.10.8",
|
||||
"@types/express": "^4.17.6",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"dockerode": "^3.3.1",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
|
||||
@@ -36,6 +36,7 @@ import path from 'path';
|
||||
import { Writable } from 'stream';
|
||||
import { Logger } from 'winston';
|
||||
import { BuildMetadataStorage } from './BuildMetadataStorage';
|
||||
import { TechDocsCache } from '../cache';
|
||||
|
||||
type DocsBuilderArguments = {
|
||||
preparers: PreparerBuilder;
|
||||
@@ -46,6 +47,7 @@ type DocsBuilderArguments = {
|
||||
config: Config;
|
||||
scmIntegrations: ScmIntegrationRegistry;
|
||||
logStream?: Writable;
|
||||
cache?: TechDocsCache;
|
||||
};
|
||||
|
||||
export class DocsBuilder {
|
||||
@@ -57,6 +59,7 @@ export class DocsBuilder {
|
||||
private config: Config;
|
||||
private scmIntegrations: ScmIntegrationRegistry;
|
||||
private logStream: Writable | undefined;
|
||||
private cache?: TechDocsCache;
|
||||
|
||||
constructor({
|
||||
preparers,
|
||||
@@ -67,6 +70,7 @@ export class DocsBuilder {
|
||||
config,
|
||||
scmIntegrations,
|
||||
logStream,
|
||||
cache,
|
||||
}: DocsBuilderArguments) {
|
||||
this.preparer = preparers.get(entity);
|
||||
this.generator = generators.get(entity);
|
||||
@@ -76,6 +80,7 @@ export class DocsBuilder {
|
||||
this.config = config;
|
||||
this.scmIntegrations = scmIntegrations;
|
||||
this.logStream = logStream;
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,11 +215,19 @@ export class DocsBuilder {
|
||||
)}`,
|
||||
);
|
||||
|
||||
await this.publisher.publish({
|
||||
const published = await this.publisher.publish({
|
||||
entity: this.entity,
|
||||
directory: outputDir,
|
||||
});
|
||||
|
||||
// Invalidate the cache for any published objects.
|
||||
if (this.cache && published && published?.objects?.length) {
|
||||
this.logger.debug(
|
||||
`Invalidating ${published.objects.length} cache objects`,
|
||||
);
|
||||
await this.cache.invalidateMultiple(published.objects);
|
||||
}
|
||||
|
||||
try {
|
||||
// Not a blocker hence no need to await this.
|
||||
fs.remove(outputDir);
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* 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 { CacheClient, getVoidLogger } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { CacheInvalidationError, TechDocsCache } from './TechDocsCache';
|
||||
|
||||
const cached = (str: string): string => {
|
||||
return Buffer.from(str).toString('base64');
|
||||
};
|
||||
|
||||
describe('TechDocsCache', () => {
|
||||
let CacheUnderTest: TechDocsCache;
|
||||
let MockClient: jest.Mocked<CacheClient>;
|
||||
|
||||
beforeEach(() => {
|
||||
MockClient = {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
CacheUnderTest = TechDocsCache.fromConfig(new ConfigReader({}), {
|
||||
cache: MockClient,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('returns undefined if no response', async () => {
|
||||
const expectedPath = 'some/index.html';
|
||||
MockClient.get.mockResolvedValueOnce(undefined);
|
||||
|
||||
const actual = await CacheUnderTest.get(expectedPath);
|
||||
expect(MockClient.get).toHaveBeenCalledWith(expectedPath);
|
||||
expect(actual).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns undefined if cache get throws', async () => {
|
||||
const expectedPath = 'some/index.html';
|
||||
MockClient.get.mockRejectedValueOnce(new Error());
|
||||
|
||||
const actual = await CacheUnderTest.get(expectedPath);
|
||||
expect(actual).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns undefined if no response after 1s by default', async () => {
|
||||
const expectedPath = 'some/index.html';
|
||||
MockClient.get.mockImplementationOnce(() => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(cached('value')), 1500);
|
||||
});
|
||||
});
|
||||
const actual = await CacheUnderTest.get(expectedPath);
|
||||
expect(actual).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns undefined if no response after configured readTimeout', async () => {
|
||||
const expectedPath = 'some/index.html';
|
||||
MockClient.get.mockImplementationOnce(() => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(cached('value')), 20);
|
||||
});
|
||||
});
|
||||
|
||||
CacheUnderTest = TechDocsCache.fromConfig(
|
||||
new ConfigReader({
|
||||
techdocs: { cache: { readTimeout: 10 } },
|
||||
}),
|
||||
{
|
||||
cache: MockClient,
|
||||
logger: getVoidLogger(),
|
||||
},
|
||||
);
|
||||
|
||||
const actual = await CacheUnderTest.get(expectedPath);
|
||||
expect(actual).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns data if cache get returns it', async () => {
|
||||
const expectedPath = 'some/index.html';
|
||||
MockClient.get.mockResolvedValueOnce(cached('expected value'));
|
||||
|
||||
const actual = await CacheUnderTest.get(expectedPath);
|
||||
expect(actual?.toString()).toBe('expected value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('set', () => {
|
||||
it('sets a base64-encoded string', async () => {
|
||||
const expectedPath = 'some/index.html';
|
||||
MockClient.set.mockResolvedValueOnce(undefined);
|
||||
|
||||
await CacheUnderTest.set(expectedPath, Buffer.from('some data'));
|
||||
expect(MockClient.set).toHaveBeenCalledWith(
|
||||
expectedPath,
|
||||
cached('some data'),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw if client throws', () => {
|
||||
MockClient.set.mockRejectedValueOnce(new Error());
|
||||
expect(() => CacheUnderTest.set('i.html', Buffer.from(''))).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalidate', () => {
|
||||
it('calls delete on client', async () => {
|
||||
const expectedPath = 'some/index.html';
|
||||
MockClient.delete.mockResolvedValueOnce(undefined);
|
||||
|
||||
await CacheUnderTest.invalidate(expectedPath);
|
||||
expect(MockClient.delete).toHaveBeenCalledWith(expectedPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalidateMultiple', () => {
|
||||
it('calls delete once per given path', async () => {
|
||||
const expectedPaths = ['one/index.html', 'two/index.html'];
|
||||
MockClient.delete.mockResolvedValue(undefined);
|
||||
|
||||
await CacheUnderTest.invalidateMultiple(expectedPaths);
|
||||
expect(MockClient.delete).toHaveBeenNthCalledWith(1, expectedPaths[0]);
|
||||
expect(MockClient.delete).toHaveBeenNthCalledWith(2, expectedPaths[1]);
|
||||
});
|
||||
|
||||
it('returns an array of as many paths provided', async () => {
|
||||
const expectedPaths = ['one/index.html', 'two/index.html'];
|
||||
MockClient.delete.mockResolvedValue(undefined);
|
||||
|
||||
const actual = await CacheUnderTest.invalidateMultiple(expectedPaths);
|
||||
expect(actual.length).toBe(2);
|
||||
});
|
||||
|
||||
it('calls delete on all paths even if the first rejects', async () => {
|
||||
const expectedPaths = ['one/index.html', 'two/index.html'];
|
||||
MockClient.delete.mockRejectedValueOnce(new Error());
|
||||
MockClient.delete.mockResolvedValueOnce(undefined);
|
||||
|
||||
await expect(
|
||||
CacheUnderTest.invalidateMultiple(expectedPaths),
|
||||
).rejects.toThrowError(CacheInvalidationError);
|
||||
expect(MockClient.delete).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('rejects with invalidations error response', async () => {
|
||||
const expectedPaths = ['one/index.html', 'two/index.html'];
|
||||
MockClient.delete.mockResolvedValueOnce(undefined);
|
||||
MockClient.delete.mockRejectedValueOnce(new Error());
|
||||
|
||||
await expect(
|
||||
CacheUnderTest.invalidateMultiple.bind(CacheUnderTest, expectedPaths),
|
||||
).rejects.toThrow(CacheInvalidationError);
|
||||
});
|
||||
});
|
||||
});
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 { CacheClient } from '@backstage/backend-common';
|
||||
import { assertError, CustomErrorBase } from '@backstage/errors';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export class CacheInvalidationError extends CustomErrorBase {}
|
||||
|
||||
export class TechDocsCache {
|
||||
protected readonly cache: CacheClient;
|
||||
protected readonly logger: Logger;
|
||||
protected readonly readTimeout: number;
|
||||
|
||||
private constructor({
|
||||
cache,
|
||||
logger,
|
||||
readTimeout,
|
||||
}: {
|
||||
cache: CacheClient;
|
||||
logger: Logger;
|
||||
readTimeout: number;
|
||||
}) {
|
||||
this.cache = cache;
|
||||
this.logger = logger;
|
||||
this.readTimeout = readTimeout;
|
||||
}
|
||||
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
{ cache, logger }: { cache: CacheClient; logger: Logger },
|
||||
) {
|
||||
const timeout = config.getOptionalNumber('techdocs.cache.readTimeout');
|
||||
const readTimeout = timeout === undefined ? 1000 : timeout;
|
||||
return new TechDocsCache({ cache, logger, readTimeout });
|
||||
}
|
||||
|
||||
async get(path: string): Promise<Buffer | undefined> {
|
||||
try {
|
||||
// Promise.race ensures we don't hang the client for long if the cache is
|
||||
// temporarily unreachable.
|
||||
const response = (await Promise.race([
|
||||
this.cache.get(path),
|
||||
new Promise(cancelAfter => setTimeout(cancelAfter, this.readTimeout)),
|
||||
])) as string | undefined;
|
||||
|
||||
if (response !== undefined) {
|
||||
this.logger.debug(`Cache hit: ${path}`);
|
||||
return Buffer.from(response, 'base64');
|
||||
}
|
||||
|
||||
this.logger.debug(`Cache miss: ${path}`);
|
||||
return response;
|
||||
} catch (e) {
|
||||
assertError(e);
|
||||
this.logger.warn(`Error getting cache entry ${path}: ${e.message}`);
|
||||
this.logger.debug(e.stack);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async set(path: string, data: Buffer): Promise<void> {
|
||||
this.logger.debug(`Writing cache entry for ${path}`);
|
||||
this.cache
|
||||
.set(path, data.toString('base64'))
|
||||
.catch(e => this.logger.error('write error', e));
|
||||
}
|
||||
|
||||
async invalidate(path: string): Promise<void> {
|
||||
return this.cache.delete(path);
|
||||
}
|
||||
|
||||
async invalidateMultiple(
|
||||
paths: string[],
|
||||
): Promise<PromiseSettledResult<void>[]> {
|
||||
const settled = await Promise.allSettled(
|
||||
paths.map(path => this.cache.delete(path)),
|
||||
);
|
||||
const rejected = settled.filter(
|
||||
s => s.status === 'rejected',
|
||||
) as PromiseRejectedResult[];
|
||||
|
||||
if (rejected.length) {
|
||||
throw new CacheInvalidationError(
|
||||
'TechDocs cache invalidation error',
|
||||
rejected,
|
||||
);
|
||||
}
|
||||
|
||||
return settled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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 { getVoidLogger } from '@backstage/backend-common';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import { createCacheMiddleware, TechDocsCache } from '.';
|
||||
|
||||
/**
|
||||
* Mocks cached HTTP response.
|
||||
*/
|
||||
const getMockHttpResponseFor = (content: string): Buffer => {
|
||||
return Buffer.concat([
|
||||
Buffer.from(`HTTP/1.1 200 OK
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Accept-Ranges: bytes
|
||||
Cache-Control: public, max-age=0
|
||||
Last-Modified: Sat, 1 Jul 2021 12:00:00 GMT
|
||||
Date: Sat, 1 Jul 2021 12:00:00 GMT
|
||||
Connection: close
|
||||
Content-Length: ${content.length}\n\n`),
|
||||
Buffer.from(content),
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for the socket to close. Works because, above, we set connection: close
|
||||
*/
|
||||
const waitForSocketClose = () => new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
describe('createCacheMiddleware', () => {
|
||||
let cache: jest.Mocked<TechDocsCache>;
|
||||
let app: express.Express;
|
||||
|
||||
beforeEach(async () => {
|
||||
cache = {
|
||||
get: jest.fn().mockResolvedValue(undefined),
|
||||
set: jest.fn().mockResolvedValue(undefined),
|
||||
invalidate: jest.fn().mockResolvedValue(undefined),
|
||||
invalidateMultiple: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as jest.Mocked<TechDocsCache>;
|
||||
const router = await createCacheMiddleware({
|
||||
logger: getVoidLogger(),
|
||||
cache,
|
||||
});
|
||||
app = express().use(router);
|
||||
app.use((req, res, next) => {
|
||||
// By default, send cacheable content.
|
||||
if (req.path !== '/static/docs/error.png') {
|
||||
res.send('default-response');
|
||||
} else {
|
||||
next(new Error());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('middleware', () => {
|
||||
it('does not apply to non-static/docs paths', async () => {
|
||||
await request(app)
|
||||
.get('/static/not-docs')
|
||||
.expect(200, 'default-response');
|
||||
|
||||
expect(cache.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('responds with cached response', async () => {
|
||||
cache.get.mockResolvedValueOnce(getMockHttpResponseFor('xyz'));
|
||||
|
||||
await request(app).get('/static/docs/foo.html').expect(200, 'xyz');
|
||||
|
||||
await waitForSocketClose();
|
||||
expect(cache.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets cache when content is cacheable', async () => {
|
||||
const expectedPath = 'default/api/xyz/index.html';
|
||||
await request(app)
|
||||
.get(`/static/docs/${expectedPath}`)
|
||||
.expect(200, 'default-response');
|
||||
|
||||
await waitForSocketClose();
|
||||
expect(cache.set).toHaveBeenCalled();
|
||||
|
||||
const [actualPath, actualBuffer] = (cache.set as jest.Mock).mock.calls[0];
|
||||
expect(actualPath).toBe(expectedPath);
|
||||
expect(actualBuffer.toString()).toContain('default-response');
|
||||
});
|
||||
|
||||
it('does not set cache on error', async () => {
|
||||
await request(app).get('/static/docs/error.png').expect(500);
|
||||
|
||||
await waitForSocketClose();
|
||||
expect(cache.set).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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 { Router } from 'express';
|
||||
import router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import { TechDocsCache } from './TechDocsCache';
|
||||
|
||||
type CacheMiddlewareOptions = {
|
||||
cache: TechDocsCache;
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
type ErrorCallback = (err?: Error) => void;
|
||||
|
||||
export const createCacheMiddleware = ({
|
||||
cache,
|
||||
}: CacheMiddlewareOptions): Router => {
|
||||
const cacheMiddleware = router();
|
||||
|
||||
// Middleware that, through socket monkey patching, captures responses as
|
||||
// they're sent over /static/docs/* and caches them. Subsequent requests are
|
||||
// loaded from cache. Cache key is the object's path (after `/static/docs/`).
|
||||
cacheMiddleware.use(async (req, res, next) => {
|
||||
const socket = res.socket;
|
||||
const isCacheable = req.path.startsWith('/static/docs/');
|
||||
|
||||
// Continue early if this is non-cacheable, or there's no socket.
|
||||
if (!isCacheable || !socket) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Make concrete references to these things.
|
||||
const reqPath = decodeURI(req.path.match(/\/static\/docs\/(.*)$/)![1]);
|
||||
const realEnd = socket.end.bind(socket);
|
||||
const realWrite = socket.write.bind(socket);
|
||||
let writeToCache = true;
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
// Monkey-patch the response's socket to keep track of chunks as they are
|
||||
// written over the wire.
|
||||
socket.write = (
|
||||
data: string | Uint8Array,
|
||||
encoding?: BufferEncoding | ErrorCallback,
|
||||
callback?: ErrorCallback,
|
||||
) => {
|
||||
chunks.push(Buffer.from(data));
|
||||
if (typeof encoding === 'function') {
|
||||
return realWrite(data, encoding);
|
||||
}
|
||||
return realWrite(data, encoding, callback);
|
||||
};
|
||||
|
||||
// When a socket is closed, if there were no errors and the data written
|
||||
// over the socket should be cached, cache it!
|
||||
socket.on('close', async hadError => {
|
||||
const content = Buffer.concat(chunks);
|
||||
const head = content.toString('utf8', 0, 12);
|
||||
if (writeToCache && !hadError && head.match(/HTTP\/\d\.\d 200/)) {
|
||||
await cache.set(reqPath, content);
|
||||
}
|
||||
});
|
||||
|
||||
// Attempt to retrieve data from the cache.
|
||||
const cached = await cache.get(reqPath);
|
||||
|
||||
// If there is a cache hit, write it out on the socket, ensure we don't re-
|
||||
// cache the data, and prevent going back to canonical storage by never
|
||||
// calling next().
|
||||
if (cached) {
|
||||
writeToCache = false;
|
||||
realEnd(cached);
|
||||
return;
|
||||
}
|
||||
|
||||
// No data retrieved from cache: allow retrieval from canonical storage.
|
||||
next();
|
||||
});
|
||||
|
||||
return cacheMiddleware;
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { createCacheMiddleware } from './cacheMiddleware';
|
||||
export { TechDocsCache } from './TechDocsCache';
|
||||
@@ -25,11 +25,25 @@ import {
|
||||
PreparerBuilder,
|
||||
PublisherBase,
|
||||
} from '@backstage/techdocs-common';
|
||||
import { TechDocsCache } from '../cache';
|
||||
import { DocsBuilder, shouldCheckForUpdate } from '../DocsBuilder';
|
||||
import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer';
|
||||
|
||||
jest.mock('../DocsBuilder');
|
||||
|
||||
jest.mock('cross-fetch', () => ({
|
||||
__esModule: true,
|
||||
default: async () => {
|
||||
return {
|
||||
json: async () => {
|
||||
return {
|
||||
build_timestamp: 123,
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
const MockedDocsBuilder = DocsBuilder as jest.MockedClass<typeof DocsBuilder>;
|
||||
|
||||
describe('DocsSynchronizer', () => {
|
||||
@@ -52,6 +66,12 @@ describe('DocsSynchronizer', () => {
|
||||
getBaseUrl: jest.fn(),
|
||||
getExternalBaseUrl: jest.fn(),
|
||||
};
|
||||
const cache: jest.Mocked<TechDocsCache> = {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
invalidate: jest.fn(),
|
||||
invalidateMultiple: jest.fn(),
|
||||
} as unknown as jest.Mocked<TechDocsCache>;
|
||||
|
||||
let docsSynchronizer: DocsSynchronizer;
|
||||
const mockResponseHandler: jest.Mocked<DocsSynchronizerSyncOpts> = {
|
||||
@@ -71,6 +91,7 @@ describe('DocsSynchronizer', () => {
|
||||
config: new ConfigReader({}),
|
||||
logger: getVoidLogger(),
|
||||
scmIntegrations: ScmIntegrations.fromConfig(new ConfigReader({})),
|
||||
cache,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -193,4 +214,112 @@ describe('DocsSynchronizer', () => {
|
||||
expect(mockResponseHandler.error).toBeCalledWith(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('doCacheSync', () => {
|
||||
const entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
uid: '0',
|
||||
name: 'test',
|
||||
namespace: 'default',
|
||||
},
|
||||
};
|
||||
|
||||
it('should not check metadata too often', async () => {
|
||||
(shouldCheckForUpdate as jest.Mock).mockReturnValue(false);
|
||||
|
||||
await docsSynchronizer.doCacheSync({
|
||||
responseHandler: mockResponseHandler,
|
||||
discovery,
|
||||
token: undefined,
|
||||
entity,
|
||||
});
|
||||
|
||||
expect(mockResponseHandler.finish).toBeCalledWith({ updated: false });
|
||||
expect(shouldCheckForUpdate).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should do nothing if source/cached metadata matches', async () => {
|
||||
(shouldCheckForUpdate as jest.Mock).mockReturnValue(true);
|
||||
(publisher.fetchTechDocsMetadata as jest.Mock).mockResolvedValue({
|
||||
build_timestamp: 123,
|
||||
});
|
||||
|
||||
await docsSynchronizer.doCacheSync({
|
||||
responseHandler: mockResponseHandler,
|
||||
discovery,
|
||||
token: undefined,
|
||||
entity,
|
||||
});
|
||||
|
||||
expect(mockResponseHandler.finish).toBeCalledWith({ updated: false });
|
||||
});
|
||||
|
||||
it('should invalidate expected files when source/cached metadata differ', async () => {
|
||||
(shouldCheckForUpdate as jest.Mock).mockReturnValue(true);
|
||||
(publisher.fetchTechDocsMetadata as jest.Mock).mockResolvedValue({
|
||||
build_timestamp: 456,
|
||||
files: ['index.html'],
|
||||
});
|
||||
|
||||
await docsSynchronizer.doCacheSync({
|
||||
responseHandler: mockResponseHandler,
|
||||
discovery,
|
||||
token: undefined,
|
||||
entity,
|
||||
});
|
||||
|
||||
expect(mockResponseHandler.finish).toBeCalledWith({ updated: true });
|
||||
expect(cache.invalidateMultiple).toHaveBeenCalledWith([
|
||||
'default/component/test/index.html',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should invalidate expected files when source/cached metadata differ with legacy casing', async () => {
|
||||
(shouldCheckForUpdate as jest.Mock).mockReturnValue(true);
|
||||
(publisher.fetchTechDocsMetadata as jest.Mock).mockResolvedValue({
|
||||
build_timestamp: 456,
|
||||
files: ['index.html'],
|
||||
});
|
||||
|
||||
const docsSynchronizerWithLegacy = new DocsSynchronizer({
|
||||
publisher,
|
||||
config: new ConfigReader({
|
||||
techdocs: { legacyUseCaseSensitiveTripletPaths: true },
|
||||
}),
|
||||
logger: getVoidLogger(),
|
||||
scmIntegrations: ScmIntegrations.fromConfig(new ConfigReader({})),
|
||||
cache,
|
||||
});
|
||||
|
||||
await docsSynchronizerWithLegacy.doCacheSync({
|
||||
responseHandler: mockResponseHandler,
|
||||
discovery,
|
||||
token: undefined,
|
||||
entity,
|
||||
});
|
||||
|
||||
expect(mockResponseHandler.finish).toBeCalledWith({ updated: true });
|
||||
expect(cache.invalidateMultiple).toHaveBeenCalledWith([
|
||||
'default/Component/test/index.html',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should gracefully handle errors', async () => {
|
||||
(shouldCheckForUpdate as jest.Mock).mockReturnValue(true);
|
||||
(publisher.fetchTechDocsMetadata as jest.Mock).mockRejectedValue(
|
||||
new Error(),
|
||||
);
|
||||
|
||||
await docsSynchronizer.doCacheSync({
|
||||
responseHandler: mockResponseHandler,
|
||||
discovery,
|
||||
token: undefined,
|
||||
entity,
|
||||
});
|
||||
|
||||
expect(mockResponseHandler.finish).toBeCalledWith({ updated: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { assertError, NotFoundError } from '@backstage/errors';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
@@ -23,9 +24,15 @@ import {
|
||||
PreparerBuilder,
|
||||
PublisherBase,
|
||||
} from '@backstage/techdocs-common';
|
||||
import fetch from 'cross-fetch';
|
||||
import { PassThrough } from 'stream';
|
||||
import * as winston from 'winston';
|
||||
import { DocsBuilder, shouldCheckForUpdate } from '../DocsBuilder';
|
||||
import { TechDocsCache } from '../cache';
|
||||
import {
|
||||
BuildMetadataStorage,
|
||||
DocsBuilder,
|
||||
shouldCheckForUpdate,
|
||||
} from '../DocsBuilder';
|
||||
|
||||
export type DocsSynchronizerSyncOpts = {
|
||||
log: (message: string) => void;
|
||||
@@ -38,22 +45,26 @@ export class DocsSynchronizer {
|
||||
private readonly logger: winston.Logger;
|
||||
private readonly config: Config;
|
||||
private readonly scmIntegrations: ScmIntegrationRegistry;
|
||||
private readonly cache: TechDocsCache | undefined;
|
||||
|
||||
constructor({
|
||||
publisher,
|
||||
logger,
|
||||
config,
|
||||
scmIntegrations,
|
||||
cache,
|
||||
}: {
|
||||
publisher: PublisherBase;
|
||||
logger: winston.Logger;
|
||||
config: Config;
|
||||
scmIntegrations: ScmIntegrationRegistry;
|
||||
cache: TechDocsCache | undefined;
|
||||
}) {
|
||||
this.config = config;
|
||||
this.logger = logger;
|
||||
this.publisher = publisher;
|
||||
this.scmIntegrations = scmIntegrations;
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
async doSync({
|
||||
@@ -104,6 +115,7 @@ export class DocsSynchronizer {
|
||||
config: this.config,
|
||||
scmIntegrations: this.scmIntegrations,
|
||||
logStream,
|
||||
cache: this.cache,
|
||||
});
|
||||
|
||||
const updated = await docsBuilder.build();
|
||||
@@ -145,4 +157,76 @@ export class DocsSynchronizer {
|
||||
|
||||
finish({ updated: true });
|
||||
}
|
||||
|
||||
async doCacheSync({
|
||||
responseHandler: { finish },
|
||||
discovery,
|
||||
token,
|
||||
entity,
|
||||
}: {
|
||||
responseHandler: DocsSynchronizerSyncOpts;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
token: string | undefined;
|
||||
entity: Entity;
|
||||
}) {
|
||||
// Check if the last update check was too recent.
|
||||
if (!shouldCheckForUpdate(entity.metadata.uid!) || !this.cache) {
|
||||
finish({ updated: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch techdocs_metadata.json from the publisher and from cache.
|
||||
const baseUrl = await discovery.getBaseUrl('techdocs');
|
||||
const namespace = entity.metadata?.namespace || ENTITY_DEFAULT_NAMESPACE;
|
||||
const kind = entity.kind;
|
||||
const name = entity.metadata.name;
|
||||
const legacyPathCasing =
|
||||
this.config.getOptionalBoolean(
|
||||
'techdocs.legacyUseCaseSensitiveTripletPaths',
|
||||
) || false;
|
||||
const tripletPath = `${namespace}/${kind}/${name}`;
|
||||
const entityTripletPath = `${
|
||||
legacyPathCasing ? tripletPath : tripletPath.toLocaleLowerCase('en-US')
|
||||
}`;
|
||||
try {
|
||||
const [sourceMetadata, cachedMetadata] = await Promise.all([
|
||||
this.publisher.fetchTechDocsMetadata({ namespace, kind, name }),
|
||||
fetch(
|
||||
`${baseUrl}/static/docs/${entityTripletPath}/techdocs_metadata.json`,
|
||||
{
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
},
|
||||
).then(
|
||||
f =>
|
||||
f.json().catch(() => undefined) as ReturnType<
|
||||
PublisherBase['fetchTechDocsMetadata']
|
||||
>,
|
||||
),
|
||||
]);
|
||||
|
||||
// If build timestamps differ, merge their files[] lists and invalidate all objects.
|
||||
if (sourceMetadata.build_timestamp !== cachedMetadata.build_timestamp) {
|
||||
const files = [
|
||||
...new Set([
|
||||
...(sourceMetadata.files || []),
|
||||
...(cachedMetadata.files || []),
|
||||
]),
|
||||
].map(f => `${entityTripletPath}/${f}`);
|
||||
await this.cache.invalidateMultiple(files);
|
||||
finish({ updated: true });
|
||||
} else {
|
||||
finish({ updated: false });
|
||||
}
|
||||
} catch (e) {
|
||||
assertError(e);
|
||||
// In case of error, log and allow the user to go about their business.
|
||||
this.logger.error(
|
||||
`Error syncing cache for ${entityTripletPath}: ${e.message}`,
|
||||
);
|
||||
finish({ updated: false });
|
||||
} finally {
|
||||
// Update the last check time for the entity
|
||||
new BuildMetadataStorage(entity.metadata.uid!).setLastUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import {
|
||||
PluginEndpointDiscovery,
|
||||
PluginCacheManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
@@ -31,6 +34,7 @@ import { Knex } from 'knex';
|
||||
import { Logger } from 'winston';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer';
|
||||
import { createCacheMiddleware, TechDocsCache } from '../cache';
|
||||
|
||||
/**
|
||||
* All of the required dependencies for running TechDocs in the "out-of-the-box"
|
||||
@@ -44,6 +48,7 @@ type OutOfTheBoxDeploymentOptions = {
|
||||
discovery: PluginEndpointDiscovery;
|
||||
database?: Knex; // TODO: Make database required when we're implementing database stuff.
|
||||
config: Config;
|
||||
cache?: PluginCacheManager;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -80,12 +85,22 @@ export async function createRouter(
|
||||
const router = Router();
|
||||
const { publisher, config, logger, discovery } = options;
|
||||
const catalogClient = new CatalogClient({ discoveryApi: discovery });
|
||||
|
||||
// Set up a cache client if configured.
|
||||
let cache: TechDocsCache | undefined;
|
||||
const defaultTtl = config.getOptionalNumber('techdocs.cache.ttl');
|
||||
if (isOutOfTheBoxOption(options) && options.cache && defaultTtl) {
|
||||
const cacheClient = options.cache.getClient({ defaultTtl });
|
||||
cache = TechDocsCache.fromConfig(config, { cache: cacheClient, logger });
|
||||
}
|
||||
|
||||
const scmIntegrations = ScmIntegrations.fromConfig(config);
|
||||
const docsSynchronizer = new DocsSynchronizer({
|
||||
publisher,
|
||||
logger,
|
||||
config,
|
||||
scmIntegrations,
|
||||
cache,
|
||||
});
|
||||
|
||||
router.get('/metadata/techdocs/:namespace/:kind/:name', async (req, res) => {
|
||||
@@ -175,6 +190,17 @@ export async function createRouter(
|
||||
// If set to 'external', it will assume that an external process (e.g. CI/CD pipeline
|
||||
// of the repository) is responsible for building and publishing documentation to the storage provider
|
||||
if (config.getString('techdocs.builder') !== 'local') {
|
||||
// However, if caching is enabled, take the opportunity to check and
|
||||
// invalidate stale cache entries.
|
||||
if (cache) {
|
||||
await docsSynchronizer.doCacheSync({
|
||||
responseHandler,
|
||||
discovery,
|
||||
token,
|
||||
entity,
|
||||
});
|
||||
return;
|
||||
}
|
||||
responseHandler.finish({ updated: false });
|
||||
return;
|
||||
}
|
||||
@@ -199,6 +225,11 @@ export async function createRouter(
|
||||
);
|
||||
});
|
||||
|
||||
// If a cache manager was provided, attach the cache middleware.
|
||||
if (cache) {
|
||||
router.use(createCacheMiddleware({ logger, cache }));
|
||||
}
|
||||
|
||||
// Route middleware which serves files from the storage set in the publisher.
|
||||
router.use('/static/docs', publisher.docsRouter());
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ export type Xcode = {
|
||||
export type BuildResponse = {
|
||||
build: Build;
|
||||
targets: Target[];
|
||||
xcode: Xcode;
|
||||
xcode?: Xcode; // Can be undefined if XCMetrics version < v0.0.8
|
||||
};
|
||||
|
||||
export type BuildFilters = {
|
||||
|
||||
@@ -51,6 +51,20 @@ describe('BuildDetails', () => {
|
||||
).toBeInTheDocument();
|
||||
expect(rendered.getByText(client.mockBuild.schema)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render if xcode data is not present', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<TestApiProvider apis={[[xcmetricsApiRef, client.XcmetricsClient]]}>
|
||||
<BuildDetails
|
||||
buildData={{ ...client.mockBuildResponse, xcode: undefined }}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(
|
||||
rendered.getByText('Xcode').parentNode?.childNodes[1].textContent,
|
||||
).toEqual('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BuildDetails with request', () => {
|
||||
|
||||
@@ -78,7 +78,7 @@ export const BuildDetails = ({
|
||||
{formatStatus(build.buildStatus)}
|
||||
</>
|
||||
),
|
||||
xcode: `${xcode.version} (${xcode.buildNumber})`,
|
||||
xcode: xcode ? `${xcode.version} (${xcode.buildNumber})` : 'Unknown',
|
||||
CI: build.isCi,
|
||||
};
|
||||
|
||||
|
||||
@@ -69,25 +69,25 @@
|
||||
dependencies:
|
||||
tslib "~2.0.1"
|
||||
|
||||
"@asyncapi/avro-schema-parser@^0.2.1":
|
||||
version "0.2.1"
|
||||
resolved "https://registry.npmjs.org/@asyncapi/avro-schema-parser/-/avro-schema-parser-0.2.1.tgz#0b7d9953e12084e6f556db261ae08bd8f6690faa"
|
||||
integrity sha512-RZJaHsdYM4dChYSrb/TWrVCn/r2qcus+9/8iLL8+SMINHb0ECgH8tFZFJpr3Tq+LV2SBFaRQ+9kuecjQ8BNDSA==
|
||||
"@asyncapi/avro-schema-parser@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.npmjs.org/@asyncapi/avro-schema-parser/-/avro-schema-parser-0.3.0.tgz#6922acc559ef999c57e81297d78ffe680fc92b3c"
|
||||
integrity sha512-gWAqS2CKxbChdX8hZY+5EYQl6atP8FTSBvoG5mGGQ89XUoNdlLX14lsvbgvBnDj5sSwqfs+b5Mh5PUZMR/8maA==
|
||||
|
||||
"@asyncapi/openapi-schema-parser@^2.0.1":
|
||||
"@asyncapi/openapi-schema-parser@^2.0.0":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/@asyncapi/openapi-schema-parser/-/openapi-schema-parser-2.0.1.tgz#4d6e82cced907b14e0ad6f98261ff2562d968d96"
|
||||
integrity sha512-algbtdM1gcAOa8+V8kp7WeBhdaNac82jmZUXx8YjyNfRVo02N2juDrjeBAGJd+FNva9Mb4MM7qfkJoAFpTL5VQ==
|
||||
dependencies:
|
||||
"@openapi-contrib/openapi-schema-to-json-schema" "^3.0.0"
|
||||
|
||||
"@asyncapi/parser@^1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.5.0.tgz#d70616a1e6081c7dd86957befd5dccc46b9a77df"
|
||||
integrity sha512-HzrehCcT6R+iqtktNmrUM9wRUEMBqnCeXIrGJT0txBMS1QduNRmXGjvuDpxqwyaATPH/xu1gSp2l6pFP/hyVbA==
|
||||
"@asyncapi/parser@^1.13.0":
|
||||
version "1.13.0"
|
||||
resolved "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.13.0.tgz#bc20d53ba870ead9d26d5a4e3a838c5765ba8d4c"
|
||||
integrity sha512-UnTYSjx2Sc+VKWIIeIV5I+ogFbN5iWzNgx1UgTXQ5oPhtNlsdIkF+w7qV8GyiraaKbBqAuwepv4xbBvlEi8hkw==
|
||||
dependencies:
|
||||
"@apidevtools/json-schema-ref-parser" "^9.0.6"
|
||||
"@asyncapi/specs" "^2.7.7"
|
||||
"@asyncapi/specs" "^2.11.0"
|
||||
"@fmvilas/pseudo-yaml-ast" "^0.3.1"
|
||||
ajv "^6.10.1"
|
||||
js-yaml "^3.13.1"
|
||||
@@ -96,25 +96,24 @@
|
||||
node-fetch "^2.6.0"
|
||||
tiny-merge-patch "^0.1.2"
|
||||
|
||||
"@asyncapi/react-component@^0.23.0":
|
||||
version "0.23.1"
|
||||
resolved "https://registry.npmjs.org/@asyncapi/react-component/-/react-component-0.23.1.tgz#6ae574aac86d835e6845268d49803c4808f37b97"
|
||||
integrity sha512-xBH8F//sF4Sb7M95eVDnQDG+czklQieFKHkCr8SJmmb2j4NylAOTJBoYSZJ3QTC9r0O+SxdQglwh+u1mhMAU+A==
|
||||
"@asyncapi/react-component@^1.0.0-next.25":
|
||||
version "1.0.0-next.25"
|
||||
resolved "https://registry.npmjs.org/@asyncapi/react-component/-/react-component-1.0.0-next.25.tgz#76b27e0e239b03c32499ee5cd8977e5ceb74f754"
|
||||
integrity sha512-S+HguQ6IltQWiy+jO7hlmANJYxJ2UOvH7iMAw+YTcDtYGOe/mrDr7vbrrB3HigZRYIyZSV34cNgNOhw/psVHlg==
|
||||
dependencies:
|
||||
"@asyncapi/avro-schema-parser" "^0.2.1"
|
||||
"@asyncapi/openapi-schema-parser" "^2.0.1"
|
||||
"@asyncapi/parser" "^1.5.0"
|
||||
constate "^1.2.0"
|
||||
dompurify "^2.1.1"
|
||||
markdown-it "^11.0.1"
|
||||
merge "^2.1.0"
|
||||
openapi-sampler "^1.0.0-beta.15"
|
||||
react-use "^12.2.0"
|
||||
"@asyncapi/avro-schema-parser" "^0.3.0"
|
||||
"@asyncapi/openapi-schema-parser" "^2.0.0"
|
||||
"@asyncapi/parser" "^1.13.0"
|
||||
highlight.js "^10.7.2"
|
||||
isomorphic-dompurify "^0.13.0"
|
||||
marked "^2.1.1"
|
||||
openapi-sampler "^1.1.0"
|
||||
use-resize-observer "^7.0.0"
|
||||
|
||||
"@asyncapi/specs@^2.7.7":
|
||||
version "2.7.7"
|
||||
resolved "https://registry.npmjs.org/@asyncapi/specs/-/specs-2.7.7.tgz#10f72c95153a3cc10039f6ba9c3a6f7c3b7fecfc"
|
||||
integrity sha512-z8kj4GDJ640DU4msRsWprvmuC9n7vIeJW+D7Tp1xdefoLX5ZJrK7+4Xruna513wV0fSLpFzCmGz7McEP6CtKDg==
|
||||
"@asyncapi/specs@^2.11.0":
|
||||
version "2.12.0"
|
||||
resolved "https://registry.npmjs.org/@asyncapi/specs/-/specs-2.12.0.tgz#15534ca9e518b561d33edfdfdbb234f9b274f261"
|
||||
integrity sha512-X4Xkrl+9WXSk5EJhsueIxNx6ymHI5wpkw4ofetV+VRnPLNob/XO4trPSJClrL5hlknxbGADLvlrkI5d3XJ996g==
|
||||
|
||||
"@azure/abort-controller@^1.0.0":
|
||||
version "1.0.2"
|
||||
@@ -3674,6 +3673,11 @@
|
||||
resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
|
||||
integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==
|
||||
|
||||
"@juggle/resize-observer@^3.3.1":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0"
|
||||
integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw==
|
||||
|
||||
"@kubernetes/client-node@^0.16.0":
|
||||
version "0.16.1"
|
||||
resolved "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.16.1.tgz#c78ef667579777c1a532983922807e228dbc9b90"
|
||||
@@ -6923,9 +6927,9 @@
|
||||
commander "*"
|
||||
|
||||
"@types/compression@^1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/@types/compression/-/compression-1.7.0.tgz#8dc2a56604873cf0dd4e746d9ae4d31ae77b2390"
|
||||
integrity sha512-3LzWUM+3k3XdWOUk/RO+uSjv7YWOatYq2QADJntK1pjkk4DfVP0KrIEPDnXRJxAAGKe0VpIPRmlINLDuCedZWw==
|
||||
version "1.7.2"
|
||||
resolved "https://registry.npmjs.org/@types/compression/-/compression-1.7.2.tgz#7cc1cdb01b4730eea284615a68fc70a2cdfd5e71"
|
||||
integrity sha512-lwEL4M/uAGWngWFLSG87ZDr2kLrbuR8p7X+QZB1OQlT+qkHsCPDVFnHPyXf4Vyl4yDDorNY+mAhosxkCvppatg==
|
||||
dependencies:
|
||||
"@types/express" "*"
|
||||
|
||||
@@ -7092,6 +7096,13 @@
|
||||
"@types/docker-modem" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/dompurify@^2.1.0":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.3.1.tgz#2934adcd31c4e6b02676f9c22f9756e5091c04dd"
|
||||
integrity sha512-YJth9qa0V/E6/XPH1Jq4BC8uCMmO8V1fKWn8PCvuZcAhMn7q0ez9LW6naQT04UZzjFfAPhyRMZmI2a2rbMlEFA==
|
||||
dependencies:
|
||||
"@types/trusted-types" "*"
|
||||
|
||||
"@types/dompurify@^2.2.2":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.2.3.tgz#6e89677a07902ac1b6821c345f34bd85da239b08"
|
||||
@@ -7881,13 +7892,6 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-wait@^0.3.0":
|
||||
version "0.3.1"
|
||||
resolved "https://registry.npmjs.org/@types/react-wait/-/react-wait-0.3.1.tgz#193cbd8fe86baa53b6f65dfa73f03d562f462a27"
|
||||
integrity sha512-BS9AEjWZItDgpx6LlICcuf53M27zBFCsHx/llCbrmrt/WI7ecG2LGquCss3n8O8bwEDiTX4yYLjy8yLeIfgYTg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@>=16.9.0":
|
||||
version "16.14.18"
|
||||
resolved "https://registry.npmjs.org/@types/react/-/react-16.14.18.tgz#b2bcea05ee244fde92d409f91bd888ca8e54b20f"
|
||||
@@ -8765,6 +8769,11 @@ abab@^2.0.3:
|
||||
resolved "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
|
||||
integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==
|
||||
|
||||
abab@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
|
||||
integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
@@ -8828,6 +8837,11 @@ acorn@^7.1.1, acorn@^7.4.0:
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
|
||||
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
||||
|
||||
acorn@^8.2.4:
|
||||
version "8.6.0"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895"
|
||||
integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==
|
||||
|
||||
acorn@^8.4.1:
|
||||
version "8.4.1"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
|
||||
@@ -11639,11 +11653,6 @@ constants-browserify@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
|
||||
integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=
|
||||
|
||||
constate@^1.2.0:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.npmjs.org/constate/-/constate-1.3.2.tgz#fa5f0fc292207f1ec21b46a5eb81f59c8b0a8b84"
|
||||
integrity sha512-aaILV4vXwGTUZaQZHS5F1xBV8wRCR0Ow1505fdkS5/BPg6hbQrhNqdHL4wgxWgaDeEj43mu/Fb+LhqOKTMcrgQ==
|
||||
|
||||
contains-path@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
|
||||
@@ -11803,7 +11812,7 @@ copy-descriptor@^0.1.0:
|
||||
resolved "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
||||
|
||||
copy-to-clipboard@^3, copy-to-clipboard@^3.1.0, copy-to-clipboard@^3.2.0, copy-to-clipboard@^3.3.1:
|
||||
copy-to-clipboard@^3, copy-to-clipboard@^3.2.0, copy-to-clipboard@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
|
||||
integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==
|
||||
@@ -12294,7 +12303,7 @@ cssom@~0.3.6:
|
||||
resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
|
||||
integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
|
||||
|
||||
cssstyle@^2.2.0:
|
||||
cssstyle@^2.2.0, cssstyle@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852"
|
||||
integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
|
||||
@@ -12761,6 +12770,11 @@ decimal.js@^10.2.0:
|
||||
resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231"
|
||||
integrity sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw==
|
||||
|
||||
decimal.js@^10.2.1:
|
||||
version "10.3.1"
|
||||
resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
|
||||
integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
|
||||
|
||||
decode-uri-component@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||
@@ -13239,7 +13253,7 @@ domhandler@^4.2.0:
|
||||
dependencies:
|
||||
domelementtype "^2.2.0"
|
||||
|
||||
dompurify@^2.1.1, dompurify@^2.2.9:
|
||||
dompurify@^2.2.7, dompurify@^2.2.9:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c"
|
||||
integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg==
|
||||
@@ -13558,11 +13572,6 @@ entities@^2.0.0, entities@~2.1.0:
|
||||
resolved "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
|
||||
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
|
||||
|
||||
entities@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
|
||||
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
|
||||
|
||||
env-paths@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
|
||||
@@ -13890,6 +13899,18 @@ escodegen@^1.14.1:
|
||||
optionalDependencies:
|
||||
source-map "~0.6.1"
|
||||
|
||||
escodegen@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd"
|
||||
integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==
|
||||
dependencies:
|
||||
esprima "^4.0.1"
|
||||
estraverse "^5.2.0"
|
||||
esutils "^2.0.2"
|
||||
optionator "^0.8.1"
|
||||
optionalDependencies:
|
||||
source-map "~0.6.1"
|
||||
|
||||
eslint-config-prettier@^8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a"
|
||||
@@ -14398,9 +14419,9 @@ expect@^26.6.2:
|
||||
jest-regex-util "^26.0.0"
|
||||
|
||||
express-prom-bundle@^6.3.6:
|
||||
version "6.3.6"
|
||||
resolved "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-6.3.6.tgz#c8da1c1024edfcc54953c365991aca57ffd0cfda"
|
||||
integrity sha512-IRsTRCEKCVCHEriQlZ1FuutjEFc89KASsveXh+1HcGEnuZKiAC4LugxrsGEIdySqYvqOYSr2SWHJ6L8/BK2SHA==
|
||||
version "6.4.1"
|
||||
resolved "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-6.4.1.tgz#a688050b9e090f6969825c33143106d3e0e5a70e"
|
||||
integrity sha512-Sg0svLQe/SS5z1tHDTVfZVjNumobiDlXM0jmemt5Dm9K6BX8z9yCwEr93zbko6fNMR4zKav77iPfxUWi6gAjNA==
|
||||
dependencies:
|
||||
on-finished "^2.3.0"
|
||||
url-value-parser "^2.0.0"
|
||||
@@ -16340,7 +16361,7 @@ highlight.js@^10.1.0, highlight.js@^10.1.1, highlight.js@^10.4.1, highlight.js@^
|
||||
resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360"
|
||||
integrity sha512-oFLl873u4usRM9K63j4ME9u3etNF0PLiJhSQ8rdfuL51Wn3zkD6drf9ZW0dOzjnZI22YYG24z30JcmfCZjMgYg==
|
||||
|
||||
highlight.js@~10.7.0:
|
||||
highlight.js@^10.7.2, highlight.js@~10.7.0:
|
||||
version "10.7.3"
|
||||
resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
|
||||
integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
|
||||
@@ -17511,6 +17532,11 @@ is-potential-custom-element-name@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
|
||||
integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
|
||||
|
||||
is-potential-custom-element-name@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
|
||||
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
|
||||
|
||||
is-promise@4.0.0, is-promise@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3"
|
||||
@@ -17769,6 +17795,15 @@ isobject@^4.0.0:
|
||||
resolved "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
|
||||
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
|
||||
|
||||
isomorphic-dompurify@^0.13.0:
|
||||
version "0.13.0"
|
||||
resolved "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-0.13.0.tgz#a4dde357e8531018a85ebb2dd56c4794b6739ba3"
|
||||
integrity sha512-j2/kt/PGbxvfeEm1uiRLlttZkQdn3hFe1rMr/wm3qFnMXSIw0Nmqu79k+TIoSj+KOwO98Sz9TbuNHU7ejv7IZA==
|
||||
dependencies:
|
||||
"@types/dompurify" "^2.1.0"
|
||||
dompurify "^2.2.7"
|
||||
jsdom "^16.5.2"
|
||||
|
||||
isomorphic-fetch@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4"
|
||||
@@ -18504,6 +18539,39 @@ jsdom@^16.4.0:
|
||||
ws "^7.2.3"
|
||||
xml-name-validator "^3.0.0"
|
||||
|
||||
jsdom@^16.5.2:
|
||||
version "16.7.0"
|
||||
resolved "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710"
|
||||
integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==
|
||||
dependencies:
|
||||
abab "^2.0.5"
|
||||
acorn "^8.2.4"
|
||||
acorn-globals "^6.0.0"
|
||||
cssom "^0.4.4"
|
||||
cssstyle "^2.3.0"
|
||||
data-urls "^2.0.0"
|
||||
decimal.js "^10.2.1"
|
||||
domexception "^2.0.1"
|
||||
escodegen "^2.0.0"
|
||||
form-data "^3.0.0"
|
||||
html-encoding-sniffer "^2.0.1"
|
||||
http-proxy-agent "^4.0.1"
|
||||
https-proxy-agent "^5.0.0"
|
||||
is-potential-custom-element-name "^1.0.1"
|
||||
nwsapi "^2.2.0"
|
||||
parse5 "6.0.1"
|
||||
saxes "^5.0.1"
|
||||
symbol-tree "^3.2.4"
|
||||
tough-cookie "^4.0.0"
|
||||
w3c-hr-time "^1.0.2"
|
||||
w3c-xmlserializer "^2.0.0"
|
||||
webidl-conversions "^6.1.0"
|
||||
whatwg-encoding "^1.0.5"
|
||||
whatwg-mimetype "^2.3.0"
|
||||
whatwg-url "^8.5.0"
|
||||
ws "^7.4.6"
|
||||
xml-name-validator "^3.0.0"
|
||||
|
||||
jsesc@^2.5.1:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
|
||||
@@ -18541,7 +18609,7 @@ json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1:
|
||||
resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
|
||||
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
|
||||
|
||||
json-pointer@^0.6.0:
|
||||
json-pointer@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.1.tgz#3c6caa6ac139e2599f5a1659d39852154015054d"
|
||||
integrity sha512-3OvjqKdCBvH41DLpV4iSt6v2XhZXV1bPB4OROuknvUXI7ZQNofieCPkmE26stEJ9zdQuvIxDHCuYhfgxFAAs+Q==
|
||||
@@ -19586,7 +19654,7 @@ lodash@4.17.15:
|
||||
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
|
||||
lodash@4.17.21, lodash@^4.17.10, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.0, lodash@~4.17.15, lodash@~4.17.4:
|
||||
lodash@4.17.21, lodash@^4.17.10, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.0, lodash@~4.17.15, lodash@~4.17.4:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
@@ -19878,17 +19946,6 @@ markdown-escapes@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
|
||||
integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==
|
||||
|
||||
markdown-it@^11.0.1:
|
||||
version "11.0.1"
|
||||
resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.1.tgz#b54f15ec2a2193efa66dda1eb4173baea08993d6"
|
||||
integrity sha512-aU1TzmBKcWNNYvH9pjq6u92BML+Hz3h5S/QpfTFwiQF852pLT+9qHsrhM9JYipkOXZxGn+sGH8oyJE9FD9WezQ==
|
||||
dependencies:
|
||||
argparse "^1.0.7"
|
||||
entities "~2.0.0"
|
||||
linkify-it "^3.0.1"
|
||||
mdurl "^1.0.1"
|
||||
uc.micro "^1.0.5"
|
||||
|
||||
markdown-it@^12.2.0:
|
||||
version "12.2.0"
|
||||
resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-12.2.0.tgz#091f720fd5db206f80de7a8d1f1a7035fd0d38db"
|
||||
@@ -19925,6 +19982,11 @@ markdown-to-jsx@^7.1.3:
|
||||
resolved "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.1.3.tgz#f00bae66c0abe7dd2d274123f84cb6bd2a2c7c6a"
|
||||
integrity sha512-jtQ6VyT7rMT5tPV0g2EJakEnXLiPksnvlYtwQsVVZ611JsWGN8bQ1tVSDX4s6JllfEH6wmsYxNjTUAMrPmNA8w==
|
||||
|
||||
marked@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753"
|
||||
integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==
|
||||
|
||||
math-expression-evaluator@^1.2.14:
|
||||
version "1.2.22"
|
||||
resolved "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.22.tgz#c14dcb3d8b4d150e5dcea9c68c8dad80309b0d5e"
|
||||
@@ -20324,11 +20386,6 @@ merge2@^1.2.3, merge2@^1.3.0:
|
||||
resolved "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81"
|
||||
integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==
|
||||
|
||||
merge@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98"
|
||||
integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==
|
||||
|
||||
meros@1.1.4, meros@^1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz#c17994d3133db8b23807f62bec7f0cb276cfd948"
|
||||
@@ -21147,7 +21204,7 @@ nan@^2.14.1, nan@^2.15.0:
|
||||
resolved "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
|
||||
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
|
||||
|
||||
nano-css@^5.1.0, nano-css@^5.3.1:
|
||||
nano-css@^5.3.1:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.npmjs.org/nano-css/-/nano-css-5.3.1.tgz#b709383e07ad3be61f64edffacb9d98250b87a1f"
|
||||
integrity sha512-ENPIyNzANQRyYVvb62ajDd7PAyIgS2LIUnT9ewih4yrXSZX4hKoUwssy8WjUH++kEOA5wUTMgNnV7ko5n34kUA==
|
||||
@@ -21943,12 +22000,13 @@ open@^8.0.9:
|
||||
is-docker "^2.1.1"
|
||||
is-wsl "^2.2.0"
|
||||
|
||||
openapi-sampler@^1.0.0-beta.15:
|
||||
version "1.0.0-beta.16"
|
||||
resolved "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.0.0-beta.16.tgz#7813524d5b88d222efb772ceb5a809075d6d9174"
|
||||
integrity sha512-05+GvwMagTY7GxoDQoWJfmAUFlxfebciiEzqKmu4iq6+MqBEn62AMUkn0CTxyKhnUGIaR2KXjTeslxIeJwVIOw==
|
||||
openapi-sampler@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.1.1.tgz#7bba7000a03cd8a4630bfbe5b3ef258990c78400"
|
||||
integrity sha512-WAFsl5SPYuhQwaMTDFOcKhnEY1G1rmamrMiPmJdqwfl1lr81g63/befcsN9BNi0w5/R0L+hfcUj13PANEBeLgg==
|
||||
dependencies:
|
||||
json-pointer "^0.6.0"
|
||||
"@types/json-schema" "^7.0.7"
|
||||
json-pointer "^0.6.1"
|
||||
|
||||
openid-client@^4.1.1, openid-client@^4.2.1:
|
||||
version "4.9.0"
|
||||
@@ -22445,7 +22503,7 @@ parse5@5.1.1:
|
||||
resolved "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
|
||||
integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
|
||||
|
||||
parse5@^6.0.0:
|
||||
parse5@6.0.1, parse5@^6.0.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
|
||||
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
|
||||
@@ -24151,11 +24209,6 @@ react-error-overlay@^6.0.9:
|
||||
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
|
||||
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
|
||||
|
||||
react-fast-compare@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||
|
||||
react-fast-compare@^3.0.1, react-fast-compare@^3.1.1, react-fast-compare@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||
@@ -24473,23 +24526,6 @@ react-universal-interface@^0.6.2:
|
||||
resolved "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b"
|
||||
integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==
|
||||
|
||||
react-use@^12.2.0:
|
||||
version "12.13.0"
|
||||
resolved "https://registry.npmjs.org/react-use/-/react-use-12.13.0.tgz#dfefd8145552841f1c2213c2e79966b505a264ba"
|
||||
integrity sha512-Kh0m9ezIn9xfRycx4jdAgvsYGstGfjTBO2ecIM3+G0RqrpMxTIL5jjYraHYfUzjGBHf7dUhNYBzm5vw5LItVZA==
|
||||
dependencies:
|
||||
"@types/react-wait" "^0.3.0"
|
||||
copy-to-clipboard "^3.1.0"
|
||||
nano-css "^5.1.0"
|
||||
react-fast-compare "^2.0.4"
|
||||
react-wait "^0.3.0"
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
screenfull "^5.0.0"
|
||||
set-harmonic-interval "^1.0.1"
|
||||
throttle-debounce "^2.0.1"
|
||||
ts-easing "^0.2.0"
|
||||
tslib "^1.10.0"
|
||||
|
||||
react-use@^17.2.4:
|
||||
version "17.2.4"
|
||||
resolved "https://registry.npmjs.org/react-use/-/react-use-17.2.4.tgz#1f89be3db0a8237c79253db0a15e12bbe3cfeff1"
|
||||
@@ -24522,11 +24558,6 @@ react-virtualized@^9.21.0:
|
||||
prop-types "^15.6.0"
|
||||
react-lifecycles-compat "^3.0.4"
|
||||
|
||||
react-wait@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.npmjs.org/react-wait/-/react-wait-0.3.0.tgz#0cdd4d919012451a5bc3ab0a16d00c6fd9a8c10b"
|
||||
integrity sha512-kB5x/kMKWcn0uVr9gBdNz21/oGbQwEQnF3P9p6E9yLfJ9DRcKS0fagbgYMFI0YFOoyKDj+2q6Rwax0kTYJF37g==
|
||||
|
||||
react@^16.0.0, react@^16.12.0, react@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.npmjs.org/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
|
||||
@@ -25632,7 +25663,7 @@ sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
|
||||
resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||
|
||||
saxes@^5.0.0:
|
||||
saxes@^5.0.0, saxes@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
|
||||
integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==
|
||||
@@ -25688,7 +25719,7 @@ scoped-regex@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/scoped-regex/-/scoped-regex-2.1.0.tgz#7b9be845d81fd9d21d1ec97c61a0b7cf86d2015f"
|
||||
integrity sha512-g3WxHrqSWCZHGHlSrF51VXFdjImhwvH8ZO/pryFH56Qi0cDsZfylQa/t0jCzVQFNbNvM00HfHjkDPEuarKDSWQ==
|
||||
|
||||
screenfull@^5.0.0, screenfull@^5.1.0:
|
||||
screenfull@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.npmjs.org/screenfull/-/screenfull-5.1.0.tgz#85c13c70f4ead4c1b8a935c70010dfdcd2c0e5c8"
|
||||
integrity sha512-dYaNuOdzr+kc6J6CFcBrzkLCfyGcMg+gWkJ8us93IQ7y1cevhQAugFsaCdMHb6lw8KV3xPzSxzH7zM1dQap9mA==
|
||||
@@ -27530,11 +27561,6 @@ throat@^5.0.0:
|
||||
resolved "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
|
||||
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
|
||||
|
||||
throttle-debounce@^2.0.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.2.1.tgz#fbd933ae6793448816f7d5b3cae259d464c98137"
|
||||
integrity sha512-i9hAVld1f+woAiyNGqWelpDD5W1tpMroL3NofTz9xzwq6acWBlO2dC8k5EFSZepU6oOINtV5Q3aSPoRg7o4+fA==
|
||||
|
||||
throttle-debounce@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"
|
||||
@@ -27758,6 +27784,13 @@ tr46@^2.0.2:
|
||||
dependencies:
|
||||
punycode "^2.1.1"
|
||||
|
||||
tr46@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240"
|
||||
integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==
|
||||
dependencies:
|
||||
punycode "^2.1.1"
|
||||
|
||||
"traverse@>=0.3.0 <0.4":
|
||||
version "0.3.9"
|
||||
resolved "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
|
||||
@@ -28639,6 +28672,13 @@ use-memo-one@^1.1.1:
|
||||
resolved "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c"
|
||||
integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ==
|
||||
|
||||
use-resize-observer@^7.0.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-7.1.0.tgz#709ea7540fbe0a60ceae41ee2bef933d7782e4d4"
|
||||
integrity sha512-6DGWOnZpjAGP/MtslGg7OunZptyueQduMi0i8DC5nVKXtJ8Bdt0wR/1tSxugFRndzYCi/jtD+SlNs5PK8ijvXQ==
|
||||
dependencies:
|
||||
"@juggle/resize-observer" "^3.3.1"
|
||||
|
||||
use@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmjs.org/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||
@@ -29233,6 +29273,15 @@ whatwg-url@^8.0.0, whatwg-url@^8.4.0:
|
||||
tr46 "^2.0.2"
|
||||
webidl-conversions "^6.1.0"
|
||||
|
||||
whatwg-url@^8.5.0:
|
||||
version "8.7.0"
|
||||
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77"
|
||||
integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==
|
||||
dependencies:
|
||||
lodash "^4.7.0"
|
||||
tr46 "^2.1.0"
|
||||
webidl-conversions "^6.1.0"
|
||||
|
||||
which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
|
||||
|
||||
Reference in New Issue
Block a user