Merge remote-tracking branch 'upstream/master' into master
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/core-api': patch
|
||||
---
|
||||
|
||||
Fix for GitHub and SAML auth not properly updating session state when already logged in.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
GitLab preparer uses the right token (primarily the same one as the publisher, falling back to the integrations token)
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/create-app': minor
|
||||
---
|
||||
|
||||
Add search plugin to default template for CLI created apps
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
'@backstage/test-utils': patch
|
||||
'@backstage/plugin-graphiql': patch
|
||||
---
|
||||
|
||||
Update to use new plugin extension API
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/core-api': patch
|
||||
---
|
||||
|
||||
Introduce new plugin extension API
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Added configuration schema for the commonly used properties
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Clearer error message when preparer or publisher type can't be determined.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Bumping the version for `create-app` so that we can use the latest versions of internal packages and rebuild the version which is passed to the package.json
|
||||
@@ -36,6 +36,7 @@ Codecov
|
||||
codehilite
|
||||
Codehilite
|
||||
codeowners
|
||||
composability
|
||||
composable
|
||||
config
|
||||
Config
|
||||
@@ -49,6 +50,7 @@ dataflow
|
||||
deadnaming
|
||||
destructured
|
||||
dev
|
||||
devops
|
||||
devs
|
||||
dhenneke
|
||||
discoverability
|
||||
@@ -62,7 +64,6 @@ Docusaurus
|
||||
Dominik
|
||||
dtuite
|
||||
dzolotusky
|
||||
eg
|
||||
Ek
|
||||
env
|
||||
Env
|
||||
@@ -134,12 +135,14 @@ neuro
|
||||
newrelic
|
||||
nginx
|
||||
Niklas
|
||||
nodegit
|
||||
nohoist
|
||||
nonces
|
||||
npm
|
||||
nvarchar
|
||||
nvm
|
||||
oauth
|
||||
Oauth
|
||||
OAuth
|
||||
oidc
|
||||
Okta
|
||||
Oldsberg
|
||||
@@ -158,7 +161,6 @@ prebaked
|
||||
preconfigured
|
||||
prepack
|
||||
Preprarer
|
||||
Prerequisities
|
||||
productional
|
||||
Protobuf
|
||||
proxying
|
||||
@@ -192,7 +194,6 @@ semlas
|
||||
semver
|
||||
Serverless
|
||||
Sinon
|
||||
smartsymobls
|
||||
Snyk
|
||||
sourcemaps
|
||||
sparklines
|
||||
@@ -219,7 +220,6 @@ Templater
|
||||
templaters
|
||||
Templaters
|
||||
Thauer
|
||||
theres
|
||||
toc
|
||||
tolerations
|
||||
Tolerations
|
||||
@@ -237,6 +237,7 @@ upvote
|
||||
url
|
||||
utils
|
||||
validators
|
||||
varchar
|
||||
Voi
|
||||
Wealthsimple
|
||||
Weaveworks
|
||||
|
||||
@@ -11,6 +11,11 @@ on:
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#scanning-pull-requests
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.yml'
|
||||
- '**/*.yaml'
|
||||
schedule:
|
||||
- cron: '0 8 * * 6'
|
||||
|
||||
@@ -36,11 +41,6 @@ jobs:
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
|
||||
@@ -51,10 +51,13 @@ jobs:
|
||||
- name: test
|
||||
run: yarn lerna -- run test
|
||||
|
||||
# credit: https://github.com/appleboy/discord-action/issues/3#issuecomment-731426861
|
||||
- name: Discord notification
|
||||
if: ${{ failure() }}
|
||||
uses: Ilshidur/action-discord@0.2.0
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
with:
|
||||
args: 'Windows master build failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}'
|
||||
run: |
|
||||
$MESSAGE=@"
|
||||
{\"content\": \"Windows master build failed https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}\"}
|
||||
"@
|
||||
C:\msys64\usr\bin\curl.exe -i -H "Accept: application/json" -H "Content-Type:application/json" -X POST $env:DISCORD_WEBHOOK --data $MESSAGE
|
||||
|
||||
@@ -70,21 +70,6 @@ jobs:
|
||||
bash <(curl -s https://codecov.io/bash) -f packages/core/coverage/* -F core
|
||||
bash <(curl -s https://codecov.io/bash) -f packages/core-api/coverage/* -F core-api
|
||||
|
||||
# Publishes current version of packages that are not already present in the registry
|
||||
- name: publish
|
||||
if: matrix.node-version == '12.x'
|
||||
run: yarn lerna -- publish from-package --yes
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
# Tags the commit with the version in the core package if the tag doesn't exist
|
||||
- uses: Klemensas/action-autotag@1.2.3
|
||||
if: matrix.node-version == '12.x'
|
||||
with:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
package_root: 'packages/core'
|
||||
tag_prefix: 'v'
|
||||
|
||||
- name: Discord notification
|
||||
if: ${{ failure() }}
|
||||
uses: Ilshidur/action-discord@0.2.0
|
||||
@@ -92,3 +77,85 @@ jobs:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
with:
|
||||
args: 'Master build failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}'
|
||||
|
||||
# A separate release build that is only run for commits that are the result of merging the "Version Packages" PR
|
||||
# We can't re-use the output from the above step, but we'll have a guaranteed node_modules cache and
|
||||
# only run the build steps that are necessary for publishing
|
||||
release:
|
||||
if: contains(github.event.commits.*.author.username, 'backstage-service') && contains(github.event.head_commit.message, 'from backstage/changeset-release/master')
|
||||
needs: build
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
|
||||
env:
|
||||
CI: 'true'
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
|
||||
- name: use node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
- name: cache all node_modules
|
||||
id: cache-modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
|
||||
- name: find location of global yarn cache
|
||||
id: yarn-cache
|
||||
if: steps.cache-modules.outputs.cache-hit != 'true'
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: cache global yarn cache
|
||||
uses: actions/cache@v2
|
||||
if: steps.cache-modules.outputs.cache-hit != 'true'
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
# End of yarn setup
|
||||
|
||||
- name: build type declarations
|
||||
run: yarn tsc:full
|
||||
|
||||
- name: build packages
|
||||
run: yarn lerna -- run --ignore example-app build
|
||||
|
||||
# Publishes current version of packages that are not already present in the registry
|
||||
- name: publish
|
||||
run: yarn lerna -- publish from-package --yes
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
# Creates the next available tag with format "release-<year>-<month>-<day>[.<n>]"
|
||||
- name: Create a release tag
|
||||
id: create_tag
|
||||
run: node scripts/create-release-tag.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
|
||||
|
||||
# Convert the newly created tag into a release with changelog information
|
||||
- name: Create release on GitHub
|
||||
run: node scripts/create-github-release.js ${{ steps.create_tag.outputs.tag_name }} 1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
|
||||
|
||||
# Notify everyone about this great new release :D
|
||||
- name: Discord notification
|
||||
uses: Ilshidur/action-discord@0.2.0
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
||||
TAG_NAME: ${{ steps.create_tag.outputs.tag_name }}
|
||||
with:
|
||||
args: 'A new release has been published! https://github.com/backstage/backstage/releases/tag/{{TAG_NAME}}'
|
||||
|
||||
@@ -55,6 +55,7 @@ jobs:
|
||||
|
||||
- name: tsc
|
||||
run: yarn tsc
|
||||
|
||||
- name: build
|
||||
run: yarn build
|
||||
|
||||
|
||||
@@ -16,3 +16,4 @@
|
||||
| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks |
|
||||
| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. |
|
||||
| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. |
|
||||
| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. |
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ Harassment includes, but is not limited to:
|
||||
- Offensive comments related to gender, gender identity and expression, sexual orientation, disability, mental illness, neuro(a)typicality, physical appearance, body size, race, age, regional discrimination, political or religious affiliation
|
||||
- Unwelcome comments regarding a person’s lifestyle choices and practices, including those related to food, health, parenting, drugs, and employment
|
||||
- Deliberate misgendering. This includes deadnaming or persistently using a pronoun that does not correctly reflect a person's gender identity. You must address people by the name they give you when not addressing them by their username or handle
|
||||
- Physical contact and simulated physical contact (eg, textual descriptions like “_hug_” or “_backrub_”) without consent or after a request to stop
|
||||
- Physical contact and simulated physical contact (e.g., textual descriptions like “_hug_” or “_backrub_”) without consent or after a request to stop
|
||||
- Threats of violence, both physical and psychological
|
||||
- Incitement of violence towards any individual, including encouraging a person to commit suicide or to engage in self-harm
|
||||
- Deliberate intimidation
|
||||
|
||||
+33
-21
@@ -6,19 +6,19 @@ Therefore we want to create strong community of contributors -- all working toge
|
||||
|
||||
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. ❤️
|
||||
|
||||
Backstage is released under the Apache2.0 License, and original creations contributed to this repo are accepted under the same license.
|
||||
Backstage is released under the Apache 2.0 License, and original creations contributed to this repo are accepted under the same license.
|
||||
|
||||
# Types of Contributions
|
||||
## Types of Contributions
|
||||
|
||||
## Report bugs
|
||||
### Report bugs
|
||||
|
||||
No one likes bugs. Report bugs as an issue [here](https://github.com/backstage/backstage/issues/new?template=bug_template.md).
|
||||
|
||||
## Fix bugs or build new features
|
||||
### Fix bugs or build new features
|
||||
|
||||
Look through the GitHub issues for [bugs](https://github.com/backstage/backstage/labels/bugs), [good first issues](https://github.com/backstage/backstage/labels/good%20first%20issue) or [help wanted](https://github.com/backstage/backstage/labels/help%20wanted).
|
||||
|
||||
## Build a plugin
|
||||
### Build a plugin
|
||||
|
||||
The value of Backstage grows with every new plugin that gets added. Wouldn't it be fantastic if there was a plugin for every infrastructure project out there? We think so. And we would love your help.
|
||||
|
||||
@@ -26,27 +26,27 @@ A great reference example of a plugin can be found on [our blog](https://backsta
|
||||
|
||||
What kind of plugins should/could be created? Some inspiration from the 120+ plugins that we have developed inside Spotify can be found [here](https://backstage.io/demos), but we will keep a running list of suggestions labeled with [[plugin]](https://github.com/backstage/backstage/labels/plugin).
|
||||
|
||||
## Suggesting a plugin
|
||||
### Suggesting a plugin
|
||||
|
||||
If you start developing a plugin that you aim to release as open source, we suggest that you create a [new Issue](https://github.com/backstage/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). This helps the community know what plugins are in development.
|
||||
|
||||
You can also use this process if you have an idea for a good plugin but you hope that someone else will pick up the work.
|
||||
|
||||
## Adding Non-code Contributions
|
||||
### Adding Non-code Contributions
|
||||
|
||||
Since there is such a large landscape of possible development, build, and deployment environments, we welcome community contributions in these areas in the [`/contrib`](https://github.com/backstage/backstage/tree/master/contrib) folder of the project. This is an excellent place to put things that help out the community at large, but which may not fit within the scope of the core product to support natively. Here, you will find Helm charts, alternative Docker images, and much more.
|
||||
|
||||
## Write Documentation
|
||||
### Write Documentation
|
||||
|
||||
The current documentation is very limited. Help us make the `/docs` folder come alive.
|
||||
|
||||
## Contribute to Storybook
|
||||
### Contribute to Storybook
|
||||
|
||||
We think the best way to ensure different plugins provide a consistent experience is through a solid set of reusable UI/UX components. Backstage uses [Storybook](http://backstage.io/storybook).
|
||||
|
||||
Either help us [create new components](https://github.com/backstage/backstage/labels/help%20wanted) or improve stories for the existing ones (look for files with `*.stories.tsx`).
|
||||
|
||||
## Submit Feedback
|
||||
### Submit Feedback
|
||||
|
||||
The best way to send feedback is to file [an issue](https://github.com/backstage/backstage/issues).
|
||||
|
||||
@@ -58,47 +58,59 @@ If you are proposing a feature:
|
||||
- Remember that this is a volunteer-driven project, and that contributions
|
||||
are welcome :)
|
||||
|
||||
## Add your company to ADOPTERS
|
||||
### Add your company to ADOPTERS
|
||||
|
||||
Have you started using Backstage? Adding your company to [ADOPTERS](ADOPTERS.md) really helps the project.
|
||||
|
||||
# Get Started!
|
||||
## Get Started!
|
||||
|
||||
So...feel ready to jump in? Let's do this. 👏🏻💯
|
||||
|
||||
Start by reading our [Getting Started](https://backstage.io/docs/getting-started/) page. If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2).
|
||||
|
||||
# Coding Guidelines
|
||||
## Coding Guidelines
|
||||
|
||||
All code is formatted with `prettier` using the configuration in the repo. If possible we recommend configuring your editor to format automatically, but you can also use the `yarn prettier --write <file>` command to format files.
|
||||
|
||||
A consistent coding style is included via [EditorConfig](https://editorconfig.org/) with the file [`.editorconfig`](.editorconfig) at the root of the repo. Depending on your editor of choice, it will either support it out of the box or you can [download a plugin](https://editorconfig.org/#download) for the config to be applied.
|
||||
|
||||
If you're contributing to the backend or CLI tooling, be mindful of cross-platform support. [This](https://shapeshed.com/writing-cross-platform-node/) blog post is a good guide of what to keep in mind when writing cross-platform NodeJS.
|
||||
|
||||
Also be sure to skim through our [ADRs](https://github.com/backstage/backstage/tree/master/docs/architecture-decisions) to see if they cover what you're working on. In particular [ADR006: Avoid React.FC and React.SFC](https://github.com/backstage/backstage/blob/master/docs/architecture-decisions/adr006-avoid-react-fc.md) is one to look out for.
|
||||
|
||||
If there are any updates in `markdown` file please make sure to run `yarn run lint:docs`. Though it is checked on `lint-staged`. It is required to install [vale](https://docs.errata.ai/vale/install) separately and make sure it is accessed by global command.
|
||||
|
||||
# Creating Changesets
|
||||
## Creating Changesets
|
||||
|
||||
We use [changesets](https://github.com/atlassian/changesets) to help us prepare releases. It helps us make sure that every package affected by a change gets a proper version number and an entry in its `CHANGELOG.md`. To make the process of generating releases easy. it helps when contributors include changesets with their pull requests.
|
||||
We use [changesets](https://github.com/atlassian/changesets) to help us prepare releases. They help us make sure that every package affected by a change gets a proper version number and an entry in its `CHANGELOG.md`. To make the process of generating releases easy, it helps when contributors include changesets with their pull requests.
|
||||
|
||||
## To create a changeset
|
||||
### When to use a changeset?
|
||||
|
||||
Any time a patch, minor, or major change aligning to [Semantic Versioning](https://semver.org) is made to any published package in `packages/` or `plugins/`, a changeset should be used. It helps to align your change to the [Backstage stability index](https://backstage.io/docs/overview/stability-index) for the package you are changing, for example, when to provide additional clarity on deprecation or impacting changes which will then be included into CHANGELOGs.
|
||||
|
||||
In general, changesets are not needed for the documentation, build utilities, contributed samples in `contrib/`, or the [example `packages/app`](packages/app).
|
||||
|
||||
### How to create a changeset
|
||||
|
||||
1. Run `yarn changeset`
|
||||
2. Select which packages you want to include a changeset for
|
||||
3. Select impact of change that you're introducing (minor, major or patch)
|
||||
3. Select impact of change that you're introducing (patch, minor, or major)
|
||||
4. Add generated changeset to Git
|
||||
5. Push the commit with your changeset to the branch associated with your PR
|
||||
6. Accept our gratitude for making the release process easier on the maintainer
|
||||
6. Accept our gratitude for making the release process easier on the maintainers
|
||||
|
||||
For more information, checkout [adding a changeset](https://github.com/atlassian/changesets/blob/master/docs/adding-a-changeset.md) documentation in changesets repository.
|
||||
For more information, checkout [adding a changeset](https://github.com/atlassian/changesets/blob/master/docs/adding-a-changeset.md) documentation in the changesets repository.
|
||||
|
||||
# Code of Conduct
|
||||
## Merging to Master
|
||||
|
||||
For those contributors who have earned write access to the repository, when a pull request is approved, in general we prefer the author of the PR to perform the merge themselves. This allows them to own accountability for the change and they likely know best how or when to address pending fixes or additional follow-ups. In this way, we all help contribute to the project's successful outcomes.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project adheres to the [Spotify FOSS Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code.
|
||||
|
||||
[code-of-conduct]: https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md
|
||||
|
||||
# Security Issues?
|
||||
## Security Issues?
|
||||
|
||||
See [SECURITY](SECURITY.md).
|
||||
|
||||
@@ -10,13 +10,9 @@
|
||||
[](https://codecov.io/gh/backstage/backstage)
|
||||
[](https://github.com/backstage/backstage/releases)
|
||||
|
||||
> We recently moved from `spotify/backstage`, update your remotes!
|
||||
>
|
||||
> `git remote set-url origin git@github.com:backstage/backstage.git`
|
||||
|
||||
## What is Backstage?
|
||||
|
||||
[Backstage](https://backstage.io/) is an open platform for building developer portals. Powered by a centralized service catalog, Backstage restores order to your microservices and infrastructure. So your product teams can ship high-quality code quickly — without compromising autonomy.
|
||||
[Backstage](https://backstage.io/) is an open platform for building developer portals. Powered by a centralized service catalog, Backstage restores order to your microservices and infrastructure and enables your product teams to ship high-quality code quickly — without compromising autonomy.
|
||||
|
||||
Backstage unifies all your infrastructure tooling, services, and documentation to create a streamlined development environment from end to end.
|
||||
|
||||
|
||||
+8
-1
@@ -76,7 +76,7 @@ techdocs:
|
||||
generators:
|
||||
techdocs: 'docker' # Alternatives - 'local'
|
||||
publisher:
|
||||
type: 'local' # Alternatives - 'googleGcs'. Read documentation for using alternatives.
|
||||
type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives.
|
||||
|
||||
sentry:
|
||||
organization: my-company
|
||||
@@ -206,6 +206,13 @@ scaffolder:
|
||||
api:
|
||||
token:
|
||||
$env: AZURE_TOKEN
|
||||
bitbucket:
|
||||
api:
|
||||
host: https://bitbucket.org
|
||||
username:
|
||||
$env: BITBUCKET_USERNAME
|
||||
token:
|
||||
$env: BITBUCKET_TOKEN
|
||||
auth:
|
||||
environment: development
|
||||
### Providing an auth.session.secret will enable session support in the auth-backend
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 101 KiB |
@@ -0,0 +1,144 @@
|
||||
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="605px" height="362px" viewBox="-0.5 -0.5 605 362" content="<mxfile host="046c5cfb-a3a2-464c-a595-f674d31136ed" modified="2020-12-26T01:33:35.255Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.52.1 Chrome/83.0.4103.122 Electron/9.3.5 Safari/537.36" etag="nIR1gVsObWk5MHRRKeB7" version="13.10.0" type="embed"><diagram id="fZcCmqQ9Q5JzR6r55YHy" name="Page-1">7VhNc5swEP01HJtBiA/7iI3dHtqT20lzlIUCajCishzb/fVdQBhkSOLWjtPJ9JJo367Qat/TImzh6Wr3UZIi/SJillmOHe8sHFmO43sI/pbAXgPBuAYSyeMasltgwX+xGkQNuuExW2ushpQQmeKFCVKR54wqAyNSiq0Zdi+y2AAKkrAesKAk66O3PFapRn3bbh2fGE9SvfS4cSwJfUik2OR6PcvBczQPZ7PavSLNs3T8OiWx2HYgPLPwVAqh6tFqN2VZWVqzbPMnvIe8JcvVKRM8nccjyTasSblKTO2bYmxTrtiiILS0t8C3hSepWmVgIRje8yybikxIsHORQ9BkraR4YA0INXC9YDKOwKNXY1Kx3ZMpo0MhQF9MrJiSewjRE5xRPUNLy3Frc9vy5Nu4xtIORdjXWyVaG8nhyW19YKBLNFwu1+9VR0iVikTkJPssRKFr8oMptdeyJhslzIqxHVffYWzr8V0Hj3YdR7RvjBwSrabceI151/W10yqrnReH5XFoqQFkzsv9Vf6aqEbi6FnqKlWzWM+s68Di3jk6ohFqJTaS6ijNiyIyYTrK9YbZliwjij+ajz+Lu6DHXZn+Qpu6PuYuzyAX/SPkDlHZp/1ydLpXohMPNC4/U2VDEpByl2f/50Y0jg/rirgQApBf7FonjBL9v3rKsgHCeMXzBoWslm2k/W3NZMdVr9x4j8RmKuuFprp+YIqmLxLZ7b2Ah6MojKZ6px0cRdjzgkNSA6z2OvLT7ddFRv9Fdr8Bo8DtN+Cxd4Ez7FyN9dsFBIeUAmtqmP3TaIeyqiNuDTr10R14i5KMJzmYFMgCmeFJSRKHS0qoHSsex+Uyg2Iy5Wbo6RXVgbEhDuz3tYFQXxvOJd7N7rW08ZXRNBJ0/a6awll3Mhd5R5eyAeJfrSl41yJ+Aeq2JxsKPJzTEt4J6X7whpyPXuKcHjbd0lqWazmOu9AJ2nCHtNE95EvZoIXION3//atBMliXLKuAku/yellnUrlPfyMUgueqqq83sbxjgQx+pg1egC/RG4Lxjdkd0Aj3lNKIx/hkcy8glPEbC8VsF/+l8pxUkBOcIBVv6Ov+z6UCZvs7S+Xr/JaFZ78B</diagram></mxfile>" style="background-color: rgb(241, 250, 238);">
|
||||
<defs/>
|
||||
<g>
|
||||
<rect x="0" y="0" width="603" height="360" fill="none" stroke="#457b9d" pointer-events="all"/>
|
||||
<path d="M 213 175 L 126 222" fill="none" stroke="#457b9d" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 387 175 L 474 222" fill="none" stroke="#457b9d" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<rect x="213" y="80" width="174" height="95" fill="#a8dadc" stroke="#457b9d" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 172px; height: 1px; padding-top: 128px; margin-left: 214px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #1D3557; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||
<font style="font-size: 16px">
|
||||
<b>
|
||||
Admin
|
||||
</b>
|
||||
User
|
||||
</font>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="300" y="131" fill="#1D3557" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
Admin User
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="5" y="12" width="111" height="20" fill="none" stroke="none" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 109px; height: 1px; padding-top: 22px; margin-left: 6px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #1D3557; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||
<font style="font-size: 16px">
|
||||
<b>
|
||||
AWS Account
|
||||
</b>
|
||||
</font>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="61" y="26" fill="#1D3557" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
AWS Account
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="387" y="222" width="174" height="95" fill="#a8dadc" stroke="#457b9d" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 172px; height: 1px; padding-top: 270px; margin-left: 388px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #1D3557; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||
<font style="font-size: 16px">
|
||||
<b>
|
||||
TechDocs
|
||||
</b>
|
||||
User
|
||||
</font>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="474" y="273" fill="#1D3557" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
TechDocs User
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="39" y="222" width="174" height="95" fill="#a8dadc" stroke="#457b9d" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 172px; height: 1px; padding-top: 270px; margin-left: 40px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #1D3557; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||
<font style="font-size: 16px">
|
||||
<b>
|
||||
S3 Bucket
|
||||
</b>
|
||||
</font>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="126" y="273" fill="#1D3557" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
S3 Bucket
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="451.5" y="159" width="46" height="34" fill="none" stroke="none" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 176px; margin-left: 475px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
|
||||
<font color="#457b9d" style="font-size: 14px">
|
||||
User
|
||||
<br/>
|
||||
policy
|
||||
</font>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="475" y="180" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
User...
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="99.5" y="159" width="53" height="34" fill="none" stroke="none" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 176px; margin-left: 126px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
|
||||
<font color="#457b9d" style="font-size: 14px">
|
||||
Bucket
|
||||
<br/>
|
||||
policy
|
||||
</font>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="126" y="180" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
Bucket...
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
</g>
|
||||
<switch>
|
||||
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
|
||||
<a transform="translate(0,-5)" xlink:href="https://desk.draw.io/support/solutions/articles/16000042487" target="_blank">
|
||||
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
|
||||
Viewer does not support full SVG 1.1
|
||||
</text>
|
||||
</a>
|
||||
</switch>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -61,6 +61,19 @@ If your authentication provider is any of the above mentioned providers, you can
|
||||
configure them by setting the right variables in `app-config.yaml` under the
|
||||
`auth` section.
|
||||
|
||||
### SAML
|
||||
|
||||
The SAML Provider is currently under development. Additional validation and
|
||||
profile handling is still required before use in production.
|
||||
|
||||
To configure the SAML Auth provider, look at the configuration parameters
|
||||
supported by
|
||||
[Passport-SAML](https://github.com/node-saml/passport-saml#config-parameter-details)
|
||||
under the `auth.providers.saml` key
|
||||
|
||||
For security reasons, validate that the response from the IdP is indeed signed
|
||||
by also providing the `cert` configuration.
|
||||
|
||||
### Configuration
|
||||
|
||||
Each authentication provider (except SAML) needs five parameters: an OAuth
|
||||
@@ -96,6 +109,11 @@ auth:
|
||||
development:
|
||||
clientId:
|
||||
$env:
|
||||
saml:
|
||||
entryPoint:
|
||||
$env: AUTH_SAML_ENTRY_POINT
|
||||
issuer:
|
||||
$env: AUTH_SAML_ISSUER
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
+48
-1
@@ -28,6 +28,7 @@ app:diff Diff an existing app with the creation template
|
||||
app:serve Serve an app for local development
|
||||
|
||||
backend:build Build a backend plugin
|
||||
backend:bundle Bundle the backend into a deployment archive
|
||||
backend:build-image Bundles the package into a docker image
|
||||
backend:dev Start local development server with HMR for the backend
|
||||
|
||||
@@ -166,7 +167,7 @@ Options:
|
||||
|
||||
## backend:build
|
||||
|
||||
Scope: `backend`, `backend-plugin`
|
||||
Scope: `backend-plugin`
|
||||
|
||||
This builds a backend package for publishing and use in production. The build
|
||||
output is written to `dist/`. Be sure to list any additional file that the
|
||||
@@ -180,6 +181,52 @@ Options:
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
## backend:bundle
|
||||
|
||||
Scope: `backend`
|
||||
|
||||
Bundle the backend and all of its local dependencies into a deployment archive.
|
||||
The archive is written to `dist/bundle.tar.gz`, and contains the packaged
|
||||
version of all dependencies of the target package, along with the target package
|
||||
itself. The layout of the packages in the archive is the same as the directory
|
||||
layout in the target monorepo, and the bundle also contains the root
|
||||
`package.json` and `yarn.lock`.
|
||||
|
||||
To use the bundle, extract it into a target directory, run
|
||||
`yarn install --production`, and then start the target backend package using for
|
||||
example `node package/backend`.
|
||||
|
||||
The `dist/bundle.tar.gz` is accompanied by a `dist/skeleton.tar.gz`, which has
|
||||
the same layout, but only contains `package.json` files and `yarn.lock`. This
|
||||
can be used to run a `yarn install` in environments that will benefit from the
|
||||
caching that this enables, such as Docker image builds. To use the skeleton
|
||||
archive, simply extract it first, run install, and then extract the main bundle.
|
||||
|
||||
The following is an example of a `Dockerfile` that can be used to package the
|
||||
output of `backstage-cli backend:bundle` into an image:
|
||||
|
||||
```Dockerfile
|
||||
FROM node:14-buster
|
||||
WORKDIR /app
|
||||
|
||||
ADD yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./
|
||||
RUN yarn install --production --network-timeout 600000 && rm -rf "$(yarn cache dir)"
|
||||
|
||||
ADD packages/backend/dist/bundle.tar.gz app-config.yaml ./
|
||||
|
||||
CMD node packages/backend
|
||||
```
|
||||
|
||||
```text
|
||||
Usage: backstage-cli backend:bundle [options]
|
||||
|
||||
Bundle the backend into a deployment archive
|
||||
|
||||
Options:
|
||||
--build-dependencies Build all local package dependencies before bundling the backend
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
## backend:build-image
|
||||
|
||||
Scope: `backend`
|
||||
|
||||
@@ -36,6 +36,10 @@ export interface Config {
|
||||
* @visibility frontend
|
||||
*/
|
||||
baseUrl: string;
|
||||
|
||||
// Use @items.<name> to assign annotations to primitive array items
|
||||
/** @items.visibility frontend */
|
||||
myItems: string[];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -141,16 +141,17 @@ itself:
|
||||
$file: ./my-secret.txt
|
||||
```
|
||||
|
||||
### Data File Secrets
|
||||
### Including Files
|
||||
|
||||
This reads secrets from a path within a JSON-like data file. The file path
|
||||
behaves similar to file secrets, but with the addition of a url fragment that is
|
||||
used to point to a specific value inside the file. Supported file extensions are
|
||||
`.json`, `.yaml`, and `.yml`. For example, the following would read out
|
||||
`my-secret-key` from `my-secrets.json`:
|
||||
The `$include` keyword can be used to load in JSON data from an external file.
|
||||
It's able to load and parse data from `.json`, `.yml`, and `.yaml` files. It's
|
||||
also possible to include a url fragment (`#`) to point to a value at the given
|
||||
path in the file.
|
||||
|
||||
For example, the following would read `my-secret-key` from `my-secrets.json`:
|
||||
|
||||
```yaml
|
||||
$data: ./my-secrets.json#deployment.key
|
||||
$include: ./my-secrets.json#deployment.key
|
||||
```
|
||||
|
||||
Example `my-secrets.json` file:
|
||||
|
||||
@@ -44,8 +44,8 @@ Backstage model and the primary way to discover existing functionality in the
|
||||
ecosystem.
|
||||
|
||||
APIs are implemented by components and form boundaries between components. They
|
||||
might be defined using an RPC IDL (eg Protobuf, GraphQL, ...), a data schema (eg
|
||||
Avro, TFRecord, ...), or as code interfaces. In any case, APIs exposed by
|
||||
might be defined using an RPC IDL (e.g., Protobuf, GraphQL, ...), a data schema
|
||||
(e.g., Avro, TFRecord, ...), or as code interfaces. In any case, APIs exposed by
|
||||
components need to be in a known machine-readable format so we can build further
|
||||
tooling and analysis on top.
|
||||
|
||||
|
||||
@@ -45,8 +45,6 @@ about TechDocs and the philosophy in its
|
||||
[v2]: https://github.com/backstage/backstage/milestone/22
|
||||
[v3]: https://github.com/backstage/backstage/milestone/17
|
||||
|
||||
<!-- TODO: Add link to milestone for v3 -->
|
||||
|
||||
## Use Cases
|
||||
|
||||
#### TechDocs V.0
|
||||
@@ -110,12 +108,12 @@ providers are used.
|
||||
| GitLab | Yes ✅ |
|
||||
| GitLab Enterprise | Yes ✅ |
|
||||
|
||||
| File Storage Provider | Support Status | Track status |
|
||||
| --------------------------------- | -------------- | ----------------------------------------------------------- |
|
||||
| Local Filesystem of Backstage app | Yes ✅ | |
|
||||
| Google Cloud Storage (GCS) | Yes ✅ | |
|
||||
| Amazon Web Services (AWS) S3 | No ❌ | [#3714](https://github.com/backstage/backstage/issues/3714) |
|
||||
| Azure Storage | No ❌ | |
|
||||
| File Storage Provider | Support Status |
|
||||
| --------------------------------- | ----------------------------------------------------------------- |
|
||||
| Local Filesystem of Backstage app | Yes ✅ |
|
||||
| Google Cloud Storage (GCS) | Yes ✅ |
|
||||
| Amazon Web Services (AWS) S3 | Yes ✅ |
|
||||
| Azure Storage | No ❌ [#3938](https://github.com/backstage/backstage/issues/3938) |
|
||||
|
||||
[Reach out to us](#feedback) if you want to request more platforms.
|
||||
|
||||
|
||||
@@ -141,13 +141,13 @@ Status of all the features mentioned above.
|
||||
**In place ✅**
|
||||
|
||||
- Basic setup with techdocs-backend file server as storage.
|
||||
- Basic setup with cloud storage solution.
|
||||
|
||||
**Work in progress 🚧**
|
||||
|
||||
- Basic setup with cloud storage solution.
|
||||
- `techdocs-cli` is able to generate docs in CI/CD environment.
|
||||
- `techdocs-cli` is able to publish docs site to any storage.
|
||||
|
||||
**Not implemented yet ❌**
|
||||
|
||||
- `techdocs-cli` is able to generate docs in CI/CD environment.
|
||||
- `techdocs-cli` is able to publish docs site to any storage.
|
||||
- `techdocs-backend` integration with Backstage access control management.
|
||||
|
||||
@@ -13,18 +13,15 @@ configuration options for TechDocs.
|
||||
# File: app-config.yaml
|
||||
|
||||
techdocs:
|
||||
|
||||
# TechDocs makes API calls to techdocs-backend using this URL. e.g. get docs of an entity, get metadata, etc.
|
||||
|
||||
requestUrl: http://localhost:7000/api/techdocs
|
||||
|
||||
|
||||
# Just another route in techdocs-backend where TechDocs requests the static files from. This URL uses an HTTP middleware
|
||||
# to serve files from either a local directory or an External storage provider.
|
||||
|
||||
storageUrl: http://localhost:7000/api/techdocs/static/docs
|
||||
|
||||
|
||||
# generators.techdocs can have two values: 'docker' or 'local'. This is to determine how to run the generator - whether to
|
||||
# spin up the techdocs-container docker image or to run mkdocs locally (assuming all the dependencies are taken care of).
|
||||
# You want to change this to 'local' if you are running Backstage using your own custom Docker setup and want to avoid running
|
||||
@@ -34,7 +31,6 @@ techdocs:
|
||||
generators:
|
||||
techdocs: 'docker'
|
||||
|
||||
|
||||
# techdocs.builder can be either 'local' or 'external.
|
||||
# If builder is set to 'local' and you open a TechDocs page, techdocs-backend will try to generate the docs, publish to storage
|
||||
# and show the generated docs afterwords. This is the "Basic" setup of the TechDocs Architecture.
|
||||
@@ -44,30 +40,42 @@ techdocs:
|
||||
|
||||
builder: 'local'
|
||||
|
||||
|
||||
# techdocs.publisher is used to configure the Storage option, whether you want to use the local filesystem to store generated docs
|
||||
# or you want to use External storage providers like Google Cloud Storage, AWS S3, etc.
|
||||
|
||||
publisher:
|
||||
|
||||
# techdocs.publisher.type can be - 'local' or 'googleGcs' (awsS3, azureStorage, etc. to be available as well).
|
||||
# techdocs.publisher.type can be - 'local' or 'googleGcs' or 'awsS3' (azureStorage to be available in future).
|
||||
# When set to 'local', techdocs-backend will create a 'static' directory at its root to store generated documentation files.
|
||||
# When set to 'googleGcs', techdocs-backend will use a Google Cloud Storage Bucket to store generated documentation files.
|
||||
# When set to 'awsS3', techdocs-backend will use an Amazon Web Service (AWS) S3 bucket to store generated documentation files.
|
||||
|
||||
type: 'local'
|
||||
|
||||
|
||||
# Required when techdocs.publisher.type is set to 'googleGcs'. Skip otherwise.
|
||||
|
||||
googleGcs:
|
||||
# An API key is required to write to a storage bucket.
|
||||
credentials:
|
||||
$file: '/path/to/google_application_credentials.json',
|
||||
$file: '/path/to/google_application_credentials.json'
|
||||
|
||||
# Your GCP Project ID where the Cloud Storage Bucket is hosted.
|
||||
projectId: 'gcp-project-id'
|
||||
|
||||
# Cloud Storage Bucket Name
|
||||
bucketName: 'techdocs-storage',
|
||||
bucketName: 'techdocs-storage'
|
||||
|
||||
# Required when techdocs.publisher.type is set to 'awsS3'. Skip otherwise.
|
||||
|
||||
awsS3:
|
||||
# An API key is required to write to a storage bucket.
|
||||
credentials:
|
||||
accessKeyId:
|
||||
$env: TECHDOCS_AWSS3_ACCESS_KEY_ID_CREDENTIAL
|
||||
secretAccessKey:
|
||||
$env: TECHDOCS_AWSS3_SECRET_ACCESS_KEY_CREDENTIAL
|
||||
region:
|
||||
$env: AWSS3_REGION
|
||||
|
||||
# AWS S3 Bucket Name
|
||||
bucketName: 'techdocs-storage'
|
||||
```
|
||||
|
||||
@@ -41,7 +41,7 @@ setup for free.
|
||||
|
||||
### Manually add documentation setup to already existing repository
|
||||
|
||||
Prerequisities:
|
||||
Prerequisites:
|
||||
|
||||
- An existing component
|
||||
[registered in backstage](../software-catalog/index.md#adding-components-to-the-catalog)
|
||||
|
||||
@@ -72,12 +72,8 @@ add the following
|
||||
```typescript
|
||||
import {
|
||||
createRouter,
|
||||
DirectoryPreparer,
|
||||
Preparers,
|
||||
Generators,
|
||||
TechdocsGenerator,
|
||||
CommonGitPreparer,
|
||||
UrlPreparer,
|
||||
Publisher,
|
||||
} from '@backstage/plugin-techdocs-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
@@ -90,30 +86,25 @@ export default async function createPlugin({
|
||||
reader,
|
||||
}: PluginEnvironment) {
|
||||
// Preparers are responsible for fetching source files for documentation.
|
||||
const preparers = new Preparers();
|
||||
|
||||
const directoryPreparer = new DirectoryPreparer(logger);
|
||||
preparers.register('dir', directoryPreparer);
|
||||
|
||||
const commonGitPreparer = new CommonGitPreparer(logger);
|
||||
preparers.register('github', commonGitPreparer);
|
||||
preparers.register('gitlab', commonGitPreparer);
|
||||
preparers.register('azure/api', commonGitPreparer);
|
||||
|
||||
const urlPreparer = new UrlPreparer(reader, logger);
|
||||
preparers.register('url', urlPreparer);
|
||||
const preparers = await Preparers.fromConfig(config, {
|
||||
logger,
|
||||
reader,
|
||||
});
|
||||
|
||||
// Generators are used for generating documentation sites.
|
||||
const generators = new Generators();
|
||||
const techdocsGenerator = new TechdocsGenerator(logger, config);
|
||||
generators.register('techdocs', techdocsGenerator);
|
||||
const generators = await Generators.fromConfig(config, {
|
||||
logger,
|
||||
});
|
||||
|
||||
// Publishers are used for
|
||||
// Publisher is used for
|
||||
// 1. Publishing generated files to storage
|
||||
// 2. Fetching files from storage and passing them to TechDocs frontend.
|
||||
const publisher = Publisher.fromConfig(config, logger, discovery);
|
||||
const publisher = await Publisher.fromConfig(config, {
|
||||
logger,
|
||||
discovery,
|
||||
});
|
||||
|
||||
// Docker client used by the generators.
|
||||
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
|
||||
const dockerClient = new Docker();
|
||||
|
||||
return await createRouter({
|
||||
|
||||
@@ -93,3 +93,175 @@ techdocs:
|
||||
|
||||
Your Backstage app is now ready to use Google Cloud Storage for TechDocs, to
|
||||
store the static generated documentation files.
|
||||
|
||||
## Configuring AWS S3 Bucket with TechDocs
|
||||
|
||||
Follow the
|
||||
[official AWS S3 documentation](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html)
|
||||
for the latest instructions on the following steps involving AWS S3.
|
||||
|
||||
**1. Set `techdocs.publisher.type` config in your `app-config.yaml`**
|
||||
|
||||
Set `techdocs.publisher.type` to `'awsS3'`.
|
||||
|
||||
```yaml
|
||||
techdocs:
|
||||
publisher:
|
||||
type: 'awsS3'
|
||||
```
|
||||
|
||||
**2. AWS Policies**
|
||||
|
||||
AWS Policies lets you **control access** to Amazon Web Services (AWS) products
|
||||
and resources.
|
||||
Here we will use a user policy **and** a bucket policy to show you the different
|
||||
possibilities you have but you can use only one.
|
||||
|
||||
<img data-zoomable src="../../assets/techdocs/aws-s3.drawio.svg" alt="AWS S3" width="500" />
|
||||
|
||||
This is an example of how you can manage your policies:
|
||||
|
||||
a. Admin user creates a **bucket policy** granting a set of permissions to our
|
||||
TechDocs user.
|
||||
|
||||
b. Admin user attaches a **user policy** to the TechDocs user granting
|
||||
additional permissions.
|
||||
|
||||
c. TechDocs User then tries permissions granted via both the **bucket** policy
|
||||
and the **user** policy.
|
||||
|
||||
**2.1 Creation**
|
||||
|
||||
**2.1.1 Create an Admin user** (if you don't have one yet)
|
||||
|
||||
Create an **administrator user** account `ADMIN_USER` and grant it administrator
|
||||
privileges by attaching a user policy giving the account **full access**.
|
||||
Note down the Admin User credentials and IAM User Sign-In URL as you will need
|
||||
to use this information in the next step.
|
||||
|
||||
**2.1.2 Create an AWS S3 Bucket**
|
||||
|
||||
Using the credentials of your Admin User `ADMIN_USER`, and the special IAM user
|
||||
sign-in URL, create a dedicated **bucket** for TechDocs sites. techdocs-backend
|
||||
will publish documentation to this bucket. TechDocs will fetch files from here
|
||||
to serve documentation in Backstage.
|
||||
|
||||
Set the name of the bucket to `techdocs.publisher.awsS3.bucketName`.
|
||||
|
||||
```yaml
|
||||
techdocs:
|
||||
publisher:
|
||||
type: 'awsS3'
|
||||
awsS3:
|
||||
bucketName: 'name-of-techdocs-storage-bucket'
|
||||
```
|
||||
|
||||
**2.1.3 Create the `TechDocs` user**
|
||||
|
||||
This user will be used to interact with your bucket, it will only have
|
||||
permissions to **get - put** objects.
|
||||
|
||||
In the IAM console, do the following:
|
||||
|
||||
- Create a new user, `TechDocs`
|
||||
- Note down the TechDocs User credentials
|
||||
- Note down the Amazon Resource Name (ARN) for the TechDocs user. In the IAM
|
||||
console, select the TechDocs user, and you can find the user ARN in the
|
||||
Summary tab.
|
||||
|
||||
**2.2 Attach policies**
|
||||
|
||||
Remember that you can use Bucket policy **or** User policy.
|
||||
Just make sure that you grant all the permissions to the TechDocs user:
|
||||
`3:PutObject`, `s3:GetObject`, `s3:ListBucket` and `s3:GetBucketLocation`.
|
||||
|
||||
**2.2.1 Create the bucket policy**
|
||||
|
||||
You now have to attach the following policy to your bucket in the Permission
|
||||
section:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "statement1",
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:user/TechDocs"
|
||||
},
|
||||
"Action": ["s3:GetBucketLocation", "s3:ListBucket"],
|
||||
"Resource": ["arn:aws:s3:::name-of-techdocs-storage-bucket"]
|
||||
},
|
||||
{
|
||||
"Sid": "statement2",
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:user/TechDocs"
|
||||
},
|
||||
"Action": ["s3:GetObject"],
|
||||
"Resource": ["arn:aws:s3:::name-of-techdocs-storage-bucket/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- The first statement grants **TechDocs User** the bucket operation permissions
|
||||
`s3:GetBucketLocation` and `s3:ListBucket` which are permissions required by
|
||||
the console.
|
||||
- The second statement grants the `s3:GetObject` permission.
|
||||
(**NOTE :** if you do not use the user policy defined below you must also add
|
||||
the `s3:PutObject` permission to allow the TechDocs user to add objects.)
|
||||
|
||||
**2.2.2 Create the user policy**
|
||||
|
||||
Create an inline policy for the TechDocs user by using the following policy:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "PermissionForObjectOperations",
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:PutObject"],
|
||||
"Resource": ["arn:aws:s3:::name-of-techdocs-storage-bucket/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See more details in the section
|
||||
[Working with Inline Policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_manage.html).
|
||||
|
||||
Now you need to fill in the environment variables with the `TechDocs` User
|
||||
credentials. You can also specify a region if you want to accesses the resources
|
||||
in a specific region. Otherwise no region will be selected by default.
|
||||
|
||||
```properties
|
||||
TECHDOCS_AWSS3_ACCESS_KEY_ID_CREDENTIAL="TECHDOCS_ACCESS_KEY_ID"
|
||||
TECHDOCS_AWSS3_SECRET_ACCESS_KEY_CREDENTIAL="TECHDOCS_SECRET_ACCESS_KEY"
|
||||
AWSS3_REGION="" // Optional
|
||||
```
|
||||
|
||||
Make it available in your Backstage server and/or your local development server
|
||||
and set it in the app config techdocs.publisher.awsS3.
|
||||
|
||||
```yaml
|
||||
techdocs:
|
||||
publisher:
|
||||
type: 'awsS3'
|
||||
awsS3:
|
||||
credentials:
|
||||
accessKeyId:
|
||||
$env: TECHDOCS_AWSS3_ACCESS_KEY_ID_CREDENTIAL
|
||||
secretAccessKey:
|
||||
$env: TECHDOCS_AWSS3_SECRET_ACCESS_KEY_CREDENTIAL
|
||||
region:
|
||||
$env: AWSS3_REGION
|
||||
```
|
||||
|
||||
**3. That's it!**
|
||||
|
||||
Your Backstage app is now ready to use AWS S3 for TechDocs, to store the static
|
||||
generated documentation files.
|
||||
|
||||
@@ -9,7 +9,7 @@ Powered by a centralized service catalog, Backstage restores order to your micro
|
||||
|
||||
[Backstage](https://backstage.io/) is an open platform for building developer
|
||||
portals. Powered by a centralized service catalog, Backstage restores order to
|
||||
your microservices and infrastructure. So your product teams can ship
|
||||
your microservices and infrastructure and enables your product teams to ship
|
||||
high-quality code quickly — without compromising autonomy.
|
||||
|
||||
Backstage unifies all your infrastructure tooling, services, and documentation
|
||||
|
||||
@@ -33,4 +33,4 @@ that someone else will pick up the work.
|
||||
If your plugin isn't supposed to live as a standalone page, but rather needs to
|
||||
be presented as a part of a Service Catalog (e.g. a separate tab or a card on an
|
||||
"Overview" tab), then check out
|
||||
[the instruction](integrating-plugin-into-service-catalog.md). on how to do it.
|
||||
[the instruction](integrating-plugin-into-service-catalog.md) on how to do it.
|
||||
|
||||
+42
-33
@@ -16,15 +16,15 @@ frameworks and libraries like [Mocha](https://mochajs.org/),
|
||||
|
||||
Running all tests:
|
||||
|
||||
yarn test-react
|
||||
yarn test
|
||||
|
||||
Running an individual test (e.g. `MyComponent.test.js`):
|
||||
|
||||
yarn test-react MyComponent
|
||||
yarn test MyComponent
|
||||
|
||||
To run both `MyComponent.test.js` and `MyControl.test.js` suite of tests:
|
||||
|
||||
yarn test-react MyCo
|
||||
yarn test MyCo
|
||||
|
||||
Note: if `console.logs` are not appearing, run only the individual test you are
|
||||
working on.
|
||||
@@ -52,12 +52,12 @@ render React components.
|
||||
|
||||
TODO.
|
||||
|
||||
# Writing Unit Tests
|
||||
## Writing Unit Tests
|
||||
|
||||
The following principles are good guides for determining if you are writing high
|
||||
quality frontend unit tests.
|
||||
|
||||
## Bad Unit Test Principle
|
||||
### Bad Unit Test Principle
|
||||
|
||||
> No unit test is better than a bad one.
|
||||
|
||||
@@ -69,7 +69,7 @@ Writing a poor unit test:
|
||||
- Adds to future work by requiring updates to the unit test for irrelevant code
|
||||
changes.
|
||||
|
||||
## Input/Output Principle
|
||||
### Input/Output Principle
|
||||
|
||||
> A unit test verifies an output matches an expected input.
|
||||
|
||||
@@ -77,7 +77,7 @@ For backend, this would be that when you provide configuration X, then the
|
||||
object responds with Y. For frontend, this would be that when you provide
|
||||
properties X to a component, then the visual functionality responds with Y.
|
||||
|
||||
## Blackbox Principle
|
||||
### Blackbox Principle
|
||||
|
||||
> A good unit test does not tell the object how it should do its job but should
|
||||
> only compare inputs to outputs.
|
||||
@@ -86,7 +86,7 @@ Consider a unit test for a form. A good unit test would not test the order of
|
||||
the form fields. Instead, it would verify that the inputs to the form fields
|
||||
lead to a certain backend call when submit is clicked.
|
||||
|
||||
## Scalability Principle
|
||||
### Scalability Principle
|
||||
|
||||
> Unit test quality is directly proportionate to how much code can change
|
||||
> without having to touch the unit test.
|
||||
@@ -97,7 +97,7 @@ to the code, you have to update the unit test. A good unit test suite allows a
|
||||
lot of flexibility in _how_ the code is written so that future refactoring can
|
||||
occur without having to touch the original unit tests.
|
||||
|
||||
## Increasing Complexity Principle
|
||||
### Increasing Complexity Principle
|
||||
|
||||
> The ordering of unit tests in a suite should proceed from least specific to
|
||||
> most specific.
|
||||
@@ -116,7 +116,7 @@ throwing an error saying that output was incorrect will lead the next developer
|
||||
into thinking they may have broken the entire functionality of the object rather
|
||||
than simply letting them know they had an invalid input.
|
||||
|
||||
## Broken Functionality Principle
|
||||
### Broken Functionality Principle
|
||||
|
||||
> Generally, a unit test should not test exactly how the output appears, it
|
||||
> should test that the functionality has an expected _general_ response to an
|
||||
@@ -131,7 +131,7 @@ test a slightly different color on the button the unit test will break. A better
|
||||
unit test would verify that the button's CSS classname is assigned properly on
|
||||
hover or test for something completely different.
|
||||
|
||||
## Example: Loading Indicator
|
||||
### Example: Loading Indicator
|
||||
|
||||
A classic unit test on frontends is verifying a loading indicator displays when
|
||||
a backend request is being made.
|
||||
@@ -192,11 +192,14 @@ returns a result or displays an error or console message, like so:
|
||||
|
||||
**`StringUtil ellipsis`**
|
||||
|
||||
export function ellipsis(text, maxLength, midCharIx = 0, ellipsis = '...') {
|
||||
// Do something blackbox. We should not care about the internals, only inputs and outputs.
|
||||
...
|
||||
return someFinalValue;
|
||||
}
|
||||
```js
|
||||
export function ellipsis(text, maxLength, midCharIx = 0, ellipsis = '...') {
|
||||
// Do something blackbox. We should not care about the internals,
|
||||
// only inputs and outputs.
|
||||
...
|
||||
return someFinalValue;
|
||||
}
|
||||
```
|
||||
|
||||
There are four things to test for in a utility function:
|
||||
|
||||
@@ -207,30 +210,36 @@ There are four things to test for in a utility function:
|
||||
|
||||
> Handle Invalid Input (handle thrown errors):
|
||||
|
||||
it('Throws an error on improper arguments', () => {
|
||||
expect(() => {
|
||||
ellipsis();
|
||||
}).toThrowError('Expected \'text\' to be defined');
|
||||
});
|
||||
```js
|
||||
it('Throws an error on improper arguments', () => {
|
||||
expect(() => {
|
||||
ellipsis();
|
||||
}).toThrowError("Expected 'text' to be defined");
|
||||
});
|
||||
```
|
||||
|
||||
> Verify default input arguments:
|
||||
|
||||
it('Works with defaults', () => {
|
||||
expect(ellipsis('Hello world', 3)).toBe('Hel...');
|
||||
expect(ellipsis('', 3)).toBe('');
|
||||
expect(ellipsis('H', 3)).toBe('H');
|
||||
expect(ellipsis('Hello', 5)).toBe('Hello');
|
||||
});
|
||||
```js
|
||||
it('Works with defaults', () => {
|
||||
expect(ellipsis('Hello world', 3)).toBe('Hel...');
|
||||
expect(ellipsis('', 3)).toBe('');
|
||||
expect(ellipsis('H', 3)).toBe('H');
|
||||
expect(ellipsis('Hello', 5)).toBe('Hello');
|
||||
});
|
||||
```
|
||||
|
||||
> Verify output for expected input arguments:
|
||||
|
||||
This is especially true for edge cases!
|
||||
|
||||
it('Works with midCharIx', () => {
|
||||
expect(ellipsis('Hello world', 3, 6)).toBe('...o w...');
|
||||
expect(ellipsis('', 3, 6)).toBe('');
|
||||
expect(ellipsis('Backstage is amazing', 4, 10)).toBe('...e is...');
|
||||
});
|
||||
```js
|
||||
it('Works with midCharIx', () => {
|
||||
expect(ellipsis('Hello world', 3, 6)).toBe('...o w...');
|
||||
expect(ellipsis('', 3, 6)).toBe('');
|
||||
expect(ellipsis('Backstage is amazing', 4, 10)).toBe('...e is...');
|
||||
});
|
||||
```
|
||||
|
||||
## Non-React Classes
|
||||
|
||||
@@ -372,4 +381,4 @@ IDE.
|
||||
In most cases, we have found that using `console.log` works well.
|
||||
|
||||
Note: if your console.logs are not being displayed, focus your specific unit
|
||||
test from the command line by running them like so `yarn test-react MyTest`.
|
||||
test from the command line by running them like so `yarn test MyTest`.
|
||||
|
||||
@@ -41,8 +41,8 @@ the code.
|
||||
appreciate contributions in here and encourage them being kept up to date.
|
||||
|
||||
- [`docs/`](https://github.com/backstage/backstage/tree/master/docs) - This is
|
||||
where we keep all of our documentation Markdown files. These ends up on
|
||||
http://backstage.io/docs. Just keep in mind that changes to the
|
||||
where we keep all of our documentation Markdown files. These end up on
|
||||
https://backstage.io/docs. Just keep in mind that changes to the
|
||||
[`sidebars.json`](https://github.com/backstage/backstage/blob/master/microsite/sidebars.json)
|
||||
file may be needed as sections are added/removed.
|
||||
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: kafka-test
|
||||
description: Kafka Test
|
||||
annotations:
|
||||
kafka.apache.org/consumer-group: consumer
|
||||
tags:
|
||||
- java
|
||||
- data
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: experimental
|
||||
owner: architecture
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: Announcing the Backstage Stability Index
|
||||
author: Patrik Oldsberg, Spotify
|
||||
authorURL: https://github.com/Rugvip
|
||||
---
|
||||
|
||||
**TL;DR** Backstage is heading out of alpha and moving onto the path to stable releases and an eventual version 1.0. As the community and ecosystem continue to grow at an increasing rate, we want to provide a solid foundation for everyone building things in, with, and around Backstage. So, today we’re introducing the [Stability Index](https://backstage.io/docs/overview/stability-index) — a simple way to find out how likely (or unlikely) a specific package or plugin inside Backstage might be updated with major changes. By indicating the reliability of key features and APIs, this quick reference will help contributors and adopters better plan and coordinate their development efforts going forward.
|
||||
|
||||

|
||||
|
||||
<!--truncate-->
|
||||
|
||||
Backstage has a great and growing community of users and contributors. With so much valuable feedback, this inevitably means that sometimes we will go back and revise design decisions that introduce breaking changes. Furthermore, as we continue to ship new features and systems, we’ll continue this cycle of iterating, gathering feedback, and iterating again before reaching a stable API.
|
||||
|
||||
This rapid evolution can create uncertainty around which parts of the project are considered stable and which are not. Regardless of whether you're contributing to the project directly, setting up your own Backstage, or building plugins, you will be using various parts of the project that vary in stability. While many API surfaces of Backstage are reasonably solid and are unlikely to see any big breaking changes, others are more unstable or experimental.
|
||||
|
||||
## The Stability Index
|
||||
|
||||
In order to tackle the problem of uncertainty, and help align contributors, we have recently introduced a Stability Index. Inspired by a [similar concept with the same name in Node.js](https://nodejs.org/docs/latest-v4.x/api/documentation.html#documentation_stability_index), it’s a score assigned to subsets of the project, indicating the level of maturity of the API and the commitment to backwards compatibility. However, because of the current phase of the project, we have used a slightly different implementation. Rather than the score indicating a perceived stability, a higher score is instead a commitment to providing a smoother upgrade path for users, both through better documentation and backwards compatibility. Importantly, the Stability Index does not supersede [semantic versioning](https://semver.org/) (or semver), meaning we will still adhere to semver and only do breaking changes in minor releases as long as we are on 0.x.
|
||||
|
||||
You can find more details about the scores on the [Stability Index](https://backstage.io/docs/overview/stability-index) page, but the following is a TL;DR of the 0–3 scores:
|
||||
|
||||
- **0** — There's a changelog entry.
|
||||
- **1** — There's a migration guide.
|
||||
- **2** — Two weeks of deprecation.
|
||||
- **3** — Three months of deprecation.
|
||||
|
||||
The Stability Index has been tailored for the phase that the project is currently in, and we do not intend to keep it in place in its current form forever. It is, for example, likely that we in the future will increase the deprecation time limits.
|
||||
|
||||
## Staying up to date
|
||||
|
||||
In addition to the Stability Index, we have also recently adopted a weekly release cadence where we cut new releases every Thursday. Along with the two-week deprecation period of many of the API surfaces in Backstage, there is an excellent opportunity to form a routine to update your own Backstage projects either weekly or biweekly.
|
||||
|
||||
In order to make the update process easier, we’ve added a new command to the Backstage CLI that allows you to easily update your project to use the latest Backstage release. The command is called `version:bump` and is executed in the root of your project using the CLI. You can see what it looks like in action below.
|
||||
|
||||

|
||||
|
||||
## Impact
|
||||
|
||||
The Stability Index has already been active for a couple of weeks, and we have immediately seen an impact on the consideration and care taken when working in parts of the project that we have committed to keeping more stable. Both members of the core maintainer team and contributors from the community have taken care to roll out updates responsibly, with examples such as [PR #3524](https://github.com/backstage/backstage/pull/3524) and [PR #3465](https://github.com/backstage/backstage/pull/3465).
|
||||
|
||||
The core team is working hard to bring more stability to broader parts of Backstage, and engineering it in a way that allows for long-term evolution. There are more learnings to be discovered as we continue evolving Backstage, but we are confident that the Stability Index will allow for a clearer path and a smoother ride for everyone involved!
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 369 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
@@ -2,7 +2,7 @@
|
||||
title: AWS Lambda
|
||||
author: roadie.io
|
||||
authorUrl: https://roadie.io
|
||||
category: Monitoring
|
||||
category: Infrastructure
|
||||
description: View AWS Lambda functions for your components in Backstage.
|
||||
documentation: https://roadie.io/backstage/plugins/aws-lambda
|
||||
iconUrl: https://roadie.io/images/logos/lambda.png
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Firebase Functions
|
||||
author: roadie.io
|
||||
authorUrl: https://roadie.io/
|
||||
category: Monitoring
|
||||
category: Infrastructure
|
||||
description: View Firebase Functions details for your service in Backstage.
|
||||
documentation: https://roadie.io/backstage/plugins/firebase-functions
|
||||
iconUrl: https://roadie.io/images/logos/firebase.png
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: FOSSA
|
||||
author: SDA SE
|
||||
authorUrl: https://sda.se/
|
||||
category: Quality
|
||||
description: View FOSSA license compliance of your components in Backstage.
|
||||
documentation: https://github.com/backstage/backstage/blob/master/plugins/fossa/README.md
|
||||
iconUrl: https://avatars0.githubusercontent.com/u/9543448?s=400&v=4
|
||||
npmPackageName: '@backstage/plugin-fossa'
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: PageDuty
|
||||
title: PagerDuty
|
||||
author: Spotify
|
||||
authorUrl: https://github.com/spotify
|
||||
category: Monitoring
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"devDependencies": {
|
||||
"@spotify/prettier-config": "^9.0.0",
|
||||
"docusaurus": "^2.0.0-alpha.378053ac5",
|
||||
"js-yaml": "^3.14.1",
|
||||
"js-yaml": "^4.0.0",
|
||||
"prettier": "^2.2.1"
|
||||
},
|
||||
"prettier": "@spotify/prettier-config"
|
||||
|
||||
+10
-14
@@ -17,22 +17,18 @@ const Background = props => {
|
||||
<Block small className="stripe-bottom bg-black-grey">
|
||||
<Block.Container style={{ justifyContent: 'flex-start' }}>
|
||||
<Block.TextBox>
|
||||
<Block.Title>See us in action.</Block.Title>
|
||||
<Block.Title>See us in action</Block.Title>
|
||||
<Block.Paragraph>
|
||||
To illustrate the potential of Backstage, we’re showing you{' '}
|
||||
<a href="https://labs.spotify.com/2020/04/21/how-we-use-backstage-at-spotify/">
|
||||
how we use it here at Spotify
|
||||
To explore the UI and basic features of Backstage firsthand, go
|
||||
to: <a href="https://demo.backstage.io">demo.backstage.io</a>.
|
||||
</Block.Paragraph>
|
||||
<Block.Paragraph>
|
||||
Watch the videos below to get an introduction to Backstage and to
|
||||
see how we use different plugins to customize{' '}
|
||||
<a href="https://engineering.atspotify.com/2020/04/21/how-we-use-backstage-at-spotify/">
|
||||
our internal version of Backstage at Spotify
|
||||
</a>
|
||||
. The videos below feature the custom plugins and templates that
|
||||
we built for our internal version of Backstage. You can use
|
||||
Backstage to build the developer portal you want — integrating the
|
||||
tools that you use in your own infrastructure ecosystem. (Or get
|
||||
started by building an open source plugin for Backstage that
|
||||
everyone can use, like our{' '}
|
||||
<a href="https://backstage.io/blog/2020/04/06/lighthouse-plugin">
|
||||
Lighthouse Plugin
|
||||
</a>
|
||||
.)
|
||||
.
|
||||
</Block.Paragraph>
|
||||
</Block.TextBox>
|
||||
<Block.Graphics>
|
||||
|
||||
@@ -27,8 +27,8 @@ class Index extends React.Component {
|
||||
</Block.Title>
|
||||
<Block.Paragraph>
|
||||
Powered by a centralized service catalog, Backstage restores
|
||||
order to your infrastructure. So your product teams can ship
|
||||
high-quality code quickly — without compromising autonomy.
|
||||
order to your infrastructure and enables your product teams to
|
||||
ship high-quality code quickly — without compromising autonomy.
|
||||
</Block.Paragraph>
|
||||
<Block.LinkButton
|
||||
href={'https://github.com/backstage/backstage#getting-started'}
|
||||
|
||||
@@ -16,7 +16,7 @@ const {
|
||||
const pluginsDirectory = require('path').join(process.cwd(), 'data/plugins');
|
||||
const pluginMetadata = fs
|
||||
.readdirSync(pluginsDirectory)
|
||||
.map(file => yaml.safeLoad(fs.readFileSync(`./data/plugins/${file}`, 'utf8')))
|
||||
.map(file => yaml.load(fs.readFileSync(`./data/plugins/${file}`, 'utf8')))
|
||||
.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase()));
|
||||
const truncate = text =>
|
||||
text.length > 170 ? text.substr(0, 170) + '...' : text;
|
||||
|
||||
+13
-1
@@ -1124,6 +1124,11 @@ argparse@^1.0.10, argparse@^1.0.7:
|
||||
dependencies:
|
||||
sprintf-js "~1.0.2"
|
||||
|
||||
argparse@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||
|
||||
arr-diff@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
|
||||
@@ -3921,7 +3926,7 @@ js-tokens@^3.0.2:
|
||||
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
|
||||
|
||||
js-yaml@^3.13.1, js-yaml@^3.14.1, js-yaml@^3.8.1:
|
||||
js-yaml@^3.13.1, js-yaml@^3.8.1:
|
||||
version "3.14.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
|
||||
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
|
||||
@@ -3929,6 +3934,13 @@ js-yaml@^3.13.1, js-yaml@^3.14.1, js-yaml@^3.8.1:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
js-yaml@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
|
||||
integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
jsbn@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@
|
||||
"docker-build": "yarn tsc && yarn workspace example-backend build-image",
|
||||
"create-plugin": "backstage-cli create-plugin --scope backstage --no-private",
|
||||
"remove-plugin": "backstage-cli remove-plugin",
|
||||
"release": "changeset version && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install --frozen-lockfile",
|
||||
"release": "changeset version && yarn diff --yes && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install --frozen-lockfile",
|
||||
"prettier:check": "prettier --check .",
|
||||
"lerna": "lerna",
|
||||
"storybook": "yarn workspace storybook start",
|
||||
|
||||
@@ -1,5 +1,50 @@
|
||||
# example-app
|
||||
|
||||
## 0.2.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ab0892358]
|
||||
- Updated dependencies [37a7d26c4]
|
||||
- Updated dependencies [8e083f41f]
|
||||
- Updated dependencies [88da267cc]
|
||||
- Updated dependencies [9c09a364f]
|
||||
- Updated dependencies [01707438b]
|
||||
- Updated dependencies [edb7d0775]
|
||||
- Updated dependencies [818d45e94]
|
||||
- Updated dependencies [0588be01f]
|
||||
- Updated dependencies [b8abdda57]
|
||||
- Updated dependencies [b7a124883]
|
||||
- Updated dependencies [bc909178d]
|
||||
- Updated dependencies [947d3c269]
|
||||
- @backstage/plugin-cost-insights@0.5.5
|
||||
- @backstage/plugin-tech-radar@0.3.2
|
||||
- @backstage/cli@0.4.5
|
||||
- @backstage/plugin-scaffolder@0.3.6
|
||||
- @backstage/plugin-sentry@0.3.2
|
||||
- @backstage/plugin-catalog@0.2.10
|
||||
- @backstage/plugin-search@0.2.5
|
||||
- @backstage/plugin-catalog-import@0.3.3
|
||||
- @backstage/plugin-pagerduty@0.2.5
|
||||
|
||||
## 0.2.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [19554f6d6]
|
||||
- Updated dependencies [1dc445e89]
|
||||
- Updated dependencies [342270e4d]
|
||||
- Updated dependencies [7d72f9b09]
|
||||
- @backstage/cli@0.4.3
|
||||
- @backstage/plugin-github-actions@0.2.6
|
||||
- @backstage/plugin-scaffolder@0.3.5
|
||||
- @backstage/core@0.4.2
|
||||
- @backstage/test-utils@0.1.6
|
||||
- @backstage/plugin-graphiql@0.2.3
|
||||
- @backstage/plugin-catalog@0.2.8
|
||||
- @backstage/plugin-pagerduty@0.2.4
|
||||
- @backstage/plugin-techdocs@0.5.1
|
||||
|
||||
## 0.2.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+16
-16
@@ -1,40 +1,39 @@
|
||||
{
|
||||
"name": "example-app",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.9",
|
||||
"private": true,
|
||||
"bundled": true,
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.6.0",
|
||||
"@backstage/cli": "^0.4.2",
|
||||
"@backstage/core": "^0.4.1",
|
||||
"@backstage/cli": "^0.4.5",
|
||||
"@backstage/core": "^0.4.2",
|
||||
"@backstage/plugin-api-docs": "^0.4.1",
|
||||
"@backstage/plugin-catalog": "^0.2.7",
|
||||
"@backstage/plugin-catalog-import": "^0.3.2",
|
||||
"@backstage/plugin-catalog": "^0.2.10",
|
||||
"@backstage/plugin-catalog-import": "^0.3.3",
|
||||
"@backstage/plugin-circleci": "^0.2.5",
|
||||
"@backstage/plugin-cloudbuild": "^0.2.5",
|
||||
"@backstage/plugin-cost-insights": "^0.5.2",
|
||||
"@backstage/plugin-cost-insights": "^0.5.5",
|
||||
"@backstage/plugin-explore": "^0.2.2",
|
||||
"@backstage/plugin-gcp-projects": "^0.2.2",
|
||||
"@backstage/plugin-github-actions": "^0.2.5",
|
||||
"@backstage/plugin-github-actions": "^0.2.6",
|
||||
"@backstage/plugin-gitops-profiles": "^0.2.2",
|
||||
"@backstage/plugin-graphiql": "^0.2.2",
|
||||
"@backstage/plugin-graphiql": "^0.2.3",
|
||||
"@backstage/plugin-org": "^0.3.2",
|
||||
"@backstage/plugin-jenkins": "^0.3.4",
|
||||
"@backstage/plugin-kafka": "^0.1.0",
|
||||
"@backstage/plugin-kubernetes": "^0.3.3",
|
||||
"@backstage/plugin-lighthouse": "^0.2.6",
|
||||
"@backstage/plugin-newrelic": "^0.2.2",
|
||||
"@backstage/plugin-org": "^0.3.2",
|
||||
"@backstage/plugin-pagerduty": "0.2.3",
|
||||
"@backstage/plugin-pagerduty": "0.2.5",
|
||||
"@backstage/plugin-register-component": "^0.2.5",
|
||||
"@backstage/plugin-rollbar": "^0.2.7",
|
||||
"@backstage/plugin-scaffolder": "^0.3.4",
|
||||
"@backstage/plugin-search": "^0.2.4",
|
||||
"@backstage/plugin-sentry": "^0.3.1",
|
||||
"@backstage/plugin-tech-radar": "^0.3.1",
|
||||
"@backstage/plugin-techdocs": "^0.5.0",
|
||||
"@backstage/plugin-scaffolder": "^0.3.6",
|
||||
"@backstage/plugin-sentry": "^0.3.2",
|
||||
"@backstage/plugin-search": "^0.2.5",
|
||||
"@backstage/plugin-tech-radar": "^0.3.2",
|
||||
"@backstage/plugin-techdocs": "^0.5.1",
|
||||
"@backstage/plugin-user-settings": "^0.2.3",
|
||||
"@backstage/plugin-welcome": "^0.2.3",
|
||||
"@backstage/test-utils": "^0.1.5",
|
||||
"@backstage/theme": "^0.2.2",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
@@ -54,6 +53,7 @@
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/test-utils": "^0.1.6",
|
||||
"@testing-library/cypress": "^7.0.1",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
|
||||
@@ -29,7 +29,7 @@ import { hot } from 'react-hot-loader/root';
|
||||
import { providers } from './identityProviders';
|
||||
import { Router as CatalogRouter } from '@backstage/plugin-catalog';
|
||||
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
|
||||
import { GraphiQLPage } from '@backstage/plugin-graphiql';
|
||||
import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql';
|
||||
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
|
||||
import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse';
|
||||
import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component';
|
||||
@@ -81,7 +81,7 @@ const AppRoutes = () => (
|
||||
path="/tech-radar"
|
||||
element={<TechRadarRouter width={1500} height={800} />}
|
||||
/>
|
||||
<Route path="/graphiql" element={<GraphiQLPage />} />
|
||||
<Route path="/graphiql" element={<GraphiQLRouter />} />
|
||||
<Route path="/lighthouse/*" element={<LighthouseRouter />} />
|
||||
<Route
|
||||
path="/register-component"
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line monorepo/no-internal-import
|
||||
import '@backstage/cli/asset-types';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @backstage/backend-common
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5ecd50f8a: Fix HTTPS certificate generation and add new config switch, enabling it simply by setting `backend.https = true`. Also introduces caching of generated certificates in order to avoid having to add a browser override every time the backend is restarted.
|
||||
- 00042e73c: Moving the Git actions to isomorphic-git instead of the node binding version of nodegit
|
||||
- 0829ff126: Tweaked development log formatter to include extra fields at the end of each log line
|
||||
- 036a84373: Provide support for on-prem azure devops
|
||||
- Updated dependencies [ad5c56fd9]
|
||||
- Updated dependencies [036a84373]
|
||||
- @backstage/config-loader@0.4.1
|
||||
- @backstage/integration@0.1.5
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Vendored
+35
-20
@@ -32,26 +32,41 @@ export interface Config {
|
||||
port?: string | number;
|
||||
};
|
||||
|
||||
/** HTTPS configuration for the backend. If omitted the backend will serve HTTP */
|
||||
https?: {
|
||||
/** Certificate configuration or parameters for generating a self-signed certificate */
|
||||
certificate?:
|
||||
| {
|
||||
/** Algorithm to use to generate a self-signed certificate */
|
||||
algorithm: string;
|
||||
keySize?: number;
|
||||
days?: number;
|
||||
}
|
||||
| {
|
||||
/** PEM encoded certificate. Use $file to load in a file */
|
||||
cert: string;
|
||||
/**
|
||||
* PEM encoded certificate key. Use $file to load in a file.
|
||||
* @visibility secret
|
||||
*/
|
||||
key: string;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* HTTPS configuration for the backend. If omitted the backend will serve HTTP.
|
||||
*
|
||||
* Setting this to `true` will cause self-signed certificates to be generated, which
|
||||
* can be useful for local development or other non-production scenarios.
|
||||
*/
|
||||
https?:
|
||||
| true
|
||||
| {
|
||||
/**
|
||||
* Certificate configuration or parameters for generating a self-signed certificate
|
||||
*
|
||||
* Setting parameters for self-signed certificates is deprecated and will be removed in
|
||||
* the future, set `backend.https = true` instead.
|
||||
*/
|
||||
certificate?:
|
||||
| {
|
||||
/** Algorithm to use to generate a self-signed certificate */
|
||||
algorithm?: string;
|
||||
keySize?: number;
|
||||
days?: number;
|
||||
attributes: {
|
||||
commonName: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
/** PEM encoded certificate. Use $file to load in a file */
|
||||
cert: string;
|
||||
/**
|
||||
* PEM encoded certificate key. Use $file to load in a file.
|
||||
* @visibility secret
|
||||
*/
|
||||
key: string;
|
||||
};
|
||||
};
|
||||
|
||||
/** Database connection configuration, select database type using the `client` field */
|
||||
database:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-common",
|
||||
"description": "Common functionality library for Backstage backends",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.2",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"private": false,
|
||||
@@ -31,8 +31,8 @@
|
||||
"dependencies": {
|
||||
"@backstage/cli-common": "^0.1.1",
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@backstage/config-loader": "^0.4.0",
|
||||
"@backstage/integration": "^0.1.4",
|
||||
"@backstage/config-loader": "^0.4.1",
|
||||
"@backstage/integration": "^0.1.5",
|
||||
"@types/cors": "^2.8.6",
|
||||
"@types/express": "^4.17.6",
|
||||
"archiver": "^5.0.2",
|
||||
@@ -45,6 +45,7 @@
|
||||
"fs-extra": "^9.0.1",
|
||||
"git-url-parse": "^11.4.3",
|
||||
"helmet": "^4.0.0",
|
||||
"isomorphic-git": "^1.8.0",
|
||||
"knex": "^0.21.6",
|
||||
"lodash": "^4.17.15",
|
||||
"logform": "^2.1.1",
|
||||
@@ -65,9 +66,9 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.4.2",
|
||||
"@backstage/cli": "^0.4.5",
|
||||
"@backstage/test-utils": "^0.1.5",
|
||||
"@types/archiver": "^3.1.1",
|
||||
"@types/archiver": "^5.1.0",
|
||||
"@types/compression": "^1.7.0",
|
||||
"@types/concat-stream": "^1.6.0",
|
||||
"@types/fs-extra": "^9.0.3",
|
||||
@@ -81,7 +82,6 @@
|
||||
"@types/tar": "^4.0.3",
|
||||
"@types/unzipper": "^0.10.3",
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
"@types/yaml": "^1.9.7",
|
||||
"get-port": "^5.1.1",
|
||||
"http-errors": "^1.7.3",
|
||||
"jest": "^26.0.1",
|
||||
|
||||
@@ -24,3 +24,4 @@ export * from './reading';
|
||||
export * from './service';
|
||||
export * from './paths';
|
||||
export * from './hot';
|
||||
export * from './scm';
|
||||
|
||||
@@ -17,19 +17,23 @@ import * as winston from 'winston';
|
||||
import { TransformableInfo } from 'logform';
|
||||
|
||||
const coloredTemplate = (info: TransformableInfo) => {
|
||||
const { timestamp, level, message, plugin, service } = info;
|
||||
const { timestamp, level, message, plugin, service, ...fields } = info;
|
||||
const colorizer = winston.format.colorize();
|
||||
const prefix = plugin || service;
|
||||
const timestampColor = colorizer.colorize('timestamp', timestamp);
|
||||
const prefixColor = colorizer.colorize('prefix', prefix);
|
||||
|
||||
return `${timestampColor} ${prefixColor} ${level} ${message}`;
|
||||
const extraFields = Object.entries(fields)
|
||||
.map(([key, value]) => `${colorizer.colorize('field', `${key}`)}=${value}`)
|
||||
.join(' ');
|
||||
|
||||
return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`;
|
||||
};
|
||||
|
||||
export const coloredFormat = winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.colorize({
|
||||
colors: { timestamp: 'dim', prefix: 'blue' },
|
||||
colors: { timestamp: 'dim', prefix: 'blue', field: 'cyan' },
|
||||
}),
|
||||
winston.format.printf(coloredTemplate),
|
||||
);
|
||||
|
||||
@@ -47,13 +47,7 @@ export class AzureUrlReader implements UrlReader {
|
||||
constructor(
|
||||
private readonly options: AzureIntegrationConfig,
|
||||
private readonly deps: { treeResponseFactory: ReadTreeResponseFactory },
|
||||
) {
|
||||
if (options.host !== 'dev.azure.com') {
|
||||
throw Error(
|
||||
`Azure integration currently only supports 'dev.azure.com', tried to use host '${options.host}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
) {}
|
||||
|
||||
async read(url: string): Promise<Buffer> {
|
||||
const builtUrl = getAzureFileFetchUrl(url);
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
jest.mock('isomorphic-git');
|
||||
jest.mock('isomorphic-git/http/node');
|
||||
jest.mock('fs-extra');
|
||||
|
||||
import * as isomorphic from 'isomorphic-git';
|
||||
import { Git } from './git';
|
||||
import http from 'isomorphic-git/http/node';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
describe('Git', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
describe('add', () => {
|
||||
it('should call isomorphic-git add with the correct arguments', async () => {
|
||||
const git = Git.fromAuth({});
|
||||
const dir = 'mockdirectory';
|
||||
const filepath = 'mockfile/path';
|
||||
|
||||
await git.add({ dir, filepath });
|
||||
|
||||
expect(isomorphic.add).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
filepath,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addRemote', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const git = Git.fromAuth({});
|
||||
const dir = 'mockdirectory';
|
||||
const remote = 'origin';
|
||||
const url = 'git@github.com/something/sads';
|
||||
|
||||
await git.addRemote({ dir, remote, url });
|
||||
|
||||
expect(isomorphic.addRemote).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
remote,
|
||||
url,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('commit', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const git = Git.fromAuth({});
|
||||
const dir = 'mockdirectory';
|
||||
const message = 'Inital Commit';
|
||||
const author = {
|
||||
name: 'author',
|
||||
email: 'test@backstage.io',
|
||||
};
|
||||
const committer = {
|
||||
name: 'comitter',
|
||||
email: 'test@backstage.io',
|
||||
};
|
||||
|
||||
await git.commit({ dir, message, author, committer });
|
||||
|
||||
expect(isomorphic.commit).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
message,
|
||||
author,
|
||||
committer,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clone', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const url = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.clone({ url, dir });
|
||||
|
||||
expect(isomorphic.clone).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
url,
|
||||
dir,
|
||||
singleBranch: true,
|
||||
depth: 1,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
it('should pass a function that returns the authorization as the onAuth handler', async () => {
|
||||
const url = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.clone({ url, dir });
|
||||
|
||||
const { onAuth } = ((isomorphic.clone as unknown) as jest.Mock<
|
||||
typeof isomorphic['clone']
|
||||
>).mock.calls[0][0]!;
|
||||
|
||||
expect(onAuth()).toEqual(auth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('currentBranch', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const dir = '/some/mock/dir';
|
||||
const fullName = true;
|
||||
const git = Git.fromAuth({});
|
||||
|
||||
await git.currentBranch({ dir, fullName });
|
||||
|
||||
expect(isomorphic.currentBranch).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
fullname: true,
|
||||
});
|
||||
|
||||
await git.currentBranch({ dir });
|
||||
|
||||
expect(isomorphic.currentBranch).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
fullname: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetch', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const remote = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.fetch({ remote, dir });
|
||||
|
||||
expect(isomorphic.fetch).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
remote,
|
||||
dir,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
it('should pass a function that returns the authorization as the onAuth handler', async () => {
|
||||
const remote = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.fetch({ remote, dir });
|
||||
|
||||
const { onAuth } = ((isomorphic.fetch as unknown) as jest.Mock<
|
||||
typeof isomorphic['fetch']
|
||||
>).mock.calls[0][0]!;
|
||||
|
||||
expect(onAuth()).toEqual(auth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const dir = '/some/mock/dir';
|
||||
|
||||
const git = Git.fromAuth({});
|
||||
|
||||
await git.init({ dir });
|
||||
|
||||
expect(isomorphic.init).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('merge', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const dir = '/some/mock/dir';
|
||||
const author = {
|
||||
name: 'author',
|
||||
email: 'test@backstage.io',
|
||||
};
|
||||
const committer = {
|
||||
name: 'comitter',
|
||||
email: 'test@backstage.io',
|
||||
};
|
||||
const theirs = 'master';
|
||||
const ours = 'production';
|
||||
|
||||
const git = Git.fromAuth({});
|
||||
|
||||
await git.merge({ dir, theirs, ours, author, committer });
|
||||
|
||||
expect(isomorphic.merge).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
ours,
|
||||
theirs,
|
||||
author,
|
||||
committer,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('push', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const remote = 'origin';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.push({ dir, remote });
|
||||
|
||||
expect(isomorphic.push).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
remote,
|
||||
dir,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
it('should pass a function that returns the authorization as the onAuth handler', async () => {
|
||||
const remote = 'origin';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.push({ remote, dir });
|
||||
|
||||
const { onAuth } = ((isomorphic.push as unknown) as jest.Mock<
|
||||
typeof isomorphic['push']
|
||||
>).mock.calls[0][0]!;
|
||||
|
||||
expect(onAuth()).toEqual(auth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readCommit', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const dir = '/some/mock/dir';
|
||||
const sha = 'as43bd7';
|
||||
|
||||
const git = Git.fromAuth({});
|
||||
|
||||
await git.readCommit({ dir, sha });
|
||||
|
||||
expect(isomorphic.readCommit).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
oid: sha,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveRef', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const dir = '/some/mock/dir';
|
||||
const ref = 'as43bd7';
|
||||
|
||||
const git = Git.fromAuth({});
|
||||
|
||||
await git.resolveRef({ dir, ref });
|
||||
|
||||
expect(isomorphic.resolveRef).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
ref,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import git, {
|
||||
ProgressCallback,
|
||||
MergeResult,
|
||||
ReadCommitResult,
|
||||
} from 'isomorphic-git';
|
||||
import http from 'isomorphic-git/http/node';
|
||||
import fs from 'fs-extra';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
/*
|
||||
provider username password
|
||||
GitHub token 'x-oauth-basic'
|
||||
GitHub App token 'x-access-token'
|
||||
BitBucket 'x-token-auth' token
|
||||
GitLab 'oauth2' token
|
||||
From : https://isomorphic-git.org/docs/en/onAuth
|
||||
|
||||
Azure 'notempty' token
|
||||
*/
|
||||
export class Git {
|
||||
private constructor(
|
||||
private readonly config: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
logger?: Logger;
|
||||
},
|
||||
) {}
|
||||
|
||||
async add({
|
||||
dir,
|
||||
filepath,
|
||||
}: {
|
||||
dir: string;
|
||||
filepath: string;
|
||||
}): Promise<void> {
|
||||
this.config.logger?.info(`Adding file {dir=${dir},filepath=${filepath}}`);
|
||||
|
||||
return git.add({ fs, dir, filepath });
|
||||
}
|
||||
|
||||
async addRemote({
|
||||
dir,
|
||||
url,
|
||||
remote,
|
||||
}: {
|
||||
dir: string;
|
||||
remote: string;
|
||||
url: string;
|
||||
}): Promise<void> {
|
||||
this.config.logger?.info(
|
||||
`Creating new remote {dir=${dir},remote=${remote},url=${url}}`,
|
||||
);
|
||||
return git.addRemote({ fs, dir, remote, url });
|
||||
}
|
||||
|
||||
async commit({
|
||||
dir,
|
||||
message,
|
||||
author,
|
||||
committer,
|
||||
}: {
|
||||
dir: string;
|
||||
message: string;
|
||||
author: { name: string; email: string };
|
||||
committer: { name: string; email: string };
|
||||
}): Promise<string> {
|
||||
this.config.logger?.info(
|
||||
`Committing file to repo {dir=${dir},message=${message}}`,
|
||||
);
|
||||
|
||||
return git.commit({ fs, dir, message, author, committer });
|
||||
}
|
||||
|
||||
async clone({ url, dir }: { url: string; dir: string }): Promise<void> {
|
||||
this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`);
|
||||
return git.clone({
|
||||
fs,
|
||||
http,
|
||||
url,
|
||||
dir,
|
||||
singleBranch: true,
|
||||
depth: 1,
|
||||
onProgress: this.onProgressHandler(),
|
||||
headers: {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: this.onAuth,
|
||||
});
|
||||
}
|
||||
|
||||
// https://isomorphic-git.org/docs/en/currentBranch
|
||||
async currentBranch({
|
||||
dir,
|
||||
fullName,
|
||||
}: {
|
||||
dir: string;
|
||||
fullName?: boolean;
|
||||
}): Promise<string | undefined> {
|
||||
const fullname = fullName ?? false;
|
||||
return git.currentBranch({ fs, dir, fullname }) as Promise<
|
||||
string | undefined
|
||||
>;
|
||||
}
|
||||
|
||||
// https://isomorphic-git.org/docs/en/fetch
|
||||
async fetch({
|
||||
dir,
|
||||
remote,
|
||||
}: {
|
||||
dir: string;
|
||||
remote?: string;
|
||||
}): Promise<void> {
|
||||
const remoteValue = remote ?? 'origin';
|
||||
this.config.logger?.info(
|
||||
`Fetching remote=${remoteValue} for repository {dir=${dir}}`,
|
||||
);
|
||||
await git.fetch({
|
||||
fs,
|
||||
http,
|
||||
dir,
|
||||
remote: remoteValue,
|
||||
onProgress: this.onProgressHandler(),
|
||||
headers: {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: this.onAuth,
|
||||
});
|
||||
}
|
||||
|
||||
async init({ dir }: { dir: string }): Promise<void> {
|
||||
this.config.logger?.info(`Init git repository {dir=${dir}}`);
|
||||
|
||||
return git.init({
|
||||
fs,
|
||||
dir,
|
||||
});
|
||||
}
|
||||
|
||||
// https://isomorphic-git.org/docs/en/merge
|
||||
async merge({
|
||||
dir,
|
||||
theirs,
|
||||
ours,
|
||||
author,
|
||||
committer,
|
||||
}: {
|
||||
dir: string;
|
||||
theirs: string;
|
||||
ours?: string;
|
||||
author: { name: string; email: string };
|
||||
committer: { name: string; email: string };
|
||||
}): Promise<MergeResult> {
|
||||
this.config.logger?.info(
|
||||
`Merging branch '${theirs}' into '${ours}' for repository {dir=${dir}}`,
|
||||
);
|
||||
|
||||
// If ours is undefined, current branch is used.
|
||||
return git.merge({
|
||||
fs,
|
||||
dir,
|
||||
ours,
|
||||
theirs,
|
||||
author,
|
||||
committer,
|
||||
});
|
||||
}
|
||||
|
||||
async push({ dir, remote }: { dir: string; remote: string }) {
|
||||
this.config.logger?.info(
|
||||
`Pushing directory to remote {dir=${dir},remote=${remote}}`,
|
||||
);
|
||||
return git.push({
|
||||
fs,
|
||||
dir,
|
||||
http,
|
||||
onProgress: this.onProgressHandler(),
|
||||
headers: {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
remote: remote,
|
||||
onAuth: this.onAuth,
|
||||
});
|
||||
}
|
||||
|
||||
// https://isomorphic-git.org/docs/en/readCommit
|
||||
async readCommit({
|
||||
dir,
|
||||
sha,
|
||||
}: {
|
||||
dir: string;
|
||||
sha: string;
|
||||
}): Promise<ReadCommitResult> {
|
||||
return git.readCommit({ fs, dir, oid: sha });
|
||||
}
|
||||
|
||||
// https://isomorphic-git.org/docs/en/resolveRef
|
||||
async resolveRef({
|
||||
dir,
|
||||
ref,
|
||||
}: {
|
||||
dir: string;
|
||||
ref: string;
|
||||
}): Promise<string> {
|
||||
return git.resolveRef({ fs, dir, ref });
|
||||
}
|
||||
|
||||
private onAuth = () => ({
|
||||
username: this.config.username,
|
||||
password: this.config.password,
|
||||
});
|
||||
|
||||
private onProgressHandler = (): ProgressCallback => {
|
||||
let currentPhase = '';
|
||||
|
||||
return event => {
|
||||
if (currentPhase !== event.phase) {
|
||||
currentPhase = event.phase;
|
||||
this.config.logger?.info(event.phase);
|
||||
}
|
||||
const total = event.total
|
||||
? `${Math.round((event.loaded / event.total) * 100)}%`
|
||||
: event.loaded;
|
||||
this.config.logger?.debug(`status={${event.phase},total={${total}}}`);
|
||||
};
|
||||
};
|
||||
|
||||
static fromAuth = ({
|
||||
username,
|
||||
password,
|
||||
logger,
|
||||
}: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
logger?: Logger;
|
||||
}) => new Git({ username, password, logger });
|
||||
}
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { IconLinkVertical } from './IconLinkVertical';
|
||||
export { Git } from './git';
|
||||
@@ -145,7 +145,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
start(): Promise<http.Server> {
|
||||
async start(): Promise<http.Server> {
|
||||
const app = express();
|
||||
const {
|
||||
port,
|
||||
@@ -168,16 +168,16 @@ export class ServiceBuilderImpl implements ServiceBuilder {
|
||||
app.use(notFoundHandler());
|
||||
app.use(errorHandler());
|
||||
|
||||
const server: http.Server = httpsSettings
|
||||
? await createHttpsServer(app, httpsSettings, logger)
|
||||
: createHttpServer(app, logger);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
app.on('error', e => {
|
||||
logger.error(`Failed to start up on port ${port}, ${e}`);
|
||||
reject(e);
|
||||
});
|
||||
|
||||
const server: http.Server = httpsSettings
|
||||
? createHttpsServer(app, httpsSettings, logger)
|
||||
: createHttpServer(app, logger);
|
||||
|
||||
const stoppableServer = stoppable(
|
||||
server.listen(port, host, () => {
|
||||
logger.info(`Listening on ${host}:${port}`);
|
||||
|
||||
@@ -47,14 +47,14 @@ export type CertificateReferenceOptions = {
|
||||
};
|
||||
|
||||
export type CertificateSigningOptions = {
|
||||
algorithm: string;
|
||||
algorithm?: string;
|
||||
size?: number;
|
||||
days?: number;
|
||||
attributes?: CertificateAttributes;
|
||||
attributes: CertificateAttributes;
|
||||
};
|
||||
|
||||
export type CertificateAttributes = {
|
||||
commonName?: string;
|
||||
commonName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -193,8 +193,26 @@ export function readCspOptions(
|
||||
* ```
|
||||
*/
|
||||
export function readHttpsSettings(config: Config): HttpsSettings | undefined {
|
||||
const cc = config.getOptionalConfig('https');
|
||||
const https = config.getOptional('https');
|
||||
if (https === true) {
|
||||
const baseUrl = config.getString('baseUrl');
|
||||
let commonName;
|
||||
try {
|
||||
commonName = new URL(baseUrl).hostname;
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid backend.baseUrl "${baseUrl}"`);
|
||||
}
|
||||
|
||||
return {
|
||||
certificate: {
|
||||
attributes: {
|
||||
commonName,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const cc = config.getOptionalConfig('https');
|
||||
if (!cc) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,16 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath, dirname } from 'path';
|
||||
import express from 'express';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { Logger } from 'winston';
|
||||
import { HttpsSettings } from './config';
|
||||
import { CertificateSigningOptions, HttpsSettings } from './config';
|
||||
|
||||
const ALMOST_MONTH_IN_MS = 25 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Creates a Http server instance based on an Express application.
|
||||
@@ -45,48 +50,152 @@ export function createHttpServer(
|
||||
* @returns A Https server instance
|
||||
*
|
||||
*/
|
||||
export function createHttpsServer(
|
||||
export async function createHttpsServer(
|
||||
app: express.Express,
|
||||
httpsSettings: HttpsSettings,
|
||||
logger?: Logger,
|
||||
): http.Server {
|
||||
): Promise<http.Server> {
|
||||
logger?.info('Initializing https server');
|
||||
|
||||
const credentials: { key: string; cert: string } = {
|
||||
key: '',
|
||||
cert: '',
|
||||
};
|
||||
let credentials: { key: string | Buffer; cert: string | Buffer };
|
||||
|
||||
const signingOptions: any = httpsSettings?.certificate;
|
||||
|
||||
if (signingOptions?.algorithm !== undefined) {
|
||||
logger?.info('Generating self-signed certificate with attributes');
|
||||
|
||||
const certificateAttributes: Array<any> = Object.entries(
|
||||
signingOptions.attributes,
|
||||
).map(([name, value]) => ({ name, value }));
|
||||
|
||||
// TODO: Create a type def for selfsigned.
|
||||
const signatures = require('selfsigned').generate(certificateAttributes, {
|
||||
algorithm: signingOptions?.algorithm,
|
||||
keySize: signingOptions?.size || 2048,
|
||||
days: signingOptions?.days || 30,
|
||||
});
|
||||
|
||||
logger?.info('Bootstrapping self-signed certificate');
|
||||
|
||||
credentials.key = signatures.private;
|
||||
credentials.cert = signatures.cert;
|
||||
// TODO(Rugvip): remove support for generated certificate params and make this a more straightforward check
|
||||
if (signingOptions?.attributes) {
|
||||
credentials = await getGeneratedCertificate(signingOptions, logger);
|
||||
} else {
|
||||
logger?.info('Bootstrapping cert from config');
|
||||
logger?.info('Loading certificate from config');
|
||||
|
||||
credentials.key = signingOptions?.key;
|
||||
credentials.cert = signingOptions?.cert;
|
||||
credentials = {
|
||||
key: signingOptions?.key,
|
||||
cert: signingOptions?.cert,
|
||||
};
|
||||
}
|
||||
|
||||
if (credentials.key === '' || credentials.cert === '') {
|
||||
throw new Error('Invalid credentials');
|
||||
if (!credentials.key || !credentials.cert) {
|
||||
throw new Error('Invalid HTTPS credentials');
|
||||
}
|
||||
|
||||
return https.createServer(credentials, app) as http.Server;
|
||||
}
|
||||
|
||||
async function getGeneratedCertificate(
|
||||
options: CertificateSigningOptions,
|
||||
logger?: Logger,
|
||||
) {
|
||||
if (options?.algorithm) {
|
||||
logger?.warn(
|
||||
'Certificate generation configuration with parameters in backend.https.certificate is deprecated, set backend.https = true instead',
|
||||
);
|
||||
}
|
||||
|
||||
const hasModules = await fs.pathExists('node_modules');
|
||||
let certPath;
|
||||
if (hasModules) {
|
||||
certPath = resolvePath(
|
||||
'node_modules/.cache/backstage-backend/dev-cert.pem',
|
||||
);
|
||||
await fs.ensureDir(dirname(certPath));
|
||||
} else {
|
||||
certPath = resolvePath('.dev-cert.pem');
|
||||
}
|
||||
|
||||
let cert = undefined;
|
||||
if (await fs.pathExists(certPath)) {
|
||||
const stat = await fs.stat(certPath);
|
||||
const ageMs = Date.now() - stat.ctimeMs;
|
||||
if (stat.isFile() && ageMs < ALMOST_MONTH_IN_MS) {
|
||||
cert = await fs.readFile(certPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (cert) {
|
||||
logger?.info('Using existing self-signed certificate');
|
||||
return {
|
||||
key: cert,
|
||||
cert: cert,
|
||||
};
|
||||
}
|
||||
|
||||
logger?.info('Generating new self-signed certificate');
|
||||
const newCert = await createCertificate(options);
|
||||
await fs.writeFile(certPath, newCert.cert + newCert.key, 'utf8');
|
||||
return newCert;
|
||||
}
|
||||
|
||||
async function createCertificate(options: CertificateSigningOptions) {
|
||||
const attributes: Array<any> = Object.entries(
|
||||
options.attributes,
|
||||
).map(([name, value]) => ({ name, value }));
|
||||
|
||||
const params = {
|
||||
algorithm: options?.algorithm || 'sha256',
|
||||
keySize: options?.size || 2048,
|
||||
days: options?.days || 30,
|
||||
extensions: [
|
||||
{
|
||||
name: 'keyUsage',
|
||||
keyCertSign: true,
|
||||
digitalSignature: true,
|
||||
nonRepudiation: true,
|
||||
keyEncipherment: true,
|
||||
dataEncipherment: true,
|
||||
},
|
||||
{
|
||||
name: 'extKeyUsage',
|
||||
serverAuth: true,
|
||||
clientAuth: true,
|
||||
codeSigning: true,
|
||||
timeStamping: true,
|
||||
},
|
||||
{
|
||||
name: 'subjectAltName',
|
||||
altNames: [
|
||||
{
|
||||
type: 2, // DNS
|
||||
value: 'localhost',
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
value: 'localhost.localdomain',
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
value: '[::1]',
|
||||
},
|
||||
{
|
||||
type: 7, // IP
|
||||
ip: '127.0.0.1',
|
||||
},
|
||||
{
|
||||
type: 7,
|
||||
ip: 'fe80::1',
|
||||
},
|
||||
...(options.attributes.commonName
|
||||
? [
|
||||
{
|
||||
type: 2, // DNS
|
||||
value: options.attributes.commonName,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return new Promise<{ key: string; cert: string }>((resolve, reject) =>
|
||||
require('selfsigned').generate(
|
||||
attributes,
|
||||
params,
|
||||
(err: Error, bundle: { private: string; cert: string }) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ key: bundle.private, cert: bundle.cert });
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# example-backend
|
||||
|
||||
## 0.2.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [5eb8c9b9e]
|
||||
- Updated dependencies [7e3451700]
|
||||
- @backstage/plugin-scaffolder-backend@0.4.0
|
||||
|
||||
## 0.2.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7cfcd58ee: use node 14 for backend Dockerfile
|
||||
- Updated dependencies [19554f6d6]
|
||||
- Updated dependencies [33a82a713]
|
||||
- Updated dependencies [5de26b9a6]
|
||||
- Updated dependencies [30d6c78fb]
|
||||
- Updated dependencies [5084e5039]
|
||||
- Updated dependencies [a8573e53b]
|
||||
- Updated dependencies [aed8f7f12]
|
||||
- @backstage/plugin-scaffolder-backend@0.3.6
|
||||
- @backstage/plugin-catalog-backend@0.5.1
|
||||
- @backstage/plugin-techdocs-backend@0.5.0
|
||||
- example-app@0.2.8
|
||||
|
||||
## 0.2.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:12-buster
|
||||
FROM node:14-buster
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-backend",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.10",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -32,19 +32,19 @@
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@backstage/plugin-app-backend": "^0.3.3",
|
||||
"@backstage/plugin-auth-backend": "^0.2.7",
|
||||
"@backstage/plugin-catalog-backend": "^0.5.0",
|
||||
"@backstage/plugin-catalog-backend": "^0.5.1",
|
||||
"@backstage/plugin-graphql-backend": "^0.1.4",
|
||||
"@backstage/plugin-kubernetes-backend": "^0.2.3",
|
||||
"@backstage/plugin-kafka-backend": "^0.1.0",
|
||||
"@backstage/plugin-proxy-backend": "^0.2.3",
|
||||
"@backstage/plugin-rollbar-backend": "^0.1.5",
|
||||
"@backstage/plugin-scaffolder-backend": "^0.3.5",
|
||||
"@backstage/plugin-techdocs-backend": "^0.4.0",
|
||||
"@backstage/plugin-scaffolder-backend": "^0.4.0",
|
||||
"@backstage/plugin-techdocs-backend": "^0.5.0",
|
||||
"@gitbeaker/node": "^25.2.0",
|
||||
"@octokit/rest": "^18.0.0",
|
||||
"azure-devops-node-api": "^10.1.1",
|
||||
"dockerode": "^3.2.1",
|
||||
"example-app": "^0.2.7",
|
||||
"example-app": "^0.2.8",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"knex": "^0.21.6",
|
||||
@@ -54,7 +54,7 @@
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.4.2",
|
||||
"@backstage/cli": "^0.4.3",
|
||||
"@types/dockerode": "^3.2.1",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-serve-static-core": "^4.17.5",
|
||||
|
||||
@@ -15,12 +15,8 @@
|
||||
*/
|
||||
import {
|
||||
createRouter,
|
||||
DirectoryPreparer,
|
||||
Preparers,
|
||||
Generators,
|
||||
TechdocsGenerator,
|
||||
CommonGitPreparer,
|
||||
UrlPreparer,
|
||||
Publisher,
|
||||
} from '@backstage/plugin-techdocs-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
@@ -33,30 +29,25 @@ export default async function createPlugin({
|
||||
reader,
|
||||
}: PluginEnvironment) {
|
||||
// Preparers are responsible for fetching source files for documentation.
|
||||
const preparers = new Preparers();
|
||||
|
||||
const directoryPreparer = new DirectoryPreparer(logger);
|
||||
preparers.register('dir', directoryPreparer);
|
||||
|
||||
const commonGitPreparer = new CommonGitPreparer(logger);
|
||||
preparers.register('github', commonGitPreparer);
|
||||
preparers.register('gitlab', commonGitPreparer);
|
||||
preparers.register('azure/api', commonGitPreparer);
|
||||
|
||||
const urlPreparer = new UrlPreparer(reader, logger);
|
||||
preparers.register('url', urlPreparer);
|
||||
const preparers = await Preparers.fromConfig(config, {
|
||||
logger,
|
||||
reader,
|
||||
});
|
||||
|
||||
// Generators are used for generating documentation sites.
|
||||
const generators = new Generators();
|
||||
const techdocsGenerator = new TechdocsGenerator(logger, config);
|
||||
generators.register('techdocs', techdocsGenerator);
|
||||
const generators = await Generators.fromConfig(config, {
|
||||
logger,
|
||||
});
|
||||
|
||||
// Publishers are used for
|
||||
// Publisher is used for
|
||||
// 1. Publishing generated files to storage
|
||||
// 2. Fetching files from storage and passing them to TechDocs frontend.
|
||||
const publisher = Publisher.fromConfig(config, logger, discovery);
|
||||
const publisher = await Publisher.fromConfig(config, {
|
||||
logger,
|
||||
discovery,
|
||||
});
|
||||
|
||||
// Docker client used by the generators.
|
||||
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
|
||||
const dockerClient = new Docker();
|
||||
|
||||
return await createRouter({
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# @backstage/cli
|
||||
|
||||
## 0.4.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 37a7d26c4: Use consistent file extensions for JS output when building packages.
|
||||
- 818d45e94: Fix detection of external package child directories
|
||||
- 0588be01f: Add `backend:bundle` command for bundling a backend package with dependencies into a deployment archive.
|
||||
- b8abdda57: Add color to output from `versions:bump` in order to make it easier to spot changes. Also highlight possible breaking changes and link to changelogs.
|
||||
- Updated dependencies [ad5c56fd9]
|
||||
- @backstage/config-loader@0.4.1
|
||||
|
||||
## 0.4.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d45efbc9b: Fix typo in .app.listen.port config schema
|
||||
|
||||
## 0.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 19554f6d6: Added Github Actions for Create React App, and allow better imports of files inside a module when they're exposed using `files` in `package.json`
|
||||
- 7d72f9b09: Fix for `app.listen.host` configuration not properly overriding listening host.
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/cli",
|
||||
"description": "CLI for developing Backstage plugins and apps",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.5",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -30,7 +30,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/cli-common": "^0.1.1",
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@backstage/config-loader": "^0.4.0",
|
||||
"@backstage/config-loader": "^0.4.1",
|
||||
"@hot-loader/react-dom": "^16.13.0",
|
||||
"@lerna/package-graph": "^3.18.5",
|
||||
"@lerna/project": "^3.18.0",
|
||||
@@ -66,7 +66,7 @@
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-jest": "^24.1.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.1",
|
||||
"eslint-plugin-monorepo": "^0.2.1",
|
||||
"eslint-plugin-monorepo": "^0.3.2",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"eslint-plugin-react-hooks": "^4.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "^4.0.5",
|
||||
@@ -111,11 +111,11 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-common": "^0.4.1",
|
||||
"@backstage/backend-common": "^0.4.2",
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@backstage/core": "^0.4.1",
|
||||
"@backstage/dev-utils": "^0.1.6",
|
||||
"@backstage/test-utils": "^0.1.5",
|
||||
"@backstage/core": "^0.4.3",
|
||||
"@backstage/dev-utils": "^0.1.7",
|
||||
"@backstage/test-utils": "^0.1.6",
|
||||
"@backstage/theme": "^0.2.2",
|
||||
"@types/diff": "^4.0.2",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
@@ -125,7 +125,6 @@
|
||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||
"@types/mock-fs": "^4.13.0",
|
||||
"@types/node": "^13.7.2",
|
||||
"@types/ora": "^3.2.0",
|
||||
"@types/react-dev-utils": "^9.0.4",
|
||||
"@types/recursive-readdir": "^2.2.0",
|
||||
"@types/rollup-plugin-peer-deps-external": "^2.2.0",
|
||||
@@ -182,7 +181,7 @@
|
||||
"visibility": "frontend",
|
||||
"description": "The host that the frontend should be bound to. Only used for local development."
|
||||
},
|
||||
"post": {
|
||||
"port": {
|
||||
"type": "number",
|
||||
"visibility": "frontend",
|
||||
"description": "The port that the frontend should be bound to. Only used for local development."
|
||||
|
||||
@@ -38,7 +38,7 @@ export default async (cmd: Command) => {
|
||||
try {
|
||||
await createDistWorkspace([pkg.name], {
|
||||
targetDir: tmpDir,
|
||||
buildDependencies: Boolean(cmd.build),
|
||||
buildDependencies: Boolean(cmd.buildDependencies),
|
||||
buildExcludes: [pkg.name],
|
||||
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
|
||||
skeleton: SKELETON_FILE,
|
||||
|
||||
@@ -45,9 +45,12 @@ export function registerCommands(program: CommanderStatic) {
|
||||
.action(lazy(() => import('./backend/build').then(m => m.default)));
|
||||
|
||||
program
|
||||
.command('backend:__experimental__bundle__', { hidden: true })
|
||||
.description('Bundle all backend packages into dist-workspace')
|
||||
.option('--build', 'Build packages before packing them into the image')
|
||||
.command('backend:bundle')
|
||||
.description('Bundle the backend into a deployment archive')
|
||||
.option(
|
||||
'--build-dependencies',
|
||||
'Build all local package dependencies before bundling the backend',
|
||||
)
|
||||
.action(lazy(() => import('./backend/bundle').then(m => m.default)));
|
||||
|
||||
program
|
||||
@@ -212,6 +215,7 @@ function lazy(
|
||||
try {
|
||||
const actionFunc = await getActionFunc();
|
||||
await actionFunc(...args);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
exitWithError(error);
|
||||
|
||||
@@ -23,6 +23,15 @@ import * as runObj from '../../lib/run';
|
||||
import bump from './bump';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
// Remove log coloring to simplify log matching
|
||||
jest.mock('chalk', () => ({
|
||||
blue: (str: string) => str,
|
||||
cyan: (str: string) => str,
|
||||
green: (str: string) => str,
|
||||
magenta: (str: string) => str,
|
||||
yellow: (str: string) => str,
|
||||
}));
|
||||
|
||||
const REGISTRY_VERSIONS: { [name: string]: string } = {
|
||||
'@backstage/core': '1.0.6',
|
||||
'@backstage/core-api': '1.0.7',
|
||||
@@ -121,11 +130,15 @@ describe('bump', () => {
|
||||
'Checking for updates of @backstage/core',
|
||||
'Checking for updates of @backstage/core-api',
|
||||
'Some packages are outdated, updating',
|
||||
'Removing lockfile entry for @backstage/core@^1.0.3 to bump to 1.0.6',
|
||||
'Removing lockfile entry for @backstage/core-api@^1.0.6 to bump to 1.0.7',
|
||||
'Removing lockfile entry for @backstage/core-api@^1.0.3 to bump to 1.0.7',
|
||||
'Bumping @backstage/theme in b to ^2.0.0',
|
||||
"Running 'yarn install' to install new versions",
|
||||
'unlocking @backstage/core@^1.0.3 ~> 1.0.6',
|
||||
'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7',
|
||||
'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7',
|
||||
'bumping @backstage/theme in b to ^2.0.0',
|
||||
'Running yarn install to install new versions',
|
||||
'⚠️ The following packages may have breaking changes:',
|
||||
' @backstage/theme',
|
||||
' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md',
|
||||
'Version bump complete!',
|
||||
]);
|
||||
|
||||
expect(runObj.runPlain).toHaveBeenCalledTimes(3);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import semver from 'semver';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { run } from '../../lib/run';
|
||||
@@ -35,6 +36,7 @@ const DEP_TYPES = [
|
||||
|
||||
type PkgVersionInfo = {
|
||||
range: string;
|
||||
target: string;
|
||||
name: string;
|
||||
location: string;
|
||||
};
|
||||
@@ -69,6 +71,7 @@ export default async () => {
|
||||
name,
|
||||
location: pkg.location,
|
||||
range: `^${target}`, // TODO(Rugvip): Option to use something else than ^?
|
||||
target,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -98,9 +101,9 @@ export default async () => {
|
||||
|
||||
// Write all discovered version bumps to package.json in this repo
|
||||
if (versionBumps.size === 0 && unlocked.length === 0) {
|
||||
console.log('All Backstage packages are up to date!');
|
||||
console.log(chalk.green('All Backstage packages are up to date!'));
|
||||
} else {
|
||||
console.log('Some packages are outdated, updating');
|
||||
console.log(chalk.yellow('Some packages are outdated, updating'));
|
||||
console.log();
|
||||
|
||||
if (unlocked.length > 0) {
|
||||
@@ -115,7 +118,9 @@ export default async () => {
|
||||
if (!removed.has(key)) {
|
||||
removed.add(key);
|
||||
console.log(
|
||||
`Removing lockfile entry for ${name}@${range} to bump to ${target}`,
|
||||
`${chalk.magenta('unlocking')} ${name}@${chalk.yellow(
|
||||
range,
|
||||
)} ~> ${chalk.yellow(target)}`,
|
||||
);
|
||||
lockfile.remove(name, range);
|
||||
}
|
||||
@@ -123,16 +128,34 @@ export default async () => {
|
||||
await lockfile.save();
|
||||
}
|
||||
|
||||
const breakingUpdates = new Map<string, { from: string; to: string }>();
|
||||
await workerThreads(16, versionBumps.entries(), async ([name, deps]) => {
|
||||
const pkgPath = resolvePath(deps[0].location, 'package.json');
|
||||
const pkgJson = await fs.readJson(pkgPath);
|
||||
|
||||
for (const dep of deps) {
|
||||
console.log(`Bumping ${dep.name} in ${name} to ${dep.range}`);
|
||||
console.log(
|
||||
`${chalk.cyan('bumping')} ${dep.name} in ${chalk.cyan(
|
||||
name,
|
||||
)} to ${chalk.yellow(dep.range)}`,
|
||||
);
|
||||
|
||||
for (const depType of DEP_TYPES) {
|
||||
if (depType in pkgJson && dep.name in pkgJson[depType]) {
|
||||
const oldRange = pkgJson[depType][dep.name];
|
||||
pkgJson[depType][dep.name] = dep.range;
|
||||
|
||||
// Check if the update was at least a pre-v1 minor or post-v1 major release
|
||||
const lockfileEntry = lockfile
|
||||
.get(dep.name)
|
||||
?.find(entry => entry.range === oldRange);
|
||||
if (lockfileEntry) {
|
||||
const from = lockfileEntry.version;
|
||||
const to = dep.target;
|
||||
if (!semver.satisfies(to, `^${from}`)) {
|
||||
breakingUpdates.set(dep.name, { from, to });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,9 +164,42 @@ export default async () => {
|
||||
});
|
||||
|
||||
console.log();
|
||||
console.log("Running 'yarn install' to install new versions");
|
||||
console.log(
|
||||
`Running ${chalk.blue('yarn install')} to install new versions`,
|
||||
);
|
||||
console.log();
|
||||
await run('yarn', ['install']);
|
||||
|
||||
if (breakingUpdates.size > 0) {
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.yellow('⚠️ The following packages may have breaking changes:'),
|
||||
);
|
||||
console.log();
|
||||
|
||||
for (const name of Array.from(breakingUpdates.keys()).sort()) {
|
||||
console.log(` ${chalk.yellow(name)}`);
|
||||
|
||||
let path;
|
||||
if (name.startsWith('@backstage/plugin-')) {
|
||||
path = `plugins/${name.replace('@backstage/plugin-', '')}`;
|
||||
} else if (name.startsWith('@backstage/')) {
|
||||
path = `packages/${name.replace('@backstage/', '')}`;
|
||||
}
|
||||
if (path) {
|
||||
// TODO(Rugvip): Grab these URLs and paths from package.json, possibly verify existence
|
||||
// Possibly invent new "changelog" field in package.json or some sh*t.
|
||||
console.log(
|
||||
` https://github.com/backstage/backstage/blob/master/${path}/CHANGELOG.md`,
|
||||
);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
} else {
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log(chalk.green('Version bump complete!'));
|
||||
}
|
||||
|
||||
console.log();
|
||||
|
||||
@@ -62,7 +62,7 @@ export const makeConfigs = async (
|
||||
output.push({
|
||||
dir: 'dist',
|
||||
entryFileNames: 'index.cjs.js',
|
||||
chunkFileNames: 'cjs/[name]-[hash].js',
|
||||
chunkFileNames: 'cjs/[name]-[hash].cjs.js',
|
||||
format: 'commonjs',
|
||||
sourcemap: true,
|
||||
});
|
||||
@@ -71,7 +71,7 @@ export const makeConfigs = async (
|
||||
output.push({
|
||||
dir: 'dist',
|
||||
entryFileNames: 'index.esm.js',
|
||||
chunkFileNames: 'esm/[name]-[hash].js',
|
||||
chunkFileNames: 'esm/[name]-[hash].esm.js',
|
||||
format: 'module',
|
||||
sourcemap: true,
|
||||
});
|
||||
|
||||
@@ -14,20 +14,27 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin';
|
||||
|
||||
describe('LinkedPackageResolvePlugin', () => {
|
||||
const root = os.platform() === 'win32' ? 'C:\\root' : '/root';
|
||||
|
||||
it('should re-write paths for external packages', () => {
|
||||
const plugin = new LinkedPackageResolvePlugin('/root/repo/node_modules', [
|
||||
{
|
||||
name: 'a',
|
||||
location: '/root/external-a',
|
||||
},
|
||||
{
|
||||
name: '@s/b',
|
||||
location: '/root/external-b',
|
||||
},
|
||||
]);
|
||||
const plugin = new LinkedPackageResolvePlugin(
|
||||
path.resolve(root, 'repo/node_modules'),
|
||||
[
|
||||
{
|
||||
name: 'a',
|
||||
location: path.resolve(root, 'external-a'),
|
||||
},
|
||||
{
|
||||
name: '@s/b',
|
||||
location: path.resolve(root, 'external-b'),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const tapAsync = jest.fn();
|
||||
const doResolve = jest.fn();
|
||||
@@ -50,10 +57,10 @@ describe('LinkedPackageResolvePlugin', () => {
|
||||
const callbackX = jest.fn();
|
||||
tap(
|
||||
{
|
||||
request: '/root/repo/package/x/src/module.ts',
|
||||
path: '/root/repo/package/x/src',
|
||||
request: path.resolve(root, 'repo/package/x/src/module.ts'),
|
||||
path: path.resolve(root, 'repo/package/x/src'),
|
||||
context: {
|
||||
issuer: '/root/repo/package/x/src/index.ts',
|
||||
issuer: path.resolve(root, 'repo/package/x/src/index.ts'),
|
||||
},
|
||||
},
|
||||
'some-context',
|
||||
@@ -77,14 +84,31 @@ describe('LinkedPackageResolvePlugin', () => {
|
||||
expect(callbackFalse).toHaveBeenCalledWith();
|
||||
expect(doResolve).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Internal modules with a path prefix of an external module
|
||||
const callbackY = jest.fn();
|
||||
tap(
|
||||
{
|
||||
request: path.resolve(root, 'external-aa/src/module.ts'),
|
||||
path: path.resolve(root, 'external-aa/src'),
|
||||
context: {
|
||||
issuer: path.resolve(root, 'external-aa/src/index.ts'),
|
||||
},
|
||||
},
|
||||
'some-context',
|
||||
callbackY,
|
||||
);
|
||||
expect(callbackY).toHaveBeenCalledTimes(1);
|
||||
expect(callbackY).toHaveBeenCalledWith();
|
||||
expect(doResolve).toHaveBeenCalledTimes(0);
|
||||
|
||||
// External modules have their path and issuer context rewritten, but not the request
|
||||
const callbackA = jest.fn();
|
||||
tap(
|
||||
{
|
||||
request: '/root/external-a/src/module.ts',
|
||||
path: '/root/external-a/src',
|
||||
request: path.resolve(root, 'external-a/src/module.ts'),
|
||||
path: path.resolve(root, 'external-a/src'),
|
||||
context: {
|
||||
issuer: '/root/external-a/src/index.ts',
|
||||
issuer: path.resolve(root, 'external-a/src/index.ts'),
|
||||
},
|
||||
},
|
||||
'some-context',
|
||||
@@ -95,13 +119,16 @@ describe('LinkedPackageResolvePlugin', () => {
|
||||
expect(doResolve).toHaveBeenCalledWith(
|
||||
resolver.hooks.resolve,
|
||||
{
|
||||
request: '/root/external-a/src/module.ts',
|
||||
path: '/root/repo/node_modules/a/src',
|
||||
request: path.resolve(root, 'external-a/src/module.ts'),
|
||||
path: path.resolve(root, 'repo/node_modules/a/src'),
|
||||
context: {
|
||||
issuer: '/root/repo/node_modules/a/src/index.ts',
|
||||
issuer: path.resolve(root, 'repo/node_modules/a/src/index.ts'),
|
||||
},
|
||||
},
|
||||
'resolve /root/external-a/src/module.ts in /root/repo/node_modules/a',
|
||||
`resolve ${path.resolve(
|
||||
root,
|
||||
'external-a/src/module.ts',
|
||||
)} in ${path.resolve(root, 'repo/node_modules/a')}`,
|
||||
'some-context',
|
||||
callbackA,
|
||||
);
|
||||
@@ -110,8 +137,8 @@ describe('LinkedPackageResolvePlugin', () => {
|
||||
const callbackB = jest.fn();
|
||||
tap(
|
||||
{
|
||||
request: '/root/external-b/src/module.ts',
|
||||
path: '/root/external-b/src',
|
||||
request: path.resolve(root, 'external-b/src/module.ts'),
|
||||
path: path.resolve(root, 'external-b/src'),
|
||||
context: {
|
||||
issuer: false,
|
||||
},
|
||||
@@ -124,13 +151,16 @@ describe('LinkedPackageResolvePlugin', () => {
|
||||
expect(doResolve).toHaveBeenLastCalledWith(
|
||||
resolver.hooks.resolve,
|
||||
{
|
||||
request: '/root/external-b/src/module.ts',
|
||||
path: '/root/repo/node_modules/@s/b/src',
|
||||
request: path.resolve(root, 'external-b/src/module.ts'),
|
||||
path: path.resolve(root, 'repo/node_modules/@s/b/src'),
|
||||
context: {
|
||||
issuer: false,
|
||||
},
|
||||
},
|
||||
'resolve /root/external-b/src/module.ts in /root/repo/node_modules/@s/b',
|
||||
`resolve ${path.resolve(
|
||||
root,
|
||||
'external-b/src/module.ts',
|
||||
)} in ${path.resolve(root, 'repo/node_modules/@s/b')}`,
|
||||
'some-context',
|
||||
callbackB,
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { ResolvePlugin } from 'webpack';
|
||||
import { isChildPath } from './paths';
|
||||
import { LernaPackage } from './types';
|
||||
|
||||
// Enables proper resolution of packages when linking in external packages.
|
||||
@@ -40,7 +41,7 @@ export class LinkedPackageResolvePlugin implements ResolvePlugin {
|
||||
callback: () => void,
|
||||
) => {
|
||||
const pkg = this.packages.find(
|
||||
pkg => data.path && data.path.startsWith(pkg.location),
|
||||
pkg => data.path && isChildPath(pkg.location, data.path),
|
||||
);
|
||||
if (!pkg) {
|
||||
callback();
|
||||
|
||||
@@ -24,7 +24,7 @@ import webpack from 'webpack';
|
||||
import nodeExternals from 'webpack-node-externals';
|
||||
import { optimization } from './optimization';
|
||||
import { Config } from '@backstage/config';
|
||||
import { BundlingPaths } from './paths';
|
||||
import { BundlingPaths, isChildPath } from './paths';
|
||||
import { transforms } from './transforms';
|
||||
import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin';
|
||||
import { BundlingOptions, BackendBundlingOptions, LernaPackage } from './types';
|
||||
@@ -87,7 +87,9 @@ export async function createConfig(
|
||||
const { plugins, loaders } = transforms(options);
|
||||
// Any package that is part of the monorepo but outside the monorepo root dir need
|
||||
// separate resolution logic.
|
||||
const externalPkgs = packages.filter(p => !p.location.startsWith(paths.root));
|
||||
const externalPkgs = packages.filter(
|
||||
p => !isChildPath(paths.root, p.location),
|
||||
);
|
||||
|
||||
const baseUrl = frontendConfig.getString('app.baseUrl');
|
||||
const validBaseUrl = new URL(baseUrl);
|
||||
@@ -199,7 +201,9 @@ export async function createBackendConfig(
|
||||
const moduleDirs = packages.map((p: any) =>
|
||||
resolvePath(p.location, 'node_modules'),
|
||||
);
|
||||
const externalPkgs = packages.filter(p => !p.location.startsWith(paths.root)); // See frontend config
|
||||
const externalPkgs = packages.filter(
|
||||
p => !isChildPath(paths.root, p.location),
|
||||
); // See frontend config
|
||||
|
||||
const { loaders } = transforms(options);
|
||||
|
||||
|
||||
@@ -15,8 +15,25 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { paths } from '../paths';
|
||||
|
||||
/**
|
||||
* Checks if dir is the same as or a child of base.
|
||||
*/
|
||||
export function isChildPath(base: string, dir: string): boolean {
|
||||
const relativePath = path.relative(base, dir);
|
||||
if (relativePath === '') {
|
||||
// The same directory
|
||||
return true;
|
||||
}
|
||||
|
||||
const outsideBase = relativePath.startsWith('..'); // not outside base
|
||||
const differentDrive = path.isAbsolute(relativePath); // on Windows, this means dir is on a different drive from base.
|
||||
|
||||
return !outsideBase && !differentDrive;
|
||||
}
|
||||
|
||||
export type BundlingPathsOptions = {
|
||||
// bundle entrypoint, e.g. 'src/index'
|
||||
entry: string;
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function serveBundle(options: ServeOptions) {
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.listen(port, url.hostname, (err?: Error) => {
|
||||
server.listen(port, host, (err?: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# @backstage/config-loader
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ad5c56fd9: Deprecate `$data` and replace it with `$include` which allows for any type of json value to be read from external files. In addition, `$include` can be used without a path, which causes the value at the root of the file to be loaded.
|
||||
|
||||
Most usages of `$data` can be directly replaced with `$include`, except if the referenced value is not a string, in which case the value needs to be changed. For example:
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
foo:
|
||||
$data: foo.yaml#myValue # replacing with $include will turn the value into a number
|
||||
$data: bar.yaml#myValue # replacing with $include is safe
|
||||
|
||||
# foo.yaml
|
||||
myValue: 0xf00
|
||||
|
||||
# bar.yaml
|
||||
myValue: bar
|
||||
```
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/config-loader",
|
||||
"description": "Config loading functionality used by Backstage backend, and CLI",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
@@ -36,7 +36,7 @@
|
||||
"fs-extra": "^9.0.0",
|
||||
"json-schema": "^0.2.5",
|
||||
"json-schema-merge-allof": "^0.7.0",
|
||||
"typescript-json-schema": "^0.45.0",
|
||||
"typescript-json-schema": "^0.47.0",
|
||||
"yaml": "^1.9.2",
|
||||
"yup": "^0.29.3"
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ const ctx: ReaderContext = {
|
||||
'my-data.json': '{"a":{"b":{"c":42}}}',
|
||||
'my-data.yaml': 'some:\n yaml:\n key: 7',
|
||||
'my-data.yml': 'different: { key: hello }',
|
||||
'invalid.yaml': 'foo: [}',
|
||||
} as { [key: string]: string })[path];
|
||||
|
||||
if (!content) {
|
||||
@@ -84,6 +85,41 @@ describe('readSecret', () => {
|
||||
).rejects.toThrow('File not found!');
|
||||
});
|
||||
|
||||
it('should include extra files', async () => {
|
||||
// New format with path in fragment
|
||||
await expect(
|
||||
readSecret({ include: 'my-data.json#a.b.c' }, ctx),
|
||||
).resolves.toBe(42);
|
||||
await expect(
|
||||
readSecret({ include: 'my-data.json#a.b' }, ctx),
|
||||
).resolves.toEqual({ c: 42 });
|
||||
await expect(
|
||||
readSecret({ include: 'my-data.yaml#some.yaml.key' }, ctx),
|
||||
).resolves.toBe(7);
|
||||
await expect(readSecret({ include: 'my-data.yaml' }, ctx)).resolves.toEqual(
|
||||
{
|
||||
some: { yaml: { key: 7 } },
|
||||
},
|
||||
);
|
||||
await expect(
|
||||
readSecret({ include: 'my-data.yaml#' }, ctx),
|
||||
).resolves.toEqual({
|
||||
some: { yaml: { key: 7 } },
|
||||
});
|
||||
await expect(
|
||||
readSecret({ include: 'my-data.yml#different.key' }, ctx),
|
||||
).resolves.toBe('hello');
|
||||
await expect(
|
||||
readSecret({ include: 'no-data.yml#different.key' }, ctx),
|
||||
).rejects.toThrow('File not found!');
|
||||
await expect(
|
||||
readSecret({ include: 'my-data.yml#missing.key' }, ctx),
|
||||
).rejects.toThrow('Value is not an object at missing in my-data.yml');
|
||||
await expect(readSecret({ include: 'invalid.yaml' }, ctx)).rejects.toThrow(
|
||||
'Failed to parse included file invalid.yaml, YAMLSyntaxError: Flow sequence contains an unexpected }',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject invalid secrets', async () => {
|
||||
await expect(readSecret('hello' as any, ctx)).rejects.toThrow(
|
||||
'secret must be a `object` type, but the final value was: `"hello"`.',
|
||||
|
||||
@@ -42,7 +42,12 @@ type DataSecret = {
|
||||
path?: string;
|
||||
};
|
||||
|
||||
type Secret = FileSecret | EnvSecret | DataSecret;
|
||||
// TODO(Rugvip): Move this out of secret reading when we remove the deprecated DataSecret and $secret format
|
||||
type IncludeSecret = {
|
||||
include: string;
|
||||
};
|
||||
|
||||
type Secret = FileSecret | EnvSecret | DataSecret | IncludeSecret;
|
||||
|
||||
// Schema for each type of secret description
|
||||
const secretLoaderSchemas = {
|
||||
@@ -55,6 +60,9 @@ const secretLoaderSchemas = {
|
||||
data: yup.object({
|
||||
data: yup.string().required(),
|
||||
}),
|
||||
include: yup.object({
|
||||
include: yup.string().required(),
|
||||
}),
|
||||
};
|
||||
|
||||
// The top-level secret schema, which figures out what type of secret it is.
|
||||
@@ -94,7 +102,7 @@ const dataSecretParser: {
|
||||
export async function readSecret(
|
||||
data: JsonObject,
|
||||
ctx: ReaderContext,
|
||||
): Promise<string | undefined> {
|
||||
): Promise<JsonValue | undefined> {
|
||||
const secret = secretSchema.validateSync(data, { strict: true }) as Secret;
|
||||
|
||||
if ('file' in secret) {
|
||||
@@ -104,6 +112,9 @@ export async function readSecret(
|
||||
return ctx.env[secret.env];
|
||||
}
|
||||
if ('data' in secret) {
|
||||
console.warn(
|
||||
`Configuration uses deprecated $data key, use $include instead.`,
|
||||
);
|
||||
const url =
|
||||
'path' in secret ? `${secret.data}#${secret.path}` : secret.data;
|
||||
const [filePath, dataPath] = url.split(/#(.*)/);
|
||||
@@ -134,6 +145,35 @@ export async function readSecret(
|
||||
|
||||
return String(value);
|
||||
}
|
||||
if ('include' in secret) {
|
||||
const [filePath, dataPath] = secret.include.split(/#(.*)/);
|
||||
|
||||
const ext = extname(filePath);
|
||||
const parser = dataSecretParser[ext];
|
||||
if (!parser) {
|
||||
throw new Error(`No data secret parser available for extension ${ext}`);
|
||||
}
|
||||
|
||||
const content = await ctx.readFile(filePath);
|
||||
|
||||
const parts = dataPath ? dataPath.split('.') : [];
|
||||
|
||||
let value: JsonValue | undefined;
|
||||
try {
|
||||
value = await parser(content);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse included file ${filePath}, ${error}`);
|
||||
}
|
||||
for (const [index, part] of parts.entries()) {
|
||||
if (!isObject(value)) {
|
||||
const errPath = parts.slice(0, index).join('.');
|
||||
throw new Error(`Value is not an object at ${errPath} in ${filePath}`);
|
||||
}
|
||||
value = value[part];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
isNever<typeof secret>();
|
||||
throw new Error('Secret was left unhandled');
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { JsonObject } from '@backstage/config';
|
||||
import { JsonObject, JsonValue } from '@backstage/config';
|
||||
|
||||
export type ReadFileFunc = (path: string) => Promise<string>;
|
||||
export type ReadSecretFunc = (
|
||||
path: string,
|
||||
desc: JsonObject,
|
||||
) => Promise<string | undefined>;
|
||||
) => Promise<JsonValue | undefined>;
|
||||
export type SkipFunc = (path: string) => boolean;
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath, dirname, isAbsolute } from 'path';
|
||||
import { AppConfig, JsonObject } from '@backstage/config';
|
||||
import { AppConfig, JsonObject, JsonValue } from '@backstage/config';
|
||||
import { readConfigFile, readEnvConfig, readSecret } from './lib';
|
||||
|
||||
export type LoadConfigOptions = {
|
||||
@@ -49,7 +49,7 @@ class Context {
|
||||
async readSecret(
|
||||
_path: string,
|
||||
desc: JsonObject,
|
||||
): Promise<string | undefined> {
|
||||
): Promise<JsonValue | undefined> {
|
||||
return readSecret(desc, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @backstage/core-api
|
||||
|
||||
## 0.2.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ab0892358: Remove test dependencies from production package list
|
||||
|
||||
## 0.2.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a08c32ced: Add `FlatRoutes` component to replace the top-level `Routes` component from `react-router` within apps, removing the need for manually appending `/*` to paths or sorting routes.
|
||||
- 86c3c652a: Deprecate `RouteRef` path parameter and member, and remove deprecated `routeRef.createSubRouteRef`.
|
||||
- 27f2af935: Delay auth loginPopup close to avoid race condition with callers of authFlowHelpers.
|
||||
|
||||
## 0.2.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d681db2b5: Fix for GitHub and SAML auth not properly updating session state when already logged in.
|
||||
- 1dc445e89: Introduce new plugin extension API
|
||||
- Updated dependencies [1dc445e89]
|
||||
- @backstage/test-utils@0.1.6
|
||||
|
||||
## 0.2.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/core-api",
|
||||
"description": "Internal Core API used by Backstage plugins and apps",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.9",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
@@ -30,7 +30,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@backstage/test-utils": "^0.1.5",
|
||||
"@backstage/theme": "^0.2.2",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
@@ -43,7 +42,8 @@
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.4.2",
|
||||
"@backstage/cli": "^0.4.5",
|
||||
"@backstage/test-utils": "^0.1.6",
|
||||
"@backstage/test-utils-core": "^0.1.1",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ReactNode, Children, isValidElement, Fragment } from 'react';
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
|
||||
type RouteObject = {
|
||||
path: string;
|
||||
element: JSX.Element;
|
||||
children?: RouteObject[];
|
||||
};
|
||||
|
||||
// Similar to the same function from react-router, this collects routes from the
|
||||
// children, but only the first level of routes
|
||||
function createRoutesFromChildren(children: ReactNode): RouteObject[] {
|
||||
return Children.toArray(children)
|
||||
.flatMap(child => {
|
||||
if (!isValidElement(child)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { children } = child.props;
|
||||
|
||||
if (child.type === Fragment) {
|
||||
return createRoutesFromChildren(children);
|
||||
}
|
||||
|
||||
let path = child.props.path as string | undefined;
|
||||
|
||||
// TODO(Rugvip): Work around plugins registering empty paths, remove once deprecated routes are gone
|
||||
if (path === '') {
|
||||
return [];
|
||||
}
|
||||
path = path?.replace(/\/\*$/, '') ?? '/';
|
||||
|
||||
return [
|
||||
{
|
||||
path,
|
||||
element: child,
|
||||
children: children && [
|
||||
{
|
||||
path: '/*',
|
||||
element: children,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
})
|
||||
.sort((a, b) => b.path.localeCompare(a.path))
|
||||
.map(obj => {
|
||||
obj.path = obj.path === '/' ? '/' : `${obj.path}/*`;
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
type FlatRoutesProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const FlatRoutes = (props: FlatRoutesProps): JSX.Element | null => {
|
||||
const routes = createRoutesFromChildren(props.children);
|
||||
return useRoutes(routes);
|
||||
};
|
||||
@@ -25,25 +25,15 @@ export class AbsoluteRouteRef<Params extends { [param in string]: string }> {
|
||||
|
||||
// TODO(Rugvip): Remove this, routes are looked up via the registry instead
|
||||
get path() {
|
||||
return this.config.path;
|
||||
return this.config.path ?? '';
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.config.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should not be used, create a separate RouteRef instead
|
||||
* @deprecated
|
||||
*/
|
||||
createSubRoute(): any {
|
||||
throw new Error(
|
||||
'This method should not be called, create a separate RouteRef instead',
|
||||
);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `routeRef{path=${this.path}}`;
|
||||
return `routeRef{title=${this.title}}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,13 +44,7 @@ export function createRouteRef<
|
||||
return new AbsoluteRouteRef<Params>(config);
|
||||
}
|
||||
|
||||
const create = Symbol('create-external-route-ref');
|
||||
|
||||
export class ExternalRouteRef {
|
||||
static [create]() {
|
||||
return new ExternalRouteRef();
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
toString() {
|
||||
@@ -69,5 +53,5 @@ export class ExternalRouteRef {
|
||||
}
|
||||
|
||||
export function createExternalRouteRef(): ExternalRouteRef {
|
||||
return ExternalRouteRef[create]();
|
||||
return new ((ExternalRouteRef as unknown) as { new (): ExternalRouteRef })();
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
*/
|
||||
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { routePathCollector, routeParentCollector } from './collectors';
|
||||
import {
|
||||
routePathCollector,
|
||||
routeParentCollector,
|
||||
routeObjectCollector,
|
||||
} from './collectors';
|
||||
|
||||
import {
|
||||
traverseElementTree,
|
||||
@@ -24,21 +28,22 @@ import {
|
||||
} from '../extensions/traversal';
|
||||
import { createRouteRef } from './RouteRef';
|
||||
import { createPlugin } from '../plugin';
|
||||
import { createRoutableExtension } from '../extensions';
|
||||
import { attachComponentData, createRoutableExtension } from '../extensions';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
import { RouteRef } from './types';
|
||||
|
||||
const mockConfig = () => ({ path: '/foo', title: 'Foo' });
|
||||
const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => (
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
const plugin = createPlugin({ id: 'my-plugin' });
|
||||
|
||||
const ref1 = createRouteRef(mockConfig());
|
||||
const ref2 = createRouteRef(mockConfig());
|
||||
const ref3 = createRouteRef(mockConfig());
|
||||
const ref4 = createRouteRef(mockConfig());
|
||||
const ref5 = createRouteRef(mockConfig());
|
||||
const ref1 = createRouteRef({ path: '/foo1', title: 'Foo' });
|
||||
const ref2 = createRouteRef({ path: '/foo2', title: 'Foo' });
|
||||
const ref3 = createRouteRef({ path: '/foo3', title: 'Foo' });
|
||||
const ref4 = createRouteRef({ path: '/foo4', title: 'Foo' });
|
||||
const ref5 = createRouteRef({ path: '/foo5', title: 'Foo' });
|
||||
const refOrder = [ref1, ref2, ref3, ref4, ref5];
|
||||
|
||||
const Extension1 = plugin.provide(
|
||||
createRoutableExtension({
|
||||
@@ -71,6 +76,30 @@ const Extension5 = plugin.provide(
|
||||
}),
|
||||
);
|
||||
|
||||
const AggregationComponent = ({
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
path: string;
|
||||
}>) => <>{children}</>;
|
||||
|
||||
attachComponentData(AggregationComponent, 'core.gatherMountPoints', true);
|
||||
|
||||
function sortedEntries<T>(map: Map<RouteRef, T>): [RouteRef, T][] {
|
||||
return Array.from(map).sort(
|
||||
([a], [b]) => refOrder.indexOf(a) - refOrder.indexOf(b),
|
||||
);
|
||||
}
|
||||
|
||||
function routeObj(path: string, refs: RouteRef[], children: any[] = []) {
|
||||
return {
|
||||
path: path,
|
||||
caseSensitive: false,
|
||||
element: null,
|
||||
routeRefs: new Set(refs),
|
||||
children: children,
|
||||
};
|
||||
}
|
||||
|
||||
describe('discovery', () => {
|
||||
it('should collect routes', () => {
|
||||
const list = [
|
||||
@@ -109,33 +138,40 @@ describe('discovery', () => {
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const { routes, routeParents } = traverseElementTree({
|
||||
const { routes, routeParents, routeObjects } = traverseElementTree({
|
||||
root,
|
||||
discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
collectors: {
|
||||
routes: routePathCollector,
|
||||
routeParents: routeParentCollector,
|
||||
routeObjects: routeObjectCollector,
|
||||
},
|
||||
});
|
||||
expect(routes).toEqual(
|
||||
new Map([
|
||||
[ref1, '/foo'],
|
||||
[ref2, '/bar/:id'],
|
||||
[ref3, '/baz'],
|
||||
[ref4, '/divsoup'],
|
||||
[ref5, '/blop'],
|
||||
]),
|
||||
);
|
||||
|
||||
expect(routeParents).toEqual(
|
||||
new Map([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, ref2],
|
||||
[ref4, undefined],
|
||||
[ref5, ref1],
|
||||
]),
|
||||
);
|
||||
expect(sortedEntries(routes)).toEqual([
|
||||
[ref1, '/foo'],
|
||||
[ref2, '/bar/:id'],
|
||||
[ref3, '/baz'],
|
||||
[ref4, '/divsoup'],
|
||||
[ref5, '/blop'],
|
||||
]);
|
||||
expect(sortedEntries(routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, ref2],
|
||||
[ref4, undefined],
|
||||
[ref5, ref1],
|
||||
]);
|
||||
expect(routeObjects).toEqual([
|
||||
routeObj(
|
||||
'/foo',
|
||||
[ref1],
|
||||
[
|
||||
routeObj('/bar/:id', [ref2], [routeObj('/baz', [ref3])]),
|
||||
routeObj('/blop', [ref5]),
|
||||
],
|
||||
),
|
||||
routeObj('/divsoup', [ref4]),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle all react router Route patterns', () => {
|
||||
@@ -168,24 +204,154 @@ describe('discovery', () => {
|
||||
routeParents: routeParentCollector,
|
||||
},
|
||||
});
|
||||
expect(routes).toEqual(
|
||||
new Map([
|
||||
[ref1, '/foo'],
|
||||
[ref2, '/bar/:id'],
|
||||
[ref3, '/baz'],
|
||||
[ref4, '/divsoup'],
|
||||
[ref5, '/blop'],
|
||||
]),
|
||||
expect(sortedEntries(routes)).toEqual([
|
||||
[ref1, '/foo'],
|
||||
[ref2, '/bar/:id'],
|
||||
[ref3, '/baz'],
|
||||
[ref4, '/divsoup'],
|
||||
[ref5, '/blop'],
|
||||
]);
|
||||
expect(sortedEntries(routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, undefined],
|
||||
[ref4, ref3],
|
||||
[ref5, ref3],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use the route aggregator key to bind child routes to the same path', () => {
|
||||
const root = (
|
||||
<MemoryRouter>
|
||||
<Routes>
|
||||
<AggregationComponent path="/foo">
|
||||
<Extension1 />
|
||||
<div>
|
||||
<Extension2 />
|
||||
</div>
|
||||
HELLO
|
||||
</AggregationComponent>
|
||||
<Extension3 path="/bar">
|
||||
<AggregationComponent path="/baz">
|
||||
<Extension4>
|
||||
<Extension5 />
|
||||
</Extension4>
|
||||
</AggregationComponent>
|
||||
</Extension3>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(routeParents).toEqual(
|
||||
new Map([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, undefined],
|
||||
[ref4, ref3],
|
||||
[ref5, ref3],
|
||||
]),
|
||||
|
||||
const { routes, routeParents, routeObjects } = traverseElementTree({
|
||||
root,
|
||||
discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
collectors: {
|
||||
routes: routePathCollector,
|
||||
routeParents: routeParentCollector,
|
||||
routeObjects: routeObjectCollector,
|
||||
},
|
||||
});
|
||||
expect(sortedEntries(routes)).toEqual([
|
||||
[ref1, '/foo'],
|
||||
[ref2, '/foo'],
|
||||
[ref3, '/bar'],
|
||||
[ref4, '/baz'],
|
||||
[ref5, '/baz'],
|
||||
]);
|
||||
expect(sortedEntries(routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, undefined],
|
||||
[ref3, undefined],
|
||||
[ref4, ref3],
|
||||
[ref5, ref3],
|
||||
]);
|
||||
expect(routeObjects).toEqual([
|
||||
routeObj('/foo', [ref1, ref2]),
|
||||
routeObj('/bar', [ref3], [routeObj('/baz', [ref4, ref5])]),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use the route aggregator but stop when encountering explicit path', () => {
|
||||
const root = (
|
||||
<MemoryRouter>
|
||||
<Routes>
|
||||
<Extension1 path="/foo">
|
||||
<AggregationComponent path="/bar">
|
||||
<Extension2>
|
||||
<Extension3 path="/baz">
|
||||
<Extension4 path="/blop" />
|
||||
</Extension3>
|
||||
<Extension5 />
|
||||
</Extension2>
|
||||
</AggregationComponent>
|
||||
</Extension1>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const { routes, routeParents, routeObjects } = traverseElementTree({
|
||||
root,
|
||||
discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
collectors: {
|
||||
routes: routePathCollector,
|
||||
routeParents: routeParentCollector,
|
||||
routeObjects: routeObjectCollector,
|
||||
},
|
||||
});
|
||||
expect(sortedEntries(routes)).toEqual([
|
||||
[ref1, '/foo'],
|
||||
[ref2, '/bar'],
|
||||
[ref3, '/baz'],
|
||||
[ref4, '/blop'],
|
||||
[ref5, '/bar'],
|
||||
]);
|
||||
expect(sortedEntries(routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, ref1],
|
||||
[ref4, ref3],
|
||||
[ref5, ref1],
|
||||
]);
|
||||
expect(routeObjects).toEqual([
|
||||
routeObj(
|
||||
'/foo',
|
||||
[ref1],
|
||||
[
|
||||
routeObj(
|
||||
'/bar',
|
||||
[ref2, ref5],
|
||||
[routeObj('/baz', [ref3], [routeObj('/blop', [ref4])])],
|
||||
),
|
||||
],
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should stop gathering mount points after encountering explicit path', () => {
|
||||
const root = (
|
||||
<MemoryRouter>
|
||||
<Routes>
|
||||
<Extension1 path="/foo">
|
||||
<AggregationComponent path="/bar">
|
||||
<Extension2 path="/baz">
|
||||
<Extension3 />
|
||||
</Extension2>
|
||||
</AggregationComponent>
|
||||
</Extension1>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
traverseElementTree({
|
||||
root,
|
||||
discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
collectors: {
|
||||
routes: routePathCollector,
|
||||
routeParents: routeParentCollector,
|
||||
},
|
||||
});
|
||||
}).toThrow('Mounted routable extension must have a path');
|
||||
});
|
||||
|
||||
it('should not visit the same element twice', () => {
|
||||
|
||||
@@ -32,25 +32,48 @@ function getMountPoint(node: ReactElement): RouteRef | undefined {
|
||||
|
||||
export const routePathCollector = createCollector(
|
||||
() => new Map<RouteRef, string>(),
|
||||
(acc, node, parent) => {
|
||||
(acc, node, parent, ctxPath: string | undefined) => {
|
||||
// The context path is used during mount point gathering to assign the same path
|
||||
// to all discovered mount points
|
||||
let currentCtxPath = ctxPath;
|
||||
|
||||
if (parent?.props.element === node) {
|
||||
return;
|
||||
return currentCtxPath;
|
||||
}
|
||||
|
||||
// Start gathering mount points when we encounter a mount point gathering flag
|
||||
if (getComponentData<boolean>(node, 'core.gatherMountPoints')) {
|
||||
const path: string | undefined = node.props?.path;
|
||||
if (!path) {
|
||||
throw new Error('Mount point gatherer must have a path');
|
||||
}
|
||||
currentCtxPath = path;
|
||||
}
|
||||
|
||||
const routeRef = getMountPoint(node);
|
||||
if (routeRef) {
|
||||
const path: string | undefined = node.props?.path;
|
||||
let path: string | undefined = node.props?.path;
|
||||
// If we're gathering mount points we use the context path as out path, unless
|
||||
// the element has its own path, in which case we use that instead and stop gathering
|
||||
if (currentCtxPath) {
|
||||
if (path) {
|
||||
currentCtxPath = undefined;
|
||||
} else {
|
||||
path = currentCtxPath;
|
||||
}
|
||||
}
|
||||
if (!path) {
|
||||
throw new Error('Mounted routable extension must have a path');
|
||||
}
|
||||
acc.set(routeRef, path);
|
||||
}
|
||||
return currentCtxPath;
|
||||
},
|
||||
);
|
||||
|
||||
export const routeParentCollector = createCollector(
|
||||
() => new Map<RouteRef, RouteRef | undefined>(),
|
||||
(acc, node, parent, parentRouteRef?: RouteRef) => {
|
||||
(acc, node, parent, parentRouteRef?: RouteRef | { sticky: RouteRef }) => {
|
||||
if (parent?.props.element === node) {
|
||||
return parentRouteRef;
|
||||
}
|
||||
@@ -59,8 +82,27 @@ export const routeParentCollector = createCollector(
|
||||
|
||||
const routeRef = getMountPoint(node);
|
||||
if (routeRef) {
|
||||
acc.set(routeRef, parentRouteRef);
|
||||
nextParent = routeRef;
|
||||
// "sticky" route ref is when we've encountered a mount point gatherer, and we want a
|
||||
// mount points beneath it to have the same parent, regardless of internal structure
|
||||
if (parentRouteRef && 'sticky' in parentRouteRef) {
|
||||
acc.set(routeRef, parentRouteRef.sticky);
|
||||
|
||||
// When we encounter a mount point with an explicit path, we stop gathering
|
||||
// mount points withing the children and remove the sticky state
|
||||
if (node.props?.path) {
|
||||
nextParent = routeRef;
|
||||
} else {
|
||||
nextParent = parentRouteRef;
|
||||
}
|
||||
} else {
|
||||
acc.set(routeRef, parentRouteRef);
|
||||
nextParent = routeRef;
|
||||
}
|
||||
}
|
||||
|
||||
// Mount point gatherers are marked as "sticky"
|
||||
if (getComponentData<boolean>(node, 'core.gatherMountPoints')) {
|
||||
return { sticky: nextParent };
|
||||
}
|
||||
|
||||
return nextParent;
|
||||
@@ -69,9 +111,10 @@ export const routeParentCollector = createCollector(
|
||||
|
||||
export const routeObjectCollector = createCollector(
|
||||
() => Array<BackstageRouteObject>(),
|
||||
(acc, node, parent, parentChildArr: BackstageRouteObject[] = acc) => {
|
||||
(acc, node, parent, parentObj: BackstageRouteObject | undefined) => {
|
||||
const parentChildren = parentObj?.children ?? acc;
|
||||
if (parent?.props.element === node) {
|
||||
return parentChildArr;
|
||||
return parentObj;
|
||||
}
|
||||
|
||||
const path: string | undefined = node.props?.path;
|
||||
@@ -79,20 +122,40 @@ export const routeObjectCollector = createCollector(
|
||||
|
||||
const routeRef = getMountPoint(node);
|
||||
if (routeRef) {
|
||||
const children: BackstageRouteObject[] = [];
|
||||
if (!path) {
|
||||
throw new Error(`No path found for mount point ${routeRef}`);
|
||||
if (path) {
|
||||
const newObject: BackstageRouteObject = {
|
||||
caseSensitive,
|
||||
path,
|
||||
element: null,
|
||||
routeRefs: new Set([routeRef]),
|
||||
children: [],
|
||||
};
|
||||
parentChildren.push(newObject);
|
||||
return newObject;
|
||||
}
|
||||
parentChildArr.push({
|
||||
|
||||
parentObj?.routeRefs.add(routeRef);
|
||||
}
|
||||
|
||||
const isGatherer = getComponentData<boolean>(
|
||||
node,
|
||||
'core.gatherMountPoints',
|
||||
);
|
||||
if (isGatherer) {
|
||||
if (!path) {
|
||||
throw new Error('Mount point gatherer must have a path');
|
||||
}
|
||||
const newObject: BackstageRouteObject = {
|
||||
caseSensitive,
|
||||
path,
|
||||
element: null,
|
||||
routeRef,
|
||||
children,
|
||||
});
|
||||
return children;
|
||||
routeRefs: new Set(),
|
||||
children: [],
|
||||
};
|
||||
parentChildren.push(newObject);
|
||||
return newObject;
|
||||
}
|
||||
|
||||
return parentChildArr;
|
||||
return parentObj;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -60,8 +60,8 @@ class RouteResolver {
|
||||
currentRouteRef;
|
||||
currentRouteRef = this.routeParents.get(currentRouteRef)
|
||||
) {
|
||||
matchIndex = match.findIndex(
|
||||
m => (m.route as BackstageRouteObject).routeRef === currentRouteRef,
|
||||
matchIndex = match.findIndex(m =>
|
||||
(m.route as BackstageRouteObject).routeRefs.has(currentRouteRef!),
|
||||
);
|
||||
if (matchIndex !== -1) {
|
||||
break;
|
||||
|
||||
@@ -21,5 +21,6 @@ export type {
|
||||
ConcreteRoute,
|
||||
MutableRouteRef,
|
||||
} from './types';
|
||||
export { FlatRoutes } from './FlatRoutes';
|
||||
export { createRouteRef } from './RouteRef';
|
||||
export { useRouteRef } from './hooks';
|
||||
|
||||
@@ -19,14 +19,10 @@ import { IconComponent } from '../icons';
|
||||
// @ts-ignore, we're just embedding the Params type for usage in other places
|
||||
export type RouteRef<Params extends { [param in string]: string } = {}> = {
|
||||
// TODO(Rugvip): Remove path, look up via registry instead
|
||||
/** @deprecated paths are no longer accessed directly from RouteRefs, use useRouteRef instead */
|
||||
path: string;
|
||||
icon?: IconComponent;
|
||||
title: string;
|
||||
/**
|
||||
* This function should not be used, create a separate RouteRef instead
|
||||
* @deprecated
|
||||
*/
|
||||
createSubRoute(): any;
|
||||
};
|
||||
|
||||
export type AnyRouteRef = RouteRef<any>;
|
||||
@@ -51,7 +47,8 @@ export type MutableRouteRef = RouteRef<{}>;
|
||||
|
||||
export type RouteRefConfig<Params extends { [param in string]: string }> = {
|
||||
params?: Array<keyof Params>;
|
||||
path: string;
|
||||
/** @deprecated Route refs no longer decide their own path */
|
||||
path?: string;
|
||||
icon?: IconComponent;
|
||||
title: string;
|
||||
};
|
||||
@@ -62,5 +59,5 @@ export interface BackstageRouteObject {
|
||||
children?: BackstageRouteObject[];
|
||||
element: React.ReactNode;
|
||||
path: string;
|
||||
routeRef: AnyRouteRef;
|
||||
routeRefs: Set<AnyRouteRef>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# @backstage/core
|
||||
|
||||
## 0.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a08c32ced: Add `FlatRoutes` component to replace the top-level `Routes` component from `react-router` within apps, removing the need for manually appending `/*` to paths or sorting routes.
|
||||
- Updated dependencies [a08c32ced]
|
||||
- Updated dependencies [86c3c652a]
|
||||
- Updated dependencies [27f2af935]
|
||||
- @backstage/core-api@0.2.8
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1dc445e89: Update to use new plugin extension API
|
||||
- 342270e4d: Create AboutCard in core and use it in pagerduty and catalog plugin
|
||||
- Updated dependencies [d681db2b5]
|
||||
- Updated dependencies [1dc445e89]
|
||||
- @backstage/core-api@0.2.7
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/core",
|
||||
"description": "Core API used by Backstage plugins and apps",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.3",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@backstage/core-api": "^0.2.6",
|
||||
"@backstage/core-api": "^0.2.8",
|
||||
"@backstage/theme": "^0.2.2",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
@@ -65,8 +65,8 @@
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.4.2",
|
||||
"@backstage/test-utils": "^0.1.5",
|
||||
"@backstage/cli": "^0.4.4",
|
||||
"@backstage/test-utils": "^0.1.6",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
+5
-12
@@ -14,8 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { VerticalIcon } from './VerticalIcon';
|
||||
import { SubHeaderLink } from '../types';
|
||||
import { IconLinkVertical, IconLinkVerticalProps } from './IconLinkVertical';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
@@ -29,21 +28,15 @@ const useStyles = makeStyles(theme => ({
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
links: SubHeaderLink[];
|
||||
links: IconLinkVerticalProps[];
|
||||
};
|
||||
|
||||
export const SubHeader = ({ links }: Props) => {
|
||||
export const HeaderIconLinkRow = ({ links }: Props) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<nav className={classes.links}>
|
||||
{links.map(link => (
|
||||
<VerticalIcon
|
||||
key={link.title}
|
||||
label={link.title}
|
||||
href={link.href ?? '#'}
|
||||
action={link.action}
|
||||
icon={link.icon}
|
||||
/>
|
||||
{links.map((link, index) => (
|
||||
<IconLinkVertical key={index + 1} {...link} />
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
+12
-10
@@ -13,21 +13,21 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { makeStyles, Link } from '@material-ui/core';
|
||||
import LinkIcon from '@material-ui/icons/Link';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { Link as RouterLink } from '../Link';
|
||||
|
||||
export type IconLinkVerticalProps = {
|
||||
icon?: React.ReactNode;
|
||||
href?: string;
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
label: string;
|
||||
action?: React.ReactNode;
|
||||
};
|
||||
|
||||
const useIconStyles = makeStyles({
|
||||
const useIconStyles = makeStyles(theme => ({
|
||||
link: {
|
||||
display: 'grid',
|
||||
justifyItems: 'center',
|
||||
@@ -43,12 +43,16 @@ const useIconStyles = makeStyles({
|
||||
fontWeight: 600,
|
||||
letterSpacing: 1.2,
|
||||
},
|
||||
});
|
||||
linkStyle: {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
}));
|
||||
|
||||
export function IconLinkVertical({
|
||||
icon = <LinkIcon />,
|
||||
href = '#',
|
||||
disabled = false,
|
||||
action,
|
||||
...props
|
||||
}: IconLinkVerticalProps) {
|
||||
const classes = useIconStyles();
|
||||
@@ -58,7 +62,6 @@ export function IconLinkVertical({
|
||||
<Link
|
||||
className={classnames(classes.link, classes.disabled)}
|
||||
underline="none"
|
||||
title={props.title}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
@@ -67,12 +70,11 @@ export function IconLinkVertical({
|
||||
);
|
||||
}
|
||||
|
||||
// Absolute links should not be using RouterLink
|
||||
if (href?.startsWith('//') || href?.includes('://')) {
|
||||
if (action) {
|
||||
return (
|
||||
<Link className={classes.link} href={href} {...props}>
|
||||
<Link className={classnames(classes.link, classes.linkStyle)} {...props}>
|
||||
{icon}
|
||||
<span className={classes.label}>{props.label}</span>
|
||||
{action}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user