Merge remote-tracking branch 'origin/master' into mob/stages-refactor
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Honor the branch ref in the url when cloning.
|
||||
|
||||
This fixes a bug in the scaffolder prepare stage where a non-default branch
|
||||
was specified in the scaffolder URL but the default branch was cloned.
|
||||
For example, even though the `other` branch is specified in this example, the
|
||||
`master` branch was actually cloned:
|
||||
|
||||
```yaml
|
||||
catalog:
|
||||
locations:
|
||||
- type: url
|
||||
target: https://github.com/backstage/backstage/blob/other/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
|
||||
```
|
||||
|
||||
This also fixes a 404 in the prepare stage for GitLab URLs.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-cost-insights': minor
|
||||
---
|
||||
|
||||
Add support for additional breakdowns of daily cost data.
|
||||
This changes the type of Cost.groupedCosts returned by CostInsightsApi.getGroupDailyCost.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/integration': patch
|
||||
---
|
||||
|
||||
Fix default branch API url for custom hosted Bitbucket server
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/integration': patch
|
||||
'@backstage/techdocs-common': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Fixed serialization issue with caching of public keys in AWS ALB auth provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Support supplying a custom catalog descriptor file parser
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Fix AWS ALB issuer check
|
||||
@@ -0,0 +1,110 @@
|
||||
name: Tugboat E2E Tests
|
||||
on: deployment_status
|
||||
jobs:
|
||||
set-pending:
|
||||
if: github.event.deployment_status.state != 'success' && github.event.deployment_status.state != 'failed'
|
||||
name: Set pending waiting for Tugboat
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Set an initial commit status message to indicate that the tests are
|
||||
# running.
|
||||
- name: set pending status
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
|
||||
debug: true
|
||||
script: |
|
||||
return github.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.sha,
|
||||
state: 'pending',
|
||||
context: 'Backstage Tugboat E2E Tests',
|
||||
description: 'Waiting for Tugboat to complete deployment',
|
||||
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
|
||||
});
|
||||
|
||||
run-tests:
|
||||
# Only run after a successful Tugboat deployment.
|
||||
if: github.event.deployment_status.state == 'success'
|
||||
name: Run tests against Tugboat deployment
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Set an initial commit status message to indicate that the tests are
|
||||
# running.
|
||||
- name: set pending status
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
|
||||
debug: true
|
||||
script: |
|
||||
return github.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.sha,
|
||||
state: 'pending',
|
||||
context: 'Backstage Tugboat E2E Tests',
|
||||
description: 'Running against tugboat preview',
|
||||
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
|
||||
});
|
||||
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
# This is required because the environment_url param that Tugboat uses
|
||||
# to tell us where the preview is located isn't supported unless you
|
||||
# specify the custom Accept header when getting the deployment_status,
|
||||
# and GitHub actions doesn't do that by default. So instead we have to
|
||||
# load the status object manually and get the data we need.
|
||||
# https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/
|
||||
- name: get deployment status
|
||||
id: get-status-env
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
|
||||
result-encoding: string
|
||||
script: |
|
||||
const result = await github.repos.getDeploymentStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
deployment_id: context.payload.deployment.id,
|
||||
status_id: context.payload.deployment_status.id,
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.ant-man-preview+json'
|
||||
},
|
||||
});
|
||||
console.log(result);
|
||||
return result.data.environment_url;
|
||||
- name: echo tugboat preview url
|
||||
run: |
|
||||
curl ${{steps.get-status-env.outputs.result}}
|
||||
- name: set status
|
||||
if: ${{ failure() }}
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
|
||||
script: |
|
||||
return github.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.sha,
|
||||
state: "error",
|
||||
context: 'Backstage Tugboat E2E Tests',
|
||||
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
|
||||
});
|
||||
- name: set status
|
||||
if: ${{ success() }}
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
|
||||
script: |
|
||||
return github.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.sha,
|
||||
state: "success",
|
||||
context: 'Backstage Tugboat E2E Tests',
|
||||
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
backstage:
|
||||
image: tugboatqa/node:lts
|
||||
expose: 7000
|
||||
default: true
|
||||
commands:
|
||||
init:
|
||||
- mkdir -p /etc/service/node
|
||||
- echo "#!/bin/sh" > /etc/service/node/run
|
||||
- echo "yarn --cwd ${TUGBOAT_ROOT} start-backend --config ${TUGBOAT_ROOT}/app-config.yaml --config ${TUGBOAT_ROOT}/.tugboat/tugboat.app-config.production.yaml" >> /etc/service/node/run
|
||||
- chmod +x /etc/service/node/run
|
||||
build:
|
||||
- yarn workspace example-app build
|
||||
update:
|
||||
- yarn install
|
||||
@@ -0,0 +1,13 @@
|
||||
app:
|
||||
title: Backstage Tugboat Preview
|
||||
baseUrl:
|
||||
$env: TUGBOAT_DEFAULT_SERVICE_URL
|
||||
|
||||
backend:
|
||||
baseUrl:
|
||||
$env: TUGBOAT_DEFAULT_SERVICE_URL
|
||||
cors:
|
||||
origin:
|
||||
$env: TUGBOAT_DEFAULT_SERVICE_URL
|
||||
methods: [GET, POST, PUT, DELETE]
|
||||
credentials: true
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
id: adrs-adr011
|
||||
title: ADR011: Plugin Package Structure
|
||||
description: Architecture Decision Record (ADR) for Plugin Package Structure
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
A core feature of Backstage is the extensibility via plugins. The Backstage
|
||||
repository is open for contributions of plugins. Even most of the core features
|
||||
are implemented as plugins. A plugin consists of one or multiple packages in the
|
||||
`plugins/` directory. Up till now, we have a simple conventions for naming
|
||||
plugin packages: Plugins are named `x`, with the option of having a related
|
||||
backend plugin called `x-backend` (where `x` is the plugin name, like `catalog`
|
||||
or `techdocs`). There is a need for sharing code between the frontend and
|
||||
backend of a plugin, between backend plugins, or components and hooks between
|
||||
different frontend plugins
|
||||
([some examples](https://github.com/backstage/backstage/issues/3655#issuecomment-758166746)).
|
||||
This results in emerging plugin packages with shared code, like
|
||||
`packages/catalog-client` or `packages/techdocs-common`.
|
||||
|
||||
> There is a common phrase in software development:
|
||||
> [Naming things is hard](https://martinfowler.com/bliki/TwoHardThings.html)
|
||||
|
||||
To keep the contributed plugins consistent, this Architecture Decision Record
|
||||
provides rules for naming plugin packages.
|
||||
|
||||
## Decision
|
||||
|
||||
We will place all plugin related code in the `plugins/` directory. The
|
||||
`packages/` directory is reserved for core package of Backstage.
|
||||
|
||||
We follow this structure for plugin packages (where `x` is the plugin name, for
|
||||
example `catalog` or `techdocs`):
|
||||
|
||||
- `x`: Contains the main frontend code of the plugin.
|
||||
- `x-backend`: Contains the main backend code of the plugin.
|
||||
- `x-react`: Contains shared widgets, hooks and similar that both the plugin
|
||||
itself (`x`) and third-party frontend plugins can depend on.
|
||||
- `x-node`: Contains utilities for backends that both the plugin backend itself
|
||||
(`x-backend`) and third-party backend plugins can depend on.
|
||||
- `x-common`: An isomorphic package with platform agnostic models, clients, and
|
||||
utilities that all packages above or any third-party plugin package can depend
|
||||
on.
|
||||
|
||||
We prefix the package names with `@backstage/plugin-`.
|
||||
|
||||
This structure is based on a
|
||||
[suggestion in issue #3655](https://github.com/backstage/backstage/issues/3655#issuecomment-758166746).
|
||||
|
||||
## Consequences
|
||||
|
||||
We will actively migrate existing packages that are part of a plugin to the
|
||||
`plugins/` folder. This affects packages like:
|
||||
|
||||
- `packages/techdocs-common` which should be moved to `plugins/techdocs-node`
|
||||
and named `@backstage/plugin-techdocs-node`.
|
||||
- `packages/catalog-client` which will be part of a future
|
||||
`plugins/catalog-common` and named `@backstage/plugin-catalog-common`.
|
||||
- While the new location of `packages/catalog-model` should be
|
||||
`plugins/catalog-common` we might want to do an exception here, as it's a very
|
||||
central package.
|
||||
|
||||
The limited set of rules might not be sufficient in the future. If additional
|
||||
packages are required, we will revisit this decision and extend the pattern.
|
||||
|
||||
If possible, we will add tools, such as lint rules, to help enforce the package
|
||||
names and dependencies between them or CLI commands to generate these packages.
|
||||
|
||||
The distinction between core packages and plugins helps us to setup
|
||||
[CODEOWNERS](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners)
|
||||
in the repository. We can set the code owners for the `packages/` folder to the
|
||||
core team and create additional rules (like `plugins/x*`) for plugin
|
||||
maintainers.
|
||||
@@ -173,7 +173,12 @@ and access to a running Docker daemon. You can create a GitHub access token
|
||||
docs on creating private GitHub access tokens is available
|
||||
[here](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
|
||||
Note that the need for private GitHub access tokens will be replaced with GitHub
|
||||
Apps integration further down the line.
|
||||
Apps integration further down the line by using the existing `integrations`
|
||||
config.
|
||||
|
||||
> Note: Some of this configuration may already be set up as part of your
|
||||
> `app-config.yaml`. We're moving away from the duplicated config for
|
||||
> authentication in the `scaffolder` section and using `integrations` instead.
|
||||
|
||||
#### GitHub
|
||||
|
||||
@@ -187,10 +192,14 @@ by specifying `visibility` option. Valid options are `public`, `private` and
|
||||
public within the enterprise.
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
github:
|
||||
- host: github.com
|
||||
token:
|
||||
$env: GITHUB_TOKEN
|
||||
|
||||
scaffolder:
|
||||
github:
|
||||
token:
|
||||
$env: GITHUB_TOKEN
|
||||
visibility: public # or 'internal' or 'private'
|
||||
```
|
||||
|
||||
@@ -201,10 +210,9 @@ allows to configure the private access token and the base URL of a GitLab
|
||||
instance:
|
||||
|
||||
```yaml
|
||||
scaffolder:
|
||||
integrations:
|
||||
gitlab:
|
||||
api:
|
||||
baseUrl: https://gitlab.com
|
||||
- host: gitlab.com
|
||||
token:
|
||||
$env: GITLAB_TOKEN
|
||||
```
|
||||
@@ -218,10 +226,9 @@ will hopefully support on-prem installations as well but that has not been
|
||||
verified.
|
||||
|
||||
```yaml
|
||||
scaffolder:
|
||||
integrations:
|
||||
azure:
|
||||
baseUrl: https://dev.azure.com/{your-organization}
|
||||
api:
|
||||
- host: dev.azure.com
|
||||
token:
|
||||
$env: AZURE_TOKEN
|
||||
```
|
||||
|
||||
@@ -4,19 +4,97 @@ title: Other
|
||||
description: Documentation on different ways of Deployment
|
||||
---
|
||||
|
||||
## Deploying Locally
|
||||
## Docker
|
||||
|
||||
### Try on Docker
|
||||
Here we have an example Dockerfile that you can use to build everything together
|
||||
in one container. This Dockerfile uses multi-stage builds, and a
|
||||
`backend:bundle` command from the CLI.
|
||||
|
||||
Run the following commands if you have Docker environment
|
||||
It also provides caching on the `yarn install`'s so that you don't have to do it
|
||||
unless absolutely necessary.
|
||||
|
||||
```bash
|
||||
$ yarn install
|
||||
$ yarn docker-build
|
||||
$ docker run --rm -it -p 7000:7000 -e NODE_ENV=development example-backend:latest
|
||||
> Note: This Dockerfile assumes that you're running SQLite, or your
|
||||
> configuration is setup to connect to an external PostgreSQL Database.
|
||||
|
||||
```Dockerfile
|
||||
# Stage 1 - Create yarn install skeleton layer
|
||||
FROM node:14-buster AS packages
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
COPY packages packages
|
||||
|
||||
# Uncomment this line if you have a local plugins folder
|
||||
# COPY plugins plugins
|
||||
|
||||
RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
||||
|
||||
# Stage 2 - Install dependencies and build packages
|
||||
FROM node:14-buster AS build
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=packages /app .
|
||||
|
||||
RUN yarn install --network-timeout 600000 && rm -rf "$(yarn cache dir)"
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn tsc
|
||||
RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies
|
||||
|
||||
# Stage 3 - Build the actual backend image and install production dependencies
|
||||
FROM node:14-buster
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy from build stage
|
||||
COPY --from=build /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton.tar.gz ./
|
||||
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
|
||||
|
||||
RUN yarn install --production --network-timeout 600000 && rm -rf "$(yarn cache dir)"
|
||||
|
||||
COPY --from=build /app/packages/backend/dist/bundle.tar.gz .
|
||||
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz
|
||||
|
||||
COPY app-config.yaml app-config.production.yaml ./
|
||||
|
||||
CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"]
|
||||
```
|
||||
|
||||
Then open http://localhost:7000 on your browser.
|
||||
Before building you should also include a `.dockerignore`. This will greatly
|
||||
improve the context boot up time of Docker as we are no longer sending all of
|
||||
the `node_modules` into the context. It also helps us avoid some limitations and
|
||||
errors that may occur when trying to share the `node_modules` folder to inside
|
||||
the build.
|
||||
|
||||
You can add the following contents to the root of your repository at
|
||||
`.dockerignore` and it might look something like the following:
|
||||
|
||||
```dockerignore
|
||||
.git
|
||||
node_modules
|
||||
packages/*/node_modules
|
||||
plugins/*/node_modules
|
||||
plugins/*/dist
|
||||
```
|
||||
|
||||
Once you have added both the `Dockerfile` and `.dockerignore` to the root of
|
||||
your project, and run the following to build the container under a specified
|
||||
tag.
|
||||
|
||||
```sh
|
||||
$ docker build -t example-deployment .
|
||||
```
|
||||
|
||||
To run the image locally you can run:
|
||||
|
||||
```sh
|
||||
$ docker run -p -it 7000:7000 example-deployment
|
||||
```
|
||||
|
||||
You should then start to get logs in your terminal, and then you can open your
|
||||
browser at `http://localhost:7000`
|
||||
|
||||
## Heroku
|
||||
|
||||
|
||||
@@ -188,7 +188,8 @@
|
||||
"architecture-decisions/adrs-adr007",
|
||||
"architecture-decisions/adrs-adr008",
|
||||
"architecture-decisions/adrs-adr009",
|
||||
"architecture-decisions/adrs-adr010"
|
||||
"architecture-decisions/adrs-adr010",
|
||||
"architecture-decisions/adrs-adr011"
|
||||
],
|
||||
"Contribute": ["../CONTRIBUTING"],
|
||||
"Support": ["support/support", "support/project-structure"],
|
||||
|
||||
@@ -119,6 +119,7 @@ nav:
|
||||
- ADR008 - Default Catalog File Name: 'architecture-decisions/adr008-default-catalog-file-name.md'
|
||||
- ADR009 - Entity References: 'architecture-decisions/adr009-entity-references.md'
|
||||
- ADR010 - Luxon Date Library: 'architecture-decisions/adr010-luxon-date-library.md'
|
||||
- ADR011 - Plugin Package Structure: 'architecture-decisions/adr011-plugin-package-structure.md'
|
||||
- Contribute: '../CONTRIBUTING.md'
|
||||
- Support:
|
||||
- 'support/support.md'
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"fs-extra": "^9.0.1",
|
||||
"git-url-parse": "^11.4.3",
|
||||
"git-url-parse": "^11.4.4",
|
||||
"helmet": "^4.0.0",
|
||||
"isomorphic-git": "^1.8.0",
|
||||
"knex": "^0.21.6",
|
||||
|
||||
@@ -126,12 +126,12 @@ describe('BitbucketUrlReader', () => {
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/repositories/backstage/mock/commits/some-branch',
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
|
||||
values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -161,13 +161,18 @@ export class BitbucketUrlReader implements UrlReader {
|
||||
}
|
||||
|
||||
private async getLastCommitShortHash(url: string): Promise<string> {
|
||||
const { name: repoName, owner: project, ref } = parseGitUrl(url);
|
||||
const { resource, name: repoName, owner: project, ref } = parseGitUrl(url);
|
||||
|
||||
let branch = ref;
|
||||
if (!branch) {
|
||||
branch = await getBitbucketDefaultBranch(url, this.config);
|
||||
}
|
||||
const commitsApiUrl = `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`;
|
||||
|
||||
const isHosted = resource === 'bitbucket.org';
|
||||
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222
|
||||
const commitsApiUrl = isHosted
|
||||
? `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`
|
||||
: `${this.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`;
|
||||
|
||||
const commitsResponse = await fetch(
|
||||
commitsApiUrl,
|
||||
@@ -182,14 +187,26 @@ export class BitbucketUrlReader implements UrlReader {
|
||||
}
|
||||
|
||||
const commits = await commitsResponse.json();
|
||||
if (
|
||||
commits &&
|
||||
commits.values &&
|
||||
commits.values.length > 0 &&
|
||||
commits.values[0].hash
|
||||
) {
|
||||
return commits.values[0].hash.substring(0, 12);
|
||||
if (isHosted) {
|
||||
if (
|
||||
commits &&
|
||||
commits.values &&
|
||||
commits.values.length > 0 &&
|
||||
commits.values[0].hash
|
||||
) {
|
||||
return commits.values[0].hash.substring(0, 12);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
commits &&
|
||||
commits.values &&
|
||||
commits.values.length > 0 &&
|
||||
commits.values[0].id
|
||||
) {
|
||||
return commits.values[0].id.substring(0, 12);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to read response from ${commitsApiUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,13 +86,22 @@ export class Git {
|
||||
return git.commit({ fs, dir, message, author, committer });
|
||||
}
|
||||
|
||||
async clone({ url, dir }: { url: string; dir: string }): Promise<void> {
|
||||
async clone({
|
||||
url,
|
||||
dir,
|
||||
ref,
|
||||
}: {
|
||||
url: string;
|
||||
dir: string;
|
||||
ref?: string;
|
||||
}): Promise<void> {
|
||||
this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`);
|
||||
return git.clone({
|
||||
fs,
|
||||
http,
|
||||
url,
|
||||
dir,
|
||||
ref,
|
||||
singleBranch: true,
|
||||
depth: 1,
|
||||
onProgress: this.onProgressHandler(),
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.2",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"git-url-parse": "^11.4.3",
|
||||
"git-url-parse": "^11.4.4",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"@octokit/auth-app": "^2.10.5",
|
||||
"luxon": "^1.25.0"
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('bitbucket core', () => {
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default',
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
@@ -144,7 +144,7 @@ describe('bitbucket core', () => {
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default',
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
@@ -231,7 +231,7 @@ describe('bitbucket core', () => {
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default',
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
|
||||
@@ -31,9 +31,10 @@ export async function getBitbucketDefaultBranch(
|
||||
const { name: repoName, owner: project, resource } = parseGitUrl(url);
|
||||
|
||||
const isHosted = resource === 'bitbucket.org';
|
||||
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184
|
||||
const branchUrl = isHosted
|
||||
? `${config.apiBaseUrl}/repositories/${project}/${repoName}`
|
||||
: `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`;
|
||||
: `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`;
|
||||
|
||||
const response = await fetch(branchUrl, getBitbucketRequestOptions(config));
|
||||
if (!response.ok) {
|
||||
@@ -50,7 +51,10 @@ export async function getBitbucketDefaultBranch(
|
||||
defaultBranch = displayId;
|
||||
}
|
||||
if (!defaultBranch) {
|
||||
throw new Error(`Failed to read default branch from ${branchUrl}`);
|
||||
throw new Error(
|
||||
`Failed to read default branch from ${branchUrl}. ` +
|
||||
`Response ${response.status} ${response.json()}`,
|
||||
);
|
||||
}
|
||||
return defaultBranch;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"dockerode": "^3.2.1",
|
||||
"express": "^4.17.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"git-url-parse": "^11.4.3",
|
||||
"git-url-parse": "^11.4.4",
|
||||
"js-yaml": "^4.0.0",
|
||||
"json5": "^2.1.3",
|
||||
"mime-types": "^2.1.27",
|
||||
|
||||
Vendored
+4
@@ -72,6 +72,10 @@ export interface Config {
|
||||
onelogin?: {
|
||||
development: { [key: string]: string };
|
||||
};
|
||||
awsalb?: {
|
||||
issuer?: string;
|
||||
region: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('AwsALBAuthProvider', () => {
|
||||
const mockResponseSend = jest.fn();
|
||||
const mockRequest = ({
|
||||
header: jest.fn(() => {
|
||||
return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzc3VlciI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.zUkMYAuMwC1T0tyHMpxXrkbFDa4aGhB8d9um_tI2hsI';
|
||||
return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo';
|
||||
}),
|
||||
} as unknown) as express.Request;
|
||||
const mockRequestWithoutJwt = ({
|
||||
|
||||
@@ -34,7 +34,7 @@ const ALB_JWT_HEADER = 'x-amzn-oidc-data';
|
||||
*/
|
||||
type AwsAlbAuthProviderOptions = {
|
||||
region: string;
|
||||
issuer: string;
|
||||
issuer?: string;
|
||||
identityResolutionCallback: ExperimentalIdentityResolver;
|
||||
};
|
||||
export const getJWTHeaders = (input: string) => {
|
||||
@@ -70,10 +70,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
|
||||
const key = await this.getKey(headers.kid);
|
||||
const payload = JWT.verify(jwt, key);
|
||||
|
||||
if (
|
||||
this.options.issuer !== '' &&
|
||||
headers.issuer !== this.options.issuer
|
||||
) {
|
||||
if (this.options.issuer && headers.iss !== this.options.issuer) {
|
||||
throw new Error('issuer mismatch on JWT');
|
||||
}
|
||||
|
||||
@@ -98,13 +95,13 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
|
||||
async getKey(keyId: string): Promise<KeyObject> {
|
||||
const optionalCacheKey = this.keyCache.get<KeyObject>(keyId);
|
||||
if (optionalCacheKey) {
|
||||
return optionalCacheKey;
|
||||
return crypto.createPublicKey(optionalCacheKey);
|
||||
}
|
||||
const keyText: string = await fetch(
|
||||
`https://public-keys.auth.elb.${this.options.region}.amazonaws.com/${keyId}`,
|
||||
).then(response => response.text());
|
||||
const keyValue = crypto.createPublicKey(keyText);
|
||||
this.keyCache.set(keyId, keyValue);
|
||||
this.keyCache.set(keyId, keyValue.export({ format: 'pem', type: 'spki' }));
|
||||
return keyValue;
|
||||
}
|
||||
}
|
||||
@@ -116,7 +113,7 @@ export const createAwsAlbProvider = ({
|
||||
identityResolver,
|
||||
}: AuthProviderFactoryOptions) => {
|
||||
const region = config.getString('region');
|
||||
const issuer = config.getString('iss');
|
||||
const issuer = config.getOptionalString('iss');
|
||||
if (identityResolver !== undefined) {
|
||||
return new AwsAlbAuthProvider(logger, catalogApi, {
|
||||
region,
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"fs-extra": "^9.0.0",
|
||||
"git-url-parse": "^11.4.3",
|
||||
"git-url-parse": "^11.4.4",
|
||||
"knex": "^0.21.6",
|
||||
"ldapjs": "^2.2.0",
|
||||
"lodash": "^4.17.15",
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
CatalogProcessorEntityResult,
|
||||
CatalogProcessorErrorResult,
|
||||
CatalogProcessorLocationResult,
|
||||
CatalogProcessorParser,
|
||||
CatalogProcessorResult,
|
||||
} from './processors/types';
|
||||
import { LocationReader, ReadLocationResult } from './types';
|
||||
@@ -41,6 +42,7 @@ const MAX_DEPTH = 10;
|
||||
|
||||
type Options = {
|
||||
reader: UrlReader;
|
||||
parser: CatalogProcessorParser;
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
processors: CatalogProcessor[];
|
||||
@@ -137,7 +139,6 @@ export class LocationReaders implements LocationReader {
|
||||
if (emitResult.type === 'relation') {
|
||||
throw new Error('readLocation may not emit entity relations');
|
||||
}
|
||||
|
||||
emit(emitResult);
|
||||
};
|
||||
|
||||
@@ -149,6 +150,7 @@ export class LocationReaders implements LocationReader {
|
||||
item.location,
|
||||
item.optional,
|
||||
validatedEmit,
|
||||
this.options.parser,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
CatalogProcessorErrorResult,
|
||||
CatalogProcessorResult,
|
||||
} from './types';
|
||||
import { defaultEntityDataParser } from './util/parse';
|
||||
|
||||
describe('UrlReaderProcessor', () => {
|
||||
const mockApiOrigin = 'http://localhost';
|
||||
@@ -52,7 +53,7 @@ describe('UrlReaderProcessor', () => {
|
||||
);
|
||||
|
||||
const generated = (await new Promise<CatalogProcessorResult>(emit =>
|
||||
processor.readLocation(spec, false, emit),
|
||||
processor.readLocation(spec, false, emit, defaultEntityDataParser),
|
||||
)) as CatalogProcessorEntityResult;
|
||||
|
||||
expect(generated.type).toBe('entity');
|
||||
@@ -81,7 +82,7 @@ describe('UrlReaderProcessor', () => {
|
||||
);
|
||||
|
||||
const generated = (await new Promise<CatalogProcessorResult>(emit =>
|
||||
processor.readLocation(spec, false, emit),
|
||||
processor.readLocation(spec, false, emit, defaultEntityDataParser),
|
||||
)) as CatalogProcessorErrorResult;
|
||||
|
||||
expect(generated.type).toBe('error');
|
||||
|
||||
@@ -18,8 +18,11 @@ import { UrlReader } from '@backstage/backend-common';
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import { Logger } from 'winston';
|
||||
import * as result from './results';
|
||||
import { CatalogProcessor, CatalogProcessorEmit } from './types';
|
||||
import { parseEntityYaml } from './util/parse';
|
||||
import {
|
||||
CatalogProcessor,
|
||||
CatalogProcessorEmit,
|
||||
CatalogProcessorParser,
|
||||
} from './types';
|
||||
|
||||
// TODO(Rugvip): Added for backwards compatibility when moving to UrlReader, this
|
||||
// can be removed in a bit
|
||||
@@ -43,6 +46,7 @@ export class UrlReaderProcessor implements CatalogProcessor {
|
||||
location: LocationSpec,
|
||||
optional: boolean,
|
||||
emit: CatalogProcessorEmit,
|
||||
parser: CatalogProcessorParser,
|
||||
): Promise<boolean> {
|
||||
if (deprecatedTypes.includes(location.type)) {
|
||||
// TODO(Rugvip): Remove this warning a month or two into 2021, and remove support for the deprecated types.
|
||||
@@ -57,7 +61,7 @@ export class UrlReaderProcessor implements CatalogProcessor {
|
||||
try {
|
||||
const data = await this.options.reader.read(location.target);
|
||||
|
||||
for (const parseResult of parseEntityYaml(data, location)) {
|
||||
for await (const parseResult of parser({ data, location })) {
|
||||
emit(parseResult);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -27,12 +27,15 @@ export type CatalogProcessor = {
|
||||
* @param location The location to read
|
||||
* @param optional Whether a missing target should trigger an error
|
||||
* @param emit A sink for items resulting from the read
|
||||
* @param parser A parser, that is able to take the raw catalog descriptor
|
||||
* data and turn it into the actual result pieces.
|
||||
* @returns True if handled by this processor, false otherwise
|
||||
*/
|
||||
readLocation?(
|
||||
location: LocationSpec,
|
||||
optional: boolean,
|
||||
emit: CatalogProcessorEmit,
|
||||
parser: CatalogProcessorParser,
|
||||
): Promise<boolean>;
|
||||
|
||||
/**
|
||||
@@ -100,6 +103,16 @@ export type CatalogProcessor = {
|
||||
): Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A parser, that is able to take the raw catalog descriptor data and turn it
|
||||
* into the actual result pieces. The default implementation performs a YAML
|
||||
* document parsing.
|
||||
*/
|
||||
export type CatalogProcessorParser = (options: {
|
||||
data: Buffer;
|
||||
location: LocationSpec;
|
||||
}) => AsyncIterable<CatalogProcessorResult>;
|
||||
|
||||
export type CatalogProcessorEmit = (generated: CatalogProcessorResult) => void;
|
||||
|
||||
export type CatalogProcessorLocationResult = {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Entity, LocationSpec } from '@backstage/catalog-model';
|
||||
import lodash from 'lodash';
|
||||
import yaml from 'yaml';
|
||||
import * as result from '../results';
|
||||
import { CatalogProcessorResult } from '../types';
|
||||
import { CatalogProcessorParser, CatalogProcessorResult } from '../types';
|
||||
|
||||
export function* parseEntityYaml(
|
||||
data: Buffer,
|
||||
@@ -50,3 +50,12 @@ export function* parseEntityYaml(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultEntityDataParser: CatalogProcessorParser = async function* defaultEntityDataParser({
|
||||
data,
|
||||
location,
|
||||
}) {
|
||||
for (const e of parseEntityYaml(data, location)) {
|
||||
yield e;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ConfigReader } from '@backstage/config';
|
||||
import Knex from 'knex';
|
||||
import yaml from 'yaml';
|
||||
import { DatabaseManager } from '../database';
|
||||
import { CatalogProcessorParser } from '../ingestion';
|
||||
import * as result from '../ingestion/processors/results';
|
||||
import { CatalogBuilder, CatalogEnvironment } from './CatalogBuilder';
|
||||
|
||||
@@ -209,4 +210,26 @@ describe('CatalogBuilder', () => {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('setEntityDataParser works', async () => {
|
||||
const mockParser: CatalogProcessorParser = jest
|
||||
.fn()
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const builder = new CatalogBuilder(env)
|
||||
.setEntityDataParser(mockParser)
|
||||
.replaceProcessors([
|
||||
{
|
||||
async readLocation(_location, _optional, _emit, parser) {
|
||||
expect(parser).toBe(mockParser);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { higherOrderOperation } = await builder.build();
|
||||
await higherOrderOperation.addLocation({ type: 'x', target: 'y' });
|
||||
|
||||
expect.assertions(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
AnnotateLocationEntityProcessor,
|
||||
BuiltinKindsEntityProcessor,
|
||||
CatalogProcessor,
|
||||
CatalogProcessorParser,
|
||||
CodeOwnersProcessor,
|
||||
FileReaderProcessor,
|
||||
GithubOrgReaderProcessor,
|
||||
@@ -60,6 +61,7 @@ import {
|
||||
textPlaceholderResolver,
|
||||
yamlPlaceholderResolver,
|
||||
} from '../ingestion/processors/PlaceholderProcessor';
|
||||
import { defaultEntityDataParser } from '../ingestion/processors/util/parse';
|
||||
import { LocationAnalyzer } from '../ingestion/types';
|
||||
|
||||
export type CatalogEnvironment = {
|
||||
@@ -96,6 +98,7 @@ export class CatalogBuilder {
|
||||
private fieldFormatValidators: Partial<Validators>;
|
||||
private processors: CatalogProcessor[];
|
||||
private processorsReplace: boolean;
|
||||
private parser: CatalogProcessorParser | undefined;
|
||||
|
||||
constructor(env: CatalogEnvironment) {
|
||||
this.env = env;
|
||||
@@ -105,6 +108,7 @@ export class CatalogBuilder {
|
||||
this.fieldFormatValidators = {};
|
||||
this.processors = [];
|
||||
this.processorsReplace = false;
|
||||
this.parser = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,6 +201,20 @@ export class CatalogBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the catalog to use a custom parser for entity data.
|
||||
*
|
||||
* This is the function that gets called immediately after some raw entity
|
||||
* specification data has been read from a remote source, and needs to be
|
||||
* parsed and emitted as structured data.
|
||||
*
|
||||
* @param parser The custom parser
|
||||
*/
|
||||
setEntityDataParser(parser: CatalogProcessorParser): CatalogBuilder {
|
||||
this.parser = parser;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires up and returns all of the component parts of the catalog
|
||||
*/
|
||||
@@ -211,9 +229,11 @@ export class CatalogBuilder {
|
||||
const policy = this.buildEntityPolicy();
|
||||
const processors = this.buildProcessors();
|
||||
const rulesEnforcer = CatalogRulesEnforcer.fromConfig(config);
|
||||
const parser = this.parser || defaultEntityDataParser;
|
||||
|
||||
const locationReader = new LocationReaders({
|
||||
...this.env,
|
||||
parser,
|
||||
processors,
|
||||
rulesEnforcer,
|
||||
policy,
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"git-url-parse": "^11.4.3",
|
||||
"git-url-parse": "^11.4.4",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-hook-form": "^6.6.0",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@types/react": "^16.9",
|
||||
"classnames": "^2.2.6",
|
||||
"git-url-parse": "^11.4.3",
|
||||
"git-url-parse": "^11.4.4",
|
||||
"moment": "^2.26.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
|
||||
@@ -33,11 +33,12 @@ import {
|
||||
UnlabeledDataflowAlert,
|
||||
} from '../src/utils/alerts';
|
||||
import {
|
||||
trendlineOf,
|
||||
aggregationFor,
|
||||
changeOf,
|
||||
entityOf,
|
||||
getGroupedProducts,
|
||||
aggregationFor,
|
||||
getGroupedProjects,
|
||||
trendlineOf,
|
||||
} from './utils/mockData';
|
||||
|
||||
export class ExampleCostInsightsClient implements CostInsightsApi {
|
||||
@@ -99,9 +100,12 @@ export class ExampleCostInsightsClient implements CostInsightsApi {
|
||||
aggregation: aggregation,
|
||||
change: changeOf(aggregation),
|
||||
trendline: trendlineOf(aggregation),
|
||||
// Optional field on Cost which needs to be supplied in order to see
|
||||
// the product breakdown view in the top panel.
|
||||
groupedCosts: getGroupedProducts(intervals),
|
||||
// Optional field providing cost groupings / breakdowns keyed by the type. In this example,
|
||||
// daily cost grouped by cloud product OR by project / billing account.
|
||||
groupedCosts: {
|
||||
product: getGroupedProducts(intervals),
|
||||
project: getGroupedProjects(intervals),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -117,9 +121,11 @@ export class ExampleCostInsightsClient implements CostInsightsApi {
|
||||
aggregation: aggregation,
|
||||
change: changeOf(aggregation),
|
||||
trendline: trendlineOf(aggregation),
|
||||
// Optional field on Cost which needs to be supplied in order to see
|
||||
// the product breakdown view in the top panel.
|
||||
groupedCosts: getGroupedProducts(intervals),
|
||||
// Optional field providing cost groupings / breakdowns keyed by the type. In this example,
|
||||
// daily project cost grouped by cloud product.
|
||||
groupedCosts: {
|
||||
product: getGroupedProducts(intervals),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+59
-56
@@ -56,26 +56,26 @@ import { BarChartLegendOptions } from '../BarChart/BarChartLegend';
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
export type CostOverviewByProductChartProps = {
|
||||
costsByProduct: Cost[];
|
||||
export type CostOverviewBreakdownChartProps = {
|
||||
costBreakdown: Cost[];
|
||||
};
|
||||
|
||||
const LOW_COST_THRESHOLD = 0.1;
|
||||
|
||||
export const CostOverviewByProductChart = ({
|
||||
costsByProduct,
|
||||
}: CostOverviewByProductChartProps) => {
|
||||
export const CostOverviewBreakdownChart = ({
|
||||
costBreakdown,
|
||||
}: CostOverviewBreakdownChartProps) => {
|
||||
const theme = useTheme<CostInsightsTheme>();
|
||||
const classes = useStyles(theme);
|
||||
const lastCompleteBillingDate = useLastCompleteBillingDate();
|
||||
const { duration } = useFilters(mapFiltersToProps);
|
||||
const [isExpanded, setExpanded] = useState(false);
|
||||
|
||||
if (!costsByProduct) {
|
||||
if (!costBreakdown) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const flattenedAggregation = costsByProduct
|
||||
const flattenedAggregation = costBreakdown
|
||||
.map(cost => cost.aggregation)
|
||||
.flat();
|
||||
|
||||
@@ -87,44 +87,46 @@ export const CostOverviewByProductChart = ({
|
||||
lastCompleteBillingDate,
|
||||
);
|
||||
const currentPeriodTotal = totalCost - previousPeriodTotal;
|
||||
const otherProducts: string[] = [];
|
||||
const canExpand = costBreakdown.length >= 8;
|
||||
const otherCategoryIds: string[] = [];
|
||||
|
||||
const productsByDate = costsByProduct.reduce((prodByDate, product) => {
|
||||
const productTotal = aggregationSum(product.aggregation);
|
||||
// Group products with less than 10% of the total cost into "Other" category
|
||||
// when we have >= 8 products.
|
||||
const isOtherProduct =
|
||||
costsByProduct.length >= 8 &&
|
||||
productTotal < totalCost * LOW_COST_THRESHOLD;
|
||||
const breakdownsByDate = costBreakdown.reduce(
|
||||
(breakdownByDate, breakdown) => {
|
||||
const breakdownTotal = aggregationSum(breakdown.aggregation);
|
||||
// Group breakdown items with less than 10% of the total cost into "Other" category if needed
|
||||
const isOtherCategory =
|
||||
canExpand && breakdownTotal < totalCost * LOW_COST_THRESHOLD;
|
||||
|
||||
const updatedProdByDate = { ...prodByDate };
|
||||
if (isOtherProduct) {
|
||||
otherProducts.push(product.id);
|
||||
}
|
||||
product.aggregation.forEach(curAggregation => {
|
||||
const productCostsForDate = updatedProdByDate[curAggregation.date] || {};
|
||||
const updatedBreakdownByDate = { ...breakdownByDate };
|
||||
if (isOtherCategory) {
|
||||
otherCategoryIds.push(breakdown.id);
|
||||
}
|
||||
breakdown.aggregation.forEach(curAggregation => {
|
||||
const costsForDate = updatedBreakdownByDate[curAggregation.date] || {};
|
||||
|
||||
updatedProdByDate[curAggregation.date] = {
|
||||
...productCostsForDate,
|
||||
[product.id]:
|
||||
(productCostsForDate[product.id] || 0) + curAggregation.amount,
|
||||
};
|
||||
});
|
||||
updatedBreakdownByDate[curAggregation.date] = {
|
||||
...costsForDate,
|
||||
[breakdown.id]:
|
||||
(costsForDate[breakdown.id] || 0) + curAggregation.amount,
|
||||
};
|
||||
});
|
||||
|
||||
return updatedProdByDate;
|
||||
}, {} as Record<string, Record<string, number>>);
|
||||
return updatedBreakdownByDate;
|
||||
},
|
||||
{} as Record<string, Record<string, number>>,
|
||||
);
|
||||
|
||||
const chartData: Record<string, number>[] = Object.keys(productsByDate).map(
|
||||
const chartData: Record<string, number>[] = Object.keys(breakdownsByDate).map(
|
||||
date => {
|
||||
const costsForDate = Object.keys(productsByDate[date]).reduce(
|
||||
(dateCosts, product) => {
|
||||
// Group costs for products that belong to 'Other' in the chart.
|
||||
const cost = productsByDate[date][product];
|
||||
const productCost =
|
||||
!isExpanded && otherProducts.includes(product)
|
||||
const costsForDate = Object.keys(breakdownsByDate[date]).reduce(
|
||||
(dateCosts, breakdown) => {
|
||||
// Group costs for items that belong to 'Other' in the chart.
|
||||
const cost = breakdownsByDate[date][breakdown];
|
||||
const breakdownCost =
|
||||
!isExpanded && otherCategoryIds.includes(breakdown)
|
||||
? { Other: (dateCosts.Other || 0) + cost }
|
||||
: { [product]: cost };
|
||||
return { ...dateCosts, ...productCost };
|
||||
: { [breakdown]: cost };
|
||||
return { ...dateCosts, ...breakdownCost };
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
@@ -135,40 +137,41 @@ export const CostOverviewByProductChart = ({
|
||||
},
|
||||
);
|
||||
|
||||
const sortedProducts = costsByProduct.sort(
|
||||
const sortedBreakdowns = costBreakdown.sort(
|
||||
(a, b) => aggregationSum(a.aggregation) - aggregationSum(b.aggregation),
|
||||
);
|
||||
|
||||
const renderAreas = () => {
|
||||
const separatedProducts = sortedProducts
|
||||
// Check that product is a separate group and hasn't been added to 'Other'
|
||||
const separatedBreakdowns = sortedBreakdowns
|
||||
// Check that the breakdown is a separate group and hasn't been added to 'Other'
|
||||
.filter(
|
||||
product =>
|
||||
product.id !== 'Other' && !otherProducts.includes(product.id),
|
||||
breakdown =>
|
||||
breakdown.id !== 'Other' && !otherCategoryIds.includes(breakdown.id),
|
||||
)
|
||||
.map(product => product.id);
|
||||
.map(breakdown => breakdown.id);
|
||||
// Keep 'Other' category at the bottom of the stack
|
||||
const productsToDisplay = isExpanded
|
||||
? sortedProducts.map(product => product.id)
|
||||
: ['Other', ...separatedProducts];
|
||||
const breakdownsToDisplay = isExpanded
|
||||
? sortedBreakdowns.map(breakdown => breakdown.id)
|
||||
: ['Other', ...separatedBreakdowns];
|
||||
|
||||
return productsToDisplay.map((product, i) => {
|
||||
// Logic to handle case where there are more products than data viz colors.
|
||||
const productColor =
|
||||
return breakdownsToDisplay.map((breakdown, i) => {
|
||||
// Logic to handle case where there are more items than data viz colors.
|
||||
const color =
|
||||
theme.palette.dataViz[
|
||||
(productsToDisplay.length - 1 - i) %
|
||||
(breakdownsToDisplay.length - 1 - i) %
|
||||
(theme.palette.dataViz.length - 1)
|
||||
];
|
||||
return (
|
||||
<Area
|
||||
dataKey={product}
|
||||
key={breakdown}
|
||||
dataKey={breakdown}
|
||||
isAnimationActive={false}
|
||||
stackId="1"
|
||||
stroke={productColor}
|
||||
fill={productColor}
|
||||
stroke={color}
|
||||
fill={color}
|
||||
onClick={() => setExpanded(true)}
|
||||
style={{
|
||||
cursor: product === 'Other' && !isExpanded ? 'pointer' : null,
|
||||
cursor: breakdown === 'Other' && !isExpanded ? 'pointer' : null,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -206,7 +209,7 @@ export const CostOverviewByProductChart = ({
|
||||
{items.reverse().map((item, index) => (
|
||||
<TooltipItem key={`${item.label}-${index}`} item={item} />
|
||||
))}
|
||||
{!isExpanded ? expandText : null}
|
||||
{canExpand && !isExpanded ? expandText : null}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { CostOverviewCard } from './CostOverviewCard';
|
||||
import { Cost } from '../../types';
|
||||
import {
|
||||
changeOf,
|
||||
getGroupedProducts,
|
||||
getGroupedProjects,
|
||||
MockAggregatedDailyCosts,
|
||||
trendlineOf,
|
||||
} from '../../utils/mockData';
|
||||
import {
|
||||
MockBillingDateProvider,
|
||||
MockConfigProvider,
|
||||
MockFilterProvider,
|
||||
MockScrollProvider,
|
||||
} from '../../utils/tests';
|
||||
import { CostInsightsThemeProvider } from '../CostInsightsPage/CostInsightsThemeProvider';
|
||||
|
||||
const mockGroupDailyCost: Cost = {
|
||||
id: 'test-group',
|
||||
aggregation: MockAggregatedDailyCosts,
|
||||
change: changeOf(MockAggregatedDailyCosts),
|
||||
trendline: trendlineOf(MockAggregatedDailyCosts),
|
||||
};
|
||||
|
||||
function renderInContext(children: JSX.Element) {
|
||||
return renderInTestApp(
|
||||
<CostInsightsThemeProvider>
|
||||
<MockConfigProvider>
|
||||
<MockFilterProvider>
|
||||
<MockBillingDateProvider>
|
||||
<MockScrollProvider>{children}</MockScrollProvider>
|
||||
</MockBillingDateProvider>
|
||||
</MockFilterProvider>
|
||||
</MockConfigProvider>
|
||||
</CostInsightsThemeProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('<CostOverviewCard/>', () => {
|
||||
it('Renders without exploding', async () => {
|
||||
const { getByText } = await renderInContext(
|
||||
<CostOverviewCard dailyCostData={mockGroupDailyCost} metricData={null} />,
|
||||
);
|
||||
expect(getByText('Cloud Cost')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Shows breakdown tabs if provided', async () => {
|
||||
const mockDailyCostWithBreakdowns = {
|
||||
...mockGroupDailyCost,
|
||||
groupedCosts: {
|
||||
product: getGroupedProducts('R2/P90D/2021-01-01'),
|
||||
project: getGroupedProjects('R2/P90D/2021-01-01'),
|
||||
},
|
||||
};
|
||||
const { getByText } = await renderInContext(
|
||||
<CostOverviewCard
|
||||
dailyCostData={mockDailyCostWithBreakdowns}
|
||||
metricData={null}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('Cloud Cost')).toBeInTheDocument();
|
||||
expect(getByText('Breakdown by product')).toBeInTheDocument();
|
||||
expect(getByText('Breakdown by project')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(getByText('Breakdown by product'));
|
||||
expect(getByText('Cloud Cost By Product')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(getByText('Breakdown by project'));
|
||||
expect(getByText('Cloud Cost By Project')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -14,22 +14,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
capitalize,
|
||||
Card,
|
||||
CardContent,
|
||||
Divider,
|
||||
useTheme,
|
||||
Tab,
|
||||
Tabs,
|
||||
useTheme,
|
||||
} from '@material-ui/core';
|
||||
import { CostOverviewChart } from './CostOverviewChart';
|
||||
import { CostOverviewByProductChart } from './CostOverviewByProductChart';
|
||||
import { CostOverviewBreakdownChart } from './CostOverviewBreakdownChart';
|
||||
import { CostOverviewHeader } from './CostOverviewHeader';
|
||||
import { MetricSelect } from '../MetricSelect';
|
||||
import { PeriodSelect } from '../PeriodSelect';
|
||||
import { useScroll, useFilters, useConfig } from '../../hooks';
|
||||
import { useConfig, useFilters, useScroll } from '../../hooks';
|
||||
import { mapFiltersToProps } from './selector';
|
||||
import { DefaultNavigation } from '../../utils/navigation';
|
||||
import { findAlways } from '../../utils/assert';
|
||||
@@ -49,6 +50,15 @@ export const CostOverviewCard = ({
|
||||
const config = useConfig();
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
|
||||
// Reset tabIndex if breakdowns available change
|
||||
useEffect(() => {
|
||||
// Intentionally off-by-one to account for the overview tab
|
||||
const lastIndex = Object.keys(dailyCostData.groupedCosts ?? {}).length;
|
||||
if (tabIndex > lastIndex) {
|
||||
setTabIndex(0);
|
||||
}
|
||||
}, [dailyCostData, tabIndex, setTabIndex]);
|
||||
|
||||
const { ScrollAnchor } = useScroll(DefaultNavigation.CostOverviewCard);
|
||||
const { setDuration, setProject, setMetric, ...filters } = useFilters(
|
||||
mapFiltersToProps,
|
||||
@@ -59,14 +69,18 @@ export const CostOverviewCard = ({
|
||||
: null;
|
||||
const styles = useOverviewTabsStyles(theme);
|
||||
|
||||
const breakdownTabs = Object.keys(dailyCostData.groupedCosts ?? {}).map(
|
||||
key => ({
|
||||
id: key,
|
||||
label: `Breakdown by ${key}`,
|
||||
title: `Cloud Cost By ${capitalize(key)}`,
|
||||
}),
|
||||
);
|
||||
const tabs = [
|
||||
{ id: 'overview', label: 'Total cost', title: 'Cloud Cost' },
|
||||
{
|
||||
id: 'breakdown',
|
||||
label: 'Breakdown by product',
|
||||
title: 'Cloud Cost By Product',
|
||||
},
|
||||
];
|
||||
].concat(breakdownTabs);
|
||||
// tabIndex can temporarily be invalid while the useEffect above processes
|
||||
const safeTabIndex = tabIndex > tabs.length - 1 ? 0 : tabIndex;
|
||||
|
||||
const OverviewTabs = () => {
|
||||
return (
|
||||
@@ -74,7 +88,7 @@ export const CostOverviewCard = ({
|
||||
<Tabs
|
||||
indicatorColor="primary"
|
||||
onChange={(_, index) => setTabIndex(index)}
|
||||
value={tabIndex}
|
||||
value={safeTabIndex}
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
<Tab
|
||||
@@ -91,27 +105,27 @@ export const CostOverviewCard = ({
|
||||
};
|
||||
|
||||
// Metrics can only be selected on the total cost graph
|
||||
const showMetricSelect = config.metrics.length && tabIndex === 0;
|
||||
const showMetricSelect = config.metrics.length && safeTabIndex === 0;
|
||||
|
||||
return (
|
||||
<Card style={{ position: 'relative' }}>
|
||||
<ScrollAnchor behavior="smooth" top={-20} />
|
||||
<CardContent>
|
||||
{dailyCostData.groupedCosts && <OverviewTabs />}
|
||||
<CostOverviewHeader title={tabs[tabIndex].title}>
|
||||
<CostOverviewHeader title={tabs[safeTabIndex].title}>
|
||||
<PeriodSelect duration={filters.duration} onSelect={setDuration} />
|
||||
</CostOverviewHeader>
|
||||
<Divider />
|
||||
<Box ml={2} my={1} display="flex" flexDirection="column">
|
||||
{tabIndex === 0 ? (
|
||||
{safeTabIndex === 0 ? (
|
||||
<CostOverviewChart
|
||||
dailyCostData={dailyCostData}
|
||||
metric={metric}
|
||||
metricData={metricData}
|
||||
/>
|
||||
) : (
|
||||
<CostOverviewByProductChart
|
||||
costsByProduct={dailyCostData.groupedCosts!}
|
||||
<CostOverviewBreakdownChart
|
||||
costBreakdown={dailyCostData.groupedCosts![tabs[safeTabIndex].id]}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -23,5 +23,5 @@ export interface Cost {
|
||||
aggregation: DateAggregation[];
|
||||
change?: ChangeStatistic;
|
||||
trendline?: Trendline;
|
||||
groupedCosts?: Cost[];
|
||||
groupedCosts?: Record<string, Cost[]>;
|
||||
}
|
||||
|
||||
@@ -1064,3 +1064,18 @@ export const getGroupedProducts = (intervals: string) => [
|
||||
aggregation: aggregationFor(intervals, 250),
|
||||
},
|
||||
];
|
||||
|
||||
export const getGroupedProjects = (intervals: string) => [
|
||||
{
|
||||
id: 'project-a',
|
||||
aggregation: aggregationFor(intervals, 1_700),
|
||||
},
|
||||
{
|
||||
id: 'project-b',
|
||||
aggregation: aggregationFor(intervals, 350),
|
||||
},
|
||||
{
|
||||
id: 'project-c',
|
||||
aggregation: aggregationFor(intervals, 1_300),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"fs-extra": "^9.0.0",
|
||||
"git-url-parse": "^11.4.3",
|
||||
"git-url-parse": "^11.4.4",
|
||||
"globby": "^11.0.0",
|
||||
"helmet": "^4.0.0",
|
||||
"isomorphic-git": "^1.8.0",
|
||||
|
||||
@@ -67,6 +67,22 @@ describe('AzurePreparer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the clone command with the correct arguments for a repository with a specified branch', async () => {
|
||||
await preparer.prepare({
|
||||
url:
|
||||
'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml&version=GBmaster',
|
||||
logger,
|
||||
workspacePath,
|
||||
});
|
||||
|
||||
expect(mockGitClient.clone).toHaveBeenCalledWith({
|
||||
url:
|
||||
'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo',
|
||||
dir: checkoutPath,
|
||||
ref: 'master',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the clone command with the correct arguments for a repository when no path is provided', async () => {
|
||||
await preparer.prepare({
|
||||
url:
|
||||
|
||||
@@ -48,6 +48,7 @@ export class AzurePreparer implements PreparerBase {
|
||||
|
||||
await git.clone({
|
||||
url: parsedGitUrl.toString('https'),
|
||||
ref: parsedGitUrl.ref,
|
||||
dir: checkoutPath,
|
||||
});
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ describe('BitbucketPreparer', () => {
|
||||
expect(mockGitClient.clone).toHaveBeenCalledWith({
|
||||
url: 'https://bitbucket.org/backstage-project/backstage-repo',
|
||||
dir: checkoutPath,
|
||||
ref: expect.any(String),
|
||||
});
|
||||
expect(fs.move).toHaveBeenCalledWith(checkoutPath, templatePath);
|
||||
expect(fs.rmdir).toHaveBeenCalledWith(resolve(templatePath, '.git'));
|
||||
@@ -80,6 +81,7 @@ describe('BitbucketPreparer', () => {
|
||||
expect(mockGitClient.clone).toHaveBeenCalledWith({
|
||||
url: 'https://bitbucket.org/backstage-project/backstage-repo',
|
||||
dir: checkoutPath,
|
||||
ref: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ export class BitbucketPreparer implements PreparerBase {
|
||||
await git.clone({
|
||||
url: parsedGitUrl.toString('https'),
|
||||
dir: checkoutPath,
|
||||
ref: parsedGitUrl.ref,
|
||||
});
|
||||
|
||||
await fs.move(fullPathToTemplate, targetPath);
|
||||
|
||||
@@ -54,6 +54,7 @@ describe('GitHubPreparer', () => {
|
||||
expect(mockGitClient.clone).toHaveBeenCalledWith({
|
||||
url: 'https://github.com/benjdlambert/backstage-graphql-template',
|
||||
dir: checkoutPath,
|
||||
ref: expect.any(String),
|
||||
});
|
||||
expect(fs.move).toHaveBeenCalledWith(
|
||||
resolve(checkoutPath, 'templates', 'graphql-starter', 'template'),
|
||||
@@ -73,6 +74,7 @@ describe('GitHubPreparer', () => {
|
||||
expect(mockGitClient.clone).toHaveBeenCalledWith({
|
||||
url: 'https://github.com/benjdlambert/backstage-graphql-template',
|
||||
dir: checkoutPath,
|
||||
ref: 'master',
|
||||
});
|
||||
expect(fs.move).toHaveBeenCalledWith(checkoutPath, templatePath);
|
||||
expect(fs.rmdir).toHaveBeenCalledWith(resolve(templatePath, '.git'));
|
||||
|
||||
@@ -47,6 +47,7 @@ export class GithubPreparer implements PreparerBase {
|
||||
await git.clone({
|
||||
url: parsedGitUrl.toString('https'),
|
||||
dir: checkoutPath,
|
||||
ref: parsedGitUrl.ref,
|
||||
});
|
||||
|
||||
await fs.move(fullPathToTemplate, targetPath);
|
||||
|
||||
@@ -50,8 +50,9 @@ describe('GitLabPreparer', () => {
|
||||
});
|
||||
|
||||
expect(mockGitClient.clone).toHaveBeenCalledWith({
|
||||
url: 'https://gitlab.com/benjdlambert/backstage-graphql-template',
|
||||
url: 'https://gitlab.com/benjdlambert/backstage-graphql-template.git',
|
||||
dir: checkoutPath,
|
||||
ref: expect.any(String),
|
||||
});
|
||||
|
||||
expect(Git.fromAuth).toHaveBeenCalledWith({
|
||||
|
||||
@@ -35,6 +35,7 @@ export class GitlabPreparer implements PreparerBase {
|
||||
checkoutPath,
|
||||
parsedGitUrl.filepath,
|
||||
);
|
||||
parsedGitUrl.git_suffix = true;
|
||||
|
||||
const git = this.config.token
|
||||
? Git.fromAuth({
|
||||
@@ -47,6 +48,7 @@ export class GitlabPreparer implements PreparerBase {
|
||||
await git.clone({
|
||||
url: parsedGitUrl.toString('https'),
|
||||
dir: checkoutPath,
|
||||
ref: parsedGitUrl.ref,
|
||||
});
|
||||
|
||||
await fs.move(fullPathToTemplate, targetPath);
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"@rjsf/core": "^2.4.0",
|
||||
"@rjsf/material-ui": "^2.4.0",
|
||||
"classnames": "^2.2.6",
|
||||
"git-url-parse": "^11.4.3",
|
||||
"git-url-parse": "^11.4.4",
|
||||
"moment": "^2.26.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
|
||||
@@ -5301,17 +5301,17 @@
|
||||
global "^4.3.2"
|
||||
regenerator-runtime "^0.13.7"
|
||||
|
||||
"@storybook/addons@6.1.14", "@storybook/addons@^6.1.11":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.14.tgz#2b81304bbe696923df95cdcf85cfc592d10f4065"
|
||||
integrity sha512-HlpmV7aejp/MeW8bo/WKME3i71gi0men9qcwoovjDjnSF6jXoNLT336a5udKXdHqYSZgzdyURlgLtilCWkWaJQ==
|
||||
"@storybook/addons@6.1.15", "@storybook/addons@^6.1.11":
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.15.tgz#09eb8d962f58bd20b4ac2f83b515831c83226352"
|
||||
integrity sha512-ENyHapLFOG93VaoQXPX8O3IWjLRyVBox9C9P20LMruKX/SfXAXx20qsoAWKKPGssopyOin17aoQX9pj+lFmCZQ==
|
||||
dependencies:
|
||||
"@storybook/api" "6.1.14"
|
||||
"@storybook/channels" "6.1.14"
|
||||
"@storybook/client-logger" "6.1.14"
|
||||
"@storybook/core-events" "6.1.14"
|
||||
"@storybook/router" "6.1.14"
|
||||
"@storybook/theming" "6.1.14"
|
||||
"@storybook/api" "6.1.15"
|
||||
"@storybook/channels" "6.1.15"
|
||||
"@storybook/client-logger" "6.1.15"
|
||||
"@storybook/core-events" "6.1.15"
|
||||
"@storybook/router" "6.1.15"
|
||||
"@storybook/theming" "6.1.15"
|
||||
core-js "^3.0.1"
|
||||
global "^4.3.2"
|
||||
regenerator-runtime "^0.13.7"
|
||||
@@ -5341,20 +5341,20 @@
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/api@6.1.14":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.14.tgz#20035dd336aba1c5a0f8c83c8c14a2edaf4db891"
|
||||
integrity sha512-gWcC/xEW8HL5DsocLujHBUdoNsl4YW1Zx1Y4SBbLCyrhj8v4JudJpylwJpOUBDe/GESXq1zqvNKvUPtI8DQNyw==
|
||||
"@storybook/api@6.1.15":
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.15.tgz#285ba42f7a8efcd3bd0e586a5e978487d826fbb4"
|
||||
integrity sha512-C4D08e2ZbSe62nNKtmh9YBraoWb2j6Chw8VCkuj91kuKHh3YDNc1gjj5Fi+KYZwIcy0EllzW3RFQs+YR1/Vg1g==
|
||||
dependencies:
|
||||
"@reach/router" "^1.3.3"
|
||||
"@storybook/channels" "6.1.14"
|
||||
"@storybook/client-logger" "6.1.14"
|
||||
"@storybook/core-events" "6.1.14"
|
||||
"@storybook/channels" "6.1.15"
|
||||
"@storybook/client-logger" "6.1.15"
|
||||
"@storybook/core-events" "6.1.15"
|
||||
"@storybook/csf" "0.0.1"
|
||||
"@storybook/router" "6.1.14"
|
||||
"@storybook/router" "6.1.15"
|
||||
"@storybook/semver" "^7.3.2"
|
||||
"@storybook/theming" "6.1.14"
|
||||
"@types/reach__router" "^1.3.5"
|
||||
"@storybook/theming" "6.1.15"
|
||||
"@types/reach__router" "^1.3.7"
|
||||
core-js "^3.0.1"
|
||||
fast-deep-equal "^3.1.1"
|
||||
global "^4.3.2"
|
||||
@@ -5379,14 +5379,14 @@
|
||||
qs "^6.6.0"
|
||||
telejson "^5.0.2"
|
||||
|
||||
"@storybook/channel-postmessage@6.1.14":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.14.tgz#41f3115895010dad9fb30f4ac381e4f904b1e50c"
|
||||
integrity sha512-If83dXXW9mKIRuvuWhWa/zkEw/F0FDgikp33x8436J3rWCh3recp27kffFRrKG0YDMpFSk/Ci5G47E9zn9SCjw==
|
||||
"@storybook/channel-postmessage@6.1.15":
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.15.tgz#80ea2346d18496f9710dd7f87fd2a9eca46ef36f"
|
||||
integrity sha512-Es4B5zpLrW28KSbY8FhGVEDgUnKspJ7wPuJyKExUpZ5L9w52RkTD6lRnVPzLUfoQ4luPsExy5fiuo878/Wc9ag==
|
||||
dependencies:
|
||||
"@storybook/channels" "6.1.14"
|
||||
"@storybook/client-logger" "6.1.14"
|
||||
"@storybook/core-events" "6.1.14"
|
||||
"@storybook/channels" "6.1.15"
|
||||
"@storybook/client-logger" "6.1.15"
|
||||
"@storybook/core-events" "6.1.15"
|
||||
core-js "^3.0.1"
|
||||
global "^4.3.2"
|
||||
qs "^6.6.0"
|
||||
@@ -5401,10 +5401,10 @@
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/channels@6.1.14":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.14.tgz#c479190ebb853a603f3ed90fc470534a02eb46eb"
|
||||
integrity sha512-vP19IB2FXj8SiFbQ9ETljEBienL+KRMLgMzz3Ta3nZj/OfjJJbIuj42ZfexQGV4mS0Bo+OW+qT7VMIY6fulnFw==
|
||||
"@storybook/channels@6.1.15":
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.15.tgz#22bb06a671a5ae09d2537bcf63aaf90d7f6b9f6b"
|
||||
integrity sha512-HIKHDeL/0BDk9a7xc2PLiFFoHjUMKUd2djhUGdeKgdKqoWejp4JJ60fI68+2QuSRbkB8k+rAwmuWJzV7EfB5fg==
|
||||
dependencies:
|
||||
core-js "^3.0.1"
|
||||
ts-dedent "^2.0.0"
|
||||
@@ -5434,16 +5434,16 @@
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/client-api@6.1.14":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.14.tgz#6daf56743cc72e13f05fff3d2ac554897cc9f9fd"
|
||||
integrity sha512-pIDSlS48bhJdtgNg7sXV1NmLJtB0ebRHJI9htIiqtL7EGQenb4+Bbwflhj1j51OEkuM+bQsAAZxq5deiUQEGVw==
|
||||
"@storybook/client-api@6.1.15":
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.15.tgz#8f8ead111459b94621571bdb2276f8a0aace17b1"
|
||||
integrity sha512-iwuDlgNdB6Y4OidlhWPob3tEIax9taymdKEe9by4rLJ3nfXu7viHcvCAjN24oI4NFW3NZsmtqJotgftRYk0r1Q==
|
||||
dependencies:
|
||||
"@storybook/addons" "6.1.14"
|
||||
"@storybook/channel-postmessage" "6.1.14"
|
||||
"@storybook/channels" "6.1.14"
|
||||
"@storybook/client-logger" "6.1.14"
|
||||
"@storybook/core-events" "6.1.14"
|
||||
"@storybook/addons" "6.1.15"
|
||||
"@storybook/channel-postmessage" "6.1.15"
|
||||
"@storybook/channels" "6.1.15"
|
||||
"@storybook/client-logger" "6.1.15"
|
||||
"@storybook/core-events" "6.1.15"
|
||||
"@storybook/csf" "0.0.1"
|
||||
"@types/qs" "^6.9.0"
|
||||
"@types/webpack-env" "^1.15.3"
|
||||
@@ -5466,10 +5466,10 @@
|
||||
core-js "^3.0.1"
|
||||
global "^4.3.2"
|
||||
|
||||
"@storybook/client-logger@6.1.14":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.14.tgz#216b9c1332ffa3a3473dad837780a3b14f686bae"
|
||||
integrity sha512-NSO8nVsp6o0eoQ1Drlu66KXpl6DPuq02Kj8AhttGzvqSYB50SV4CV+wceBcg77tIVu5QmQ+71hAEVXhx7sjRHA==
|
||||
"@storybook/client-logger@6.1.15":
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.15.tgz#b558d6ecbee82c038d684717d8c598eaa4a9324d"
|
||||
integrity sha512-lUpatG8SxzrUapWMsIPWiR+5qRVT5ebn8tGHQeBeRHXbdmEqyq5DOlrotLUemkA5nNTCs1pMFNvKSpCHznG+fg==
|
||||
dependencies:
|
||||
core-js "^3.0.1"
|
||||
global "^4.3.2"
|
||||
@@ -5500,15 +5500,15 @@
|
||||
react-textarea-autosize "^8.1.1"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/components@6.1.14":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.14.tgz#4ea47edfa0a3e4a26882aa5a1eb90c1ec86e6f71"
|
||||
integrity sha512-Nxsp/9o1tqfY8s6RBWNHyM03A5D9k56Kr/4VNa++CbDrz1+TIxpYlDgS4sllUlXyvICLfk3sUtg3KS5CPl2iZA==
|
||||
"@storybook/components@6.1.15":
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.15.tgz#b4a2af23ee6b9cba4c255191eae3d3463e29bfb7"
|
||||
integrity sha512-lPbA/zyBfctdlpDhRTcRFLWlZPJ3PB4+wI0FUvYs69iG3/bNbQPYu8vRmNhCZOsaGt+b+dik4Tfcth8Bu+eQug==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.5.4"
|
||||
"@storybook/client-logger" "6.1.14"
|
||||
"@storybook/client-logger" "6.1.15"
|
||||
"@storybook/csf" "0.0.1"
|
||||
"@storybook/theming" "6.1.14"
|
||||
"@storybook/theming" "6.1.15"
|
||||
"@types/overlayscrollbars" "^1.9.0"
|
||||
"@types/react-color" "^3.0.1"
|
||||
"@types/react-syntax-highlighter" "11.0.4"
|
||||
@@ -5533,17 +5533,17 @@
|
||||
dependencies:
|
||||
core-js "^3.0.1"
|
||||
|
||||
"@storybook/core-events@6.1.14":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.14.tgz#a3165e32cefd6be7326bbad4b8140653bdfa0426"
|
||||
integrity sha512-tpM3VDvzqgRY7S17CRglgt1625rxNoyEwrLQiNcZkUPyO0rpaacPqVEbPCtcTmUeboI1bLdnSQIjT9B0/Y2Pww==
|
||||
"@storybook/core-events@6.1.15":
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.15.tgz#f66e30cbed8afdb8df2254d2aa47fe139e641c60"
|
||||
integrity sha512-2sz02hdGZshanoq83jaB+goAcapVEWrxe+RJZn/gu2OymlEioWNjPPtOVGgi5DNIiJFnYvc66adayNwX39+tDA==
|
||||
dependencies:
|
||||
core-js "^3.0.1"
|
||||
|
||||
"@storybook/core@6.1.14":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/core/-/core-6.1.14.tgz#17e724a5b94d6e1bb557e213b8176660d2d14762"
|
||||
integrity sha512-lHKZmfLAo2VGtF/yrZkkWMYgmFRNKbzIDxYJGp8USyUQyTfEpz2qqJlBdoD6rxr1hFPM2954tIKwh8iPhT2PFQ==
|
||||
"@storybook/core@6.1.15":
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/core/-/core-6.1.15.tgz#7ff8c314d3857497bf2e26c69a1fa93ef37301aa"
|
||||
integrity sha512-mQeKAXcowUwF+pOdWZEFwb5M6sz4yv5cOv1vTci3/1pMmB8QpYlH+P61p4lsRO17Vlak70h18TworPka/4+mhA==
|
||||
dependencies:
|
||||
"@babel/core" "^7.12.3"
|
||||
"@babel/plugin-proposal-class-properties" "^7.12.1"
|
||||
@@ -5567,20 +5567,20 @@
|
||||
"@babel/preset-react" "^7.12.1"
|
||||
"@babel/preset-typescript" "^7.12.1"
|
||||
"@babel/register" "^7.12.1"
|
||||
"@storybook/addons" "6.1.14"
|
||||
"@storybook/api" "6.1.14"
|
||||
"@storybook/channel-postmessage" "6.1.14"
|
||||
"@storybook/channels" "6.1.14"
|
||||
"@storybook/client-api" "6.1.14"
|
||||
"@storybook/client-logger" "6.1.14"
|
||||
"@storybook/components" "6.1.14"
|
||||
"@storybook/core-events" "6.1.14"
|
||||
"@storybook/addons" "6.1.15"
|
||||
"@storybook/api" "6.1.15"
|
||||
"@storybook/channel-postmessage" "6.1.15"
|
||||
"@storybook/channels" "6.1.15"
|
||||
"@storybook/client-api" "6.1.15"
|
||||
"@storybook/client-logger" "6.1.15"
|
||||
"@storybook/components" "6.1.15"
|
||||
"@storybook/core-events" "6.1.15"
|
||||
"@storybook/csf" "0.0.1"
|
||||
"@storybook/node-logger" "6.1.14"
|
||||
"@storybook/router" "6.1.14"
|
||||
"@storybook/node-logger" "6.1.15"
|
||||
"@storybook/router" "6.1.15"
|
||||
"@storybook/semver" "^7.3.2"
|
||||
"@storybook/theming" "6.1.14"
|
||||
"@storybook/ui" "6.1.14"
|
||||
"@storybook/theming" "6.1.15"
|
||||
"@storybook/ui" "6.1.15"
|
||||
"@types/glob-base" "^0.3.0"
|
||||
"@types/micromatch" "^4.0.1"
|
||||
"@types/node-fetch" "^2.5.4"
|
||||
@@ -5654,10 +5654,10 @@
|
||||
dependencies:
|
||||
lodash "^4.17.15"
|
||||
|
||||
"@storybook/node-logger@6.1.14":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.1.14.tgz#e5294f986e3ec5c67b2738895b9d16c9a2b667fa"
|
||||
integrity sha512-3jrw7coAwFXZu4qK1vm54bCPhNRvxjG+7jISbhhocDoNIv0nLWL3+tJyrC5/k/XHQiUlLkhEzpMaASADmkttNw==
|
||||
"@storybook/node-logger@6.1.15":
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.1.15.tgz#fcf786d3a323feb6821e40e26f98a513a60d1a79"
|
||||
integrity sha512-lrO0ei3W7BRci2iUkWTr/rXgHkzxwZTrlkx0iBzbQQRy7K1AJ9bjzhurCH9B8C9XGLmn60LXT81RWD3iCLZjcw==
|
||||
dependencies:
|
||||
"@types/npmlog" "^4.1.2"
|
||||
chalk "^4.0.0"
|
||||
@@ -5666,16 +5666,16 @@
|
||||
pretty-hrtime "^1.0.3"
|
||||
|
||||
"@storybook/react@^6.1.11":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/react/-/react-6.1.14.tgz#436e9b90096b1d7c83f7f073b5baf47212b2e425"
|
||||
integrity sha512-M99wHjc/5z+Wz1FdFaScVs6dyAi/6PdcIx5Fyip6Qd8aKwm1XyYoOMql5Vu3Cf560feDYCKS4phzyEZ7EJy+EQ==
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/react/-/react-6.1.15.tgz#e17d00b05b8980ad381ba701805309ed46d1fdcd"
|
||||
integrity sha512-7WoYLOZuAlzgQsL9oy4JCr9NcB4NBCuxslPSncN5l/7ewGXgfVXTAOMOfw+EVNrtUeVJU2fC8gFiHVl0SJpTZw==
|
||||
dependencies:
|
||||
"@babel/preset-flow" "^7.12.1"
|
||||
"@babel/preset-react" "^7.12.1"
|
||||
"@pmmmwh/react-refresh-webpack-plugin" "^0.4.2"
|
||||
"@storybook/addons" "6.1.14"
|
||||
"@storybook/core" "6.1.14"
|
||||
"@storybook/node-logger" "6.1.14"
|
||||
"@storybook/addons" "6.1.15"
|
||||
"@storybook/core" "6.1.15"
|
||||
"@storybook/node-logger" "6.1.15"
|
||||
"@storybook/semver" "^7.3.2"
|
||||
"@types/webpack-env" "^1.15.3"
|
||||
babel-plugin-add-react-displayname "^0.0.5"
|
||||
@@ -5704,13 +5704,13 @@
|
||||
memoizerific "^1.11.3"
|
||||
qs "^6.6.0"
|
||||
|
||||
"@storybook/router@6.1.14":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/router/-/router-6.1.14.tgz#f6aef8c9dabf19bf06dddd80907e66369261fdde"
|
||||
integrity sha512-rMaUCYzgfVLwFWo3A1Q/weSv8FBqCLmHY+3+t6ao7OV6NYjR0XgLKRzHrXq1uYdbMxWeIKhN2tIt/LR43bmDjQ==
|
||||
"@storybook/router@6.1.15":
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/router/-/router-6.1.15.tgz#e0cd7440a2ddc9b265e506b1cb590d3eeab56476"
|
||||
integrity sha512-HlxDkGpiTSxXCJuqRoZ9Viq6Y/h/7efI8LPhhopr50qWRBTh/PEQzDqWBXG3sj8ISmi9GyUaTSAuqRwdA3lJQQ==
|
||||
dependencies:
|
||||
"@reach/router" "^1.3.3"
|
||||
"@types/reach__router" "^1.3.5"
|
||||
"@types/reach__router" "^1.3.7"
|
||||
core-js "^3.0.1"
|
||||
global "^4.3.2"
|
||||
memoizerific "^1.11.3"
|
||||
@@ -5759,15 +5759,15 @@
|
||||
resolve-from "^5.0.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/theming@6.1.14":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.1.14.tgz#fecb66cab22d3b3218b4a98a9c210eb8a7be91e8"
|
||||
integrity sha512-S+t30y4FqBTXWoVr+dtxVJ/ywiQGHBclBd9aUunbdCV4mMFra5InNo2CWn+RJlNEauLZ93gRIEzSFchIbzLk1A==
|
||||
"@storybook/theming@6.1.15":
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.1.15.tgz#01083ab89904dd959429b0b3fd1c76bd0ecc59ef"
|
||||
integrity sha512-88IdYaPzp4NMKf/GKBrPggxD6/d/lkdQ4SNowXxN9g9eONd9M7HtTbjuJGRCbGMJ52xGcbpj2exEnAqKQ2iodA==
|
||||
dependencies:
|
||||
"@emotion/core" "^10.1.1"
|
||||
"@emotion/is-prop-valid" "^0.8.6"
|
||||
"@emotion/styled" "^10.0.23"
|
||||
"@storybook/client-logger" "6.1.14"
|
||||
"@storybook/client-logger" "6.1.15"
|
||||
core-js "^3.0.1"
|
||||
deep-object-diff "^1.1.0"
|
||||
emotion-theming "^10.0.19"
|
||||
@@ -5777,21 +5777,21 @@
|
||||
resolve-from "^5.0.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/ui@6.1.14":
|
||||
version "6.1.14"
|
||||
resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.1.14.tgz#766d696480ee6f6a5a0454ccb2f101c38a0eb9d2"
|
||||
integrity sha512-DTW2TM05jTMKxh8LzUGk3g5a528PgJxrtgODFU6zzwSg2+LwdmSDtd1HAxopt2vpfTyQyX+6WN2H+lMNwfQTAQ==
|
||||
"@storybook/ui@6.1.15":
|
||||
version "6.1.15"
|
||||
resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.1.15.tgz#a0f6c49fcf81cf172cd2de4c8dba2be1296891f6"
|
||||
integrity sha512-quyhJWlOxhk95he7s5/TSYM3eEsaz3s4+98kUZE6r3ssME8u6zDvqa/qa6EWs5/nvZ2V3+12efIzCNbiiT3v3g==
|
||||
dependencies:
|
||||
"@emotion/core" "^10.1.1"
|
||||
"@storybook/addons" "6.1.14"
|
||||
"@storybook/api" "6.1.14"
|
||||
"@storybook/channels" "6.1.14"
|
||||
"@storybook/client-logger" "6.1.14"
|
||||
"@storybook/components" "6.1.14"
|
||||
"@storybook/core-events" "6.1.14"
|
||||
"@storybook/router" "6.1.14"
|
||||
"@storybook/addons" "6.1.15"
|
||||
"@storybook/api" "6.1.15"
|
||||
"@storybook/channels" "6.1.15"
|
||||
"@storybook/client-logger" "6.1.15"
|
||||
"@storybook/components" "6.1.15"
|
||||
"@storybook/core-events" "6.1.15"
|
||||
"@storybook/router" "6.1.15"
|
||||
"@storybook/semver" "^7.3.2"
|
||||
"@storybook/theming" "6.1.14"
|
||||
"@storybook/theming" "6.1.15"
|
||||
"@types/markdown-to-jsx" "^6.11.0"
|
||||
copy-to-clipboard "^3.0.8"
|
||||
core-js "^3.0.1"
|
||||
@@ -7093,6 +7093,13 @@
|
||||
"@types/history" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/reach__router@^1.3.7":
|
||||
version "1.3.7"
|
||||
resolved "https://registry.npmjs.org/@types/reach__router/-/reach__router-1.3.7.tgz#de8ab374259ae7f7499fc1373b9697a5f3cd6428"
|
||||
integrity sha512-cyBEb8Ef3SJNH5NYEIDGPoMMmYUxROatuxbICusVRQIqZUB85UCt6R2Ok60tKS/TABJsJYaHyNTW3kqbpxlMjg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-color@^3.0.1":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.4.tgz#c63daf012ad067ac0127bdd86725f079d02082bd"
|
||||
@@ -10780,9 +10787,9 @@ core-js-pure@^3.0.0, core-js-pure@^3.0.1:
|
||||
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
|
||||
|
||||
core-js@3, core-js@^3.0.1, core-js@^3.0.4, core-js@^3.5.0, core-js@^3.6.0, core-js@^3.6.5:
|
||||
version "3.6.5"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
|
||||
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
|
||||
version "3.8.3"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0"
|
||||
integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==
|
||||
|
||||
core-js@^2.4.0, core-js@^2.5.7, core-js@^2.6.10, core-js@^2.6.5:
|
||||
version "2.6.11"
|
||||
@@ -14173,10 +14180,17 @@ git-url-parse@^11.1.2:
|
||||
dependencies:
|
||||
git-up "^4.0.0"
|
||||
|
||||
git-url-parse@^11.4.3:
|
||||
version "11.4.3"
|
||||
resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.3.tgz#1610284edf1f14964180f5b3399ec68b692cfd87"
|
||||
integrity sha512-LZTTk0nqJnKN48YRtOpR8H5SEfp1oM2tls90NuZmBxN95PnCvmuXGzqQ4QmVirBgKx2KPYfPGteX3/raWjKenQ==
|
||||
git-url-parse@^11.4.4:
|
||||
version "11.4.4"
|
||||
resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77"
|
||||
integrity sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw==
|
||||
dependencies:
|
||||
git-up "^4.0.0"
|
||||
|
||||
git-url-parse@^11.4.4:
|
||||
version "11.4.4"
|
||||
resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77"
|
||||
integrity sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw==
|
||||
dependencies:
|
||||
git-up "^4.0.0"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user