diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000000..4f3b76b096 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000000..02236339b9 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@1.3.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "linked": [ + [ + "example-app", + "@backstage/backend-common", + "example-backend", + "@backstage/catalog-model", + "@backstage/cli-common", + "@backstage/cli", + "@backstage/config-loader", + "@backstage/config", + "@backstage/core-api", + "@backstage/core", + "@backstage/create-app", + "@backstage/dev-utils", + "docgen", + "e2e-test", + "storybook", + "@techdocs/cli", + "@backstage/test-utils-core", + "@backstage/test-utils", + "@backstage/theme", + "@backstage/plugin-api-docs", + "@backstage/plugin-app-backend", + "@backstage/plugin-auth-backend", + "@backstage/plugin-catalog-backend", + "@backstage/plugin-catalog", + "@backstage/plugin-circleci", + "@backstage/plugin-gcp-projects", + "@backstage/plugin-github-actions", + "@backstage/plugin-gitops-profiles", + "@backstage/plugin-graphiql", + "@backstage/plugin-graphql-backend", + "@backstage/plugin-identity-backend", + "@backstage/plugin-jenkins", + "@backstage/plugin-lighthouse", + "@backstage/plugin-newrelic", + "@backstage/plugin-proxy-backend", + "@backstage/plugin-register-component", + "@backstage/plugin-rollbar-backend", + "@backstage/plugin-rollbar", + "@backstage/plugin-scaffolder-backend", + "@backstage/plugin-scaffolder", + "@backstage/plugin-sentry-backend", + "@backstage/plugin-sentry", + "@backstage/plugin-tech-radar", + "@backstage/plugin-techdocs-backend", + "@backstage/plugin-techdocs", + "@backstage/plugin-welcome" + ] + ], + "access": "public", + "baseBranch": "master", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000000..d03ad99836 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,18 @@ +comment: false # Ref: https://docs.codecov.io/docs/pull-request-comments + +coverage: + status: + project: + default: + threshold: 0% # Ref: https://docs.codecov.io/docs/codecovyml-reference#coveragestatus + target: auto + +# Since Backstage is a mono repo, flags here help in getting the code coverage of individual packages. +# Documentation: https://docs.codecov.io/docs/flags +flags: + core: + paths: + - packages/core/ + core-api: + paths: + - packages/core-api/ diff --git a/.eslintignore b/.eslintignore index c73074efb3..46bb1ad2b2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,3 +7,5 @@ **/.git/** **/public/** **/microsite/** +**/templates/** +**/sample-templates/** diff --git a/.github/workflows/changeset.yml b/.github/workflows/changeset.yml new file mode 100644 index 0000000000..ac7872ea93 --- /dev/null +++ b/.github/workflows/changeset.yml @@ -0,0 +1,19 @@ +name: Changeset + +on: + push: + branches: + - master + +jobs: + create-release-pr: + name: Create Changeset PR + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Install Dependencies + run: yarn --frozen-lockfile + - name: Create Release Pull Request + uses: changesets/action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a094a10b3..c8c92c57ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,9 +93,11 @@ jobs: if: ${{ steps.yarn-lock.outcome == 'success' }} run: yarn lerna -- run test --since origin/master -- --coverage - - name: test all packages + - name: test all packages (and upload coverage) if: ${{ steps.yarn-lock.outcome == 'failure' }} - run: yarn lerna -- run test -- --coverage + run: | + yarn lerna -- run test -- --coverage + bash <(curl -s https://codecov.io/bash) - name: verify plugin template run: yarn lerna -- run diff -- --check diff --git a/.github/workflows/e2e-win.yml b/.github/workflows/e2e-win.yml index b4ea1e350f..5bab1b1299 100644 --- a/.github/workflows/e2e-win.yml +++ b/.github/workflows/e2e-win.yml @@ -27,25 +27,12 @@ jobs: 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: find location of global yarn cache - id: yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: cache global yarn cache - uses: actions/cache@v2 - 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 - run: yarn tsc - name: yarn build diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d53ca1b607..69bd8393c8 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -3,6 +3,8 @@ name: E2E Test Linux on: pull_request: paths-ignore: + - 'contrib/**' + - 'docs/**' - 'microsite/**' jobs: diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 469ef20cda..bebca7c6f9 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -59,8 +59,13 @@ jobs: - name: verify type dependencies run: yarn lint:type-deps - - name: test - run: yarn lerna -- run test -- --coverage + - name: test (and upload coverage) + run: | + yarn lerna -- run test -- --coverage + bash <(curl -s https://codecov.io/bash) + # Upload code coverage for some specific flags. Also see .codecov.yml + 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 diff --git a/.github/workflows/techdocs-project-board.yml b/.github/workflows/techdocs-project-board.yml new file mode 100644 index 0000000000..b389cf6bf5 --- /dev/null +++ b/.github/workflows/techdocs-project-board.yml @@ -0,0 +1,53 @@ +name: Automatically add new TechDocs Issues and PRs to the GitHub project board +# Development of TechDocs in Backstage is managed by this Kanban board - https://github.com/spotify/backstage/projects/5 +# New issues with TechDocs in their title or docs-like-code label will be added to the board. + +on: + issues: + types: [opened, reopened, labeled, edited] + pull_request: + types: [opened, reopened, labeled, edited] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + assign_issue_or_pr_to_project: + runs-on: ubuntu-latest + name: Triage + steps: + - name: Assign new issue to Incoming based on its title. + uses: srggrs/assign-one-project-github-action@1.2.0 + if: | + contains(github.event.issue.title, 'TechDocs') || + contains(github.event.issue.title, 'techdocs') || + contains(github.event.issue.title, 'Techdocs') + with: + project: 'https://github.com/spotify/backstage/projects/5' + column_name: 'Incoming' + + - name: Assign new issue to Incoming based on its label. + uses: srggrs/assign-one-project-github-action@1.2.0 + if: | + contains(github.event.issue.labels.*.name, 'docs-like-code') + with: + project: 'https://github.com/spotify/backstage/projects/5' + column_name: 'Incoming' + + - name: Assign new PR to Incoming based on its title. + uses: srggrs/assign-one-project-github-action@1.2.0 + if: | + contains(github.event.pull_request.title, 'TechDocs') || + contains(github.event.pull_request.title, 'techdocs') || + contains(github.event.pull_request.title, 'Techdocs') + with: + project: 'https://github.com/spotify/backstage/projects/5' + column_name: 'Incoming' + + - name: Assign new PR to Incoming based on its label. + uses: srggrs/assign-one-project-github-action@1.2.0 + if: | + contains(github.event.pull_request.labels.*.name, 'docs-like-code') + with: + project: 'https://github.com/spotify/backstage/projects/5' + column_name: 'Incoming' diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f9235172b..25360f3e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,48 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re > Collect changes for the next release below +### @backstage/core + +- Renamed `SessionStateApi` to `SessionApi` and `logout` to `signOut`. Custom implementations of the `SingInPage` app-component will need to rename their `logout` function. The different auth provider items for the `UserSettingsMenu` have been consolidated into a single `ProviderSettingsItem`, meaning you need to replace existing usages of `OAuthProviderSettings` and `OIDCProviderSettings`. [#2555](https://github.com/spotify/backstage/pull/2555). + +## v0.1.1-alpha.22 + +### @backstage/core + +- Introduced initial version of an inverted app/plugin relationship, where plugins export components for apps to use, instead registering themselves directly into the app. This enables more fine-grained control of plugin features, and also composition of plugins such as catalog pages with additional cards and tabs. This breaks the use of `RouteRef`s, and there will be more changes related to this in the future, but this change lays the initial foundation. See `packages/app` and followup PRs for how to update plugins for this change. [#2076](https://github.com/spotify/backstage/pull/2076) +- Switch to an automatic dependency injection mechanism for all Utility APIs, allowing plugins to ship default implementations of their APIs. See [https://backstage.io/docs/api/utility-apis](https://backstage.io/docs/api/utility-apis). [#2285](https://github.com/spotify/backstage/pull/2285) + +### @backstage/cli + +- Change `backstage-cli backend:build-image` to forward all args to `docker image build`, instead of just tag. Also add `--build` flag for building all dependent packages before packaging the workspace for the docker build. [#2299](https://github.com/spotify/backstage/pull/2299) + +### @backstage/create-app + +- Change root `tsc` output dir to `dist-types`, in order to allow for standalone plugin repos. [#2278](https://github.com/spotify/backstage/pull/2278) + +### @backstage/catalog-backend + +- We have simplified the way that GitHub ingestion works. The `catalog.processors.githubApi` key is deprecated, in favor of `catalog.processors.github`. At the same time, the location type `github/api` is likewise deprecated, in favor of `github`. This location type now serves both raw HTTP reads and APIv3 reads, depending on how you configure it. It also supports having several providers at once - for example, both public GitHub and an internal GitHub Enterprise, with different keys. If you still use the `catalog.processors.githubApi` config key, things will work but you will get a deprecation warning at startup. In a later release, support for the old key will go away entirely. See the [configuration section in the docs](https://backstage.io/docs/features/software-catalog/configuration) for more details. + +## v0.1.1-alpha.21 + +- Added many more frontend plugins to the template along with the sidebar. [#1942](https://github.com/spotify/backstage/pull/1942), [#2084](https://github.com/spotify/backstage/pull/2084) + +### @backstage/core + - Material-UI: Bumped to 4.11.0, which is the version that create-app will resolve to, because we wanted to get the renaming of ExpansionPanel to Accordion into place. This gets rid of a lot of console deprecation warnings in newly scaffolded apps. +### @backstage/cli + +- Set `NODE_ENV` to `test` when running test. [#2214](https://github.com/spotify/backstage/pull/2214) + +- Fix for backend plugins names requiring to be prefixed with `@backstage` to build. [#2224](https://github.com/spotify/backstage/pull/2224) + +### @backstage/backend-common + - The backend plugin [service builder](https://github.com/spotify/backstage/blob/master/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts) no longer adds `express.json()` automatically to all routes. While convenient @@ -22,14 +59,31 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re still had to cater to that manually. We therefore decided to let plugins add `express.json()` themselves if they happen to deal with JSON data. +### @backstage/catalog-backend + +- Add rules configuration for catalog location and entity kinds. The default rules should cover most use-cases, but you may need to allow specific entity kinds when using things like Template or Group entities. [#2118](https://github.com/spotify/backstage/pull/2118) + ## v0.1.1-alpha.20 -- Includes https://github.com/spotify/backstage/pull/2097 to resolve issues with create-plugin command. +### @backstage/cli + +- Use config files according to `NODE_ENV` when serving and building frontend packages. [#2077](https://github.com/spotify/backstage/pull/2077) + +- Pin `rollup-plugin-dts` to avoid a later broken version. [#2097](https://github.com/spotify/backstage/pull/2097) ## v0.1.1-alpha.19 +### @backstage/backend-common + +- Allow listen host and port to be configured separately, in order to support PORT environment variables. [#1950](https://github.com/spotify/backstage/pull/1950) + +### @backstage/core + +- Added new `DiscoveryApi` for discovering backend endpoint in the frontend, and use in most plugins. See [packages/app/src/apis.ts](https://github.com/spotify/backstage/blob/master/packages/app/src/apis.ts) for how to register in your app. [#2074](https://github.com/spotify/backstage/pull/2074) + ### @backstage/create-app +- Added catalog and scaffolder frontend plugins to the template along with the sidebar. [#1942](https://github.com/spotify/backstage/pull/1942), [#2084](https://github.com/spotify/backstage/pull/2084) - Many plugins have been added to the catalog and will for now be required to be added to separate apps as well. This will be solved as [#1536](https://github.com/spotify/backstage/issues/1536) gets sorted out, but for now you may need to install some plugins just to get pages to work. ### @backstage/catalog-backend diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 853332f74d..a6f7e5e5e1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,35 +62,7 @@ Have you started using Backstage? Adding your company to [ADOPTERS](ADOPTERS.md) So...feel ready to jump in? Let's do this. 👏🏻💯 -To run a Backstage app, you will need to have the following installed: - -- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -- [NodeJS](https://nodejs.org/en/download/) - Active LTS Release, currently v12 -- [yarn](https://classic.yarnpkg.com/en/docs/install) - -After cloning this repo, open a terminal window and start the example app using the following commands from the project root: - -```bash -yarn install # Install dependencies - -yarn start # Start dev server, use --check to enable linting and type-checks -``` - -The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal. - -Depending on the work you're doing, you often also want to run the example backend. Start the backend in a separate terminal session using the following: - -```bash -cd packages/backend - -yarn start - -yarn lerna run mock-data # Populate the backend with mock data -``` - -And that's it! You are good to go 👍 - -If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2). +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 @@ -100,6 +72,21 @@ If you're contributing to the backend or CLI tooling, be mindful of cross-platfo Also be sure to skim through our [ADRs](https://github.com/spotify/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/spotify/backstage/blob/master/docs/architecture-decisions/adr006-avoid-react-fc.md) is one to look out for. +# 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. + +## 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) +4. Add generated changset 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 + +For more information, checkout [adding a changeset](https://github.com/atlassian/changesets/blob/master/docs/adding-a-changeset.md) documentation in changesets repository. + # 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. diff --git a/README.md b/README.md index 4009864edc..9ef09aee06 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ ![](https://github.com/spotify/backstage/workflows/Frontend%20CI/badge.svg) [![Discord](https://img.shields.io/discord/687207715902193673)](https://discord.gg/EBHEGzX) ![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) +[![Codecov](https://img.shields.io/codecov/c/github/spotify/backstage)](https://codecov.io/gh/spotify/backstage) [![](https://img.shields.io/npm/v/@backstage/core?label=Version)](https://github.com/spotify/backstage/releases) ## What is Backstage? @@ -31,33 +32,7 @@ A detailed project roadmap, including already delivered milestones, is available ## Getting Started -There are two different ways to get started with Backstage, either by creating a standalone app, or by cloning this repo. Which method you use depends on what you're planning to do. - -Creating a standalone instance makes it simpler to customize the application for your needs whilst staying up to date with the project. You will also depend on `@backstage` packages from NPM, making the project much smaller. This is the recommended approach if you want to kick the tyres of Backstage or setup your own instance. - -On the other hand, if you want to contribute plugins or to the project in general, it's easier to fork and clone this project. That will let you stay up to date with the latest changes, and gives you an easier path to make Pull Requests towards this repo. - -### Creating a Standalone App - -Backstage provides the `@backstage/create-app` package to scaffold standalone instances of Backstage. You will need to have -[NodeJS](https://nodejs.org/en/download/) Active LTS Release installed -(currently v12), and [yarn](https://classic.yarnpkg.com/en/docs/install). You will also need to have [Docker](https://docs.docker.com/engine/install/) installed to use some features like Software Templates and TechDocs. - -Using `npx` you can then run the following to create an app in a chosen subdirectory of your current working directory: - -```bash -npx @backstage/create-app -``` - -You will be taken through a wizard to create your app, and the output should look something like this. You can read more about this process [here](https://backstage.io/docs/getting-started/create-an-app) - -### Contributing to Backstage - -You can read more in our [CONTRIBUTING.md](./CONTRIBUTING.md#get-started) guide, which can help you get setup with a Backstage development environment. - -### Next steps - -Take a look at the [Getting Started](https://backstage.io/docs/getting-started/index) guide to learn how to set up Backstage, and how to develop on the platform. +Check out [the documentation](https://backstage.io/docs/getting-started) on how to start using Backstage. ## Documentation diff --git a/app-config.yaml b/app-config.yaml index 2d7f31e72d..60ecb813f7 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -35,6 +35,8 @@ organization: techdocs: storageUrl: http://localhost:7000/techdocs/static/docs requestUrl: http://localhost:7000/techdocs/docs + generators: + techdocs: 'docker' sentry: organization: spotify @@ -57,10 +59,24 @@ catalog: rules: - allow: [Component, API, Group, Template, Location] processors: - githubApi: - privateToken: - $secret: - env: GITHUB_PRIVATE_TOKEN + github: + providers: + - target: https://github.com + token: + $secret: + env: GITHUB_PRIVATE_TOKEN + #### Example for how to add your GitHub Enterprise instance using the API: + # - target: https://ghe.example.net + # apiBaseUrl: https://ghe.example.net/api/v3 + # token: + # $secret: + # env: GHE_PRIVATE_TOKEN + #### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional): + # - target: https://ghe.example.net + # rawBaseUrl: https://ghe.example.net/raw + # token: + # $secret: + # env: GHE_PRIVATE_TOKEN bitbucketApi: username: $secret: @@ -76,23 +92,40 @@ catalog: privateToken: $secret: env: AZURE_PRIVATE_TOKEN - exampleEntityLocations: - github: - - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml - - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-order-component.yaml - - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/podcast-api-component.yaml - - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/queue-proxy-component.yaml - - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/searcher-component.yaml - - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-lib-component.yaml - - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml - - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml - - https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml - - https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml - - https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml - - https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml - - https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml - - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/hello-world-api.yaml - - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/streetlights-api.yaml + + locations: + # Backstage example components + - type: github + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/all-components.yaml + + # Example component for github-actions + - type: github + target: https://github.com/spotify/backstage/blob/master/plugins/github-actions/examples/sample.yaml + + # Example component for techdocs + - type: github + target: https://github.com/spotify/backstage/blob/master/plugins/techdocs-backend/examples/documented-component/documented-component.yaml + + # Backstage example APIs + - type: github + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/all-apis.yaml + + # Backstage example templates + - type: github + target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/all-templates.yaml + +scaffolder: + github: + token: + $secret: + env: GITHUB_ACCESS_TOKEN + visibility: public # or 'internal' or 'private' + gitlab: + api: + baseUrl: https://gitlab.com + token: + $secret: + env: GITLAB_ACCESS_TOKEN auth: providers: diff --git a/contrib/README.md b/contrib/README.md new file mode 100644 index 0000000000..2bf33c063b --- /dev/null +++ b/contrib/README.md @@ -0,0 +1,9 @@ +# Backstage Contrib + +This directory contains various community contributions related to Backstage. + +Unless otherwise specified, all content in this hierarchy fall under the same +[licensing terms](../LICENSE) as in the rest of the repository, and come with +no guarantees of functionality or fitness of purpose. That being said, we +really appreciate contributions in here and encourage them being kept up to +date. diff --git a/contrib/docker/multi-stage-frontend/Dockerfile b/contrib/docker/multi-stage-frontend/Dockerfile new file mode 100644 index 0000000000..0c492478d1 --- /dev/null +++ b/contrib/docker/multi-stage-frontend/Dockerfile @@ -0,0 +1,23 @@ +FROM node:12 AS build + +RUN mkdir /app +COPY . /app +WORKDIR /app + +RUN yarn install +RUN yarn workspace example-app build + +# Contruct backstage-frontend image +FROM nginx:mainline + +RUN apt-get update && apt-get -y install jq && rm -rf /var/lib/apt/lists/* + +# Copy from build stage +COPY --from=build /app/packages/app/dist /usr/share/nginx/html + +COPY docker/default.conf.template /etc/nginx/conf.d/default.conf.template +COPY docker/run.sh /usr/local/bin/run.sh + +CMD run.sh + +ENV PORT 80 diff --git a/contrib/docker/multi-stage-frontend/README.md b/contrib/docker/multi-stage-frontend/README.md new file mode 100644 index 0000000000..b6ae4fdc6c --- /dev/null +++ b/contrib/docker/multi-stage-frontend/README.md @@ -0,0 +1,19 @@ +# Standalone Dockerfile for frontend + +This directory contains the resources which will help you build backstage without any requirements +other than docker itself. It uses a multi-stage Dockerfile to build and ship backstage. + +## Usage + +You can simply run the following command to build backstage. + +``` +# Make sure you are in the root directory of backstage then run +docker build -t backstage-frontend -f ./contrib/docker/multi-stage-frontend/Dockerfile . +``` + +After a successful build, You can simply run backstage frontend with the following command. + +``` +docker run -it --rm -p 3080:80 backstage-frontend +``` diff --git a/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md b/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md new file mode 100644 index 0000000000..855608c8b1 --- /dev/null +++ b/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md @@ -0,0 +1,58 @@ +### Source repo: https://github.com/johnson-jesse/simple-backstage-app-plugin + +ExampleComponent.tsx reference + +```tsx +import React, { FC } from 'react'; +import { Typography, Grid } from '@material-ui/core'; +import { + InfoCard, + Header, + Page, + pageTheme, + Content, + ContentHeader, + HeaderLabel, + SupportButton, + identityApiRef, +} from '@backstage/core'; +import { useApi } from '@backstage/core-api'; +import ExampleFetchComponent from '../ExampleFetchComponent'; + +const ExampleComponent: FC<{}> = () => { + const identityApi = useApi(identityApiRef); + const userId = identityApi.getUserId(); + const profile = identityApi.getProfile(); + + return ( + +
+ + +
+ + + A description of your plugin goes here. + + + + + + {`${profile.displayName} | ${profile.email}`} + + + + + + + + +
+ ); +}; + +export default ExampleComponent; +``` diff --git a/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md b/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md new file mode 100644 index 0000000000..08d14e8c4c --- /dev/null +++ b/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md @@ -0,0 +1,111 @@ +### Source repo: https://github.com/johnson-jesse/simple-backstage-app-plugin + +ExampleFetchComponent.tsx reference + +```tsx +import React, { FC } from 'react'; +import { useAsync } from 'react-use'; +import Alert from '@material-ui/lab/Alert'; +import { + Table, + TableColumn, + Progress, + githubAuthApiRef, +} from '@backstage/core'; +import { useApi } from '@backstage/core-api'; +import { graphql } from '@octokit/graphql'; + +const query = `{ + viewer { + repositories(first: 100) { + totalCount + nodes { + name + createdAt + description + diskUsage + isFork + } + pageInfo { + endCursor + hasNextPage + } + } + } +}`; + +type Node = { + name: string; + createdAt: string; + description: string; + diskUsage: number; + isFork: boolean; +}; + +type Viewer = { + repositories: { + totalCount: number; + nodes: Node[]; + pageInfo: { + endCursor: string; + hasNextPage: boolean; + }; + }; +}; + +type DenseTableProps = { + viewer: Viewer; +}; + +export const DenseTable: FC = ({ viewer }) => { + const columns: TableColumn[] = [ + { title: 'Name', field: 'name' }, + { title: 'Created', field: 'createdAt' }, + { title: 'Description', field: 'description' }, + { title: 'Disk Usage', field: 'diskUsage' }, + { title: 'Fork', field: 'isFork' }, + ]; + + return ( + + ); +}; + +const ExampleFetchComponent: FC<{}> = () => { + const auth = useApi(githubAuthApiRef); + + const { value, loading, error } = useAsync(async (): Promise => { + const token = await auth.getAccessToken(); + + const gqlEndpoint = graphql.defaults({ + // Uncomment baseUrl if using enterprise + // baseUrl: 'https://github.MY-BIZ.com/api', + headers: { + authorization: `token ${token}`, + }, + }); + const { viewer } = await gqlEndpoint(query); + return viewer; + }, []); + + if (loading) return ; + if (error) return {error.message}; + if (value && value.repositories) return ; + + return ( +
+ ); +}; + +export default ExampleFetchComponent; +``` diff --git a/contrib/kubernetes/basic_kubernetes_example_with_helm/README.md b/contrib/kubernetes/basic_kubernetes_example_with_helm/README.md new file mode 100644 index 0000000000..2dc875f671 --- /dev/null +++ b/contrib/kubernetes/basic_kubernetes_example_with_helm/README.md @@ -0,0 +1 @@ +# Basic Kubernetes example with Helm diff --git a/install/kubernetes/app.yaml b/contrib/kubernetes/basic_kubernetes_example_with_helm/app.yaml similarity index 100% rename from install/kubernetes/app.yaml rename to contrib/kubernetes/basic_kubernetes_example_with_helm/app.yaml diff --git a/install/kubernetes/backend.yaml b/contrib/kubernetes/basic_kubernetes_example_with_helm/backend.yaml similarity index 100% rename from install/kubernetes/backend.yaml rename to contrib/kubernetes/basic_kubernetes_example_with_helm/backend.yaml diff --git a/install/kubernetes/backstage/.helmignore b/contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/.helmignore similarity index 100% rename from install/kubernetes/backstage/.helmignore rename to contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/.helmignore diff --git a/install/kubernetes/backstage/Chart.yaml b/contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/Chart.yaml similarity index 100% rename from install/kubernetes/backstage/Chart.yaml rename to contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/Chart.yaml diff --git a/install/kubernetes/backstage/README.md b/contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/README.md similarity index 100% rename from install/kubernetes/backstage/README.md rename to contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/README.md diff --git a/install/kubernetes/backstage/templates/_helpers.tpl b/contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/templates/_helpers.tpl similarity index 100% rename from install/kubernetes/backstage/templates/_helpers.tpl rename to contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/templates/_helpers.tpl diff --git a/install/kubernetes/backstage/templates/deployment.yaml b/contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/templates/deployment.yaml similarity index 100% rename from install/kubernetes/backstage/templates/deployment.yaml rename to contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/templates/deployment.yaml diff --git a/install/kubernetes/backstage/templates/ingress.yaml b/contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/templates/ingress.yaml similarity index 100% rename from install/kubernetes/backstage/templates/ingress.yaml rename to contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/templates/ingress.yaml diff --git a/install/kubernetes/backstage/templates/service.yaml b/contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/templates/service.yaml similarity index 100% rename from install/kubernetes/backstage/templates/service.yaml rename to contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/templates/service.yaml diff --git a/install/kubernetes/backstage/values.yaml b/contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/values.yaml similarity index 100% rename from install/kubernetes/backstage/values.yaml rename to contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/values.yaml diff --git a/install/kubernetes/ingress.yaml b/contrib/kubernetes/basic_kubernetes_example_with_helm/ingress.yaml similarity index 100% rename from install/kubernetes/ingress.yaml rename to contrib/kubernetes/basic_kubernetes_example_with_helm/ingress.yaml diff --git a/install/kubernetes/service.yaml b/contrib/kubernetes/basic_kubernetes_example_with_helm/service.yaml similarity index 100% rename from install/kubernetes/service.yaml rename to contrib/kubernetes/basic_kubernetes_example_with_helm/service.yaml diff --git a/docs/FAQ.md b/docs/FAQ.md index 4b5df3ace0..b4c4970ff8 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,6 +1,7 @@ --- id: FAQ title: FAQ +description: All FAQ related to Backstage --- ## Product FAQ diff --git a/docs/api/backend.md b/docs/api/backend.md index bc44b8350c..0990343a01 100644 --- a/docs/api/backend.md +++ b/docs/api/backend.md @@ -1,6 +1,7 @@ --- id: backend title: Backend +description: About Backend --- ## TODO diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index 37cde87262..d192cbd6ee 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -1,6 +1,7 @@ --- id: utility-apis title: Utility APIs +description: Backstage Utility APIs --- ## Introduction diff --git a/docs/architecture-decisions/adr001-add-adr-log.md b/docs/architecture-decisions/adr001-add-adr-log.md index 8a484fa264..16783367ab 100644 --- a/docs/architecture-decisions/adr001-add-adr-log.md +++ b/docs/architecture-decisions/adr001-add-adr-log.md @@ -1,7 +1,7 @@ --- id: adrs-adr001 title: ADR001: Architecture Decision Record (ADR) log -sidebar_label: ADR001 +description: Architecture Decision Record (ADR) logs as a reference point for the team --- | Created | Status | diff --git a/docs/architecture-decisions/adr002-default-catalog-file-format.md b/docs/architecture-decisions/adr002-default-catalog-file-format.md index af22d33020..f4fd3a1158 100644 --- a/docs/architecture-decisions/adr002-default-catalog-file-format.md +++ b/docs/architecture-decisions/adr002-default-catalog-file-format.md @@ -1,7 +1,7 @@ --- id: adrs-adr002 title: ADR002: Default Software Catalog File Format -sidebar_label: ADR002 +description: Architecture Decision Record (ADR) log on Default Software Catalog File Format --- | Created | Status | diff --git a/docs/architecture-decisions/adr003-avoid-default-exports.md b/docs/architecture-decisions/adr003-avoid-default-exports.md index 7a8df28f49..8634a19632 100644 --- a/docs/architecture-decisions/adr003-avoid-default-exports.md +++ b/docs/architecture-decisions/adr003-avoid-default-exports.md @@ -1,7 +1,7 @@ --- id: adrs-adr003 title: ADR003: Avoid Default Exports and Prefer Named Exports -sidebar_label: ADR003 +description: Architecture Decision Record (ADR) log on Avoid Default Exports and Prefer Named Exports --- | Created | Status | diff --git a/docs/architecture-decisions/adr004-module-export-structure.md b/docs/architecture-decisions/adr004-module-export-structure.md index 077f8c35d1..12408abac1 100644 --- a/docs/architecture-decisions/adr004-module-export-structure.md +++ b/docs/architecture-decisions/adr004-module-export-structure.md @@ -1,7 +1,7 @@ --- id: adrs-adr004 title: ADR004: Module Export Structure -sidebar_label: ADR004 +description: Architecture Decision Record (ADR) log on Module Export Structure --- | Created | Status | diff --git a/docs/architecture-decisions/adr005-catalog-core-entities.md b/docs/architecture-decisions/adr005-catalog-core-entities.md index 8f39acd1cf..f91698c5ff 100644 --- a/docs/architecture-decisions/adr005-catalog-core-entities.md +++ b/docs/architecture-decisions/adr005-catalog-core-entities.md @@ -1,7 +1,7 @@ --- id: adrs-adr005 title: ADR005: Catalog Core Entities -sidebar_label: ADR005 +description: Architecture Decision Record (ADR) log on Catalog Core Entities --- | Created | Status | diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md index e48fceb562..bea36712d7 100644 --- a/docs/architecture-decisions/adr006-avoid-react-fc.md +++ b/docs/architecture-decisions/adr006-avoid-react-fc.md @@ -1,7 +1,7 @@ --- id: adrs-adr006 title: ADR006: Avoid React.FC and React.SFC -sidebar_label: ADR006 +description: Architecture Decision Record (ADR) log on Avoid React.FC and React.SFC --- ## Context diff --git a/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md index 773f08be30..6b18f67d40 100644 --- a/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md +++ b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md @@ -1,7 +1,7 @@ --- id: adrs-adr007 title: ADR007: Use MSW to mock http requests -sidebar_label: ADR007 +description: Architecture Decision Record (ADR) log on Use MSW to mock http requests --- ## Context diff --git a/docs/architecture-decisions/adr008-default-catalog-file-name.md b/docs/architecture-decisions/adr008-default-catalog-file-name.md index 979ea4de33..c57d10c0b1 100644 --- a/docs/architecture-decisions/adr008-default-catalog-file-name.md +++ b/docs/architecture-decisions/adr008-default-catalog-file-name.md @@ -1,7 +1,7 @@ --- id: adrs-adr008 title: ADR008: Default Catalog File Name -sidebar_label: ADR008 +description: Architecture Decision Record (ADR) log on Default Catalog File Name --- ## Background diff --git a/docs/architecture-decisions/index.md b/docs/architecture-decisions/index.md index 0e9ff21812..ddf2805e75 100644 --- a/docs/architecture-decisions/index.md +++ b/docs/architecture-decisions/index.md @@ -2,6 +2,7 @@ id: adrs-overview title: Architecture Decision Records (ADR) sidebar_label: Overview +description: Overview of Architecture Decision Records (ADR) --- The substantial architecture decisions made in the Backstage project live here. diff --git a/docs/auth/oauth-popup-flow.svg b/docs/assets/auth/oauth-popup-flow.svg similarity index 98% rename from docs/auth/oauth-popup-flow.svg rename to docs/assets/auth/oauth-popup-flow.svg index 4d9e79787a..2132903783 100644 --- a/docs/auth/oauth-popup-flow.svg +++ b/docs/assets/auth/oauth-popup-flow.svg @@ -1,5 +1,5 @@ -OAuth Consent and Refresh FlowBrowserBrowserPopup WindowPopup Windowauth-backend pluginauth-backend pluginConsent ScreenConsent ScreenOAuth ProviderOAuth ProviderComponents on page ask for anaccess token with greaterscope than the existing session.Open popupGET /auth/<provider>/start?scope=some%20scopesRedirect to consent screen withrandom nonce in OAuth state andshort-lived cookie with the same nonce.GET /consent_url?redirect_uri=<redirect_uri>?nonce=<n>where redirect_uri=<app-origin>/auth/<provider>/handler/frameUser consents toaccess the new scope.Redirect to given redirect URL, with authorization codeGET /auth/<provider>/handler/frame?code=<c>&nonce=<n>Request includes the previously set none cookieVerify that the nonce in the cookiematches the nonce in the OAuth stateAuthorization CodeClient IDClient SecretVerify and generate tokensAccess Token(ID Token)(Refresh Token)ScopeExpire TimeSmall HTML page with inlined response payloadStore Refresh Token in HTTP-only cookiepostMessage() with tokens and info or errorClose selfA later point when a refreshis needed. Either because ofa reload or an expiring session.GET /auth/<provider>/tokenRefresh Token cookie includedRefresh TokenClient IDClient SecretAccess Token(ID Token)ScopeExpire TimeTokens and info -Wait, what is TechDocs? TechDocs is Spotify’s homegrown docs-like-code solution -built directly into Backstage. Today, it is now one of the core products in -Spotify’s developer experience offering with 2,400+ documentation sites and -1,000+ engineers using it daily. +TechDocs is Spotify’s homegrown docs-like-code solution built directly into +Backstage. This means engineers write their documentation in Markdown files +which live together with their code. + +Today, it is one of the core products in Spotify’s developer experience offering +with 2,400+ documentation sites and 1,000+ engineers using it daily. Read more +about TechDocs and the philosophy in its +[announcement blog post](https://backstage.io/blog/2020/09/08/announcing-tech-docs). +🎉 ## Features -- A centralized place to discover documentation. +- A centralized place to discover and read documentation. -- A clear end-to-end docs-like-code solution. (_Coming soon in V.1_) +- A clear end-to-end docs-like-code solution. - A tightly coupled feedback loop with the developer workflow. (_Coming soon in V.3_) @@ -26,12 +33,12 @@ Spotify’s developer experience offering with 2,400+ documentation sites and ## Project roadmap -| Version | Description | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| [TechDocs V.0 ✅][v0] | Read docs in Backstage - Enable anyone to get a reader experience working in Backstage. [See V.0 Use Cases.](./#techdocs-v0) | -| [TechDocs V.1 🚧][v1] | TechDocs end to end (alpha) - Alpha of TechDocs that you can use end to end - and contribute to. [See V.1 Use Cases.](./#techdocs-v1) | -| [TechDocs V.2 🔮⌛][v2] | Platform stability and compatibility improvements. [See V.2 Use Cases.](./#techdocs-v2) | -| TechDocs V.3 🔮⌛ | Widget Architecture - TechDocs widget architecture available, so the community can create their own customized features. | +| Version | Description | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| [TechDocs V.0 ✅][v0] | Read docs in Backstage - Enable anyone to get a reader experience working in Backstage. [See V.0 Use Cases.](#techdocs-v0) | +| [TechDocs V.1 ✅][v1] | TechDocs end to end (alpha) - Alpha of TechDocs that you can use end to end - and contribute to. [See V.1 Use Cases.](#techdocs-v1) | +| [TechDocs V.2 🔮⌛][v2] | Platform stability and compatibility improvements. [See V.2 Use Cases.](#techdocs-v2) | +| TechDocs V.3 🔮⌛ | Widget Architecture - TechDocs widget architecture available, so the community can create their own customized features. | [v0]: https://github.com/spotify/backstage/milestone/15 [v1]: https://github.com/spotify/backstage/milestone/16 @@ -44,7 +51,7 @@ Spotify’s developer experience offering with 2,400+ documentation sites and #### TechDocs V.0 - As a user I can navigate to a manually curated docs explore page. -- As a user I can navigte to and read mock documentation that is manually +- As a user I can navigate to and read mock documentation that is manually uploaded by the TechDocs core team. #### TechDocs V.1 @@ -101,7 +108,3 @@ more to come... https://github.com/spotify/backstage/blob/master/packages/techdocs-container [techdocs/cli]: https://github.com/spotify/backstage/blob/master/packages/techdocs-cli - -## TechDocs Big Picture - -![TechDocs Big Picture](../..//assets/techdocs/techdocs_big_picture.png) diff --git a/docs/features/techdocs/architecture.md b/docs/features/techdocs/architecture.md new file mode 100644 index 0000000000..926ec238e1 --- /dev/null +++ b/docs/features/techdocs/architecture.md @@ -0,0 +1,7 @@ +--- +id: architecture +title: Architecture +description: Documentation on Architecture +--- + +![TechDocs Big Picture](../../assets/techdocs/techdocs_big_picture.png) diff --git a/docs/features/techdocs/concepts.md b/docs/features/techdocs/concepts.md index 2fa84cebd6..f13f9933f5 100644 --- a/docs/features/techdocs/concepts.md +++ b/docs/features/techdocs/concepts.md @@ -1,6 +1,8 @@ --- id: concepts title: Concepts +description: Documentation on concepts that are introduced with +Spotify's docs-like-code solution in Backstage --- This page describes concepts that are introduced with Spotify's docs-like-code @@ -8,9 +10,9 @@ solution in Backstage. ### TechDocs Core Plugin -The TechDocs Core Plugin is a MkDocs plugin created as a wrapper around multiple -MkDocs plugins and Python Markdown extensions to standardize the configuration -of MkDocs used for TechDocs. +The TechDocs Core Plugin is an [MkDocs](https://www.mkdocs.org/) plugin created +as a wrapper around multiple MkDocs plugins and Python Markdown extensions to +standardize the configuration of MkDocs used for TechDocs. [TechDocs Core](https://github.com/spotify/backstage/blob/master/packages/techdocs-container/techdocs-core/README.md) diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index 0dfa40f819..e1adedf25e 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -2,6 +2,7 @@ id: creating-and-publishing title: Creating and publishing your docs sidebar_label: Creating and Publishing Documentation +description: Guidance on how to create and publish documentation --- This section will guide you through: @@ -11,7 +12,7 @@ This section will guide you through: - [Manually add documentation setup to already existing repository](#manually-add-documentation-setup-to-already-existing-repository) - [Writing and previewing your documentation](#writing-and-previewing-your-documentation) -## Prerequisities +## Prerequisites - A working Backstage instance with TechDocs installed (see [TechDocs getting started](getting-started.md)) @@ -30,11 +31,6 @@ the documentation template. Create an entity from the documentation template and you will get the needed setup for free. -!!! warning Currently the Backstage Software Templates are limited to create -repositories inside GitHub organizations. You also need to generate an personal -access token and use as an environment variable. Read more about this -[here](../software-templates/installation.md#runtime-dependencies). - ### Manually add documentation setup to already existing repository Prerequisities: @@ -81,12 +77,12 @@ updated documentation next time you run Backstage! ## Writing and previewing your documentation Using the `techdocs-cli` you can preview your docs inside a local Backstage -instance and get automatic recompilation on changes. This is useful for when you -want to write your documentation. +instance and get live reload on changes. This is useful when you want to preview +your documentation while writing. To do this you can run: ```bash -cd ~// +cd /path/to/docs-repository/ npx @techdocs/cli serve ``` diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index 33edfecbe8..b9d215fd8a 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -1,40 +1,14 @@ --- id: getting-started title: Getting Started +description: Getting Started Guidelines --- TechDocs functions as a plugin to Backstage, so you will need to use Backstage to use TechDocs. -## What is Backstage? - -Backstage is an open platform for building developer portals. It’s based on the -developer portal we’ve been using internally at Spotify for over four years. -[Read more here](https://github.com/spotify/backstage). - -## Prerequisities - -In order to use Backstage and TechDocs, you need to have the following -installed: - -- [Node.js](https://nodejs.org) Active LTS (long term support), currently v12 -- [Yarn](https://yarnpkg.com/getting-started/install) - -## Creating a new Backstage app - -> If you have already created a Backstage application, jump to -> [Installing TechDocs](#installing-techdocs), otherwise complete this step. - -To create a new Backstage application for TechDocs, run the following command: - -```bash -npx @backstage/create-app -``` - -You will then be prompted to enter a name for your application. Once that's -done, a new Backstage application will be created in a new folder. For example, -if you choose the name `hello-world`, a new `hello-world` folder is created -containing your new Backstage application. +If you haven't setup Backstage already, start +[here](../../getting-started/index.md). ## Installing TechDocs @@ -74,7 +48,7 @@ export { plugin as TechDocs } from '@backstage/plugin-techdocs'; ### Setting the configuration TechDocs allows for configuration of the docs storage URL through your -`app-config` file. We provide two different values to be configured, +`app-config.yaml` file. We provide two different values to be configured, `requestUrl` and `storageUrl`. The `requestUrl` is what the reader will request its data from, and `storageUrl` is where the backend can find the stored documentation. @@ -87,16 +61,40 @@ techdocs: requestUrl: http://localhost:7000/techdocs/docs ``` -If you want `techdocs-backend` to manage building and publishing you want +If you want `techdocs-backend` to manage building and publishing, you want `requestUrl` to point to the default value (or wherever `techdocs-backend` is hosted). `storageUrl` should be where your publisher publishes your docs. Using the default `LocalPublish` that is the default value. If you have a setup where you are not using `techdocs-backend` for managing -building and publishing of your documentation you want to change the +building and publishing of your documentation, you want to change the `requestUrl` to point to your storage. In this case `storageUrl` is not required. +### Disable Docker in Docker situation (Optional) + +The TechDocs backend plugin runs a docker container with mkdocs to generate the +frontend of the docs from source files (Markdown). If you are deploying +Backstage using Docker, this will mean that your Backstage Docker container will +try to run another Docker container for TechDocs backend. + +To avoid this problem, we have a configuration available. You can set a value in +your `app-config.yaml` that tells the techdocs generator if it should run the +`local` mkdocs or run it from `docker`. This defaults to running as `docker` if +no config is provided. + +```yaml +techdocs: + generators: + techdocs: local +``` + +Setting `generators.techdocs` to `local` means you will have to make sure your +environment is compatible with techdocs. You will have to install the +`mkdocs-techdocs-container` and 'mkdocs' package from pip, as well as graphviz +and plantuml from your package manager. This has only been tested with python +3.7 and python 3.8. + ## Run Backstage locally Change folder to `/packages/backend` and run the diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index e555c6b52d..97876c2ba1 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -1,6 +1,7 @@ --- id: app-custom-theme title: Customize the look-and-feel of your App +description: Documentation on Customizing look and feel of the App --- Backstage ships with a default theme with a light and dark mode variant. The diff --git a/docs/getting-started/configure-app-with-plugins.md b/docs/getting-started/configure-app-with-plugins.md index 515b52afa7..1d02f147c1 100644 --- a/docs/getting-started/configure-app-with-plugins.md +++ b/docs/getting-started/configure-app-with-plugins.md @@ -1,6 +1,7 @@ --- id: configure-app-with-plugins title: Configuring App with plugins +description: Documentation on How Configuring App with plugins --- ## Adding existing plugins to your app diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md index cebf3f5de2..452e90add5 100644 --- a/docs/getting-started/create-an-app.md +++ b/docs/getting-started/create-an-app.md @@ -1,6 +1,7 @@ --- id: create-an-app title: Create an App +description: Documentation on Creating an App --- To get set up quickly with your own Backstage project you can create a Backstage diff --git a/docs/getting-started/deployment-k8s.md b/docs/getting-started/deployment-k8s.md index 2a184779e2..ddcbd290c6 100644 --- a/docs/getting-started/deployment-k8s.md +++ b/docs/getting-started/deployment-k8s.md @@ -1,6 +1,7 @@ --- id: deployment-k8s title: Kubernetes +description: Documentation on Kubernetes and K8s Deployment --- Coming soon! diff --git a/docs/getting-started/deployment-other.md b/docs/getting-started/deployment-other.md index d3fb9d932b..2cd9708e2a 100644 --- a/docs/getting-started/deployment-other.md +++ b/docs/getting-started/deployment-other.md @@ -1,6 +1,7 @@ --- id: deployment-other title: Other +description: Documentation on different ways of Deployment --- ## Deploying Locally diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index 9c75c8eabf..154a959f85 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -1,6 +1,8 @@ --- id: development-environment title: Development Environment +description: Documentation on how to get set up for doing development on +the Backstage repository --- This section describes how to get set up for doing development on the Backstage diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index c1c6ea9231..a9ac770f90 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -1,84 +1,51 @@ --- id: index -title: Running Backstage Locally +title: Getting Started +description: Documentation on How to get started with Backstage --- -First make sure you are using NodeJS with an Active LTS Release, currently v12. -This is made easy with a version manager such as nvm which allows for version -switching. +There are two different ways to get started with Backstage, either by creating a +standalone app, or by cloning this repo. Which method you use depends on what +you're planning to do. + +Creating a standalone instance makes it simpler to customize the application for +your needs whilst staying up to date with the project. You will also depend on +`@backstage` packages from NPM, making the project much smaller. This is the +recommended approach if you want to kick the tyres of Backstage or setup your +own instance. + +On the other hand, if you want to contribute plugins or to the project in +general, it's easier to fork and clone this project. That will let you stay up +to date with the latest changes, and gives you an easier path to make Pull +Requests towards this repo. + +### Creating a Standalone App + +Backstage provides the `@backstage/create-app` package to scaffold standalone +instances of Backstage. You will need to have +[NodeJS](https://nodejs.org/en/download/) Active LTS Release installed +(currently v12), and [yarn](https://classic.yarnpkg.com/en/docs/install). You +will also need to have [Docker](https://docs.docker.com/engine/install/) +installed to use some features like Software Templates and TechDocs. + +Using `npx` you can then run the following to create an app in a chosen +subdirectory of your current working directory: ```bash -# Checking your version -node --version -> v14.7.0 - -# Adding a second node version -nvm install 12 -> Downloading and installing node v12.18.3... -> Now using node v12.18.3 (npm v6.14.6) +npx @backstage/create-app ``` -To get up and running with a local Backstage to evaluate it, let's clone it off -of GitHub and run an initial build. +You will be taken through a wizard to create your app, and the output should +look something like this. You can read more about this process +[here](https://backstage.io/docs/getting-started/create-an-app). -```bash -# Start from your local development folder -git clone git@github.com:spotify/backstage.git -cd backstage +### Contributing to Backstage -# Fetch our dependencies and run an initial build -yarn install -yarn tsc -yarn build -``` +You can read more in our +[CONTRIBUTING](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md) +guide, which can help you get setup with a Backstage development environment. -Phew! Now you have a local repository that's ready to run and to add any open -source contributions into. +### Next steps -We are now going to launch two things: an example Backstage frontend app, and an -example Backstage backend that the frontend talks to. You are going to need two -terminal windows, both starting from the Backstage project root. - -In the first window, run - -```bash -cd packages/backend -yarn start -``` - -That starts up a backend instance on port 7000. - -In the other window, we will first populate the catalog with some nice mock data -to look at, and then launch the frontend. These commands are run from the -project root, not inside the backend directory. - -```bash -yarn lerna run mock-data -yarn start -``` - -That starts up the frontend on port 3000, and should automatically open a -browser window showing it. - -Congratulations! That should be it. Let us know how it went -[on discord](https://discord.gg/EBHEGzX), file issues for any -[feature](https://github.com/spotify/backstage/issues/new?labels=help+wanted&template=feature_template.md) -or -[plugin suggestions](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME), -or -[bugs](https://github.com/spotify/backstage/issues/new?labels=bug&template=bug_template.md) -you have, and feel free to -[contribute](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md)! - -## Creating a Plugin - -The value of Backstage grows with every new plugin that gets added. Here is a -collection of tutorials that will guide you through setting up and extending an -instance of Backstage with your own plugins. - -- [Development Environment](development-environment.md) -- [Create a Backstage Plugin](../plugins/create-a-plugin.md) -- [Structure of a Plugin](../plugins/structure-of-a-plugin.md) -- [Utility APIs](../api/utility-apis.md) - -[Back to Docs](../README.md) +Take a look at the [Running Backstage Locally](./running-backstage-locally.md) +guide to learn how to set up Backstage, and how to develop on the platform. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index a4f91b6e95..3b11d2b8e1 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,6 +1,7 @@ --- id: installation title: Installation +description: Documentation on Installation --- Coming soon! diff --git a/docs/getting-started/running-backstage-locally.md b/docs/getting-started/running-backstage-locally.md new file mode 100644 index 0000000000..a0cf10998e --- /dev/null +++ b/docs/getting-started/running-backstage-locally.md @@ -0,0 +1,110 @@ +--- +id: running-backstage-locally +title: Running Backstage Locally +description: Documentation on How to run Backstage Locally +--- + +## Prerequisites + +- Node.js + +First make sure you are using NodeJS with an Active LTS Release, currently v12. +This is made easy with a version manager such as +[nvm](https://github.com/nvm-sh/nvm) which allows for version switching. + +```bash +# Installing a new version +nvm install 12 +> Downloading and installing node v12.18.3... +> Now using node v12.18.3 (npm v6.14.6) + +# Checking your version +node --version +> v12.18.3 +``` + +- yarn + +Please refer to the +[installation instructions for yarn](https://classic.yarnpkg.com/en/docs/install/). + +- Docker + +We use Docker for few of our core features. So, you will need Docker installed +locally to use features like Software Templates and TechDocs. Please refer to +the +[installation instructions for Docker](https://docs.docker.com/engine/install/). + +## Clone and Build + +To get up and running with a local Backstage to evaluate it, let's clone it off +of GitHub and run an initial build. + +```bash +# Start from your local development folder +git clone git@github.com:spotify/backstage.git +cd backstage + +# Fetch our dependencies and run an initial build +yarn install +yarn tsc +yarn build +``` + +Phew! Now you have a local repository that's ready to run and to add any open +source contributions into. + +We are now going to launch two things: an example Backstage frontend app, and an +example Backstage backend that the frontend talks to. You are going to need two +terminal windows, both starting from the Backstage project root. + +In the first window, run + +```bash +cd packages/backend +yarn start +``` + +That starts up a backend instance on port 7000. + +In the other window, we will then launch the frontend. This command is run from +the project root, not inside the backend directory. + +```bash +yarn start +``` + +That starts up the frontend on port 3000, and should automatically open a +browser window showing it. + +## Authentication + +When Backstage starts, you can choose to enter as a Guest user and start +exploring. + +But you can also set up any of the available authentication methods. The easiest +option will be GitHub. To setup GitHub authentication in Backstage, see +[these instructions](https://github.com/spotify/backstage/tree/master/plugins/auth-backend#github). + +--- + +Congratulations! That should be it. Let us know how it went +[on discord](https://discord.gg/EBHEGzX), file issues for any +[feature](https://github.com/spotify/backstage/issues/new?labels=help+wanted&template=feature_template.md) +or +[plugin suggestions](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME), +or +[bugs](https://github.com/spotify/backstage/issues/new?labels=bug&template=bug_template.md) +you have, and feel free to +[contribute](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md)! + +## Creating a Plugin + +The value of Backstage grows with every new plugin that gets added. Here is a +collection of tutorials that will guide you through setting up and extending an +instance of Backstage with your own plugins. + +- [Development Environment](development-environment.md) +- [Create a Backstage Plugin](../plugins/create-a-plugin.md) +- [Structure of a Plugin](../plugins/structure-of-a-plugin.md) +- [Utility APIs](../api/utility-apis.md) diff --git a/docs/overview/adopting.md b/docs/overview/adopting.md index 893eac6398..422adfb288 100644 --- a/docs/overview/adopting.md +++ b/docs/overview/adopting.md @@ -1,6 +1,8 @@ --- id: adopting title: Strategies for adopting +description: Documentation on some general best practices that have been key +to Backstage's success inside Spotify --- This document outlines some general best practices that have been key to diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index 55a1aeec04..f2c6dad61f 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -1,6 +1,7 @@ --- id: architecture-overview title: Architecture overview +description: Documentation on Architecture overview --- ## Overview diff --git a/docs/overview/architecture-terminology.md b/docs/overview/architecture-terminology.md index aee581f927..092ca7fb01 100644 --- a/docs/overview/architecture-terminology.md +++ b/docs/overview/architecture-terminology.md @@ -1,6 +1,7 @@ --- id: architecture-terminology title: Architecture terminology +description: Documentation on Architecture terminology --- Backstage is constructed out of three parts. We separate Backstage in this way diff --git a/docs/overview/background.md b/docs/overview/background.md index ef226653fb..486260bac3 100644 --- a/docs/overview/background.md +++ b/docs/overview/background.md @@ -1,6 +1,7 @@ --- id: background title: The Spotify Story +description: Documentation on Background and Story behind making of Backstage --- Backstage was born out of necessity at Spotify. We found that as we grew, our diff --git a/docs/overview/logos.md b/docs/overview/logos.md index 39071ed793..2a684420bc 100644 --- a/docs/overview/logos.md +++ b/docs/overview/logos.md @@ -2,6 +2,7 @@ id: logos title: Logos sidebar_label: Logo assets +description: Guidelines for how to use the Backstage logos and icons --- Guidelines for how to use the Backstage logo and icon can be found diff --git a/docs/overview/roadmap.md b/docs/overview/roadmap.md index 5a06f4544f..d6c1d2d641 100644 --- a/docs/overview/roadmap.md +++ b/docs/overview/roadmap.md @@ -1,6 +1,7 @@ --- id: roadmap title: Project roadmap +description: Roadmap of Backstage Project --- ## Current status diff --git a/docs/overview/support.md b/docs/overview/support.md index 9e4e9eca15..6e00c17fab 100644 --- a/docs/overview/support.md +++ b/docs/overview/support.md @@ -1,6 +1,7 @@ --- id: support title: Support and community +description: Support and Community Details and Links --- - [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the diff --git a/docs/overview/vision.md b/docs/overview/vision.md index 2ab7cdc6f4..df75a4f0a6 100644 --- a/docs/overview/vision.md +++ b/docs/overview/vision.md @@ -1,6 +1,8 @@ --- id: vision title: Vision +description: Goal is to provide engineers with the best developer experience in +the world --- Our goal is to provide engineers with the best developer experience in the diff --git a/docs/overview/what-is-backstage.md b/docs/overview/what-is-backstage.md index 96e89618a8..0d16943c9b 100644 --- a/docs/overview/what-is-backstage.md +++ b/docs/overview/what-is-backstage.md @@ -1,6 +1,8 @@ --- id: what-is-backstage title: What is Backstage? +description: Backsatge is an open platform for building developer portals. +Powered by a centralized service catalog, Backstage restores order to your microservices and infrastructure --- ![service-catalog](https://backstage.io/blog/assets/6/header.png) diff --git a/docs/plugins/add-to-marketplace.md b/docs/plugins/add-to-marketplace.md index d85807a226..59cca07bff 100644 --- a/docs/plugins/add-to-marketplace.md +++ b/docs/plugins/add-to-marketplace.md @@ -1,6 +1,7 @@ --- id: add-to-marketplace title: Add to Marketplace +description: Documentation on Adding Plugin to Marketplace --- ## Adding a Plugin to the Marketplace diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 986af66c20..516ba4b111 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -1,6 +1,7 @@ --- id: backend-plugin title: Backend plugin +description: Documentation on Backend plugin --- ## TODO diff --git a/docs/plugins/call-existing-api.md b/docs/plugins/call-existing-api.md index 9658176224..14ea4c1f5a 100644 --- a/docs/plugins/call-existing-api.md +++ b/docs/plugins/call-existing-api.md @@ -1,6 +1,8 @@ --- id: call-existing-api title: Call Existing API +description: Describes the various options that Backstage frontend plugins have, +in communicating with service APIs that already exist --- This article describes the various options that Backstage frontend plugins have, diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index 4bcc4da387..bcb86024fd 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -1,6 +1,7 @@ --- id: create-a-plugin title: Create a Backstage Plugin +description: Documentation on How to Create a Backstage Plugin --- A Backstage Plugin adds functionality to Backstage. diff --git a/docs/plugins/existing-plugins.md b/docs/plugins/existing-plugins.md index 8598ae02c4..c1fbda379a 100644 --- a/docs/plugins/existing-plugins.md +++ b/docs/plugins/existing-plugins.md @@ -1,6 +1,7 @@ --- id: existing-plugins title: Existing plugins +description: Lists of existing open source plugins --- ## Open source plugins diff --git a/docs/plugins/index.md b/docs/plugins/index.md index 5aaf8acadf..8a61aeee80 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -1,6 +1,7 @@ --- id: index title: Intro to plugins +description: Documentation on Introduction to Plugins --- Backstage is a single-page application composed of a set of plugins. diff --git a/docs/plugins/integrating-plugin-into-service-catalog.md b/docs/plugins/integrating-plugin-into-service-catalog.md index 4408c27ccc..51d2331140 100644 --- a/docs/plugins/integrating-plugin-into-service-catalog.md +++ b/docs/plugins/integrating-plugin-into-service-catalog.md @@ -1,6 +1,7 @@ --- id: integrating-plugin-into-service-catalog title: Integrate into the Service Catalog +description: Documentation on How to integrate plugin into service catalog --- > This is an advanced use case and currently is an experimental feature. Expect diff --git a/docs/plugins/plugin-development.md b/docs/plugins/plugin-development.md index faf3c6ddbd..c5d9763f7d 100644 --- a/docs/plugins/plugin-development.md +++ b/docs/plugins/plugin-development.md @@ -1,6 +1,7 @@ --- id: plugin-development title: Plugin Development +description: Documentation on Plugin Development --- Backstage plugins provide features to a Backstage App. diff --git a/docs/plugins/proxying.md b/docs/plugins/proxying.md index 658df3dda1..2f809ffd10 100644 --- a/docs/plugins/proxying.md +++ b/docs/plugins/proxying.md @@ -1,6 +1,7 @@ --- id: proxying title: Proxying +description: Documentation on Proxying --- ## Overview @@ -52,7 +53,10 @@ configuration will lead to the proxy acting on backend requests to The value inside each route is either a simple URL string, or an object on the format accepted by -[http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware). +[http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware). It +is also possible to limit the forwarded HTTP methods with the configuration +`allowedMethods`, for example `allowedMethods: ['GET']` to enforce read-only +access. If the value is a string, it is assumed to correspond to: diff --git a/docs/plugins/publish-private.md b/docs/plugins/publish-private.md index 6361f70854..d04449d465 100644 --- a/docs/plugins/publish-private.md +++ b/docs/plugins/publish-private.md @@ -1,6 +1,7 @@ --- id: publish-private title: Publish private +description: Documentation on How to Publish private --- ## TODO diff --git a/docs/plugins/publishing.md b/docs/plugins/publishing.md index 6fee4f9b1d..0ed85e8cdc 100644 --- a/docs/plugins/publishing.md +++ b/docs/plugins/publishing.md @@ -1,6 +1,7 @@ --- id: publishing title: Publishing +description: Documentation on Publishing NPM packages --- ## NPM diff --git a/docs/plugins/structure-of-a-plugin.md b/docs/plugins/structure-of-a-plugin.md index 20e72c539d..f061d2f4a8 100644 --- a/docs/plugins/structure-of-a-plugin.md +++ b/docs/plugins/structure-of-a-plugin.md @@ -1,6 +1,7 @@ --- id: structure-of-a-plugin title: Structure of a Plugin +description: Details about structure of a plugin --- Nice, you have a new plugin! We'll soon see how we can develop it into doing diff --git a/docs/plugins/testing.md b/docs/plugins/testing.md index 110bc97515..696f8b1368 100644 --- a/docs/plugins/testing.md +++ b/docs/plugins/testing.md @@ -1,6 +1,7 @@ --- id: testing title: Testing with Jest +description: Documentation on How to do unit testing with Jest --- Backstage uses [Jest](https://facebook.github.io/jest/) for all our unit testing diff --git a/docs/reference/createPlugin-feature-flags.md b/docs/reference/createPlugin-feature-flags.md index bc38656939..ebb8a6b503 100644 --- a/docs/reference/createPlugin-feature-flags.md +++ b/docs/reference/createPlugin-feature-flags.md @@ -1,6 +1,7 @@ --- id: createPlugin-feature-flags title: createPlugin - feature flags +description: Documentation on createPlugin - feature flags --- The `featureFlags` object passed to the `register` function makes it possible diff --git a/docs/reference/createPlugin-router.md b/docs/reference/createPlugin-router.md index 361b485df9..89ee44e558 100644 --- a/docs/reference/createPlugin-router.md +++ b/docs/reference/createPlugin-router.md @@ -1,6 +1,7 @@ --- id: createPlugin-router title: createPlugin - router +description: Documentation on createPlugin - router --- The router that is passed to the `register` function makes it possible for diff --git a/docs/reference/createPlugin.md b/docs/reference/createPlugin.md index 0cabe63b39..45e3303124 100644 --- a/docs/reference/createPlugin.md +++ b/docs/reference/createPlugin.md @@ -1,6 +1,7 @@ --- id: createPlugin title: createPlugin +description: Documentation on createPlugin --- Taking a plugin config as argument and returns a new plugin. diff --git a/docs/reference/utility-apis/AlertApi.md b/docs/reference/utility-apis/AlertApi.md index d000a6d6a2..d096276c3b 100644 --- a/docs/reference/utility-apis/AlertApi.md +++ b/docs/reference/utility-apis/AlertApi.md @@ -1,7 +1,7 @@ # AlertApi The AlertApi type is defined at -[packages/core-api/src/apis/definitions/AlertApi.ts:29](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/AlertApi.ts#L29). +[packages/core-api/src/apis/definitions/AlertApi.ts:29](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/AlertApi.ts#L29). The following Utility API implements this type: [alertApiRef](./README.md#alert) @@ -38,7 +38,7 @@ export type AlertMessage = { Defined at -[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/AlertApi.ts#L19). +[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/AlertApi.ts#L19). Referenced by: [post](#post), [alert\$](#alert). @@ -67,7 +67,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L53). Referenced by: [alert\$](#alert). @@ -86,7 +86,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -109,6 +109,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/AppThemeApi.md b/docs/reference/utility-apis/AppThemeApi.md index e7d5296ceb..d37282adea 100644 --- a/docs/reference/utility-apis/AppThemeApi.md +++ b/docs/reference/utility-apis/AppThemeApi.md @@ -1,7 +1,7 @@ # AppThemeApi The AppThemeApi type is defined at -[packages/core-api/src/apis/definitions/AppThemeApi.ts:50](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L50). +[packages/core-api/src/apis/definitions/AppThemeApi.ts:50](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/AppThemeApi.ts#L50). The following Utility API implements this type: [appThemeApiRef](./README.md#apptheme) @@ -76,7 +76,7 @@ export type AppTheme = { Defined at -[packages/core-api/src/apis/definitions/AppThemeApi.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L24). +[packages/core-api/src/apis/definitions/AppThemeApi.ts:24](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/AppThemeApi.ts#L24). Referenced by: [getInstalledThemes](#getinstalledthemes). @@ -87,7 +87,7 @@ export type BackstagePalette = Palette & Palette Defined at -[packages/theme/src/types.ts:70](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/theme/src/types.ts#L70). +[packages/theme/src/types.ts:70](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/theme/src/types.ts#L70). Referenced by: [BackstageTheme](#backstagetheme). @@ -100,7 +100,7 @@ export interface BackstageTheme extends Theme { Defined at -[packages/theme/src/types.ts:73](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/theme/src/types.ts#L73). +[packages/theme/src/types.ts:73](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/theme/src/types.ts#L73). Referenced by: [AppTheme](#apptheme). @@ -129,7 +129,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L53). Referenced by: [activeThemeId\$](#activethemeid). @@ -148,7 +148,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -204,7 +204,7 @@ type PaletteAdditions = { Defined at -[packages/theme/src/types.ts:23](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/theme/src/types.ts#L23). +[packages/theme/src/types.ts:23](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/theme/src/types.ts#L23). Referenced by: [BackstagePalette](#backstagepalette). @@ -227,6 +227,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/BackstageIdentityApi.md b/docs/reference/utility-apis/BackstageIdentityApi.md index 87318ecff5..529b73c576 100644 --- a/docs/reference/utility-apis/BackstageIdentityApi.md +++ b/docs/reference/utility-apis/BackstageIdentityApi.md @@ -1,7 +1,7 @@ # BackstageIdentityApi The BackstageIdentityApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:144](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L144). +[packages/core-api/src/apis/definitions/auth.ts:134](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L134). The following Utility APIs implement this type: @@ -15,6 +15,8 @@ The following Utility APIs implement this type: - [microsoftAuthApiRef](./README.md#microsoftauth) +- [oauth2ApiRef](./README.md#oauth2) + - [oktaAuthApiRef](./README.md#oktaauth) ## Members @@ -66,7 +68,7 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getBackstageIdentity](#getbackstageidentity). @@ -87,6 +89,6 @@ export type BackstageIdentity = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:157](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L157). +[packages/core-api/src/apis/definitions/auth.ts:147](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L147). Referenced by: [getBackstageIdentity](#getbackstageidentity). diff --git a/docs/reference/utility-apis/Config.md b/docs/reference/utility-apis/Config.md index 9374fb9962..b54e122c03 100644 --- a/docs/reference/utility-apis/Config.md +++ b/docs/reference/utility-apis/Config.md @@ -1,7 +1,7 @@ # Config The Config type is defined at -[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/config/src/types.ts#L32). +[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/config/src/types.ts#L32). The following Utility API implements this type: [configApiRef](./README.md#config) @@ -140,7 +140,7 @@ export type Config = { Defined at -[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/config/src/types.ts#L32). +[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/config/src/types.ts#L32). Referenced by: [getConfig](#getconfig), [getOptionalConfig](#getoptionalconfig), [getConfigArray](#getconfigarray), @@ -153,7 +153,7 @@ export type JsonArray = JsonValue[] Defined at -[packages/config/src/types.ts:18](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/config/src/types.ts#L18). +[packages/config/src/types.ts:18](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/config/src/types.ts#L18). Referenced by: [JsonValue](#jsonvalue). @@ -164,7 +164,7 @@ export type JsonObject = { [key in string]?: JsonValue Defined at -[packages/config/src/types.ts:17](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/config/src/types.ts#L17). +[packages/config/src/types.ts:17](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/config/src/types.ts#L17). Referenced by: [JsonValue](#jsonvalue). @@ -181,7 +181,7 @@ export type JsonValue = Defined at -[packages/config/src/types.ts:19](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/config/src/types.ts#L19). +[packages/config/src/types.ts:19](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/config/src/types.ts#L19). Referenced by: [get](#get), [getOptional](#getoptional), [JsonObject](#jsonobject), [JsonArray](#jsonarray), [Config](#config). diff --git a/docs/reference/utility-apis/DiscoveryApi.md b/docs/reference/utility-apis/DiscoveryApi.md index 39902789cd..24371c1729 100644 --- a/docs/reference/utility-apis/DiscoveryApi.md +++ b/docs/reference/utility-apis/DiscoveryApi.md @@ -1,7 +1,7 @@ # DiscoveryApi The DiscoveryApi type is defined at -[packages/core-api/src/apis/definitions/DiscoveryApi.ts:30](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L30). +[packages/core-api/src/apis/definitions/DiscoveryApi.ts:30](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L30). The following Utility API implements this type: [discoveryApiRef](./README.md#discovery) diff --git a/docs/reference/utility-apis/ErrorApi.md b/docs/reference/utility-apis/ErrorApi.md index c05f060ec3..4de58b34a1 100644 --- a/docs/reference/utility-apis/ErrorApi.md +++ b/docs/reference/utility-apis/ErrorApi.md @@ -1,7 +1,7 @@ # ErrorApi The ErrorApi type is defined at -[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/ErrorApi.ts#L53). +[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/ErrorApi.ts#L53). The following Utility API implements this type: [errorApiRef](./README.md#error) @@ -41,7 +41,7 @@ type Error = { Defined at -[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/ErrorApi.ts#L24). +[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/ErrorApi.ts#L24). Referenced by: [post](#post), [error\$](#error). @@ -58,7 +58,7 @@ export type ErrorContext = { Defined at -[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/ErrorApi.ts#L33). +[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/ErrorApi.ts#L33). Referenced by: [post](#post), [error\$](#error). @@ -87,7 +87,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L53). Referenced by: [error\$](#error). @@ -106,7 +106,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -129,6 +129,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/FeatureFlagsApi.md b/docs/reference/utility-apis/FeatureFlagsApi.md index 5efdb176a4..8fbcb794fb 100644 --- a/docs/reference/utility-apis/FeatureFlagsApi.md +++ b/docs/reference/utility-apis/FeatureFlagsApi.md @@ -1,7 +1,7 @@ # FeatureFlagsApi The FeatureFlagsApi type is defined at -[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:41](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L41). +[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:41](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L41). The following Utility API implements this type: [featureFlagsApiRef](./README.md#featureflags) diff --git a/docs/reference/utility-apis/IdentityApi.md b/docs/reference/utility-apis/IdentityApi.md index 4e37ad5300..5ee5c582b6 100644 --- a/docs/reference/utility-apis/IdentityApi.md +++ b/docs/reference/utility-apis/IdentityApi.md @@ -1,7 +1,7 @@ # IdentityApi The IdentityApi type is defined at -[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/IdentityApi.ts#L22). +[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/IdentityApi.ts#L22). The following Utility API implements this type: [identityApiRef](./README.md#identity) @@ -40,12 +40,12 @@ identity, such as a demo user or mocked user for e2e tests. getIdToken(): Promise<string | undefined> -### logout() +### signOut() -Log out the current user +Sign out the current user
-logout(): Promise<void>
+signOut(): Promise<void>
 
## Supporting types @@ -76,6 +76,6 @@ export type ProfileInfo = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L172). +[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L162). Referenced by: [getProfile](#getprofile). diff --git a/docs/reference/utility-apis/OAuthApi.md b/docs/reference/utility-apis/OAuthApi.md index e73d5f645f..a489db76c5 100644 --- a/docs/reference/utility-apis/OAuthApi.md +++ b/docs/reference/utility-apis/OAuthApi.md @@ -1,7 +1,7 @@ # OAuthApi The OAuthApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L67). +[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L67). The following Utility APIs implement this type: @@ -50,14 +50,6 @@ getAccessToken( ): Promise<string> -### logout() - -Log out the user's session. This will reload the page. - -
-logout(): Promise<void>
-
- ## Supporting types These types are part of the API declaration, but may not be unique to this API. @@ -90,7 +82,7 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getAccessToken](#getaccesstoken). @@ -116,6 +108,6 @@ export type OAuthScope = string | string[] Defined at -[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L38). +[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L38). Referenced by: [getAccessToken](#getaccesstoken). diff --git a/docs/reference/utility-apis/OAuthRequestApi.md b/docs/reference/utility-apis/OAuthRequestApi.md index c6b9e09189..ac11764682 100644 --- a/docs/reference/utility-apis/OAuthRequestApi.md +++ b/docs/reference/utility-apis/OAuthRequestApi.md @@ -1,7 +1,7 @@ # OAuthRequestApi The OAuthRequestApi type is defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99). The following Utility API implements this type: [oauthRequestApiRef](./README.md#oauthrequest) @@ -72,7 +72,7 @@ export type AuthProvider = { Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27). Referenced by: [AuthRequesterOptions](#authrequesteroptions), [PendingAuthRequest](#pendingauthrequest). @@ -96,7 +96,7 @@ export type AuthRequester<AuthResponse> = ( Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66). Referenced by: [createAuthRequester](#createauthrequester). @@ -121,7 +121,7 @@ export type AuthRequesterOptions<AuthResponse> = { Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43). Referenced by: [createAuthRequester](#createauthrequester). @@ -150,7 +150,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L53). Referenced by: [authRequest\$](#authrequest). @@ -169,7 +169,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -204,7 +204,7 @@ export type PendingAuthRequest = { Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77). Referenced by: [authRequest\$](#authrequest). @@ -227,6 +227,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/OpenIdConnectApi.md b/docs/reference/utility-apis/OpenIdConnectApi.md index 41a3247af6..efd79593d6 100644 --- a/docs/reference/utility-apis/OpenIdConnectApi.md +++ b/docs/reference/utility-apis/OpenIdConnectApi.md @@ -1,7 +1,7 @@ # OpenIdConnectApi The OpenIdConnectApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:104](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L104). +[packages/core-api/src/apis/definitions/auth.ts:99](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L99). The following Utility APIs implement this type: @@ -34,14 +34,6 @@ user rejects the login request. getIdToken(options?: AuthRequestOptions): Promise<string> -### logout() - -Log out the user's session. This will reload the page. - -
-logout(): Promise<void>
-
- ## Supporting types These types are part of the API declaration, but may not be unique to this API. @@ -74,6 +66,6 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getIdToken](#getidtoken). diff --git a/docs/reference/utility-apis/ProfileInfoApi.md b/docs/reference/utility-apis/ProfileInfoApi.md index 09c0f88f83..402b5ba504 100644 --- a/docs/reference/utility-apis/ProfileInfoApi.md +++ b/docs/reference/utility-apis/ProfileInfoApi.md @@ -1,7 +1,7 @@ # ProfileInfoApi The ProfileInfoApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:127](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L127). +[packages/core-api/src/apis/definitions/auth.ts:117](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L117). The following Utility APIs implement this type: @@ -65,7 +65,7 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getProfile](#getprofile). @@ -93,6 +93,6 @@ export type ProfileInfo = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L172). +[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L162). Referenced by: [getProfile](#getprofile). diff --git a/docs/reference/utility-apis/README.md b/docs/reference/utility-apis/README.md index cfdc3b6ef8..71931a5d5a 100644 --- a/docs/reference/utility-apis/README.md +++ b/docs/reference/utility-apis/README.md @@ -12,7 +12,7 @@ Used to report alerts and forward them to the app Implemented type: [AlertApi](./AlertApi.md) ApiRef: -[alertApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/AlertApi.ts#L41) +[alertApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/AlertApi.ts#L41) ### appTheme @@ -21,7 +21,7 @@ API Used to configure the app theme, and enumerate options Implemented type: [AppThemeApi](./AppThemeApi.md) ApiRef: -[appThemeApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L74) +[appThemeApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/AppThemeApi.ts#L74) ### auth0Auth @@ -29,11 +29,10 @@ Provides authentication towards Auth0 APIs Implemented types: [OpenIdConnectApi](./OpenIdConnectApi.md), [ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), -[SessionStateApi](./SessionStateApi.md) +[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[auth0AuthApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L273) +[auth0AuthApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L275) ### config @@ -42,7 +41,7 @@ Used to access runtime configuration Implemented type: [Config](./Config.md) ApiRef: -[configApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/ConfigApi.ts#L22) +[configApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/ConfigApi.ts#L22) ### discovery @@ -51,7 +50,7 @@ Provides service discovery of backend plugins Implemented type: [DiscoveryApi](./DiscoveryApi.md) ApiRef: -[discoveryApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L44) +[discoveryApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L44) ### error @@ -60,7 +59,7 @@ Used to report errors and forward them to the app Implemented type: [ErrorApi](./ErrorApi.md) ApiRef: -[errorApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/ErrorApi.ts#L65) +[errorApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/ErrorApi.ts#L65) ### featureFlags @@ -69,7 +68,7 @@ Used to toggle functionality in features across Backstage Implemented type: [FeatureFlagsApi](./FeatureFlagsApi.md) ApiRef: -[featureFlagsApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L58) +[featureFlagsApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L58) ### githubAuth @@ -77,11 +76,10 @@ Provides authentication towards GitHub APIs Implemented types: [OAuthApi](./OAuthApi.md), [ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), -[SessionStateApi](./SessionStateApi.md) +[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[githubAuthApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L230) +[githubAuthApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L232) ### gitlabAuth @@ -89,11 +87,10 @@ Provides authentication towards GitLab APIs Implemented types: [OAuthApi](./OAuthApi.md), [ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), -[SessionStateApi](./SessionStateApi.md) +[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[gitlabAuthApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L260) +[gitlabAuthApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L262) ### googleAuth @@ -102,11 +99,10 @@ Provides authentication towards Google APIs and identities Implemented types: [OAuthApi](./OAuthApi.md), [OpenIdConnectApi](./OpenIdConnectApi.md), [ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), -[SessionStateApi](./SessionStateApi.md) +[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[googleAuthApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L213) +[googleAuthApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L215) ### identity @@ -115,7 +111,7 @@ Provides access to the identity of the signed in user Implemented type: [IdentityApi](./IdentityApi.md) ApiRef: -[identityApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/IdentityApi.ts#L54) +[identityApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/IdentityApi.ts#L54) ### microsoftAuth @@ -124,11 +120,10 @@ Provides authentication towards Microsoft APIs and identities Implemented types: [OAuthApi](./OAuthApi.md), [OpenIdConnectApi](./OpenIdConnectApi.md), [ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), -[SessionStateApi](./SessionStateApi.md) +[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[microsoftAuthApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L287) +[microsoftAuthApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L289) ### oauth2 @@ -136,10 +131,11 @@ Example of how to use oauth2 custom provider Implemented types: [OAuthApi](./OAuthApi.md), [OpenIdConnectApi](./OpenIdConnectApi.md), -[ProfileInfoApi](./ProfileInfoApi.md), [SessionStateApi](./SessionStateApi.md) +[ProfileInfoApi](./ProfileInfoApi.md), +[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[oauth2ApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L301) +[oauth2ApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L303) ### oauthRequest @@ -148,7 +144,7 @@ An API for implementing unified OAuth flows in Backstage Implemented type: [OAuthRequestApi](./OAuthRequestApi.md) ApiRef: -[oauthRequestApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130) +[oauthRequestApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130) ### oktaAuth @@ -157,11 +153,10 @@ Provides authentication towards Okta APIs Implemented types: [OAuthApi](./OAuthApi.md), [OpenIdConnectApi](./OpenIdConnectApi.md), [ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), -[SessionStateApi](./SessionStateApi.md) +[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[oktaAuthApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L243) +[oktaAuthApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L245) ### storage @@ -170,4 +165,4 @@ Provides the ability to store data which is unique to the user Implemented type: [StorageApi](./StorageApi.md) ApiRef: -[storageApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/StorageApi.ts#L68) +[storageApiRef](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/StorageApi.ts#L68) diff --git a/docs/reference/utility-apis/SessionApi.md b/docs/reference/utility-apis/SessionApi.md new file mode 100644 index 0000000000..5e40b3317b --- /dev/null +++ b/docs/reference/utility-apis/SessionApi.md @@ -0,0 +1,138 @@ +# SessionApi + +The SessionApi type is defined at +[packages/core-api/src/apis/definitions/auth.ts:190](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L190). + +The following Utility APIs implement this type: + +- [auth0AuthApiRef](./README.md#auth0auth) + +- [githubAuthApiRef](./README.md#githubauth) + +- [gitlabAuthApiRef](./README.md#gitlabauth) + +- [googleAuthApiRef](./README.md#googleauth) + +- [microsoftAuthApiRef](./README.md#microsoftauth) + +- [oauth2ApiRef](./README.md#oauth2) + +- [oktaAuthApiRef](./README.md#oktaauth) + +## Members + +### signIn() + +Sign in with a minimum set of permissions. + +
+signIn(): Promise<void>
+
+ +### signOut() + +Sign out from the current session. This will reload the page. + +
+signOut(): Promise<void>
+
+ +### sessionState\$() + +Observe the current state of the auth session. Emits the current state on +subscription. + +
+sessionState$(): Observable<SessionState>
+
+ +## Supporting types + +These types are part of the API declaration, but may not be unique to this API. + +### Observable + +Observable sequence of values and errors, see TC39. + +https://github.com/tc39/proposal-observable + +This is used as a common return type for observable values and can be created +using many different observable implementations, such as zen-observable or +RxJS 5. + +
+export type Observable<T> = {
+  /**
+   * Subscribes to this observable to start receiving new values.
+   */
+  subscribe(observer: Observer<T>): Subscription;
+  subscribe(
+    onNext: (value: T) => void,
+    onError?: (error: Error) => void,
+    onComplete?: () => void,
+  ): Subscription;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L53). + +Referenced by: [sessionState\$](#sessionstate). + +### Observer + +This file contains non-react related core types used throught Backstage. + +Observer interface for consuming an Observer, see TC39. + +
+export type Observer<T> = {
+  next?(value: T): void;
+  error?(error: Error): void;
+  complete?(): void;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L24). + +Referenced by: [Observable](#observable). + +### SessionState + +Session state values passed to subscribers of the SessionApi. + +
+export enum SessionState {
+  SignedIn = 'SignedIn',
+  SignedOut = 'SignedOut',
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/auth.ts:182](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/auth.ts#L182). + +Referenced by: [sessionState\$](#sessionstate). + +### Subscription + +Subscription returned when subscribing to an Observable, see TC39. + +
+export type Subscription = {
+  /**
+   * Cancels the subscription
+   */
+  unsubscribe(): void;
+
+  /**
+   * Value indicating whether the subscription is closed.
+   */
+  readonly closed: Boolean;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L33). + +Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/StorageApi.md b/docs/reference/utility-apis/StorageApi.md index bee52935da..3fd75a42d6 100644 --- a/docs/reference/utility-apis/StorageApi.md +++ b/docs/reference/utility-apis/StorageApi.md @@ -1,7 +1,7 @@ # StorageApi The StorageApi type is defined at -[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/StorageApi.ts#L31). +[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/StorageApi.ts#L31). The following Utility API implements this type: [storageApiRef](./README.md#storage) @@ -79,7 +79,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L53). Referenced by: [observe\$](#observe), [StorageApi](#storageapi). @@ -98,7 +98,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -144,7 +144,7 @@ export interface StorageApi { Defined at -[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/StorageApi.ts#L31). +[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/StorageApi.ts#L31). Referenced by: [forBucket](#forbucket). @@ -158,7 +158,7 @@ export type StorageValueChange<T = any> = { Defined at -[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/StorageApi.ts#L21). +[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/apis/definitions/StorageApi.ts#L21). Referenced by: [observe\$](#observe), [StorageApi](#storageapi). @@ -181,6 +181,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/0406ace29aba7332a98ff9ef9feedd65adc75223/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/tutorials/journey.md b/docs/tutorials/journey.md index 3c399c53ba..24765e8531 100644 --- a/docs/tutorials/journey.md +++ b/docs/tutorials/journey.md @@ -1,6 +1,7 @@ --- id: journey title: Future developer journey +description: This document describes a possible journey of a future Backstage --- > This document describes a possible journey of a **_future_** Backstage plugin diff --git a/docs/tutorials/quickstart-app-auth.md b/docs/tutorials/quickstart-app-auth.md new file mode 100644 index 0000000000..82f9cab537 --- /dev/null +++ b/docs/tutorials/quickstart-app-auth.md @@ -0,0 +1,200 @@ +--- +id: quickstart-app-auth +title: Monorepo App Setup With Authentication +--- + +###### September 15th 2020 - @backstage/create-app - v0.1.1-alpha.21 + +
+ +> This document takes you through setting up a backstage app that runs in your +> own environment. It starts with a skeleton install and verifying of the +> monorepo's functionality. Next, GitHub authentication is added and tested. +> +> This document assumes you have NodeJS 12 active along with Yarn. Please note, +> that at the time of this writing, the current version is 0.1.1-alpha.21. This +> guide can still be used with future versions, just, verify as you go. If you +> run into issues, you can compare your setup with mine here > +> [simple-backstage-app](https://github.com/johnson-jesse/simple-backstage-app). + +# The Skeleton Application + +From the terminal: + +1. Create a (monorepo) application: `npx @backstage/create-app` +1. Enter an `id` for your new app like `mybiz-backstage` I went with + `simple-backstage-app` +1. Choose `SQLite` as your database. This is the quickest way to get started as + PostgreSQL requires additional setup not covered here. +1. Start your backend: `yarn --cwd packages/backend start` + +```zsh +# You should see positive verbiage in your terminal output +2020-09-11T22:20:26.712Z backstage info Listening on :7000 +``` + +5. Finally, start the frontend. Open a new terminal window and from the root of + your project, run: `yarn start` + +```zsh +# You should see positive verbiage in your terminal output +ℹ 「wds」: Project is running at http://localhost:3000/ +``` + +Once the app compiles, a browser window should have popped with your stand-alone +application loaded at `localhost:3000`. This could take a couple minutes. + +```zsh +# You should see positive verbiage in your terminal output +ℹℹ 「wdm」: Compiled successfully. +``` + +Since there is no auth currently configured, you are automatically entered as a +guest. Let's fix that now and add auth. + +# The Auth Configuration + +1. Open `app-config.yaml` and change it as follows + +_from:_ + +```yaml +auth: + providers: {} +``` + +_to:_ + +```yaml +auth: + providers: + github: + development: + clientId: + $secret: + env: AUTH_GITHUB_CLIENT_ID + clientSecret: + $secret: + env: AUTH_GITHUB_CLIENT_SECRET + ## uncomment the following three lines if using enterprise + # enterpriseInstanceUrl: + # $secret: + # env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL +``` + +2. Set environment variables in whatever fashion is easiest for you. I chose to + add mine to my `.zshrc` profile. + +```zsh +# For macOS Catalina & Z Shell +# ------ simple-backstage-app GitHub +export AUTH_GITHUB_CLIENT_ID=xxx +export AUTH_GITHUB_CLIENT_SECRET=xxx +# export AUTH_GITHUB_ENTERPRISE_INSTANCE_URL=https://github.{MY_BIZ}.com +``` + +3. And of course I need to source that file. + +```zsh +# Loading the new variables +% source ~/.zshrc + +# Any other currently opened terminals need to be restarted to pick up the new values +# verify your setup by running env +% env +# should output something like +> ... +> AUTH_GITHUB_CLIENT_ID=xxx +> AUTH_GITHUB_CLIENT_SECRET=xxx +> ... +``` + +4. The values to replace `xxx` above come from your oauth app setup. + +``` +> Log into http://github.com +> Navigate to (Settings > Developer Settings > OAuth Apps > New OAuth App)[https://github.com/settings/applications/new] +> Set Homepage URL = http://localhost:3000 +> Set Callback URL = http://localhost:7000/auth/github +> Click [Register application] +> On the next page, copy and paste your new Client ID and Client Secret to the environment variables above, `AUTH_GITHUB_CLIENT_ID` & `AUTH_GITHUB_CLIENT_SECRET` +> Don't forget to `source` that profile file again if necessary. +``` + +5. Open and change _root > packages > app > src >_`App.tsx` as follows + +```tsx +// Add the following imports to the existing list from core +import { githubAuthApiRef, SignInPage } from '@backstage/core'; +``` + +6. In the same file, change the createApp function as follows + +```tsx +const app = createApp({ + apis, + plugins: Object.values(plugins), + components: { + SignInPage: props => { + return ( + + ); + }, + }, +}); +``` + +6. Open and change _root > packages > app > src >_ `apis.ts` as follows + +```ts +// Add the following imports to the existing list from core +import { githubAuthApiRef, GithubAuth } from '@backstage/core'; +``` + +7. In the same file, change the builder block for oauthRequestApiRef as follows + +_from:_ + +```ts +builder.add(oauthRequestApiRef, new OAuthRequestManager()); +``` + +_to:_ + +```ts +const oauthRequestApi = builder.add( + oauthRequestApiRef, + new OAuthRequestManager(), +); + +builder.add( + githubAuthApiRef, + GithubAuth.create({ + discoveryApi, + oauthRequestApi, + }), +); +``` + +8. Start the backend and frontend as before + +When the browser loads, you should be presented with a login page for GitHub. +Login as usual with your GitHub account. If this is your first time, you will be +asked to authorize and then are redirected to the catalog page if all is well. + +# Where to go from here + +> You're probably eager to write your first custom plugin. Follow this next +> tutorial for an in-depth look at a custom GitHub repository browser plugin. +> [Adding Custom Plugin to Existing Monorepo App](quickstart-app-plugin.md). diff --git a/docs/tutorials/quickstart-app-plugin.md b/docs/tutorials/quickstart-app-plugin.md new file mode 100644 index 0000000000..51a7f8ded6 --- /dev/null +++ b/docs/tutorials/quickstart-app-plugin.md @@ -0,0 +1,317 @@ +--- +id: quickstart-app-plugin +title: Adding Custom Plugin to Existing Monorepo App +--- + +###### September 15th 2020 - v0.1.1-alpha.21 + +
+ +> This document takes you through setting up a new plugin for your existing +> monorepo with a _GitHub provider already setup_. If you don't have either of +> those, you can clone +> [simple-backstage-app](https://github.com/johnson-jesse/simple-backstage-app) +> which this document builds on. +> +> This document does not cover authoring a plugin for sharing with the Backstage +> community. That will have to be a later discussion. +> +> We start with a skeleton plugin install. And after verifying its +> functionality, extend the Sidebar to make our life easy. Finally, we add +> custom code to display GitHub repository information. +> +> This document assumes you have NodeJS 12 active along with Yarn. Please note, +> that at the time of this writing, the current version is 0.1.1-alpha.21. This +> guide can still be used with future versions, just, verify as you go. If you +> run into issues, you can compare your setup with mine here > +> [simple-backstage-app-plugin](https://github.com/johnson-jesse/simple-backstage-app-plugin). + +# The Skeleton Plugin + +1. Start by using the built in creator. From the terminal and root of your + project run: `yarn create-plugin` +1. Enter a plugin ID. I used `github-playground` +1. When the process finishes, let's start the backend: + `yarn --cwd packages/backend start` +1. If you see errors starting, refer to + [Auth Configuration](https://backstage.io/docs/tutorials/quickstart-app-auth#the-auth-configuration) + for more information on environment variables. +1. And now the frontend, from a new terminal window and the root of your + project: `yarn start` +1. As usual, a browser window should popup loading the App. +1. Now manually navigate to our plugin page from your browser: + `http://localhost:3000/github-playground` +1. You should see successful verbiage for this endpoint, + `Welcome to github-playground!` + +# The Shortcut + +Let's add a shortcut. + +1. Open and modify `root: packages > app > src > sidebar.tsx` with the + following: + +```tsx +import GitHubIcon from '@material-ui/icons/GitHub'; +... + +``` + +Simple! The App will reload with your changes automatically. You should now see +a github icon displayed in the sidebar. Clicking that will link to our new +plugin. And now, the API fun begins. + +# The Identity + +Our first modification will be to extract information from the Identity API. + +1. Start by opening + `root: plugins > github-playground > src > components > ExampleComponent > ExampleComponent.tsx` +1. Add two new imports + +```tsx +// Add identityApiRef to the list of imported from core +import { identityApiRef } from '@backstage/core'; +import { useApi } from '@backstage/core-api'; +``` + +3. Adjust the ExampleComponent from inline to block + +_from inline:_ + +```tsx +const ExampleComponent: FC<{}> = () => ( ... ) +``` + +_to block:_ + +```tsx +const ExampleComponent: FC<{}> = () => { + + return ( + ... + ) +} +``` + +4. Now add our hook and const data before the return statement + +```tsx +// our API hook +const identityApi = useApi(identityApiRef); + +// data to use +const userId = identityApi.getUserId(); +const profile = identityApi.getProfile(); +``` + +5. Finally, update the InfoCard's jsx to use our new data + +```tsx + + + {`${profile.displayName} | ${profile.email}`} + + +``` + +If everything is saved, you should see your name, id, and email on the +github-playground page. Our data accessed is synchronous. So we just grab and +go. + +https://github.com/spotify/backstage/tree/master/contrib + +6. Here is the entire file for reference + [ExampleComponent.tsx](https://github.com/spotify/backstage/tree/master/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md) + +# The Wipe + +The last file we will touch is ExampleFetchComponent. Because of the number of +changes, let's start by wiping this component clean. + +1. Start by opening + `root: plugins > github-playground > src > components > ExampleFetchComponent > ExampleFetchComponent.tsx` +1. Replace everything in the file with the following: + +```tsx +import React, { FC } from 'react'; +import { useAsync } from 'react-use'; +import Alert from '@material-ui/lab/Alert'; +import { + Table, + TableColumn, + Progress, + githubAuthApiRef, +} from '@backstage/core'; +import { useApi } from '@backstage/core-api'; +import { graphql } from '@octokit/graphql'; + +const ExampleFetchComponent: FC<{}> = () => { + return
Nothing to see yet
; +}; + +export default ExampleFetchComponent; +``` + +3. Save that and ensure you see no errors. Comment out the unused imports if + your linter gets in the way. + +###### We will add a lot to this file for the sake of ease. Please don't do this in productional code! + +# The Graph Model + +GitHub has a graphql API available for interacting. Let's start by adding our +basic repository query + +1. Add the query const statement outside ExampleFetchComponent + +```tsx +const query = `{ + viewer { + repositories(first: 100) { + totalCount + nodes { + name + createdAt + description + diskUsage + isFork + } + pageInfo { + endCursor + hasNextPage + } + } + } +}`; +``` + +2. Using this structure as a guide, we will break our query into type parts +3. Add the following outside of ExampleFetchComponent + +```tsx +type Node = { + name: string; + createdAt: string; + description: string; + diskUsage: number; + isFork: boolean; +}; + +type Viewer = { + repositories: { + totalCount: number; + nodes: Node[]; + pageInfo: { + endCursor: string; + hasNextPage: boolean; + }; + }; +}; +``` + +# The Tabel Model + +Using Backstage's own component library, let's define a custom table. This +component will get used if we have data to display. + +1. Add the following outside of ExampleFetchComponent + +```tsx +type DenseTableProps = { + viewer: Viewer; +}; + +export const DenseTable: FC = ({ viewer }) => { + const columns: TableColumn[] = [ + { title: 'Name', field: 'name' }, + { title: 'Created', field: 'createdAt' }, + { title: 'Description', field: 'description' }, + { title: 'Disk Usage', field: 'diskUsage' }, + { title: 'Fork', field: 'isFork' }, + ]; + + return ( +
+ ); +}; +``` + +# The Fetch + +We're ready to flush out our fetch component + +1. Add our api hook inside ExampleFetchComponent + +```tsx +const auth = useApi(githubAuthApiRef); +``` + +2. The access token we need to make our GitHub request and the request itself is + obtained in an asynchronous manner. +3. Add the useAsync block inside the ExampleFetchComponent + +```tsx +const { value, loading, error } = useAsync(async (): Promise => { + const token = await auth.getAccessToken(); + + const gqlEndpoint = graphql.defaults({ + // Uncomment baseUrl if using enterprise + // baseUrl: 'https://github.MY-BIZ.com/api', + headers: { + authorization: `token ${token}`, + }, + }); + const { viewer } = await gqlEndpoint(query); + return viewer; +}, []); +``` + +4. The resolved data is conveniently destructured with `value` containing our + Viewer type. `loading` as a boolean, self explanatory. And `error` which is + present only if necessary. So let's use those as the first 3 of 4 multi + return statements. +5. Add the _if return_ blocks below our async block + +```tsx +if (loading) return ; +if (error) return {error.message}; +if (value && value.repositories) return ; +``` + +6. The third line here utilizes our custom table accepting our Viewer type. +7. Finally, we add our _else return_ block to catch any other scenarios. + +```tsx +return ( +
+); +``` + +8. After saving that, and given we don't have any errors, you should see a table + with basic information on your repositories. +9. Here is the entire file for reference + [ExampleFetchComponent.tsx](https://github.com/spotify/backstage/tree/master/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md) +10. We finished! You should see your own GitHub repository's information + displayed in a basic table. If you run into issues, you can compare the repo + that backs this document, + [simple-backstage-app-plugin](https://github.com/johnson-jesse/simple-backstage-app-plugin) + +# Where to go from here + +> Break apart ExampleFetchComponent into smaller logical parts contained in +> their own files. Rename your components to something other than ExampleXxx. +> +> You might be real proud of a plugin you develop. Follow this next tutorial for +> an in-depth look at publishing and including that for the entire Backstage +> community. [TODO](#). diff --git a/lerna.json b/lerna.json index 63e4701ce3..d43d4ff160 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "packages": ["packages/*", "plugins/*"], "npmClient": "yarn", "useWorkspaces": true, - "version": "0.1.1-alpha.20" + "version": "0.1.1-alpha.23" } diff --git a/microsite/blog/2020-08-05-announcing-backstage-software-templates.md b/microsite/blog/2020-08-05-announcing-backstage-software-templates.md index 51c1da2885..81d495e1c3 100644 --- a/microsite/blog/2020-08-05-announcing-backstage-software-templates.md +++ b/microsite/blog/2020-08-05-announcing-backstage-software-templates.md @@ -39,7 +39,7 @@ You can customize Backstage Software Templates to fit your organization’s stan ## Getting started -The sample Software Templates are available under `/create`. If you're setting up Backstage for the first time, follow [Getting Started with Backstage](https://backstage.io/docs/getting-started/) and go to `http://localhost:3000/create`. If you’ve already been running Backstage locally, run the command `yarn lerna run mock-data` to load the new sample templates into the Service Catalog first. +The sample Software Templates are available under `/create`. If you're setting up Backstage for the first time, follow [Getting Started with Backstage](https://backstage.io/docs/getting-started/) and go to `http://localhost:3000/create`. ![available-templates](assets/2020-08-05/templates.png) diff --git a/microsite/blog/2020-09-23-backstage-cncf-sandbox.md b/microsite/blog/2020-09-23-backstage-cncf-sandbox.md new file mode 100644 index 0000000000..bcfd7c6245 --- /dev/null +++ b/microsite/blog/2020-09-23-backstage-cncf-sandbox.md @@ -0,0 +1,21 @@ +--- +title: Backstage has been accepted into the CNCF Sandbox +author: Stefan Ålund +authorURL: https://twitter.com/stalund +--- + +**TL;DR** The Cloud Native Computing Foundation (CNCF) announced that Backstage can begin incubating as an early stage project in the [CNCF Sandbox](https://www.cncf.io/sandbox-projects/). Released open source in March, the platform is built around an advanced service catalog and is designed to streamline software development from end to end. + +![cncf](assets/cncf-sandbox/cncf.png) + + + +Backstage garnered quite a bit of interest from developers and organizations when it was first announced, and community interest continues to grow as plugins and new features are added with the open source community. We released the open source version of Backstage ‘early’. That was intentional. Because even though we’ve been using Backstage internally for years, we wanted the open source version to be developed with input and contributions from the community. And that’s exactly the product that’s going into the [CNCF Sandbox](https://www.cncf.io/sandbox-projects/) today. + +Backstage’s ability to simplify tooling and standardize engineering practices has attracted interest from other major tech companies, as well as airlines, auto manufacturers, investment firms, and global retailers. We know that Backstage solves a problem — infrastructure complexity — that’s common to a lot of large and growing companies today. But different companies work differently, use particular toolsets, and have unique use cases. By making Backstage open source, we can build it with people working inside a variety of engineering organizations all over the world. It makes for a better product that serves a wider group of users (beyond that of Spotify’s) and their needs. + +The Backstage community is healthy and growing quickly. Over [130 people](https://github.com/spotify/backstage/graphs/contributors) have contributed to the project, and roughly 40% of pull requests are now coming from external, non-Spotify, contributors. With companies now deciding to [adopt Backstage](https://github.com/spotify/backstage/blob/master/ADOPTERS.md) we are also seeing a shift in the kinds of contributions we are getting from the community. It is truly amazing to see contributions to core parts of the platform as well as significant functionality additions through working [plugins](https://backstage.io/plugins). + +We’re excited to embark on this journey with the CNCF community. There’s so much great tech being built here, and it’s about time we share it to build even greater products, together. Entering into the CNCF Sandbox is just the first step. We are committed to working with the community to bring Backstage through the Incubation step, and finally all the way to becoming a Graduated, top-level project. + +Thanks to everyone for your support so far. We hope you [join us](https://mailchi.mp/spotify/backstage-community) in this next chapter of Backstage's journey. If you have questions or feedback, feel free to [email](mailto:alund@spotify.com) me directly. diff --git a/microsite/blog/assets/cncf-sandbox/cncf.png b/microsite/blog/assets/cncf-sandbox/cncf.png new file mode 100644 index 0000000000..624094b016 Binary files /dev/null and b/microsite/blog/assets/cncf-sandbox/cncf.png differ diff --git a/microsite/package.json b/microsite/package.json index 25598c18b4..734f1e4d49 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -13,7 +13,7 @@ "rename-version": "docusaurus-rename-version" }, "devDependencies": { - "docusaurus": "^2.0.0-alpha.61", + "docusaurus": "^2.0.0-alpha.64", "js-yaml": "^3.14.0" } } diff --git a/microsite/pages/en/index.js b/microsite/pages/en/index.js index f595032bd0..6e649aa574 100644 --- a/microsite/pages/en/index.js +++ b/microsite/pages/en/index.js @@ -462,6 +462,19 @@ class Index extends React.Component { Contribute + + + + + Backstage is a{' '} + + Cloud Native Computing Foundation + {' '} + sandbox project +
+ + + ); } diff --git a/microsite/sidebars.json b/microsite/sidebars.json index a3c11beeac..8fa4935a38 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -12,6 +12,7 @@ ], "Getting Started": [ "getting-started/index", + "getting-started/running-backstage-locally", "getting-started/installation", "getting-started/development-environment", "getting-started/create-an-app", @@ -39,8 +40,10 @@ "ids": [ "features/software-catalog/software-catalog-overview", "features/software-catalog/installation", + "features/software-catalog/configuration", "features/software-catalog/system-model", "features/software-catalog/descriptor-format", + "features/software-catalog/well-known-annotations", "features/software-catalog/extending-the-model", "features/software-catalog/external-integrations", "features/software-catalog/software-catalog-api" @@ -61,11 +64,12 @@ }, { "type": "subcategory", - "label": "Docs-like-code", + "label": "TechDocs", "ids": [ "features/techdocs/techdocs-overview", "features/techdocs/getting-started", "features/techdocs/concepts", + "features/techdocs/architecture", "features/techdocs/creating-and-publishing", "features/techdocs/faqs" ] @@ -140,7 +144,11 @@ "ids": ["api/backend"] } ], - "Tutorials": ["tutorials/journey"], + "Tutorials": [ + "tutorials/journey", + "tutorials/quickstart-app-auth", + "tutorials/quickstart-app-plugin" + ], "Architecture Decision Records (ADRs)": [ "architecture-decisions/adrs-overview", "architecture-decisions/adrs-adr001", diff --git a/microsite/siteConfig.js b/microsite/siteConfig.js index cc5ad89e09..d7a3bd67d0 100644 --- a/microsite/siteConfig.js +++ b/microsite/siteConfig.js @@ -67,8 +67,9 @@ const siteConfig = { primaryColor: '#36BAA2', secondaryColor: '#121212', textColor: '#FFFFFF', - navigatorTitleTextColor: '#9e9e9e', - navigatorItemTextColor: '#616161', + navigatorTitleTextColor: '#e4e4e4', + navigatorItemTextColor: '#9e9e9e', + navGroupSubcategoryTitleColor: '#9e9e9e', }, /* Colors for syntax highlighting */ @@ -93,8 +94,10 @@ const siteConfig = { cleanUrl: true, // Open Graph and Twitter card images. - ogImage: 'img/logo-gradient-on-dark.svg', - twitterImage: 'img/logo-gradient-on-dark.svg', + ogImage: + 'logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png', + twitterImage: + 'logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png', // For sites with a sizable amount of content, set collapsible to true. // Expand/collapse the links and subcategories under categories. diff --git a/microsite/static/css/custom.css b/microsite/static/css/custom.css index 5e078582cc..79b8fd9de4 100644 --- a/microsite/static/css/custom.css +++ b/microsite/static/css/custom.css @@ -113,6 +113,10 @@ td { color: $navigatorTitleTextColor; } +.toc .toggleNav .navGroup .navGroupSubcategoryTitle { + color: $navGroupSubcategoryTitleColor; +} + .toc .toggleNav ul li a, .onPageNav a { color: $navigatorItemTextColor; @@ -203,6 +207,54 @@ td { border-radius: 0.25rem; } +/* + * Fix for viewing Table of Contents bar on documentation + * and blog pages on smaller screens. + */ +@media only screen and (max-width: 1023px) { + /* Nav bar hides the docs toc bar */ + .docMainWrapper { + margin-top: 4rem; + } + + /* Toc bar does not have to be fixed */ + .docsNavContainer { + position: unset; + width: 95vw; + z-index: 100; + margin-left: -2vw; + } + + /* Toc bar does not have to be fixed when slider is active */ + .docsSliderActive .toc .navBreadcrumb, + .tocActive .navBreadcrumb { + position: unset; + } + + /* Fix unexpected width increase when toc is toggled */ + .docsSliderActive .toc .navBreadcrumb, + .tocActive .navBreadcrumb { + width: inherit; + } + + /* This pseudo-element stops toc toggle button to be clicked */ + header.postHeader::before { + height: 2em !important; + margin-top: -2em !important; + } + + /* This pseudo-element stops toc toggle button to be clicked */ + #__docusaurus.postHeaderTitle::before { + height: 1em; + margin-top: -1em; + } + + /* Useless button causing trouble */ + .tocToggler { + display: none; + } +} + /* content */ .postContainer blockquote { color: $textColor; @@ -288,17 +340,17 @@ code { background: linear-gradient(70.44deg, #121212 75%, #5d817b 100%); } .bg-teal-top-right { - background: + background: /* linear-gradient( 178.64deg, rgba(255, 255, 255, 0) 57.61%, rgba(255, 255, 255, 0.17) 127.71% - ), */ + ), */ /* linear-gradient( 144.35deg, rgba(98, 197, 179, 0) 56.68%, rgba(98, 197, 179, 0.59) 109.25% - ), */ + ), */ /* linear-gradient( 192.29deg, rgba(155, 240, 225, 0) 54.17%, @@ -1018,3 +1070,16 @@ code { margin: auto; } } + +.cncf-block { + padding-top: 40px; + text-align: center; +} + +.cncf-logo { + background: center no-repeat url(../img/cncf-white.svg); + width: 100%; + height: 100px; + margin-bottom: 40px; + margin-top: 20px; +} diff --git a/microsite/static/img/cncf-color.svg b/microsite/static/img/cncf-color.svg new file mode 100644 index 0000000000..12f7d3e48a --- /dev/null +++ b/microsite/static/img/cncf-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/microsite/static/img/cncf-white.svg b/microsite/static/img/cncf-white.svg new file mode 100644 index 0000000000..d94aaf3249 --- /dev/null +++ b/microsite/static/img/cncf-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/microsite/static/logo_assets/ai/Backstage Identity_Assets_Artwork_RGB.ai b/microsite/static/logo_assets/ai/Backstage_Identity_Assets_Artwork_RGB.ai similarity index 100% rename from microsite/static/logo_assets/ai/Backstage Identity_Assets_Artwork_RGB.ai rename to microsite/static/logo_assets/ai/Backstage_Identity_Assets_Artwork_RGB.ai diff --git a/microsite/static/logo_assets/jpeg/Backstage Identity_Assets_Artwork_RGB_05 Logo_Black.jpg b/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_05 Logo_Black.jpg similarity index 100% rename from microsite/static/logo_assets/jpeg/Backstage Identity_Assets_Artwork_RGB_05 Logo_Black.jpg rename to microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_05 Logo_Black.jpg diff --git a/microsite/static/logo_assets/jpeg/Backstage Identity_Assets_Artwork_RGB_06 Icon_Black.jpg b/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.jpg similarity index 100% rename from microsite/static/logo_assets/jpeg/Backstage Identity_Assets_Artwork_RGB_06 Icon_Black.jpg rename to microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.jpg diff --git a/microsite/static/logo_assets/pdf/Backstage Identity_Assets_Artwork_RGB.pdf b/microsite/static/logo_assets/pdf/Backstage_Identity_Assets_Artwork_RGB.pdf similarity index 100% rename from microsite/static/logo_assets/pdf/Backstage Identity_Assets_Artwork_RGB.pdf rename to microsite/static/logo_assets/pdf/Backstage_Identity_Assets_Artwork_RGB.pdf diff --git a/microsite/static/logo_assets/pdf/Individual/01 Logo_White.pdf b/microsite/static/logo_assets/pdf/Individual/01_Logo_White.pdf similarity index 100% rename from microsite/static/logo_assets/pdf/Individual/01 Logo_White.pdf rename to microsite/static/logo_assets/pdf/Individual/01_Logo_White.pdf diff --git a/microsite/static/logo_assets/pdf/Individual/02 Icon_White.pdf b/microsite/static/logo_assets/pdf/Individual/02_Icon_White.pdf similarity index 100% rename from microsite/static/logo_assets/pdf/Individual/02 Icon_White.pdf rename to microsite/static/logo_assets/pdf/Individual/02_Icon_White.pdf diff --git a/microsite/static/logo_assets/pdf/Individual/03 Logo_Teal.pdf b/microsite/static/logo_assets/pdf/Individual/03_Logo_Teal.pdf similarity index 100% rename from microsite/static/logo_assets/pdf/Individual/03 Logo_Teal.pdf rename to microsite/static/logo_assets/pdf/Individual/03_Logo_Teal.pdf diff --git a/microsite/static/logo_assets/pdf/Individual/04 Icon_Teal.pdf b/microsite/static/logo_assets/pdf/Individual/04_Icon_Teal.pdf similarity index 100% rename from microsite/static/logo_assets/pdf/Individual/04 Icon_Teal.pdf rename to microsite/static/logo_assets/pdf/Individual/04_Icon_Teal.pdf diff --git a/microsite/static/logo_assets/pdf/Individual/05 Logo_Black.pdf b/microsite/static/logo_assets/pdf/Individual/05_Logo_Black.pdf similarity index 100% rename from microsite/static/logo_assets/pdf/Individual/05 Logo_Black.pdf rename to microsite/static/logo_assets/pdf/Individual/05_Logo_Black.pdf diff --git a/microsite/static/logo_assets/pdf/Individual/06 Icon_Black.pdf b/microsite/static/logo_assets/pdf/Individual/06_Icon_Black.pdf similarity index 100% rename from microsite/static/logo_assets/pdf/Individual/06 Icon_Black.pdf rename to microsite/static/logo_assets/pdf/Individual/06_Icon_Black.pdf diff --git a/microsite/static/logo_assets/pdf/Individual/07 Large Icon_Gradient.pdf b/microsite/static/logo_assets/pdf/Individual/07_Large Icon_Gradient.pdf similarity index 100% rename from microsite/static/logo_assets/pdf/Individual/07 Large Icon_Gradient.pdf rename to microsite/static/logo_assets/pdf/Individual/07_Large Icon_Gradient.pdf diff --git a/microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_01 Logo_White.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_01_Logo_White.png similarity index 100% rename from microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_01 Logo_White.png rename to microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_01_Logo_White.png diff --git a/microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_02 Icon_White.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_02_Icon_White.png similarity index 100% rename from microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_02 Icon_White.png rename to microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_02_Icon_White.png diff --git a/microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_03 Logo_Teal.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_03_Logo_Teal.png similarity index 100% rename from microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_03 Logo_Teal.png rename to microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_03_Logo_Teal.png diff --git a/microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_04 Icon_Teal.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png similarity index 100% rename from microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_04 Icon_Teal.png rename to microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png diff --git a/microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_05 Logo_Black.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_05_Logo_Black.png similarity index 100% rename from microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_05 Logo_Black.png rename to microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_05_Logo_Black.png diff --git a/microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_06 Icon_Black.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.png similarity index 100% rename from microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_06 Icon_Black.png rename to microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.png diff --git a/microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_07 Large Icon_Gradient.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_07_Large_Icon_Gradient.png similarity index 100% rename from microsite/static/logo_assets/png/Backstage Identity_Assets_Artwork_RGB_07 Large Icon_Gradient.png rename to microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_07_Large_Icon_Gradient.png diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 422959d3fd..84b5d50d60 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -2228,10 +2228,10 @@ dir-glob@2.0.0: arrify "^1.0.1" path-type "^3.0.0" -docusaurus@^2.0.0-alpha.61: - version "2.0.0-alpha.63" - resolved "https://registry.npmjs.org/docusaurus/-/docusaurus-2.0.0-alpha.63.tgz#40402d47b18c42b62e93beb78ce06e000cb88312" - integrity sha512-R19pAqcTemJMt7Qykd7ogB2i6R86vbE4/vA/l5/Uuh7xg7ixSKOVZ5M7d9uSoX5jCALOwEyp5iJdleznXUVwAw== +docusaurus@^2.0.0-alpha.64: + version "2.0.0-alpha.64" + resolved "https://registry.yarnpkg.com/docusaurus/-/docusaurus-2.0.0-alpha.64.tgz#7833960e9d338403894a27b79058aa4076a676d3" + integrity sha512-ARCx0GwAvc5qx7AHvRVZidZuoDTfaaGXzgmkU23NahU6jzO/aK2Q1bH8IKNEQ5C2JuDerQ/hHDh80N20ijk82g== dependencies: "@babel/core" "^7.9.0" "@babel/plugin-proposal-class-properties" "^7.8.3" diff --git a/mkdocs.yml b/mkdocs.yml index bcf4969892..ba607f8a6e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,7 +12,8 @@ nav: - Strategies for adopting: 'overview/adopting.md' - Logo assets: 'overview/logos.md' - Getting started: - - Running Backstage locally: 'getting-started/index.md' + - Getting Started: 'getting-started/index.md' + - Running Backstage locally: 'getting-started/running-backstage-locally.md' - Installation: 'getting-started/installation.md' - Local development: 'getting-started/development-environment.md' - Demo deployment: https://backstage-demo.roadie.io @@ -27,9 +28,11 @@ nav: - Features: - Software Catalog: - Overview: 'features/software-catalog/index.md' + - Installation: 'features/software-catalog/installation.md' + - Configuration: 'features/software-catalog/configuration.md' - System model: 'features/software-catalog/system-model.md' - YAML File Format: 'features/software-catalog/descriptor-format.md' - - Configuration: 'features/software-catalog/configuration.md' + - Well-known Annotations: 'features/software-catalog/well-known-annotations.md' - Extending the model: 'features/software-catalog/extending-the-model.md' - External integrations: 'features/software-catalog/external-integrations.md' - API: 'features/software-catalog/api.md' @@ -42,10 +45,11 @@ nav: - Create your own Templater: 'features/software-templates/extending/create-your-own-templater.md' - Create your own Publisher: 'features/software-templates/extending/create-your-own-publisher.md' - Create your own Preparer: 'features/software-templates/extending/create-your-own-preparer.md' - - Docs-like-code: + - TechDocs: - Overview: 'features/techdocs/README.md' - Getting Started: 'features/techdocs/getting-started.md' - Concepts: 'features/techdocs/concepts.md' + - TechDocs Architecture: 'features/techdocs/architecture.md' - Creating and Publishing Documentation: 'features/techdocs/creating-and-publishing.md' - FAQ: 'features/techdocs/FAQ.md' - Plugins: diff --git a/package.json b/package.json index c64b7f6951..3a77fe6880 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "docgen": "lerna run docgen", "docker-build:app": "yarn workspace example-app build && docker build . -t spotify/backstage", "docker-build": "yarn tsc && yarn workspace example-backend build-image", - "create-plugin": "backstage-cli create-plugin", + "create-plugin": "backstage-cli create-plugin --scope backstage --no-private", "remove-plugin": "backstage-cli remove-plugin", "release": "if [ \"$(git symbolic-ref --short HEAD)\" = master ]; then echo \"don't try to release master\"; exit 1; else lerna version --no-push --force-publish; fi", "prettier:check": "prettier --check .", @@ -37,6 +37,7 @@ }, "version": "1.0.0", "devDependencies": { + "@changesets/cli": "2.10.2", "@spotify/eslint-config-oss": "^1.0.1", "@spotify/prettier-config": "^8.0.0", "concurrently": "^5.2.0", diff --git a/packages/app/package.json b/packages/app/package.json index bf0f340a40..77b1882b47 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,32 +1,33 @@ { "name": "example-app", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": true, "bundled": true, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.21", - "@backstage/catalog-model": "^0.1.1-alpha.21", - "@backstage/core": "^0.1.1-alpha.21", - "@backstage/plugin-api-docs": "^0.1.1-alpha.21", - "@backstage/plugin-catalog": "^0.1.1-alpha.21", - "@backstage/plugin-circleci": "^0.1.1-alpha.21", - "@backstage/plugin-explore": "^0.1.1-alpha.21", - "@backstage/plugin-gcp-projects": "^0.1.1-alpha.21", - "@backstage/plugin-github-actions": "^0.1.1-alpha.21", - "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.21", - "@backstage/plugin-graphiql": "^0.1.1-alpha.21", - "@backstage/plugin-jenkins": "^0.1.1-alpha.21", - "@backstage/plugin-lighthouse": "^0.1.1-alpha.21", - "@backstage/plugin-newrelic": "^0.1.1-alpha.21", - "@backstage/plugin-register-component": "^0.1.1-alpha.21", - "@backstage/plugin-rollbar": "^0.1.1-alpha.21", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.21", - "@backstage/plugin-sentry": "^0.1.1-alpha.21", - "@backstage/plugin-tech-radar": "^0.1.1-alpha.21", - "@backstage/plugin-techdocs": "^0.1.1-alpha.21", - "@backstage/plugin-welcome": "^0.1.1-alpha.21", - "@backstage/test-utils": "^0.1.1-alpha.21", - "@backstage/theme": "^0.1.1-alpha.21", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/plugin-api-docs": "^0.1.1-alpha.23", + "@backstage/plugin-catalog": "^0.1.1-alpha.23", + "@backstage/plugin-circleci": "^0.1.1-alpha.23", + "@backstage/plugin-explore": "^0.1.1-alpha.23", + "@backstage/plugin-gcp-projects": "^0.1.1-alpha.23", + "@backstage/plugin-github-actions": "^0.1.1-alpha.23", + "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.23", + "@backstage/plugin-graphiql": "^0.1.1-alpha.23", + "@backstage/plugin-jenkins": "^0.1.1-alpha.23", + "@backstage/plugin-kubernetes": "^0.1.1-alpha.23", + "@backstage/plugin-lighthouse": "^0.1.1-alpha.23", + "@backstage/plugin-newrelic": "^0.1.1-alpha.23", + "@backstage/plugin-register-component": "^0.1.1-alpha.23", + "@backstage/plugin-rollbar": "^0.1.1-alpha.23", + "@backstage/plugin-scaffolder": "^0.1.1-alpha.23", + "@backstage/plugin-sentry": "^0.1.1-alpha.23", + "@backstage/plugin-tech-radar": "^0.1.1-alpha.23", + "@backstage/plugin-techdocs": "^0.1.1-alpha.23", + "@backstage/plugin-welcome": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@octokit/rest": "^18.0.0", diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index e91504d619..03e24cf4ae 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -35,8 +35,6 @@ import { SidebarSearchField, SidebarSpace, SidebarUserSettings, - SidebarThemeToggle, - SidebarPinButton, DefaultProviderSettings, } from '@backstage/core'; import { NavLink } from 'react-router-dom'; @@ -103,9 +101,7 @@ const Root: FC<{}> = ({ children }) => ( /> - } /> - {children} diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 257fde3c24..d96fe9841f 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -17,6 +17,11 @@ import { Router as GitHubActionsRouter, isPluginApplicableToEntity as isGitHubActionsAvailable, } from '@backstage/plugin-github-actions'; +import { + Router as JenkinsRouter, + isPluginApplicableToEntity as isJenkinsAvailable, + LatestRunCard as JenkinsLatestRunCard, +} from '@backstage/plugin-jenkins'; import { Router as CircleCIRouter, isPluginApplicableToEntity as isCircleCIAvailable, @@ -24,6 +29,7 @@ import { import { Router as ApiDocsRouter } from '@backstage/plugin-api-docs'; import { Router as SentryRouter } from '@backstage/plugin-sentry'; import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs'; +import { Router as KubernetesRouter } from '@backstage/plugin-kubernetes'; import React from 'react'; import { AboutCard, @@ -38,6 +44,8 @@ const CICDSwitcher = ({ entity }: { entity: Entity }) => { // This component is just an example of how you can implement your company's logic in entity page. // You can for example enforce that all components of type 'service' should use GitHubActions switch (true) { + case isJenkinsAvailable(entity): + return ; case isGitHubActionsAvailable(entity): return ; case isCircleCIAvailable(entity): @@ -57,6 +65,11 @@ const OverviewContent = ({ entity }: { entity: Entity }) => ( + {isJenkinsAvailable(entity) && ( + + + + )} ); @@ -87,6 +100,11 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( title="Docs" element={} /> + } + /> ); @@ -112,6 +130,11 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => ( title="Docs" element={} /> + } + /> ); const DefaultEntityPage = ({ entity }: { entity: Entity }) => ( diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index b36d0323e0..aa68bc5fad 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -33,3 +33,4 @@ export { plugin as Jenkins } from '@backstage/plugin-jenkins'; export { plugin as ApiDocs } from '@backstage/plugin-api-docs'; export { plugin as GithubPullRequests } from '@roadiehq/backstage-plugin-github-pull-requests'; export { plugin as GcpProjects } from '@backstage/plugin-gcp-projects'; +export { plugin as Kubernetes } from '@backstage/plugin-kubernetes'; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index c9103375eb..72f047761d 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli-common": "^0.1.1-alpha.21", - "@backstage/config": "^0.1.1-alpha.21", - "@backstage/config-loader": "^0.1.1-alpha.21", + "@backstage/cli-common": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", + "@backstage/config-loader": "^0.1.1-alpha.23", "@types/cors": "^2.8.6", "@types/express": "^4.17.6", "compression": "^1.7.4", @@ -42,12 +42,12 @@ "helmet": "^4.0.0", "knex": "^0.21.1", "lodash": "^4.17.15", + "logform": "^2.1.1", "morgan": "^1.10.0", "prom-client": "^12.0.0", "selfsigned": "^1.10.7", "stoppable": "^1.1.0", - "winston": "^3.2.1", - "logform": "^2.1.1" + "winston": "^3.2.1" }, "peerDependencies": { "pg-connection-string": "^2.3.0" @@ -58,7 +58,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/cli": "^0.1.1-alpha.23", "@types/compression": "^1.7.0", "@types/http-errors": "^1.6.3", "@types/morgan": "^1.9.0", diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 6f7500bb86..247eb593cc 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -23,7 +23,7 @@ import { loadConfig } from '@backstage/config-loader'; export async function loadBackendConfig() { const paths = findPaths(__dirname); const configs = await loadConfig({ - env: process.env.NODE_ENV, + env: process.env.NODE_ENV ?? 'development', rootPaths: [paths.targetRoot, paths.targetDir], shouldReadSecrets: true, }); diff --git a/packages/backend/README.md b/packages/backend/README.md index 860520f4b5..c45a0d28d9 100644 --- a/packages/backend/README.md +++ b/packages/backend/README.md @@ -43,21 +43,13 @@ The backend starts up on port 7000 per default. ## Populating The Catalog -If you want to use the catalog functionality, you need to add so called locations -to the backend. These are places where the backend can find some entity descriptor -data to consume and serve. +If you want to use the catalog functionality, you need to add so called +locations to the backend. These are places where the backend can find some +entity descriptor data to consume and serve. For more information, see +[Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview#adding-components-to-the-catalog). -To get started, you can issue the following after starting the backend, from inside -the `plugins/catalog-backend` directory: - -```bash -yarn mock-data -``` - -You should then start seeing data on `localhost:7000/catalog/entities`. - -The catalog currently runs in-memory only, so feel free to try it out, but it will -need to be re-populated on next startup. +For convenience we already include some statically configured example locations +in `app-config.yaml` under `catalog.locations`. For local development you can override these in your own `app-config.local.yaml`. ## Authentication diff --git a/packages/backend/package.json b/packages/backend/package.json index d944ac6535..e1b8665b57 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "main": "dist/index.cjs.js", "types": "src/index.ts", "private": true, @@ -18,22 +18,24 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.21", - "@backstage/catalog-model": "^0.1.1-alpha.21", - "@backstage/config": "^0.1.1-alpha.21", - "@backstage/plugin-app-backend": "^0.1.1-alpha.21", - "@backstage/plugin-auth-backend": "^0.1.1-alpha.21", - "@backstage/plugin-catalog-backend": "^0.1.1-alpha.21", - "@backstage/plugin-graphql-backend": "^0.1.1-alpha.21", - "@backstage/plugin-identity-backend": "^0.1.1-alpha.21", - "@backstage/plugin-proxy-backend": "^0.1.1-alpha.21", - "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.21", - "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.21", - "@backstage/plugin-sentry-backend": "^0.1.1-alpha.21", - "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.21", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", + "@backstage/plugin-app-backend": "^0.1.1-alpha.23", + "@backstage/plugin-auth-backend": "^0.1.1-alpha.23", + "@backstage/plugin-catalog-backend": "^0.1.1-alpha.23", + "@backstage/plugin-graphql-backend": "^0.1.1-alpha.23", + "@backstage/plugin-identity-backend": "^0.1.1-alpha.23", + "@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.23", + "@backstage/plugin-proxy-backend": "^0.1.1-alpha.23", + "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.23", + "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.23", + "@backstage/plugin-sentry-backend": "^0.1.1-alpha.23", + "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.23", + "@gitbeaker/node": "^23.5.0", "@octokit/rest": "^18.0.0", "dockerode": "^3.2.0", - "example-app": "^0.1.1-alpha.21", + "example-app": "^0.1.1-alpha.23", "express": "^4.17.1", "knex": "^0.21.1", "pg": "^8.3.0", @@ -42,7 +44,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/cli": "^0.1.1-alpha.23", "@types/dockerode": "^2.5.32", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 0423047cf7..7c52175d76 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -34,6 +34,7 @@ import healthcheck from './plugins/healthcheck'; import auth from './plugins/auth'; import catalog from './plugins/catalog'; import identity from './plugins/identity'; +import kubernetes from './plugins/kubernetes'; import rollbar from './plugins/rollbar'; import scaffolder from './plugins/scaffolder'; import sentry from './plugins/sentry'; @@ -64,7 +65,6 @@ async function main() { const configs = await loadBackendConfig(); const configReader = ConfigReader.fromConfigs(configs); const createEnv = makeCreateEnv(configs); - const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck')); const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); @@ -74,6 +74,7 @@ async function main() { const rollbarEnv = useHotMemoize(module, () => createEnv('rollbar')); const sentryEnv = useHotMemoize(module, () => createEnv('sentry')); const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); + const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes')); const graphqlEnv = useHotMemoize(module, () => createEnv('graphql')); const appEnv = useHotMemoize(module, () => createEnv('app')); @@ -87,6 +88,7 @@ async function main() { .addRouter('/auth', await auth(authEnv)) .addRouter('/identity', await identity(identityEnv)) .addRouter('/techdocs', await techdocs(techdocsEnv)) + .addRouter('/kubernetes', await kubernetes(kubernetesEnv)) .addRouter('/proxy', await proxy(proxyEnv, '/proxy')) .addRouter('/graphql', await graphql(graphqlEnv)) .addRouter('', await app(appEnv)); diff --git a/packages/backend/src/plugins/graphql.ts b/packages/backend/src/plugins/graphql.ts index 0f9b167d19..d52e28f9cb 100644 --- a/packages/backend/src/plugins/graphql.ts +++ b/packages/backend/src/plugins/graphql.ts @@ -30,10 +30,14 @@ */ import { createRouter } from '@backstage/plugin-graphql-backend'; -import type { PluginEnvironment } from '../types'; +import { PluginEnvironment } from '../types'; -export default async function createPlugin({ logger }: PluginEnvironment) { +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { return await createRouter({ logger, + config, }); } diff --git a/packages/backend/src/plugins/kubernetes.ts b/packages/backend/src/plugins/kubernetes.ts new file mode 100644 index 0000000000..42b7722ff6 --- /dev/null +++ b/packages/backend/src/plugins/kubernetes.ts @@ -0,0 +1,22 @@ +/* + * 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 { createRouter } from '@backstage/plugin-kubernetes-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ logger }: PluginEnvironment) { + return await createRouter({ logger }); +} diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index a9e5f185ec..f902ee76f0 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -19,16 +19,24 @@ import { createRouter, FilePreparer, GithubPreparer, + GitlabPreparer, Preparers, + Publishers, GithubPublisher, + GitlabPublisher, CreateReactAppTemplater, Templaters, + RepoVisibilityOptions, } from '@backstage/plugin-scaffolder-backend'; import { Octokit } from '@octokit/rest'; +import { Gitlab } from '@gitbeaker/node'; import type { PluginEnvironment } from '../types'; import Docker from 'dockerode'; -export default async function createPlugin({ logger }: PluginEnvironment) { +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { const cookiecutterTemplater = new CookieCutter(); const craTemplater = new CreateReactAppTemplater(); const templaters = new Templaters(); @@ -37,19 +45,77 @@ export default async function createPlugin({ logger }: PluginEnvironment) { const filePreparer = new FilePreparer(); const githubPreparer = new GithubPreparer(); + const gitlabPreparer = new GitlabPreparer(config); const preparers = new Preparers(); preparers.register('file', filePreparer); preparers.register('github', githubPreparer); + preparers.register('gitlab', gitlabPreparer); + preparers.register('gitlab/api', gitlabPreparer); - const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN }); - const publisher = new GithubPublisher({ client: githubClient }); + const publishers = new Publishers(); + + const githubConfig = config.getOptionalConfig('scaffolder.github'); + + if (githubConfig) { + try { + const repoVisibility = githubConfig.getString( + 'visibility', + ) as RepoVisibilityOptions; + + const githubToken = githubConfig.getString('token'); + const githubClient = new Octokit({ auth: githubToken }); + const githubPublisher = new GithubPublisher({ + client: githubClient, + token: githubToken, + repoVisibility, + }); + publishers.register('file', githubPublisher); + publishers.register('github', githubPublisher); + } catch (e) { + const providerName = 'github'; + if (process.env.NODE_ENV !== 'development') { + throw new Error( + `Failed to initialize ${providerName} scaffolding provider, ${e.message}`, + ); + } + + logger.warn( + `Skipping ${providerName} scaffolding provider, ${e.message}`, + ); + } + } + + const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab.api'); + if (gitLabConfig) { + try { + const gitLabToken = gitLabConfig.getString('token'); + const gitLabClient = new Gitlab({ + host: gitLabConfig.getOptionalString('baseUrl'), + token: gitLabToken, + }); + const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken); + publishers.register('gitlab', gitLabPublisher); + publishers.register('gitlab/api', gitLabPublisher); + } catch (e) { + const providerName = 'gitlab'; + if (process.env.NODE_ENV !== 'development') { + throw new Error( + `Failed to initialize ${providerName} scaffolding provider, ${e.message}`, + ); + } + + logger.warn( + `Skipping ${providerName} scaffolding provider, ${e.message}`, + ); + } + } const dockerClient = new Docker(); return await createRouter({ preparers, templaters, - publisher, + publishers, logger, dockerClient, }); diff --git a/packages/backend/src/plugins/techdocs.ts b/packages/backend/src/plugins/techdocs.ts index 7364cc4d83..58dca83b43 100644 --- a/packages/backend/src/plugins/techdocs.ts +++ b/packages/backend/src/plugins/techdocs.ts @@ -31,7 +31,7 @@ export default async function createPlugin({ config, }: PluginEnvironment) { const generators = new Generators(); - const techdocsGenerator = new TechdocsGenerator(logger); + const techdocsGenerator = new TechdocsGenerator(logger, config); generators.register('techdocs', techdocsGenerator); const preparers = new Preparers(); diff --git a/packages/catalog-model/examples/all-apis.yaml b/packages/catalog-model/examples/all-apis.yaml new file mode 100644 index 0000000000..33b000d1ae --- /dev/null +++ b/packages/catalog-model/examples/all-apis.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Location +metadata: + name: example-apis + description: A collection of all Backstage example APIs +spec: + type: github + targets: + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/hello-world-api.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/streetlights-api.yaml diff --git a/packages/catalog-model/examples/all-components.yaml b/packages/catalog-model/examples/all-components.yaml new file mode 100644 index 0000000000..cad4eee044 --- /dev/null +++ b/packages/catalog-model/examples/all-components.yaml @@ -0,0 +1,16 @@ +apiVersion: backstage.io/v1alpha1 +kind: Location +metadata: + name: example-components + description: A collection of all Backstage example components +spec: + type: github + targets: + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-order-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/podcast-api-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/queue-proxy-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/searcher-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-lib-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml diff --git a/packages/catalog-model/examples/hello-world-api.yaml b/packages/catalog-model/examples/hello-world-api.yaml index 298b0cb835..659c48adfb 100644 --- a/packages/catalog-model/examples/hello-world-api.yaml +++ b/packages/catalog-model/examples/hello-world-api.yaml @@ -5,6 +5,8 @@ metadata: description: Hello World example for gRPC spec: type: grpc + lifecycle: deprecated + owner: grpc@example.com definition: | // Copyright 2015 gRPC authors. // diff --git a/packages/catalog-model/examples/petstore-api.yaml b/packages/catalog-model/examples/petstore-api.yaml index 01953a4a97..c8ce576cce 100644 --- a/packages/catalog-model/examples/petstore-api.yaml +++ b/packages/catalog-model/examples/petstore-api.yaml @@ -3,8 +3,13 @@ kind: API metadata: name: petstore description: The petstore API + tags: + - store + - rest spec: type: openapi + lifecycle: experimental + owner: pets@example.com definition: | openapi: "3.0.0" info: diff --git a/packages/catalog-model/examples/playback-order-component.yaml b/packages/catalog-model/examples/playback-order-component.yaml index 4f93b65edf..3e46953928 100644 --- a/packages/catalog-model/examples/playback-order-component.yaml +++ b/packages/catalog-model/examples/playback-order-component.yaml @@ -3,6 +3,9 @@ kind: Component metadata: name: playback-order description: Playback Order + tags: + - java + - playback spec: type: service lifecycle: production diff --git a/packages/catalog-model/examples/streetlights-api.yaml b/packages/catalog-model/examples/streetlights-api.yaml index 4aeef993bb..d53b05fc2d 100644 --- a/packages/catalog-model/examples/streetlights-api.yaml +++ b/packages/catalog-model/examples/streetlights-api.yaml @@ -4,9 +4,11 @@ metadata: name: streetlights description: The Smartylighting Streetlights API allows you to remotely manage the city lights. tags: - - unstable + - mqtt spec: type: asyncapi + lifecycle: production + owner: streetlights@example.com definition: | asyncapi: 2.0.0 info: diff --git a/packages/catalog-model/examples/swapi-graphql.yaml b/packages/catalog-model/examples/swapi-graphql.yaml new file mode 100644 index 0000000000..152d9c0afa --- /dev/null +++ b/packages/catalog-model/examples/swapi-graphql.yaml @@ -0,0 +1,1174 @@ +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: starwars-graphql + description: SWAPI GraphQL Schema +spec: + type: graphql + definition: | + schema { + query: Root + } + + """A single film.""" + type Film implements Node { + """The title of this film.""" + title: String + + """The episode number of this film.""" + episodeID: Int + + """The opening paragraphs at the beginning of this film.""" + openingCrawl: String + + """The name of the director of this film.""" + director: String + + """The name(s) of the producer(s) of this film.""" + producers: [String] + + """The ISO 8601 date format of film release at original creator country.""" + releaseDate: String + speciesConnection(after: String, first: Int, before: String, last: Int): FilmSpeciesConnection + starshipConnection(after: String, first: Int, before: String, last: Int): FilmStarshipsConnection + vehicleConnection(after: String, first: Int, before: String, last: Int): FilmVehiclesConnection + characterConnection(after: String, first: Int, before: String, last: Int): FilmCharactersConnection + planetConnection(after: String, first: Int, before: String, last: Int): FilmPlanetsConnection + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """The ID of an object""" + id: ID! + } + + """A connection to a list of items.""" + type FilmCharactersConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [FilmCharactersEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + characters: [Person] + } + + """An edge in a connection.""" + type FilmCharactersEdge { + """The item at the end of the edge""" + node: Person + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type FilmPlanetsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [FilmPlanetsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + planets: [Planet] + } + + """An edge in a connection.""" + type FilmPlanetsEdge { + """The item at the end of the edge""" + node: Planet + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type FilmsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [FilmsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + } + + """An edge in a connection.""" + type FilmsEdge { + """The item at the end of the edge""" + node: Film + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type FilmSpeciesConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [FilmSpeciesEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + species: [Species] + } + + """An edge in a connection.""" + type FilmSpeciesEdge { + """The item at the end of the edge""" + node: Species + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type FilmStarshipsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [FilmStarshipsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + starships: [Starship] + } + + """An edge in a connection.""" + type FilmStarshipsEdge { + """The item at the end of the edge""" + node: Starship + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type FilmVehiclesConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [FilmVehiclesEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + vehicles: [Vehicle] + } + + """An edge in a connection.""" + type FilmVehiclesEdge { + """The item at the end of the edge""" + node: Vehicle + + """A cursor for use in pagination""" + cursor: String! + } + + """An object with an ID""" + interface Node { + """The id of the object.""" + id: ID! + } + + """Information about pagination in a connection.""" + type PageInfo { + """When paginating forwards, are there more items?""" + hasNextPage: Boolean! + + """When paginating backwards, are there more items?""" + hasPreviousPage: Boolean! + + """When paginating backwards, the cursor to continue.""" + startCursor: String + + """When paginating forwards, the cursor to continue.""" + endCursor: String + } + + """A connection to a list of items.""" + type PeopleConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PeopleEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + people: [Person] + } + + """An edge in a connection.""" + type PeopleEdge { + """The item at the end of the edge""" + node: Person + + """A cursor for use in pagination""" + cursor: String! + } + + """An individual person or character within the Star Wars universe.""" + type Person implements Node { + """The name of this person.""" + name: String + + """ + The birth year of the person, using the in-universe standard of BBY or ABY - + Before the Battle of Yavin or After the Battle of Yavin. The Battle of Yavin is + a battle that occurs at the end of Star Wars episode IV: A New Hope. + """ + birthYear: String + + """ + The eye color of this person. Will be "unknown" if not known or "n/a" if the + person does not have an eye. + """ + eyeColor: String + + """ + The gender of this person. Either "Male", "Female" or "unknown", + "n/a" if the person does not have a gender. + """ + gender: String + + """ + The hair color of this person. Will be "unknown" if not known or "n/a" if the + person does not have hair. + """ + hairColor: String + + """The height of the person in centimeters.""" + height: Int + + """The mass of the person in kilograms.""" + mass: Float + + """The skin color of this person.""" + skinColor: String + + """A planet that this person was born on or inhabits.""" + homeworld: Planet + filmConnection(after: String, first: Int, before: String, last: Int): PersonFilmsConnection + + """The species that this person belongs to, or null if unknown.""" + species: Species + starshipConnection(after: String, first: Int, before: String, last: Int): PersonStarshipsConnection + vehicleConnection(after: String, first: Int, before: String, last: Int): PersonVehiclesConnection + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """The ID of an object""" + id: ID! + } + + """A connection to a list of items.""" + type PersonFilmsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PersonFilmsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + } + + """An edge in a connection.""" + type PersonFilmsEdge { + """The item at the end of the edge""" + node: Film + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type PersonStarshipsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PersonStarshipsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + starships: [Starship] + } + + """An edge in a connection.""" + type PersonStarshipsEdge { + """The item at the end of the edge""" + node: Starship + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type PersonVehiclesConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PersonVehiclesEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + vehicles: [Vehicle] + } + + """An edge in a connection.""" + type PersonVehiclesEdge { + """The item at the end of the edge""" + node: Vehicle + + """A cursor for use in pagination""" + cursor: String! + } + + """ + A large mass, planet or planetoid in the Star Wars Universe, at the time of + 0 ABY. + """ + type Planet implements Node { + """The name of this planet.""" + name: String + + """The diameter of this planet in kilometers.""" + diameter: Int + + """ + The number of standard hours it takes for this planet to complete a single + rotation on its axis. + """ + rotationPeriod: Int + + """ + The number of standard days it takes for this planet to complete a single orbit + of its local star. + """ + orbitalPeriod: Int + + """ + A number denoting the gravity of this planet, where "1" is normal or 1 standard + G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 standard Gs. + """ + gravity: String + + """The average population of sentient beings inhabiting this planet.""" + population: Float + + """The climates of this planet.""" + climates: [String] + + """The terrains of this planet.""" + terrains: [String] + + """ + The percentage of the planet surface that is naturally occuring water or bodies + of water. + """ + surfaceWater: Float + residentConnection(after: String, first: Int, before: String, last: Int): PlanetResidentsConnection + filmConnection(after: String, first: Int, before: String, last: Int): PlanetFilmsConnection + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """The ID of an object""" + id: ID! + } + + """A connection to a list of items.""" + type PlanetFilmsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PlanetFilmsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + } + + """An edge in a connection.""" + type PlanetFilmsEdge { + """The item at the end of the edge""" + node: Film + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type PlanetResidentsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PlanetResidentsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + residents: [Person] + } + + """An edge in a connection.""" + type PlanetResidentsEdge { + """The item at the end of the edge""" + node: Person + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type PlanetsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PlanetsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + planets: [Planet] + } + + """An edge in a connection.""" + type PlanetsEdge { + """The item at the end of the edge""" + node: Planet + + """A cursor for use in pagination""" + cursor: String! + } + + type Root { + allFilms(after: String, first: Int, before: String, last: Int): FilmsConnection + film(id: ID, filmID: ID): Film + allPeople(after: String, first: Int, before: String, last: Int): PeopleConnection + person(id: ID, personID: ID): Person + allPlanets(after: String, first: Int, before: String, last: Int): PlanetsConnection + planet(id: ID, planetID: ID): Planet + allSpecies(after: String, first: Int, before: String, last: Int): SpeciesConnection + species(id: ID, speciesID: ID): Species + allStarships(after: String, first: Int, before: String, last: Int): StarshipsConnection + starship(id: ID, starshipID: ID): Starship + allVehicles(after: String, first: Int, before: String, last: Int): VehiclesConnection + vehicle(id: ID, vehicleID: ID): Vehicle + + """Fetches an object given its ID""" + node( + """The ID of an object""" + id: ID! + ): Node + } + + """A type of person or character within the Star Wars Universe.""" + type Species implements Node { + """The name of this species.""" + name: String + + """The classification of this species, such as "mammal" or "reptile".""" + classification: String + + """The designation of this species, such as "sentient".""" + designation: String + + """The average height of this species in centimeters.""" + averageHeight: Float + + """The average lifespan of this species in years, null if unknown.""" + averageLifespan: Int + + """ + Common eye colors for this species, null if this species does not typically + have eyes. + """ + eyeColors: [String] + + """ + Common hair colors for this species, null if this species does not typically + have hair. + """ + hairColors: [String] + + """ + Common skin colors for this species, null if this species does not typically + have skin. + """ + skinColors: [String] + + """The language commonly spoken by this species.""" + language: String + + """A planet that this species originates from.""" + homeworld: Planet + personConnection(after: String, first: Int, before: String, last: Int): SpeciesPeopleConnection + filmConnection(after: String, first: Int, before: String, last: Int): SpeciesFilmsConnection + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """The ID of an object""" + id: ID! + } + + """A connection to a list of items.""" + type SpeciesConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [SpeciesEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + species: [Species] + } + + """An edge in a connection.""" + type SpeciesEdge { + """The item at the end of the edge""" + node: Species + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type SpeciesFilmsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [SpeciesFilmsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + } + + """An edge in a connection.""" + type SpeciesFilmsEdge { + """The item at the end of the edge""" + node: Film + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type SpeciesPeopleConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [SpeciesPeopleEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + people: [Person] + } + + """An edge in a connection.""" + type SpeciesPeopleEdge { + """The item at the end of the edge""" + node: Person + + """A cursor for use in pagination""" + cursor: String! + } + + """A single transport craft that has hyperdrive capability.""" + type Starship implements Node { + """The name of this starship. The common name, such as "Death Star".""" + name: String + + """ + The model or official name of this starship. Such as "T-65 X-wing" or "DS-1 + Orbital Battle Station". + """ + model: String + + """ + The class of this starship, such as "Starfighter" or "Deep Space Mobile + Battlestation" + """ + starshipClass: String + + """The manufacturers of this starship.""" + manufacturers: [String] + + """The cost of this starship new, in galactic credits.""" + costInCredits: Float + + """The length of this starship in meters.""" + length: Float + + """The number of personnel needed to run or pilot this starship.""" + crew: String + + """The number of non-essential people this starship can transport.""" + passengers: String + + """ + The maximum speed of this starship in atmosphere. null if this starship is + incapable of atmosphering flight. + """ + maxAtmospheringSpeed: Int + + """The class of this starships hyperdrive.""" + hyperdriveRating: Float + + """ + The Maximum number of Megalights this starship can travel in a standard hour. + A "Megalight" is a standard unit of distance and has never been defined before + within the Star Wars universe. This figure is only really useful for measuring + the difference in speed of starships. We can assume it is similar to AU, the + distance between our Sun (Sol) and Earth. + """ + MGLT: Int + + """The maximum number of kilograms that this starship can transport.""" + cargoCapacity: Float + + """ + The maximum length of time that this starship can provide consumables for its + entire crew without having to resupply. + """ + consumables: String + pilotConnection(after: String, first: Int, before: String, last: Int): StarshipPilotsConnection + filmConnection(after: String, first: Int, before: String, last: Int): StarshipFilmsConnection + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """The ID of an object""" + id: ID! + } + + """A connection to a list of items.""" + type StarshipFilmsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [StarshipFilmsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + } + + """An edge in a connection.""" + type StarshipFilmsEdge { + """The item at the end of the edge""" + node: Film + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type StarshipPilotsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [StarshipPilotsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + pilots: [Person] + } + + """An edge in a connection.""" + type StarshipPilotsEdge { + """The item at the end of the edge""" + node: Person + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type StarshipsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [StarshipsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + starships: [Starship] + } + + """An edge in a connection.""" + type StarshipsEdge { + """The item at the end of the edge""" + node: Starship + + """A cursor for use in pagination""" + cursor: String! + } + + """A single transport craft that does not have hyperdrive capability""" + type Vehicle implements Node { + """ + The name of this vehicle. The common name, such as "Sand Crawler" or "Speeder + bike". + """ + name: String + + """ + The model or official name of this vehicle. Such as "All-Terrain Attack + Transport". + """ + model: String + + """The class of this vehicle, such as "Wheeled" or "Repulsorcraft".""" + vehicleClass: String + + """The manufacturers of this vehicle.""" + manufacturers: [String] + + """The cost of this vehicle new, in Galactic Credits.""" + costInCredits: Float + + """The length of this vehicle in meters.""" + length: Float + + """The number of personnel needed to run or pilot this vehicle.""" + crew: String + + """The number of non-essential people this vehicle can transport.""" + passengers: String + + """The maximum speed of this vehicle in atmosphere.""" + maxAtmospheringSpeed: Int + + """The maximum number of kilograms that this vehicle can transport.""" + cargoCapacity: Float + + """ + The maximum length of time that this vehicle can provide consumables for its + entire crew without having to resupply. + """ + consumables: String + pilotConnection(after: String, first: Int, before: String, last: Int): VehiclePilotsConnection + filmConnection(after: String, first: Int, before: String, last: Int): VehicleFilmsConnection + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """The ID of an object""" + id: ID! + } + + """A connection to a list of items.""" + type VehicleFilmsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [VehicleFilmsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + } + + """An edge in a connection.""" + type VehicleFilmsEdge { + """The item at the end of the edge""" + node: Film + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type VehiclePilotsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [VehiclePilotsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + pilots: [Person] + } + + """An edge in a connection.""" + type VehiclePilotsEdge { + """The item at the end of the edge""" + node: Person + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type VehiclesConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [VehiclesEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + vehicles: [Vehicle] + } + + """An edge in a connection.""" + type VehiclesEdge { + """The item at the end of the edge""" + node: Vehicle + + """A cursor for use in pagination""" + cursor: String! + } diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 0d18125d43..96433a0f0e 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.21", + "@backstage/config": "^0.1.1-alpha.23", "@types/json-schema": "^7.0.5", "@types/yup": "^0.28.2", "json-schema": "^0.2.5", @@ -29,7 +29,7 @@ "yup": "^0.29.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/cli": "^0.1.1-alpha.23", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts index 48de569213..35a1c59ef8 100644 --- a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts @@ -33,6 +33,8 @@ describe('ApiV1alpha1Policy', () => { }, spec: { type: 'openapi', + lifecycle: 'production', + owner: 'me', definition: ` openapi: "3.0.0" info: @@ -109,6 +111,36 @@ components: await expect(policy.enforce(entity)).rejects.toThrow(/type/); }); + it('rejects missing lifecycle', async () => { + delete (entity as any).spec.lifecycle; + await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/); + }); + + it('rejects wrong lifecycle', async () => { + (entity as any).spec.lifecycle = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/); + }); + + it('rejects empty lifecycle', async () => { + (entity as any).spec.lifecycle = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/); + }); + + it('rejects missing owner', async () => { + delete (entity as any).spec.owner; + await expect(policy.enforce(entity)).rejects.toThrow(/owner/); + }); + + it('rejects wrong owner', async () => { + (entity as any).spec.owner = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/owner/); + }); + + it('rejects empty owner', async () => { + (entity as any).spec.owner = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/owner/); + }); + it('rejects missing definition', async () => { delete (entity as any).spec.definition; await expect(policy.enforce(entity)).rejects.toThrow(/definition/); diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts index 59e16df9a7..972f6df96d 100644 --- a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts @@ -26,6 +26,8 @@ export interface ApiEntityV1alpha1 extends Entity { kind: typeof KIND; spec: { type: string; + lifecycle: string; + owner: string; definition: string; }; } @@ -40,6 +42,8 @@ export class ApiEntityV1alpha1Policy implements EntityPolicy { spec: yup .object({ type: yup.string().required().min(1), + lifecycle: yup.string().required().min(1), + owner: yup.string().required().min(1), definition: yup.string().required().min(1), }) .required(), diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json index 4132907e70..bbd9a67387 100644 --- a/packages/cli-common/package.json +++ b/packages/cli-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli-common", "description": "Common functionality used by cli, backend, and create-app", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": false, "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index 492cd3b958..6f65658539 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public" @@ -28,9 +28,9 @@ "backstage-cli": "bin/backstage-cli" }, "dependencies": { - "@backstage/cli-common": "^0.1.1-alpha.21", - "@backstage/config": "^0.1.1-alpha.21", - "@backstage/config-loader": "^0.1.1-alpha.21", + "@backstage/cli-common": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", + "@backstage/config-loader": "^0.1.1-alpha.23", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", "@lerna/project": "^3.18.0", diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 5e6882a4a0..247812b6fd 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -21,6 +21,7 @@ import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; import { resolve as resolvePath } from 'path'; import os from 'os'; +import { Command } from 'commander'; import { parseOwnerIds, addCodeownersEntry, @@ -32,12 +33,12 @@ import { version as backstageVersion } from '../../lib/version'; const exec = promisify(execCb); -async function checkExists(rootDir: string, id: string) { - await Task.forItem('checking', id, async () => { - const destination = resolvePath(rootDir, 'plugins', id); - +async function checkExists(destination: string) { + await Task.forItem('checking', destination, async () => { if (await fs.pathExists(destination)) { - const existing = chalk.cyan(destination.replace(`${rootDir}/`, '')); + const existing = chalk.cyan( + destination.replace(`${paths.targetRoot}/`, ''), + ); throw new Error( `A plugin with the same name already exists: ${existing}\nPlease try again with a different plugin ID`, ); @@ -86,10 +87,9 @@ export const addExportStatement = async ( export async function addPluginDependencyToApp( rootDir: string, - pluginName: string, + pluginPackage: string, versionStr: string, ) { - const pluginPackage = `@backstage/plugin-${pluginName}`; const packageFilePath = 'packages/app/package.json'; const packageFile = resolvePath(rootDir, packageFilePath); @@ -116,8 +116,11 @@ export async function addPluginDependencyToApp( }); } -export async function addPluginToApp(rootDir: string, pluginName: string) { - const pluginPackage = `@backstage/plugin-${pluginName}`; +export async function addPluginToApp( + rootDir: string, + pluginName: string, + pluginPackage: string, +) { const pluginNameCapitalized = pluginName .split('-') .map(name => capitalize(name)) @@ -175,7 +178,7 @@ export async function movePlugin( }); } -export default async () => { +export default async (cmd: Command) => { const codeownersPath = await getCodeownersFilePath(paths.targetRoot); const questions: Question[] = [ @@ -221,20 +224,29 @@ export default async () => { } const answers: Answers = await inquirer.prompt(questions); - + const name = cmd.scope + ? `@${cmd.scope.replace(/^@/, '')}/plugin-${answers.id}` + : `plugin-${answers.id}`; + const npmRegistry = cmd.npmRegistry && cmd.scope ? cmd.npmRegistry : ''; + const privatePackage = cmd.private === false ? false : true; + const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json')); const appPackage = paths.resolveTargetRoot('packages/app'); const templateDir = paths.resolveOwn('templates/default-plugin'); const tempDir = resolvePath(os.tmpdir(), answers.id); - const pluginDir = paths.resolveTargetRoot('plugins', answers.id); + const pluginDir = isMonoRepo + ? paths.resolveTargetRoot('plugins', answers.id) + : paths.resolveTargetRoot(answers.id); const ownerIds = parseOwnerIds(answers.owner); - const { version } = await fs.readJson(paths.resolveTargetRoot('lerna.json')); + const { version } = isMonoRepo + ? await fs.readJson(paths.resolveTargetRoot('lerna.json')) + : { version: '0.1.0' }; Task.log(); Task.log('Creating the plugin...'); try { Task.section('Checking if the plugin ID is available'); - await checkExists(paths.targetRoot, answers.id); + await checkExists(pluginDir); Task.section('Creating a temporary plugin directory'); await createTemporaryPluginFolder(tempDir); @@ -244,6 +256,9 @@ export default async () => { ...answers, version, backstageVersion, + name, + privatePackage, + npmRegistry, }); Task.section('Moving to final location'); @@ -254,10 +269,10 @@ export default async () => { if (await fs.pathExists(appPackage)) { Task.section('Adding plugin as dependency in app'); - await addPluginDependencyToApp(paths.targetRoot, answers.id, version); + await addPluginDependencyToApp(paths.targetRoot, name, version); Task.section('Import plugin in app'); - await addPluginToApp(paths.targetRoot, answers.id); + await addPluginToApp(paths.targetRoot, answers.id, name); } if (ownerIds && ownerIds.length) { @@ -269,11 +284,7 @@ export default async () => { } Task.log(); - Task.log( - `🥇 Successfully created ${chalk.cyan( - `@backstage/plugin-${answers.id}`, - )}`, - ); + Task.log(`🥇 Successfully created ${chalk.cyan(`${name}`)}`); Task.log(); Task.exit(); } catch (error) { diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 4e031cdbc1..f1828cef84 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -62,6 +62,9 @@ export function registerCommands(program: CommanderStatic) { program .command('create-plugin') .description('Creates a new plugin in the current repository') + .option('--scope ', 'NPM scope') + .option('--npm-registry ', 'NPM registry URL') + .option('--no-private', 'Public NPM Package') .action( lazy(() => import('./create-plugin/createPlugin').then(m => m.default)), ); diff --git a/packages/cli/src/commands/plugin/diff.ts b/packages/cli/src/commands/plugin/diff.ts index 99af093f2c..f5dfa670b7 100644 --- a/packages/cli/src/commands/plugin/diff.ts +++ b/packages/cli/src/commands/plugin/diff.ts @@ -30,6 +30,9 @@ import { version as backstageVersion } from '../../lib/version'; export type PluginData = { id: string; name: string; + privatePackage: string; + version: string; + npmRegistry: string; }; const fileHandlers = [ @@ -62,11 +65,8 @@ export default async (cmd: Command) => { promptFunc = yesPromptFunc; } - const { version } = await fs.readJson(paths.resolveTargetRoot('lerna.json')); - const data = await readPluginData(); const templateFiles = await diffTemplateFiles('default-plugin', { - version, backstageVersion, ...data, }); @@ -77,9 +77,19 @@ export default async (cmd: Command) => { // Reads templating data from the existing plugin async function readPluginData(): Promise { let name: string; + let privatePackage: string; + let version: string; + let npmRegistry: string; try { const pkg = require(paths.resolveTarget('package.json')); name = pkg.name; + privatePackage = pkg.private; + version = pkg.version; + const scope = name.split('/')[0]; + if (`${scope}:registry` in pkg.publishConfig) { + const registryURL = pkg.publishConfig[`${scope}:registry`]; + npmRegistry = `"${scope}:registry" : "${registryURL}"`; + } else npmRegistry = ''; } catch (error) { throw new Error(`Failed to read target package, ${error}`); } @@ -96,5 +106,5 @@ async function readPluginData(): Promise { const id = pluginIdMatch[1]; - return { id, name }; + return { id, name, privatePackage, version, npmRegistry }; } diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index e91fc32633..afcedaae59 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -94,14 +94,7 @@ export const makeConfigs = async ( postcss(), imageFiles({ exclude: /\.icon\.svg$/, - include: [ - /\.css$/, - /\.svg$/, - /\.png$/, - /\.gif$/, - /\.jpg$/, - /\.jpeg$/, - ], + include: [/\.svg$/, /\.png$/, /\.gif$/, /\.jpg$/, /\.jpeg$/], }), json(), yaml(), diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index dbfb639b16..52cffeb621 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -1,11 +1,14 @@ { - "name": "@backstage/plugin-{{id}}", + "name": "{{name}}", "version": "{{version}}", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, +{{#if privatePackage}} "private": {{privatePackage}}, +{{/if}} "publishConfig": { +{{#if npmRegistry}} "registry": "{{npmRegistry}}", +{{/if}} "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 592b8eefb3..09f8da3b70 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.21", + "@backstage/config": "^0.1.1-alpha.23", "fs-extra": "^9.0.0", "yaml": "^1.9.2", "yup": "^0.29.1" diff --git a/packages/config/package.json b/packages/config/package.json index 236964f2bd..58c86d6e96 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 191394e536..7d338c944d 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-api", "description": "Internal Core API used by Backstage plugins and apps", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.21", - "@backstage/theme": "^0.1.1-alpha.21", + "@backstage/config": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@types/react": "^16.9", @@ -41,8 +41,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.21", - "@backstage/test-utils-core": "^0.1.1-alpha.21", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/test-utils-core": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/packages/core-api/src/apis/definitions/IdentityApi.ts b/packages/core-api/src/apis/definitions/IdentityApi.ts index 13f4cd24bc..2684422b1e 100644 --- a/packages/core-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-api/src/apis/definitions/IdentityApi.ts @@ -46,9 +46,9 @@ export type IdentityApi = { // TODO: getProfile(): Promise - We want this to be async when added, but needs more work. /** - * Log out the current user + * Sign out the current user */ - logout(): Promise; + signOut(): Promise; }; export const identityApiRef = createApiRef({ diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 1ff8c46dad..800a865fea 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -90,11 +90,6 @@ export type OAuthApi = { scope?: OAuthScope, options?: AuthRequestOptions, ): Promise; - - /** - * Log out the user's session. This will reload the page. - */ - logout(): Promise; }; /** @@ -114,11 +109,6 @@ export type OpenIdConnectApi = { * The returned promise can be rejected, but only if the user rejects the login request. */ getIdToken(options?: AuthRequestOptions): Promise; - - /** - * Log out the user's session. This will reload the page. - */ - logout(): Promise; }; /** @@ -187,7 +177,7 @@ export type ProfileInfo = { }; /** - * Session state values passed to subscribers of the SessionStateApi. + * Session state values passed to subscribers of the SessionApi. */ export enum SessionState { SignedIn = 'SignedIn', @@ -195,10 +185,22 @@ export enum SessionState { } /** - * This API provides access to an sessionState$ observable which provides an update when the - * user performs a sign in or sign out from an auth provider. + * The SessionApi provides basic controls for any auth provider that is tied to a persistent session. */ -export type SessionStateApi = { +export type SessionApi = { + /** + * Sign in with a minimum set of permissions. + */ + signIn(): Promise; + + /** + * Sign out from the current session. This will reload the page. + */ + signOut(): Promise; + + /** + * Observe the current state of the auth session. Emits the current state on subscription. + */ sessionState$(): Observable; }; @@ -215,7 +217,7 @@ export const googleAuthApiRef = createApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & - SessionStateApi + SessionApi >({ id: 'core.auth.google', description: 'Provides authentication towards Google APIs and identities', @@ -228,7 +230,7 @@ export const googleAuthApiRef = createApiRef< * for a full list of supported scopes. */ export const githubAuthApiRef = createApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >({ id: 'core.auth.github', description: 'Provides authentication towards GitHub APIs', @@ -245,7 +247,7 @@ export const oktaAuthApiRef = createApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & - SessionStateApi + SessionApi >({ id: 'core.auth.okta', description: 'Provides authentication towards Okta APIs', @@ -258,7 +260,7 @@ export const oktaAuthApiRef = createApiRef< * for a full list of supported scopes. */ export const gitlabAuthApiRef = createApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >({ id: 'core.auth.gitlab', description: 'Provides authentication towards GitLab APIs', @@ -271,7 +273,7 @@ export const gitlabAuthApiRef = createApiRef< * for a full list of supported scopes. */ export const auth0AuthApiRef = createApiRef< - OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi + OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >({ id: 'core.auth.auth0', description: 'Provides authentication towards Auth0 APIs', @@ -289,7 +291,7 @@ export const microsoftAuthApiRef = createApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & - SessionStateApi + SessionApi >({ id: 'core.auth.microsoft', description: 'Provides authentication towards Microsoft APIs and identities', @@ -302,8 +304,8 @@ export const oauth2ApiRef = createApiRef< OAuthApi & OpenIdConnectApi & ProfileInfoApi & - SessionStateApi & - BackstageIdentityApi + BackstageIdentityApi & + SessionApi >({ id: 'core.auth.oauth2', description: 'Example of how to use oauth2 custom provider', diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts index 8b9f807cd8..1d87ff3720 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -19,7 +19,7 @@ import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; import { GithubSession } from './types'; import { OAuthApi, - SessionStateApi, + SessionApi, SessionState, ProfileInfo, BackstageIdentity, @@ -61,7 +61,7 @@ const DEFAULT_PROVIDER = { icon: GithubIcon, }; -class GithubAuth implements OAuthApi, SessionStateApi { +class GithubAuth implements OAuthApi, SessionApi { static create({ discoveryApi, environment = 'development', @@ -102,12 +102,20 @@ class GithubAuth implements OAuthApi, SessionStateApi { return new GithubAuth(authSessionStore); } + constructor(private readonly sessionManager: SessionManager) {} + + async signIn() { + await this.getAccessToken(); + } + + async signOut() { + await this.sessionManager.removeSession(); + } + sessionState$(): Observable { return this.sessionManager.sessionState$(); } - constructor(private readonly sessionManager: SessionManager) {} - async getAccessToken(scope?: string, options?: AuthRequestOptions) { const session = await this.sessionManager.getSession({ ...options, @@ -128,10 +136,6 @@ class GithubAuth implements OAuthApi, SessionStateApi { return session?.profile; } - async logout() { - await this.sessionManager.removeSession(); - } - static normalizeScope(scope?: string): Set { if (!scope) { return new Set(); diff --git a/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts index 27626aac5a..d088ca9798 100644 --- a/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -32,7 +32,7 @@ import { ProfileInfo, ProfileInfoApi, SessionState, - SessionStateApi, + SessionApi, BackstageIdentityApi, } from '../../../definitions/auth'; import { OAuth2Session } from './types'; @@ -75,7 +75,7 @@ class OAuth2 OpenIdConnectApi, ProfileInfoApi, BackstageIdentityApi, - SessionStateApi { + SessionApi { static create({ discoveryApi, environment = 'development', @@ -129,6 +129,14 @@ class OAuth2 this.scopeTransform = options.scopeTransform; } + async signIn() { + await this.getAccessToken(); + } + + async signOut() { + await this.sessionManager.removeSession(); + } + sessionState$(): Observable { return this.sessionManager.sessionState$(); } @@ -150,10 +158,6 @@ class OAuth2 return session?.providerInfo.idToken ?? ''; } - async logout() { - await this.sessionManager.removeSession(); - } - async getBackstageIdentity( options: AuthRequestOptions = {}, ): Promise { diff --git a/packages/core-api/src/app/AppIdentity.ts b/packages/core-api/src/app/AppIdentity.ts index 69ee5d28ac..d3e5fe567a 100644 --- a/packages/core-api/src/app/AppIdentity.ts +++ b/packages/core-api/src/app/AppIdentity.ts @@ -26,7 +26,7 @@ export class AppIdentity implements IdentityApi { private userId?: string; private profile?: ProfileInfo; private idTokenFunc?: () => Promise; - private logoutFunc?: () => Promise; + private signOutFunc?: () => Promise; getUserId(): string { if (!this.hasIdentity) { @@ -55,13 +55,13 @@ export class AppIdentity implements IdentityApi { return this.idTokenFunc?.(); } - async logout(): Promise { + async signOut(): Promise { if (!this.hasIdentity) { throw new Error( - 'Tried to access IdentityApi logoutFunc before app was loaded', + 'Tried to access IdentityApi signOutFunc before app was loaded', ); } - await this.logoutFunc?.(); + await this.signOutFunc?.(); location.reload(); } @@ -80,6 +80,6 @@ export class AppIdentity implements IdentityApi { this.userId = result.userId; this.profile = result.profile; this.idTokenFunc = result.getIdToken; - this.logoutFunc = result.logout; + this.signOutFunc = result.signOut; } } diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx index 11c084d3ed..3db2a18a02 100644 --- a/packages/core-api/src/app/FeatureFlags.tsx +++ b/packages/core-api/src/app/FeatureFlags.tsx @@ -80,6 +80,15 @@ export class UserFlags extends Map { return output; } + toggle(name: FeatureFlagName): FeatureFlagState { + if (super.get(name) === FeatureFlagState.On) { + super.set(name, FeatureFlagState.Off); + } else { + super.set(name, FeatureFlagState.On); + } + return super.get(name) || FeatureFlagState.Off; + } + delete(name: FeatureFlagName): boolean { const output = super.delete(name); this.save(); diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index 565e073aed..882ecd3d8b 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -38,10 +38,11 @@ export type SignInResult = { * Function used to retrieve an ID token for the signed in user. */ getIdToken?: () => Promise; + /** - * Logout handler that will be called if the user requests a logout. + * Sign out handler that will be called if the user requests to sign out. */ - logout?: () => Promise; + signOut?: () => Promise; }; export type SignInPageProps = { diff --git a/packages/core/package.json b/packages/core/package.json index 7e9932bdc1..8749a37b92 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.21", - "@backstage/core-api": "^0.1.1-alpha.21", - "@backstage/theme": "^0.1.1-alpha.21", + "@backstage/config": "^0.1.1-alpha.23", + "@backstage/core-api": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -54,8 +54,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.21", - "@backstage/test-utils": "^0.1.1-alpha.21", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx index c0f57d6330..77f3ca51cc 100644 --- a/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx +++ b/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx @@ -86,3 +86,9 @@ export const Languages = () => ( ); + +export const CopyCode = () => ( + + + +); diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.test.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.test.tsx index 42efbf14af..1a76f84f44 100644 --- a/packages/core/src/components/CodeSnippet/CodeSnippet.test.tsx +++ b/packages/core/src/components/CodeSnippet/CodeSnippet.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; import { CodeSnippet } from './CodeSnippet'; @@ -55,4 +55,14 @@ describe('', () => { expect(queryByText(/2/)).toBeInTheDocument(); expect(queryByText(/3/)).toBeInTheDocument(); }); + + it('copy code using button', async () => { + document.execCommand = jest.fn(); + const rendered = render( + wrapInTestApp(), + ); + const button = rendered.getByTitle('Text copied to clipboard'); + fireEvent.click(button); + expect(document.execCommand).toHaveBeenCalled(); + }); }); diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.tsx index 324d71e936..b6d9b3a5aa 100644 --- a/packages/core/src/components/CodeSnippet/CodeSnippet.tsx +++ b/packages/core/src/components/CodeSnippet/CodeSnippet.tsx @@ -20,19 +20,22 @@ import SyntaxHighlighter from 'react-syntax-highlighter'; import { docco, dark } from 'react-syntax-highlighter/dist/cjs/styles/hljs'; import { useTheme } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; +import { CopyTextButton } from '../CopyTextButton'; type Props = { text: string; language: string; showLineNumbers?: boolean; + showCopyCodeButton?: boolean; }; const defaultProps = { showLineNumbers: false, + showCopyCodeButton: false, }; export const CodeSnippet: FC = props => { - const { text, language, showLineNumbers } = { + const { text, language, showLineNumbers, showCopyCodeButton } = { ...defaultProps, ...props, }; @@ -41,13 +44,20 @@ export const CodeSnippet: FC = props => { const mode = theme.palette.type === 'dark' ? dark : docco; return ( - - {text} - +
+ + {text} + + {showCopyCodeButton && ( +
+ +
+ )} +
); }; @@ -56,4 +66,5 @@ CodeSnippet.propTypes = { text: PropTypes.string.isRequired, language: PropTypes.string.isRequired, showLineNumbers: PropTypes.bool, + showCopyCodeButton: PropTypes.bool, }; diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx index e789035b44..cab992e11a 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; import { CopyTextButton } from './CopyTextButton'; import { @@ -76,7 +76,7 @@ describe('', () => { ), ); const button = rendered.getByTitle('mockTooltip'); - button.click(); + fireEvent.click(button); expect(document.execCommand).toHaveBeenCalled(); rendered.getByText('mockTooltip'); }); diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.tsx index e4773165cc..cb322e5b56 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.tsx +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.tsx @@ -63,7 +63,7 @@ export const CopyTextButton: FC = props => { }; const classes = useStyles(props); const errorApi = useApi(errorApiRef); - const inputRef = useRef(null); + const inputRef = useRef(null); const [open, setOpen] = useState(false); const handleCopyClick: MouseEventHandler = e => { @@ -82,9 +82,8 @@ export const CopyTextButton: FC = props => { return ( <> - diff --git a/packages/core/src/components/ProgressBars/ProgressCard.stories.tsx b/packages/core/src/components/ProgressBars/GaugeCard.stories.tsx similarity index 69% rename from packages/core/src/components/ProgressBars/ProgressCard.stories.tsx rename to packages/core/src/components/ProgressBars/GaugeCard.stories.tsx index 62c9f3be6e..922bb63b2f 100644 --- a/packages/core/src/components/ProgressBars/ProgressCard.stories.tsx +++ b/packages/core/src/components/ProgressBars/GaugeCard.stories.tsx @@ -15,26 +15,26 @@ */ import React from 'react'; -import { ProgressCard } from './ProgressCard'; +import { GaugeCard } from './GaugeCard'; import { Grid } from '@material-ui/core'; const linkInfo = { title: 'Go to XYZ Location', link: '#' }; export default { title: 'Progress Card', - component: ProgressCard, + component: GaugeCard, }; export const Default = () => ( - + - + - + ); @@ -42,21 +42,17 @@ export const Default = () => ( export const Subhead = () => ( - + - - ( export const LinkInFooter = () => ( - + - + - + ); diff --git a/packages/core/src/components/ProgressBars/ProgressCard.test.jsx b/packages/core/src/components/ProgressBars/GaugeCard.test.jsx similarity index 75% rename from packages/core/src/components/ProgressBars/ProgressCard.test.jsx rename to packages/core/src/components/ProgressBars/GaugeCard.test.jsx index 8568077a92..27ef0bb188 100644 --- a/packages/core/src/components/ProgressBars/ProgressCard.test.jsx +++ b/packages/core/src/components/ProgressBars/GaugeCard.test.jsx @@ -18,32 +18,30 @@ import React from 'react'; import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; -import { ProgressCard } from './ProgressCard'; +import { GaugeCard } from './GaugeCard'; const minProps = { title: 'Tingle upgrade', progress: 0.12 }; -describe('', () => { +describe('', () => { it('renders without exploding', () => { - const { getByText } = render(wrapInTestApp()); + const { getByText } = render(wrapInTestApp()); expect(getByText(/Tingle.*/)).toBeInTheDocument(); }); it('renders progress and title', () => { - const { getByText } = render(wrapInTestApp()); + const { getByText } = render(wrapInTestApp()); expect(getByText(/Tingle.*/)).toBeInTheDocument(); expect(getByText(/12%.*/)).toBeInTheDocument(); }); it('does not render deepLink', () => { - const { queryByText } = render( - wrapInTestApp(), - ); + const { queryByText } = render(wrapInTestApp()); expect(queryByText('View more')).not.toBeInTheDocument(); }); it('handles invalid numbers', () => { const badProps = { title: 'Tingle upgrade', progress: 'hejjo' }; - const { getByText } = render(wrapInTestApp()); + const { getByText } = render(wrapInTestApp()); expect(getByText(/N\/A.*/)).toBeInTheDocument(); }); }); diff --git a/packages/core/src/components/ProgressBars/ProgressCard.tsx b/packages/core/src/components/ProgressBars/GaugeCard.tsx similarity index 90% rename from packages/core/src/components/ProgressBars/ProgressCard.tsx rename to packages/core/src/components/ProgressBars/GaugeCard.tsx index eef6312600..fc7055f705 100644 --- a/packages/core/src/components/ProgressBars/ProgressCard.tsx +++ b/packages/core/src/components/ProgressBars/GaugeCard.tsx @@ -18,7 +18,7 @@ import React, { FC } from 'react'; import { makeStyles } from '@material-ui/core'; import { InfoCard } from '../../layout/InfoCard'; import { BottomLinkProps } from '../../layout/BottomLink'; -import { CircleProgress } from './CircleProgress'; +import { GaugeProgress } from './GaugeProgress'; type Props = { title: string; @@ -36,7 +36,7 @@ const useStyles = makeStyles({ }, }); -export const ProgressCard: FC = props => { +export const GaugeCard: FC = props => { const classes = useStyles(props); const { title, subheader, progress, deepLink, variant } = props; @@ -48,7 +48,7 @@ export const ProgressCard: FC = props => { deepLink={deepLink} variant={variant} > - +
); diff --git a/packages/core/src/components/ProgressBars/CircleProgress.test.jsx b/packages/core/src/components/ProgressBars/GaugeProgress.test.jsx similarity index 82% rename from packages/core/src/components/ProgressBars/CircleProgress.test.jsx rename to packages/core/src/components/ProgressBars/GaugeProgress.test.jsx index 9b9eae1bb8..778abdf12c 100644 --- a/packages/core/src/components/ProgressBars/CircleProgress.test.jsx +++ b/packages/core/src/components/ProgressBars/GaugeProgress.test.jsx @@ -17,32 +17,32 @@ import React from 'react'; import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; -import { CircleProgress, getProgressColor } from './CircleProgress'; +import { GaugeProgress, getProgressColor } from './GaugeProgress'; -describe('', () => { +describe('', () => { it('renders without exploding', () => { const { getByText } = render( - wrapInTestApp(), + wrapInTestApp(), ); getByText('10%'); }); it('handles fractional prop', () => { const { getByText } = render( - wrapInTestApp(), + wrapInTestApp(), ); getByText('10%'); }); it('handles max prop', () => { const { getByText } = render( - wrapInTestApp(), + wrapInTestApp(), ); getByText('1%'); }); it('handles unit prop', () => { const { getByText } = render( - wrapInTestApp(), + wrapInTestApp(), ); getByText('10m'); }); diff --git a/packages/core/src/components/ProgressBars/CircleProgress.tsx b/packages/core/src/components/ProgressBars/GaugeProgress.tsx similarity index 98% rename from packages/core/src/components/ProgressBars/CircleProgress.tsx rename to packages/core/src/components/ProgressBars/GaugeProgress.tsx index 79a451b14e..14776ed431 100644 --- a/packages/core/src/components/ProgressBars/CircleProgress.tsx +++ b/packages/core/src/components/ProgressBars/GaugeProgress.tsx @@ -77,7 +77,7 @@ export function getProgressColor( return palette.status.ok; } -export const CircleProgress: FC = props => { +export const GaugeProgress: FC = props => { const classes = useStyles(props); const theme = useTheme(); const { value, fractional, inverse, unit, max } = { diff --git a/packages/core/src/components/ProgressBars/HorizontalProgress.stories.tsx b/packages/core/src/components/ProgressBars/LinearGauge.stories.tsx similarity index 79% rename from packages/core/src/components/ProgressBars/HorizontalProgress.stories.tsx rename to packages/core/src/components/ProgressBars/LinearGauge.stories.tsx index 6e8f4ed7fd..c4492986b6 100644 --- a/packages/core/src/components/ProgressBars/HorizontalProgress.stories.tsx +++ b/packages/core/src/components/ProgressBars/LinearGauge.stories.tsx @@ -15,29 +15,29 @@ */ import React from 'react'; -import { HorizontalProgress } from './HorizontalProgress'; +import { LinearGauge } from './LinearGauge'; const containerStyle = { width: 300 }; export default { - title: 'HorizontalProgress', - component: HorizontalProgress, + title: 'LinearGauge', + component: LinearGauge, }; export const Default = () => (
- +
); export const MediumProgress = () => (
- +
); export const LowProgress = () => (
- +
); diff --git a/packages/core/src/components/ProgressBars/HorizontalProgress.tsx b/packages/core/src/components/ProgressBars/LinearGauge.tsx similarity index 92% rename from packages/core/src/components/ProgressBars/HorizontalProgress.tsx rename to packages/core/src/components/ProgressBars/LinearGauge.tsx index 72bf1f34c3..73163b345f 100644 --- a/packages/core/src/components/ProgressBars/HorizontalProgress.tsx +++ b/packages/core/src/components/ProgressBars/LinearGauge.tsx @@ -19,7 +19,7 @@ import { Tooltip, useTheme } from '@material-ui/core'; // @ts-ignore import { Line } from 'rc-progress'; import { BackstageTheme } from '@backstage/theme'; -import { getProgressColor } from './CircleProgress'; +import { getProgressColor } from './GaugeProgress'; type Props = { /** @@ -28,7 +28,7 @@ type Props = { value: number; }; -export const HorizontalProgress: FC = ({ value }) => { +export const LinearGauge: FC = ({ value }) => { const theme = useTheme(); if (isNaN(value)) { return null; diff --git a/packages/core/src/components/ProgressBars/index.ts b/packages/core/src/components/ProgressBars/index.ts index c74e283ae6..c7131c8831 100644 --- a/packages/core/src/components/ProgressBars/index.ts +++ b/packages/core/src/components/ProgressBars/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export { ProgressCard } from './ProgressCard'; -export { CircleProgress } from './CircleProgress'; -export { HorizontalProgress } from './HorizontalProgress'; +export { GaugeCard } from './GaugeCard'; +export { GaugeProgress } from './GaugeProgress'; +export { LinearGauge } from './LinearGauge'; diff --git a/packages/core/src/layout/InfoCard/InfoCard.tsx b/packages/core/src/layout/InfoCard/InfoCard.tsx index 6914d396b1..09d31735cd 100644 --- a/packages/core/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core/src/layout/InfoCard/InfoCard.tsx @@ -37,6 +37,7 @@ const useStyles = makeStyles(theme => ({ }, }, header: { + display: 'inline-block', padding: theme.spacing(2, 2, 2, 2.5), }, headerTitle: { @@ -202,7 +203,7 @@ export const InfoCard = ({ }} title={title} subheader={subheader} - style={{ display: 'inline-block', ...headerStyle }} + style={{ ...headerStyle }} {...headerProps} /> diff --git a/packages/core/src/layout/Page/Page.stories.tsx b/packages/core/src/layout/Page/Page.stories.tsx index 0b91875546..8fd779196d 100644 --- a/packages/core/src/layout/Page/Page.stories.tsx +++ b/packages/core/src/layout/Page/Page.stories.tsx @@ -30,7 +30,7 @@ import { Table, StatusOK, TableColumn, - ProgressCard, + GaugeCard, TrendLine, } from '../../components'; import { Box, Typography, Link, Chip, Grid } from '@material-ui/core'; @@ -120,14 +120,14 @@ const DataGrid = () => ( direction="row" > - - { const configApi = useApi(configApiRef); @@ -35,42 +35,42 @@ export const DefaultProviderSettings = () => { return ( <> {providers.includes('google') && ( - )} {providers.includes('microsoft') && ( - )} {providers.includes('github') && ( - )} {providers.includes('gitlab') && ( - )} {providers.includes('okta') && ( - )} {providers.includes('oauth2') && ( - (theme => { - return { - root: { - position: 'relative', - alignSelf: 'stretch', - }, - arrowButtonWrapper: { - position: 'absolute', - right: 0, - width: ARROW_BUTTON_SIZE, - height: ARROW_BUTTON_SIZE, - top: -(theme.spacing(6) + ARROW_BUTTON_SIZE) / 2, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - borderRadius: '2px 0px 0px 2px', - background: theme.palette.pinSidebarButton.background, - color: theme.palette.pinSidebarButton.icon, - border: 'none', - outline: 'none', - cursor: 'pointer', - }, - arrowButtonIcon: { - transform: ({ isPinned }) => (isPinned ? 'rotate(180deg)' : 'none'), - }, - }; -}); - -export const SidebarPinButton: FC<{}> = () => { - const { isOpen } = useContext(SidebarContext); - const { isPinned, toggleSidebarPinState } = useContext( - SidebarPinStateContext, - ); - const classes = useStyles({ isPinned }); - - return ( -
- {isOpen && ( - - )} -
- ); -}; diff --git a/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx b/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx new file mode 100644 index 0000000000..5dad178ccd --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx @@ -0,0 +1,26 @@ +/* + * 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 React from 'react'; +import { List, ListSubheader } from '@material-ui/core'; +import { SidebarThemeToggle } from './ThemeToggle'; +import { SidebarPinButton } from './PinButton'; + +export const AppSettingsList = () => ( + App Settings}> + + + +); diff --git a/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx b/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx new file mode 100644 index 0000000000..34cf6fb837 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx @@ -0,0 +1,29 @@ +/* + * 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 React from 'react'; +import List from '@material-ui/core/List'; +import ListSubheader from '@material-ui/core/ListSubheader'; + +type Props = { + providerSettings: React.ReactNode; +}; + +export const AuthProvidersList = ({ providerSettings }: Props) => ( + Available Auth Providers}> + {providerSettings} + +); diff --git a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx new file mode 100644 index 0000000000..3e40ce69cc --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx @@ -0,0 +1,73 @@ +/* + * 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 React from 'react'; +import { + FeatureFlagName, + useApi, + featureFlagsApiRef, +} from '@backstage/core-api'; +import { + ListItem, + ListItemSecondaryAction, + ListItemText, + Tooltip, +} from '@material-ui/core'; +import CheckIcon from '@material-ui/icons/CheckCircle'; +import { ToggleButton } from '@material-ui/lab'; + +export type Item = { + name: FeatureFlagName; + pluginId: string; +}; + +type Props = { + featureFlag: Item; +}; + +export const FlagItem = ({ featureFlag }: Props) => { + const api = useApi(featureFlagsApiRef); + + const [enabled, setEnabled] = React.useState( + Boolean(api.getFlags().get(featureFlag.name)), + ); + + const toggleFlag = () => { + const newState = api.getFlags().toggle(featureFlag.name); + setEnabled(Boolean(newState)); + }; + + return ( + + + + + + + + + + + ); +}; diff --git a/plugins/jenkins/src/components/Layout/Layout.tsx b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx similarity index 57% rename from plugins/jenkins/src/components/Layout/Layout.tsx rename to packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx index c6000437a3..5687446511 100644 --- a/plugins/jenkins/src/components/Layout/Layout.tsx +++ b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx @@ -13,17 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core'; -export const Layout: React.FC = ({ children }) => { - return ( - -
- - -
- {children} -
- ); +import React from 'react'; +import List from '@material-ui/core/List'; +import ListSubheader from '@material-ui/core/ListSubheader'; +import { FlagItem, Item } from './FeatureFlagsItem'; + +type Props = { + featureFlags: Item[]; }; + +export const FeatureFlagsList = ({ featureFlags }: Props) => ( + Feature Flags}> + {featureFlags.map(featureFlag => ( + + ))} + +); diff --git a/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx b/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx deleted file mode 100644 index 73659eb63d..0000000000 --- a/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 { - ApiRef, - OAuthApi, - SessionStateApi, - useApi, - Subscription, - IconComponent, - SessionState, -} from '@backstage/core-api'; -import React, { FC, useState, useEffect } from 'react'; -import { ProviderSettingsItem } from './ProviderSettingsItem'; - -type OAuthProviderSidebarProps = { - title: string; - icon: IconComponent; - apiRef: ApiRef; -}; - -export const OAuthProviderSettings: FC = ({ - title, - icon, - apiRef, -}) => { - const api = useApi(apiRef); - const [signedIn, setSignedIn] = useState(false); - - useEffect(() => { - let didCancel = false; - - const checkSession = async () => { - const session = await api.getAccessToken('', { optional: true }); - if (!didCancel) { - setSignedIn(!!session); - } - }; - let subscription: Subscription; - const observeSession = () => { - subscription = api - .sessionState$() - .subscribe((sessionState: SessionState) => { - if (!didCancel) { - setSignedIn(sessionState === SessionState.SignedIn); - } - }); - }; - - checkSession(); - observeSession(); - return () => { - didCancel = true; - subscription.unsubscribe(); - }; - }, [api]); - - return ( - api.getAccessToken()} - /> - ); -}; diff --git a/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx b/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx deleted file mode 100644 index 19ee00eee1..0000000000 --- a/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 { - ApiRef, - OpenIdConnectApi, - SessionStateApi, - useApi, - Subscription, - IconComponent, - SessionState, -} from '@backstage/core-api'; -import React, { FC, useState, useEffect } from 'react'; -import { ProviderSettingsItem } from './ProviderSettingsItem'; - -export type OIDCProviderSidebarProps = { - title: string; - icon: IconComponent; - apiRef: ApiRef; -}; - -export const OIDCProviderSettings: FC = ({ - title, - icon, - apiRef, -}) => { - const api = useApi(apiRef); - const [signedIn, setSignedIn] = useState(false); - - useEffect(() => { - let didCancel = false; - - const checkSession = async () => { - const session = await api.getIdToken({ optional: true }); - if (!didCancel) { - setSignedIn(!!session); - } - }; - - let subscription: Subscription; - const observeSession = () => { - subscription = api - .sessionState$() - .subscribe((sessionState: SessionState) => { - if (!didCancel) { - setSignedIn(sessionState === SessionState.SignedIn); - } - }); - }; - - checkSession(); - observeSession(); - return () => { - didCancel = true; - subscription.unsubscribe(); - }; - }, [api]); - - return ( - api.getIdToken()} - /> - ); -}; diff --git a/packages/core/src/layout/Sidebar/Settings/PinButton.tsx b/packages/core/src/layout/Sidebar/Settings/PinButton.tsx new file mode 100644 index 0000000000..2727313ede --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/PinButton.tsx @@ -0,0 +1,64 @@ +/* + * 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 React, { useContext } from 'react'; +import { + ListItem, + ListItemSecondaryAction, + ListItemText, + Tooltip, +} from '@material-ui/core'; +import LockIcon from '@material-ui/icons/Lock'; +import LockOpenIcon from '@material-ui/icons/LockOpen'; +import { ToggleButton } from '@material-ui/lab'; +import { SidebarPinStateContext } from '../Page'; + +export const SidebarPinButton = () => { + const { isPinned, toggleSidebarPinState } = useContext( + SidebarPinStateContext, + ); + + const PinIcon = () => ( + + {isPinned ? : } + + ); + + return ( + + + + { + toggleSidebarPinState(); + }} + > + + + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx b/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx index 3d3d7439e1..4db7ea0823 100644 --- a/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx +++ b/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx @@ -14,31 +14,77 @@ * limitations under the License. */ -import React, { FC } from 'react'; -import { OAuthApi, OpenIdConnectApi, IconComponent } from '@backstage/core-api'; -import { SidebarItem } from '../Items'; -import { IconButton, Tooltip } from '@material-ui/core'; -import StarBorder from '@material-ui/icons/StarBorder'; +import React, { FC, useState, useEffect } from 'react'; +import { + ListItem, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, + Tooltip, +} from '@material-ui/core'; import PowerButton from '@material-ui/icons/PowerSettingsNew'; +import { ToggleButton } from '@material-ui/lab'; +import { + ApiRef, + SessionApi, + useApi, + IconComponent, + SessionState, +} from '@backstage/core-api'; -export const ProviderSettingsItem: FC<{ +type OAuthProviderSidebarProps = { title: string; icon: IconComponent; - signedIn: boolean; - api: OAuthApi | OpenIdConnectApi; - signInHandler: Function; -}> = ({ title, icon, signedIn, api, signInHandler }) => { + apiRef: ApiRef; +}; + +export const ProviderSettingsItem: FC = ({ + title, + icon: Icon, + apiRef, +}) => { + const api = useApi(apiRef); + const [signedIn, setSignedIn] = useState(false); + + useEffect(() => { + let didCancel = false; + + const subscription = api + .sessionState$() + .subscribe((sessionState: SessionState) => { + if (!didCancel) { + setSignedIn(sessionState === SessionState.SignedIn); + } + }); + + return () => { + didCancel = true; + subscription.unsubscribe(); + }; + }, [api]); + return ( - - (signedIn ? api.logout() : signInHandler())}> - + + + + + + (signedIn ? api.signOut() : api.signIn())} > - - - - + + + + + + ); }; diff --git a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx new file mode 100644 index 0000000000..71c4899862 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx @@ -0,0 +1,74 @@ +/* + * 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 React from 'react'; +import { + Card, + CardContent, + CardHeader, + makeStyles, + Divider, +} from '@material-ui/core'; +import { AppSettingsList } from './AppSettingsList'; +import { AuthProvidersList } from './AuthProviderList'; +import { FeatureFlagsList } from './FeatureFlagsList'; +import { SignInAvatar } from './SignInAvatar'; +import { UserSettingsMenu } from './UserSettingsMenu'; +import { useUserProfile } from './useUserProfileInfo'; +import { useApi, featureFlagsApiRef } from '@backstage/core-api'; + +const useStyles = makeStyles({ + root: { + minWidth: 400, + }, +}); + +type Props = { + providerSettings?: React.ReactNode; +}; + +export const SettingsDialog = ({ providerSettings }: Props) => { + const classes = useStyles(); + const { profile, displayName } = useUserProfile(); + const featureFlagsApi = useApi(featureFlagsApiRef); + const featureFlags = featureFlagsApi.getRegisteredFlags(); + + return ( + + } + action={} + title={displayName} + subheader={profile.email} + /> + + + {providerSettings && ( + <> + + + + )} + {featureFlags.length > 0 && ( + <> + + + + )} + + + ); +}; diff --git a/plugins/api-docs/src/components/ApiDefinitionWidget/ApiDefinitionWidget.tsx b/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx similarity index 50% rename from plugins/api-docs/src/components/ApiDefinitionWidget/ApiDefinitionWidget.tsx rename to packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx index e1283bb525..f0430edfcf 100644 --- a/plugins/api-docs/src/components/ApiDefinitionWidget/ApiDefinitionWidget.tsx +++ b/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx @@ -15,26 +15,28 @@ */ import React from 'react'; -import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget'; -import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget'; -import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget'; +import { BackstageTheme } from '@backstage/theme'; +import { makeStyles, Avatar } from '@material-ui/core'; +import { useUserProfile } from './useUserProfileInfo'; +import { sidebarConfig } from '../config'; -type Props = { - type: string; - definition: string; -}; - -export const ApiDefinitionWidget = ({ type, definition }: Props) => { - switch (type) { - case 'openapi': - return ; - - case 'asyncapi': - return ; - - default: - return ( - - ); - } +const useStyles = makeStyles({ + avatar: { + width: ({ size }) => size, + height: ({ size }) => size, + }, +}); + +type Props = { size?: number }; + +export const SignInAvatar = ({ size }: Props) => { + const { iconSize } = sidebarConfig; + const classes = useStyles(size ? { size } : { size: iconSize }); + const { profile, displayName } = useUserProfile(); + + return ( + + {displayName[0]} + + ); }; diff --git a/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx b/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx new file mode 100644 index 0000000000..a5e703089c --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx @@ -0,0 +1,87 @@ +/* + * 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 React from 'react'; +import { useObservable } from 'react-use'; +import LightIcon from '@material-ui/icons/WbSunny'; +import DarkIcon from '@material-ui/icons/Brightness2'; +import AutoIcon from '@material-ui/icons/BrightnessAuto'; +import { appThemeApiRef, useApi } from '@backstage/core-api'; +import ToggleButton from '@material-ui/lab/ToggleButton'; +import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'; +import { + ListItem, + ListItemText, + ListItemSecondaryAction, + Tooltip, +} from '@material-ui/core'; + +export const SidebarThemeToggle = () => { + const appThemeApi = useApi(appThemeApiRef); + const themeId = useObservable( + appThemeApi.activeThemeId$(), + appThemeApi.getActiveThemeId(), + ); + + const themeIds = appThemeApi.getInstalledThemes(); + // TODO(marcuseide): can these be put on the theme itself? + const themeIcons = { + dark: , + light: , + }; + + const handleSetTheme = ( + _event: React.MouseEvent, + newThemeId: string | undefined, + ) => { + if (themeIds.some(t => t.id === newThemeId)) { + appThemeApi.setActiveThemeId(newThemeId); + } else { + appThemeApi.setActiveThemeId(undefined); + } + }; + + return ( + + + + + {themeIds.map(theme => ( + + + {themeIcons[theme.variant]} + + + ))} + + + + + + + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx deleted file mode 100644 index 3b854801c9..0000000000 --- a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 React, { FC, useRef } from 'react'; -import { makeStyles, Avatar, Divider } from '@material-ui/core'; -import { useApi, identityApiRef } from '@backstage/core-api'; -import { SidebarItem } from '../Items'; -import ExpandLess from '@material-ui/icons/ExpandLess'; -import ExpandMore from '@material-ui/icons/ExpandMore'; - -const useStyles = makeStyles({ - avatar: { - width: 24, - height: 24, - }, -}); - -export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({ - open, - setOpen, -}) => { - const ref = useRef(); // for scrolling down when collapse item opens - const classes = useStyles(); - const identityApi = useApi(identityApiRef); - - const handleClick = () => { - setOpen(!open); - setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300); - }; - - const userId = identityApi.getUserId(); - const profile = identityApi.getProfile(); - const displayName = profile.displayName ?? userId; - const SignInAvatar = () => ( - - {displayName[0]} - - ); - - return ( - <> - - - {open ? : } - - - ); -}; diff --git a/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx b/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx new file mode 100644 index 0000000000..85dbefff9b --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx @@ -0,0 +1,77 @@ +/* + * 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 React, { useEffect, useContext } from 'react'; +import { Popover } from '@material-ui/core'; +import { SignInAvatar } from './SignInAvatar'; +import { SettingsDialog } from './SettingsDialog'; +import { SidebarItem } from '../Items'; +import { useUserProfile } from './useUserProfileInfo'; +import { SidebarContext } from '../config'; + +type Props = { + providerSettings?: React.ReactNode; +}; + +export const SidebarUserSettings = ({ providerSettings }: Props) => { + const { isOpen: sidebarOpen } = useContext(SidebarContext); + const { displayName } = useUserProfile(); + const [open, setOpen] = React.useState(false); + const [anchorEl, setAnchorEl] = React.useState( + undefined, + ); + + const handleOpen = (event?: React.MouseEvent) => { + setAnchorEl(event?.currentTarget ?? undefined); + setOpen(true); + }; + + const handleClose = () => { + setAnchorEl(undefined); + setOpen(false); + }; + + useEffect(() => { + if (!sidebarOpen && open) setOpen(false); + }, [open, sidebarOpen]); + + const SidebarAvatar = () => ; + + return ( + <> + + + + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx b/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx new file mode 100644 index 0000000000..151ddb6e75 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx @@ -0,0 +1,55 @@ +/* + * 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 React from 'react'; +import { identityApiRef, useApi } from '@backstage/core-api'; +import { IconButton, ListItemIcon, Menu, MenuItem } from '@material-ui/core'; +import SignOutIcon from '@material-ui/icons/MeetingRoom'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; + +export const UserSettingsMenu = () => { + const identityApi = useApi(identityApiRef); + const [open, setOpen] = React.useState(false); + const [anchorEl, setAnchorEl] = React.useState( + undefined, + ); + + const handleOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + setOpen(true); + }; + + const handleClose = () => { + setAnchorEl(undefined); + setOpen(false); + }; + + return ( + <> + + + + + identityApi.signOut()}> + + + + Sign Out + + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/index.ts b/packages/core/src/layout/Sidebar/Settings/index.ts index 6557ace53a..15042dc85d 100644 --- a/packages/core/src/layout/Sidebar/Settings/index.ts +++ b/packages/core/src/layout/Sidebar/Settings/index.ts @@ -15,6 +15,4 @@ */ export { ProviderSettingsItem } from './ProviderSettingsItem'; -export { OAuthProviderSettings } from './OAuthProviderSettings'; -export { OIDCProviderSettings } from './OIDCProviderSettings'; -export { UserProfile } from './UserProfile'; +export { SidebarUserSettings } from './UserSettings'; diff --git a/packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts b/packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts new file mode 100644 index 0000000000..60dae294a5 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts @@ -0,0 +1,26 @@ +/* + * 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 { useApi, identityApiRef } from '@backstage/core-api'; + +export const useUserProfile = () => { + const identityApi = useApi(identityApiRef); + const userId = identityApi.getUserId(); + const profile = identityApi.getProfile(); + const displayName = profile.displayName ?? userId; + + return { profile, displayName }; +}; diff --git a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx index d7451965fe..61975a728a 100644 --- a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx +++ b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx @@ -23,7 +23,7 @@ import { SidebarSearchField, SidebarSpace, SidebarUserSettings, - OAuthProviderSettings, + ProviderSettingsItem, } from '.'; import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined'; import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; @@ -60,7 +60,7 @@ export const SampleSidebar = () => ( = () => { - const appThemeApi = useApi(appThemeApiRef); - const themeId = useObservable( - appThemeApi.activeThemeId$(), - appThemeApi.getActiveThemeId(), - ); - - let text = 'Auto'; - let icon = AutoIcon; - switch (themeId) { - case 'dark': - text = 'Dark mode'; - icon = DarkIcon; - break; - case 'light': - text = 'Light mode'; - icon = LightIcon; - break; - default: - break; - } - - const handleToggle = () => { - if (!themeId) { - appThemeApi.setActiveThemeId('light'); - } else if (themeId === 'light') { - appThemeApi.setActiveThemeId('dark'); - } else { - appThemeApi.setActiveThemeId(undefined); - } - }; - - return ; -}; diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx deleted file mode 100644 index f586f6d077..0000000000 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 { identityApiRef, useApi } from '@backstage/core-api'; -import Collapse from '@material-ui/core/Collapse'; -import SignOutIcon from '@material-ui/icons/MeetingRoom'; -import React, { useContext, useEffect } from 'react'; -import { SidebarContext } from './config'; -import { SidebarItem } from './Items'; -import { UserProfile as SidebarUserProfile } from './Settings'; - -type SidebarUserSettingsProps = { providerSettings?: React.ReactNode }; - -export function SidebarUserSettings({ - providerSettings, -}: SidebarUserSettingsProps) { - const { isOpen: sidebarOpen } = useContext(SidebarContext); - const [open, setOpen] = React.useState(false); - const identityApi = useApi(identityApiRef); - - // Close the provider list when sidebar collapse - useEffect(() => { - if (!sidebarOpen && open) setOpen(false); - }, [open, sidebarOpen]); - - return ( - <> - - - {providerSettings} - - identityApi.logout()} - /> - - - ); -} diff --git a/packages/core/src/layout/Sidebar/index.ts b/packages/core/src/layout/Sidebar/index.ts index fdca7e4eea..a644c06e14 100644 --- a/packages/core/src/layout/Sidebar/index.ts +++ b/packages/core/src/layout/Sidebar/index.ts @@ -25,14 +25,11 @@ export { SidebarSpacer, } from './Items'; export { IntroCard, SidebarIntro } from './Intro'; -export { SidebarPinButton } from './PinButton'; export { SIDEBAR_INTRO_LOCAL_STORAGE, SidebarContext, sidebarConfig, } from './config'; export type { SidebarContextType } from './config'; -export { SidebarThemeToggle } from './SidebarThemeToggle'; -export { SidebarUserSettings } from './UserSettings'; export { DefaultProviderSettings } from './DefaultProviderSettings'; export * from './Settings'; diff --git a/packages/core/src/layout/SignInPage/auth0Provider.tsx b/packages/core/src/layout/SignInPage/auth0Provider.tsx index ae4e5f8b82..423b28ea8d 100644 --- a/packages/core/src/layout/SignInPage/auth0Provider.tsx +++ b/packages/core/src/layout/SignInPage/auth0Provider.tsx @@ -37,8 +37,8 @@ const Component: ProviderComponent = ({ onResult }) => { profile: profile!, getIdToken: () => auth0AuthApi.getBackstageIdentity().then(i => i!.idToken), - logout: async () => { - await auth0AuthApi.logout(); + signOut: async () => { + await auth0AuthApi.signOut(); }, }); } catch (error) { @@ -79,8 +79,8 @@ const loader: ProviderLoader = async apis => { userId: identity.id, profile: profile!, getIdToken: () => auth0AuthApi.getBackstageIdentity().then(i => i!.idToken), - logout: async () => { - await auth0AuthApi.logout(); + signOut: async () => { + await auth0AuthApi.signOut(); }, }; }; diff --git a/packages/core/src/layout/SignInPage/commonProvider.tsx b/packages/core/src/layout/SignInPage/commonProvider.tsx index de9452a850..b7480afa49 100644 --- a/packages/core/src/layout/SignInPage/commonProvider.tsx +++ b/packages/core/src/layout/SignInPage/commonProvider.tsx @@ -44,8 +44,8 @@ const Component: ProviderComponent = ({ config, onResult }) => { getIdToken: () => { return authApi.getBackstageIdentity().then(i => i!.idToken); }, - logout: async () => { - await authApi.logout(); + signOut: async () => { + await authApi.signOut(); }, }); } catch (error) { @@ -87,8 +87,8 @@ const loader: ProviderLoader = async (apis, apiRef) => { userId: identity.id, profile: profile!, getIdToken: () => authApi.getBackstageIdentity().then(i => i!.idToken), - logout: async () => { - await authApi.logout(); + signOut: async () => { + await authApi.signOut(); }, }; }; diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx index ff60eba228..e2c17ab80d 100644 --- a/packages/core/src/layout/SignInPage/providers.tsx +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -83,14 +83,14 @@ export const useSignInProviders = ( const apiHolder = useApiHolder(); const [loading, setLoading] = useState(true); - // This decorates the result with logout logic from this hook + // This decorates the result with sign out logic from this hook const handleWrappedResult = useCallback( (result: SignInResult) => { onResult({ ...result, - logout: async () => { + signOut: async () => { localStorage.removeItem(PROVIDER_STORAGE_KEY); - await result.logout?.(); + await result.signOut?.(); }, }); }, diff --git a/packages/core/src/layout/SignInPage/types.ts b/packages/core/src/layout/SignInPage/types.ts index 9c501e8095..48945e6988 100644 --- a/packages/core/src/layout/SignInPage/types.ts +++ b/packages/core/src/layout/SignInPage/types.ts @@ -20,19 +20,16 @@ import { SignInResult, ApiHolder, ApiRef, - OAuthApi, ProfileInfoApi, BackstageIdentityApi, - SessionStateApi, + SessionApi, } from '@backstage/core-api'; export type SignInConfig = { id: string; title: string; message: string; - apiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi - >; + apiRef: ApiRef; }; export type IdentityProviders = ('guest' | 'custom' | SignInConfig)[]; @@ -43,9 +40,7 @@ export type ProviderComponent = ComponentType< export type ProviderLoader = ( apis: ApiHolder, - apiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi - >, + apiRef: ApiRef, ) => Promise; export type SignInProvider = { diff --git a/packages/create-app/package.json b/packages/create-app/package.json index af7b78eb74..46ccfa0b40 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public" @@ -27,7 +27,7 @@ "start": "nodemon --" }, "dependencies": { - "@backstage/cli-common": "^0.1.1-alpha.21", + "@backstage/cli-common": "^0.1.1-alpha.23", "chalk": "^4.0.0", "commander": "^6.1.0", "fs-extra": "^9.0.0", diff --git a/packages/create-app/templates/default-app/.gitignore b/packages/create-app/templates/default-app/.gitignore.hbs similarity index 96% rename from packages/create-app/templates/default-app/.gitignore rename to packages/create-app/templates/default-app/.gitignore.hbs index 4f9065c60b..5f5cc739f4 100644 --- a/packages/create-app/templates/default-app/.gitignore +++ b/packages/create-app/templates/default-app/.gitignore.hbs @@ -1,4 +1,3 @@ - # Logs logs *.log @@ -31,4 +30,4 @@ dist-types site # Local configuration files -*.local.yaml +*.local.yaml \ No newline at end of file diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index bdd933e810..a7d7aa8010 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -15,6 +15,7 @@ backend: connection: ':memory:' {{/if}} {{#if dbTypePG}} + # config options: https://node-postgres.com/api/client database: client: pg connection: @@ -30,6 +31,11 @@ backend: password: $secret: env: POSTGRES_PASSWORD + # https://node-postgres.com/features/ssl + #ssl: require # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require) + #ca: # if you have a CA file and want to verify it you can uncomment this section + # $secret: + # file: /ca/server.crt {{/if}} proxy: @@ -40,6 +46,8 @@ proxy: techdocs: storageUrl: http://localhost:7000/techdocs/static/docs requestUrl: http://localhost:7000/techdocs/docs + generators: + techdocs: 'docker' lighthouse: baseUrl: http://localhost:3003 @@ -47,29 +55,38 @@ lighthouse: auth: providers: {} +scaffolder: + github: + token: + $secret: + env: GITHUB_ACCESS_TOKEN + visibility: public # or 'internal' or 'private' + catalog: + rules: + - allow: [Component, API, Group, Template, Location] + processors: + github: + providers: + - target: https://github.com + token: + $secret: + env: GITHUB_PRIVATE_TOKEN + # Example for how to add your GitHub Enterprise instance: + # - target: https://ghe.example.net + # apiBaseUrl: https://ghe.example.net/api/v3 + # token: + # $secret: + # env: GHE_PRIVATE_TOKEN locations: # Backstage example components - type: github - target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/all-components.yaml + + # Backstage example APIs - type: github - target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-order-component.yaml - - type: github - target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/podcast-api-component.yaml - - type: github - target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/queue-proxy-component.yaml - - type: github - target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/searcher-component.yaml - - type: github - target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-lib-component.yaml - - type: github - target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml - - type: github - target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml - - type: github - target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/hello-world-api.yaml - - type: github - target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/streetlights-api.yaml + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/all-apis.yaml + # Backstage example templates - type: github target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index b95b5bbc5b..19e3891551 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -16,7 +16,7 @@ "test:all": "lerna run test -- --coverage", "lint": "lerna run lint --since origin/master --", "lint:all": "lerna run lint --", - "create-plugin": "backstage-cli create-plugin", + "create-plugin": "backstage-cli create-plugin --scope backstage --no-private", "remove-plugin": "backstage-cli remove-plugin" }, "workspaces": { diff --git a/packages/create-app/templates/default-app/packages/app/public/android-chrome-192x192.png b/packages/create-app/templates/default-app/packages/app/public/android-chrome-192x192.png index 3d56edbb0d..4660f988c1 100644 Binary files a/packages/create-app/templates/default-app/packages/app/public/android-chrome-192x192.png and b/packages/create-app/templates/default-app/packages/app/public/android-chrome-192x192.png differ diff --git a/packages/create-app/templates/default-app/packages/app/public/apple-touch-icon.png b/packages/create-app/templates/default-app/packages/app/public/apple-touch-icon.png index 0977175f6f..57c05cfc9a 100644 Binary files a/packages/create-app/templates/default-app/packages/app/public/apple-touch-icon.png and b/packages/create-app/templates/default-app/packages/app/public/apple-touch-icon.png differ diff --git a/packages/create-app/templates/default-app/packages/app/public/favicon-16x16.png b/packages/create-app/templates/default-app/packages/app/public/favicon-16x16.png index a455ffac7b..58cf61a35e 100644 Binary files a/packages/create-app/templates/default-app/packages/app/public/favicon-16x16.png and b/packages/create-app/templates/default-app/packages/app/public/favicon-16x16.png differ diff --git a/packages/create-app/templates/default-app/packages/app/public/favicon-32x32.png b/packages/create-app/templates/default-app/packages/app/public/favicon-32x32.png index e2707f2d1e..c0915ece75 100644 Binary files a/packages/create-app/templates/default-app/packages/app/public/favicon-32x32.png and b/packages/create-app/templates/default-app/packages/app/public/favicon-32x32.png differ diff --git a/packages/create-app/templates/default-app/packages/app/public/favicon.ico b/packages/create-app/templates/default-app/packages/app/public/favicon.ico index 5b582704a1..5e45e5dfbd 100644 Binary files a/packages/create-app/templates/default-app/packages/app/public/favicon.ico and b/packages/create-app/templates/default-app/packages/app/public/favicon.ico differ diff --git a/packages/create-app/templates/default-app/packages/app/src/LogoFull.tsx b/packages/create-app/templates/default-app/packages/app/src/LogoFull.tsx new file mode 100644 index 0000000000..d2b1bf1080 --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app/src/LogoFull.tsx @@ -0,0 +1,46 @@ +/* + * 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 React, { FC } from 'react'; +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + svg: { + width: 'auto', + height: 30, + }, + path: { + fill: '#7df3e1', + }, +}); +const LogoFull: FC<{}> = () => { + const classes = useStyles(); + + return ( + + + + ); +}; + +export default LogoFull; diff --git a/packages/create-app/templates/default-app/packages/app/src/LogoIcon.tsx b/packages/create-app/templates/default-app/packages/app/src/LogoIcon.tsx new file mode 100644 index 0000000000..d70be3dd32 --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app/src/LogoIcon.tsx @@ -0,0 +1,47 @@ +/* + * 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 React, { FC } from 'react'; +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + svg: { + width: 'auto', + height: 28, + }, + path: { + fill: '#7df3e1', + }, +}); + +const LogoIcon: FC<{}> = () => { + const classes = useStyles(); + + return ( + + + + ); +}; + +export default LogoIcon; diff --git a/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx b/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx index 9e343b86ac..9dd9ea64c3 100644 --- a/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx @@ -1,24 +1,29 @@ -import React from 'react'; +import React, { FC, useContext } from 'react'; import HomeIcon from '@material-ui/icons/Home'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import BuildIcon from '@material-ui/icons/BuildRounded'; import RuleIcon from '@material-ui/icons/AssignmentTurnedIn'; import MapIcon from '@material-ui/icons/MyLocation'; +import { Link, makeStyles } from '@material-ui/core'; +import { NavLink } from 'react-router-dom'; +import LogoFull from './LogoFull'; +import LogoIcon from './LogoIcon'; import { Sidebar, SidebarItem, SidebarDivider, + sidebarConfig, + SidebarContext, SidebarSpace, SidebarUserSettings, - SidebarThemeToggle, - SidebarPinButton, DefaultProviderSettings, } from '@backstage/core'; export const AppSidebar = () => ( + {/* Global nav, not org-specific */} @@ -32,8 +37,39 @@ export const AppSidebar = () => ( - } /> - ); + +const useSidebarLogoStyles = makeStyles({ + root: { + width: sidebarConfig.drawerWidthClosed, + height: 3 * sidebarConfig.logoHeight, + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + marginBottom: -14, + }, + link: { + width: sidebarConfig.drawerWidthClosed, + marginLeft: 24, + }, +}); + +const SidebarLogo: FC<{}> = () => { + const classes = useSidebarLogoStyles(); + const { isOpen } = useContext(SidebarContext); + + return ( +
+ + {isOpen ? : } + +
+ ); +}; diff --git a/packages/create-app/templates/default-app/packages/backend/Dockerfile b/packages/create-app/templates/default-app/packages/backend/Dockerfile index 3e8ba36cec..a7bd814b19 100644 --- a/packages/create-app/templates/default-app/packages/backend/Dockerfile +++ b/packages/create-app/templates/default-app/packages/backend/Dockerfile @@ -2,8 +2,15 @@ FROM node:12 WORKDIR /usr/src/app -COPY . . +# Copy repo skeleton first, to avoid unnecessary docker cache invalidation. +# The skeleton contains the package.json of each package in the monorepo, +# and along with yarn.lock and the root package.json, that's enough to run yarn install. +ADD yarn.lock package.json skeleton.tar ./ RUN yarn install --frozen-lockfile --production +# This will copy the contents of the dist-workspace when running the build-image command. +# Do not use this Dockerfile outside of that command, as it will copy in the source code instead. +COPY . . + CMD ["node", "packages/backend"] diff --git a/packages/create-app/templates/default-app/packages/backend/README.md b/packages/create-app/templates/default-app/packages/backend/README.md index f94904a930..5583bff625 100644 --- a/packages/create-app/templates/default-app/packages/backend/README.md +++ b/packages/create-app/templates/default-app/packages/backend/README.md @@ -3,8 +3,8 @@ This package is an EXAMPLE of a Backstage backend. The main purpose of this package is to provide a test bed for Backstage plugins -that have a backend part. Feel free to experiment locally or within your fork -by adding dependencies and routes to this backend, to try things out. +that have a backend part. Feel free to experiment locally or within your fork by +adding dependencies and routes to this backend, to try things out. Our goal is to eventually amend the create-app flow of the CLI, such that a production ready version of a backend skeleton is made alongside the frontend @@ -33,34 +33,32 @@ LOG_LEVEL=debug \ yarn start ``` -Substitute `x` for actual values, or leave them as -dummy values just to try out the backend without using the auth or sentry features. +Substitute `x` for actual values, or leave them as dummy values just to try out +the backend without using the auth or sentry features. The backend starts up on port 7000 per default. ## Populating The Catalog -If you want to use the catalog functionality, you need to add so called locations -to the backend. These are places where the backend can find some entity descriptor -data to consume and serve. +If you want to use the catalog functionality, you need to add so called +locations to the backend. These are places where the backend can find some +entity descriptor data to consume and serve. For more information, see +[Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview#adding-components-to-the-catalog). -To get started, you can issue the following after starting the backend, from inside -the `plugins/catalog-backend` directory: - -```bash -yarn mock-data -``` - -You should then start seeing data on `localhost:7000/catalog/entities`. - -The catalog currently runs in-memory only, so feel free to try it out, but it will -need to be re-populated on next startup. +To get started quickly, this template already includes some statically configured example locations +in `app-config.yaml` under `catalog.locations`. You can remove and replace these locations as you +like, and also override them for local development in `app-config.local.yaml`. ## Authentication -We chose [Passport](http://www.passportjs.org/) as authentication platform due to its comprehensive set of supported authentication [strategies](http://www.passportjs.org/packages/). +We chose [Passport](http://www.passportjs.org/) as authentication platform due +to its comprehensive set of supported authentication +[strategies](http://www.passportjs.org/packages/). -Read more about the [auth-backend](https://github.com/spotify/backstage/blob/master/plugins/auth-backend/README.md) and [how to add a new provider](https://github.com/spotify/backstage/blob/master/docs/auth/add-auth-provider.md) +Read more about the +[auth-backend](https://github.com/spotify/backstage/blob/master/plugins/auth-backend/README.md) +and +[how to add a new provider](https://github.com/spotify/backstage/blob/master/docs/auth/add-auth-provider.md) ## Documentation diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 044ee1560a..adde81e830 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -28,6 +28,7 @@ "@backstage/plugin-scaffolder-backend": "^{{version}}", "@backstage/plugin-techdocs-backend": "^{{version}}", "@octokit/rest": "^18.0.0", + "@gitbeaker/node": "^23.5.0", "dockerode": "^3.2.0", "express": "^4.17.1", "knex": "^0.21.1", diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts index ffa10cc3c6..cfdfbb0a78 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -3,16 +3,24 @@ import { createRouter, FilePreparer, GithubPreparer, + GitlabPreparer, Preparers, + Publishers, GithubPublisher, + GitlabPublisher, CreateReactAppTemplater, Templaters, + RepoVisibilityOptions, } from '@backstage/plugin-scaffolder-backend'; import { Octokit } from '@octokit/rest'; +import { Gitlab } from '@gitbeaker/node'; import type { PluginEnvironment } from '../types'; import Docker from 'dockerode'; -export default async function createPlugin({ logger }: PluginEnvironment) { +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { const cookiecutterTemplater = new CookieCutter(); const craTemplater = new CreateReactAppTemplater(); const templaters = new Templaters(); @@ -21,19 +29,48 @@ export default async function createPlugin({ logger }: PluginEnvironment) { const filePreparer = new FilePreparer(); const githubPreparer = new GithubPreparer(); + const gitlabPreparer = new GitlabPreparer(config); const preparers = new Preparers(); preparers.register('file', filePreparer); preparers.register('github', githubPreparer); + preparers.register('gitlab', gitlabPreparer); + preparers.register('gitlab/api', gitlabPreparer); - const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN }); - const publisher = new GithubPublisher({ client: githubClient }); + const publishers = new Publishers(); + + const githubToken = config.getString('scaffolder.github.token'); + const repoVisibility = config.getString( + 'scaffolder.github.visibility', + ) as RepoVisibilityOptions; + + const githubClient = new Octokit({ auth: githubToken }); + const githubPublisher = new GithubPublisher({ + client: githubClient, + token: githubToken, + repoVisibility, + }); + publishers.register('file', githubPublisher); + publishers.register('github', githubPublisher); + + const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab.api'); + + if (gitLabConfig) { + const gitLabToken = gitLabConfig.getString('token'); + const gitLabClient = new Gitlab({ + host: gitLabConfig.getOptionalString('baseUrl'), + token: gitLabToken, + }); + const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken); + publishers.register('gitlab', gitLabPublisher); + publishers.register('gitlab/api', gitLabPublisher); + } const dockerClient = new Docker(); return await createRouter({ preparers, templaters, - publisher, + publishers, logger, dockerClient, }); diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts index dfd9eb5e33..9c7de3512b 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts @@ -15,7 +15,7 @@ export default async function createPlugin({ config, }: PluginEnvironment) { const generators = new Generators(); - const techdocsGenerator = new TechdocsGenerator(logger); + const techdocsGenerator = new TechdocsGenerator(logger, config); generators.register('techdocs', techdocsGenerator); diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index fe7272bf04..54880cee21 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.21", - "@backstage/core": "^0.1.1-alpha.21", - "@backstage/test-utils": "^0.1.1-alpha.21", - "@backstage/theme": "^0.1.1-alpha.21", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/docgen/package.json b/packages/docgen/package.json index c75ea53faf..a60a4d1d70 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -1,7 +1,7 @@ { "name": "docgen", "description": "Tool for generating API Documentation for itself", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": true, "homepage": "https://backstage.io", "repository": { diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 0af233ec8e..fdd8d089c7 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,7 +1,7 @@ { "name": "e2e-test", "description": "E2E test for verifying Backstage packages", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": true, "homepage": "https://backstage.io", "repository": { @@ -21,7 +21,7 @@ "test:e2e": "yarn start" }, "devDependencies": { - "@backstage/cli-common": "^0.1.1-alpha.21", + "@backstage/cli-common": "^0.1.1-alpha.23", "@types/fs-extra": "^9.0.1", "@types/node": "^13.7.2", "fs-extra": "^9.0.0", diff --git a/packages/e2e-test/src/e2e-test.ts b/packages/e2e-test/src/e2e-test.ts index 5dfac68d6a..969a77cb2a 100644 --- a/packages/e2e-test/src/e2e-test.ts +++ b/packages/e2e-test/src/e2e-test.ts @@ -81,7 +81,11 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) { const path = paths.resolveOwnRoot(pkgJsonPath); const pkgTemplate = await fs.readFile(path, 'utf8'); const { dependencies = {}, devDependencies = {} } = JSON.parse( - handlebars.compile(pkgTemplate)({ version: '0.0.0' }), + handlebars.compile(pkgTemplate)({ + version: '0.0.0', + privatePackage: true, + scopeName: '@backstage', + }), ); Array() @@ -272,24 +276,37 @@ async function createPlugin(pluginName: string, appDir: string) { async function testAppServe(pluginName: string, appDir: string) { const startApp = spawnPiped(['yarn', 'start'], { cwd: appDir, + env: { + ...process.env, + GITHUB_ACCESS_TOKEN: 'abc', + }, }); Browser.localhost('localhost', 3000); let successful = false; try { - const browser = new Browser(); + for (let attempts = 1; ; attempts++) { + try { + const browser = new Browser(); - await waitForPageWithText(browser, '/', 'Backstage Service Catalog'); - await waitForPageWithText( - browser, - `/${pluginName}`, - `Welcome to ${pluginName}!`, - ); + await waitForPageWithText(browser, '/', 'Backstage Service Catalog'); + await waitForPageWithText( + browser, + `/${pluginName}`, + `Welcome to ${pluginName}!`, + ); - print('Both App and Plugin loaded correctly'); - successful = true; - } catch (error) { - throw new Error(`App serve test failed, ${error}`); + print('Both App and Plugin loaded correctly'); + successful = true; + break; + } catch (error) { + if (attempts >= 5) { + throw new Error(`App serve test failed, ${error}`); + } + console.log(`App serve failed, trying again, ${error}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } } finally { // Kill entire process group, otherwise we'll end up with hanging serve processes killTree(startApp.pid); @@ -342,6 +359,10 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { const child = spawnPiped(['yarn', 'workspace', 'backend', 'start'], { cwd: appDir, + env: { + ...process.env, + GITHUB_ACCESS_TOKEN: 'abc', + }, }); let stdout = ''; @@ -386,5 +407,15 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { } } -process.on('unhandledRejection', handleError); +process.on('unhandledRejection', (error: Error) => { + // Try to avoid exiting if the unhandled error is coming from jsdom, i.e. zombie. + // Those are typically errors on the page that should be benign, at least in the + // context of this test. We have other ways of asserting that the page is being + // rendered correctly. + if (error?.stack?.includes('node_modules/jsdom/lib')) { + console.log(`Ignored error inside jsdom, ${error}`); + } else { + handleError(error); + } +}); main().catch(handleError); diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index a0a220d3de..878256ff5c 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -36,7 +36,7 @@ builder.add(identityApiRef, { getUserId: () => 'guest', getProfile: () => ({ email: 'guest@example.com' }), getIdToken: () => undefined, - logout: async () => {}, + signOut: async () => {}, }); const oauthRequestApi = builder.add( diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js index 2ee58a8c4f..c83bb221be 100644 --- a/packages/storybook/.storybook/main.js +++ b/packages/storybook/.storybook/main.js @@ -22,7 +22,7 @@ module.exports = { const [jsLoader] = config.module.rules.splice(0, 1); if (jsLoader.use[0].loader !== 'babel-loader') { throw new Error( - `Unexpected loader removed from storybook config, ${jsonLoader.use[0].loader}`, + `Unexpected loader removed from storybook config, ${jsLoader.use[0].loader}`, ); } diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 8f12a2891c..87d41d47d1 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -1,6 +1,6 @@ { "name": "storybook", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "description": "Storybook build for core package", "private": true, "scripts": { @@ -14,7 +14,7 @@ ] }, "dependencies": { - "@backstage/theme": "^0.1.1-alpha.21" + "@backstage/theme": "^0.1.1-alpha.23" }, "devDependencies": { "@storybook/addon-actions": "^6.0.21", diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 222620f976..6b31b4aae7 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "CLI for running TechDocs locally.", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public" @@ -44,7 +44,7 @@ "ext": "ts" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/cli": "^0.1.1-alpha.23", "commander": "^6.1.0", "fs-extra": "^9.0.1", "http-proxy": "^1.18.1", diff --git a/packages/techdocs-container/Dockerfile b/packages/techdocs-container/Dockerfile index 2002d93d70..b34fcbd861 100644 --- a/packages/techdocs-container/Dockerfile +++ b/packages/techdocs-container/Dockerfile @@ -18,7 +18,7 @@ FROM python:3.8-alpine RUN apk update && apk --no-cache add gcc musl-dev openjdk11-jdk curl graphviz ttf-dejavu fontconfig RUN curl -L http://sourceforge.net/projects/plantuml/files/plantuml.1.2020.16.jar/download > /opt/plantuml.jar -RUN pip install --upgrade pip && pip install mkdocs-techdocs-core==0.0.4 +RUN pip install --upgrade pip && pip install mkdocs-techdocs-core==0.0.8 # Create script to call plantuml.jar from a location in path diff --git a/packages/techdocs-container/mock-docs/docs/index.md b/packages/techdocs-container/mock-docs/docs/index.md index 77c64582de..3c323d6fc1 100644 --- a/packages/techdocs-container/mock-docs/docs/index.md +++ b/packages/techdocs-container/mock-docs/docs/index.md @@ -3,10 +3,9 @@ !!! test Testing somethin +Abbreviations: Some text about MOCDOC -\*[MOCDOC]: Mock Documentation - This is a paragraph. {: #test_id .test_class } @@ -59,3 +58,42 @@ digraph G { Goofy <-- MickeyMouse: responds @enduml ``` + +:bulb: + +=== "JavaScript" + + ```javascript + import { test } from 'something'; + + const addThingToThing = (a, b) a + b; + ``` + +=== "Java" + + ```java + public void function() { + test(); + } + ``` + +```java tab="java" + public void function() { + test(); + } +``` + +```java tab="java 2" + public void function() { + test(); + } +``` + +```javascript +import { test } from 'something'; + +const addThingToThing = (a, b) a + b; +``` + + +*[MOCDOC]: Mock Documentation diff --git a/packages/techdocs-container/techdocs-core/README.md b/packages/techdocs-container/techdocs-core/README.md index a4afed0158..49b9fce4e4 100644 --- a/packages/techdocs-container/techdocs-core/README.md +++ b/packages/techdocs-container/techdocs-core/README.md @@ -48,8 +48,84 @@ python -m black src/ **Note:** This will write to all Python files in `src/` with the formatted code. If you would like to only check to see if it passes, simply append the `--check` flag. +## MkDocs plugins and extensions + +The TechDocs Core MkDocs plugin comes with a set of extensions and plugins that mkdocs supports. Below you can find a list of all extensions and plugins that are included in the +TechDocs Core plugin: + +Plugins: + +- [search](https://www.mkdocs.org/user-guide/configuration/#search) +- [mkdocs-monorepo-plugin](https://github.com/spotify/mkdocs-monorepo-plugin) + +Extensions: + +- [admonition](https://squidfunk.github.io/mkdocs-material/reference/admonitions/#admonitions) +- [toc](https://python-markdown.github.io/extensions/toc/) +- [pymdown](https://facelessuser.github.io/pymdown-extensions/) + - caret + - critic + - details + - emoji + - superfences + - inlinehilite + - magiclink + - mark + - smartsymobls + - highlight + - extra + - tabbed + - tasklist + - tilde +- [markdown_inline_graphviz](https://pypi.org/project/markdown-inline-graphviz/) +- [plantuml_markdown](https://pypi.org/project/plantuml-markdown/) + ## Changelog +### 0.0.8 + +- Superfences and Codehilite doesn't work very well together (squidfunk/mkdocs-material#1604) so therefore the codehilite extension is replaced by pymdownx.highlight + +* Uses pymdownx extensions v.7.1 instead of 8.0.0 to allow legacy_tab_classes config. This makes the techdocs core plugin compatible with the usage of tabs for grouping markdown with the following syntax: + +```` + ```java tab="java 2" + public void function() { + .... + } + ``` +```` + +as well as the new + +```` + === "Java" + + ```java + public void function() { + .... + } + ``` +```` + +The pymdownx extension will be bumped too 8.0.0 in the near future. + +- pymdownx.tabbed is added to support tabs to group markdown content, such as codeblocks. + +- "PyMdown Extensions includes three extensions that are meant to replace their counterpart in the default Python Markdown extensions." Therefore some extensions has been taken away in this version that comes by default from pymdownx.extra which is added now (https://facelessuser.github.io/pymdown-extensions/usage_notes/#incompatible-extensions) + +### 0.0.7 + +- Fix an issue with configuration of emoji support + +### 0.0.6 + +- Further adjustments to versions to find ones that are compatible + +### 0.0.5 + +- Downgrade some versions of markdown extensions to versions that are more stable + ### 0.0.4 - Added support for more mkdocs extensions diff --git a/packages/techdocs-container/techdocs-core/requirements.txt b/packages/techdocs-container/techdocs-core/requirements.txt index 4037a174ea..75546fa992 100644 --- a/packages/techdocs-container/techdocs-core/requirements.txt +++ b/packages/techdocs-container/techdocs-core/requirements.txt @@ -4,7 +4,7 @@ mkdocs==1.1.2 mkdocs-material==5.3.2 mkdocs-monorepo-plugin==0.4.5 -plantuml-markdown==3.4.0 +plantuml-markdown==3.1.2 markdown_inline_graphviz_extension==1.1 pygments==2.6.1 pymdown-extensions==7.1 diff --git a/packages/techdocs-container/techdocs-core/setup.py b/packages/techdocs-container/techdocs-core/setup.py index 72593dc433..e5a8206150 100644 --- a/packages/techdocs-container/techdocs-core/setup.py +++ b/packages/techdocs-container/techdocs-core/setup.py @@ -17,38 +17,34 @@ from setuptools import setup, find_packages setup( - name='mkdocs-techdocs-core', - version='0.0.4', - description='A Mkdocs package that contains TechDocs defaults', - long_description='', - keywords='mkdocs', - url='https://github.com/spotify/backstage', - author='TechDocs Core', - author_email='pulp-fiction@spotify.com', - license='Apache-2.0', - python_requires='>=3.7', + name="mkdocs-techdocs-core", + version="0.0.8", + description="A Mkdocs package that contains TechDocs defaults", + long_description="", + keywords="mkdocs", + url="https://github.com/spotify/backstage", + author="TechDocs Core", + author_email="pulp-fiction@spotify.com", + license="Apache-2.0", + python_requires=">=3.7", install_requires=[ - 'mkdocs>=1.1.2', - 'mkdocs-material==5.3.2', - 'mkdocs-monorepo-plugin==0.4.5', - 'plantuml-markdown==3.4.0', - 'markdown_inline_graphviz_extension==1.1', - 'pygments==2.6.1', - 'pymdown-extensions==7.1' + "mkdocs>=1.1.2", + "mkdocs-material==5.3.2", + "mkdocs-monorepo-plugin==0.4.5", + "plantuml-markdown==3.1.2", + "markdown_inline_graphviz_extension==1.1", + "pygments==2.6.1", + "pymdown-extensions==7.1", ], classifiers=[ - 'Development Status :: 1 - Planning', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7' + "Development Status :: 1 - Planning", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", ], packages=find_packages(), - entry_points={ - 'mkdocs.plugins': [ - 'techdocs-core = src.core:TechDocsCore' - ] - } + entry_points={"mkdocs.plugins": ["techdocs-core = src.core:TechDocsCore"]}, ) diff --git a/packages/techdocs-container/techdocs-core/src/core.py b/packages/techdocs-container/techdocs-core/src/core.py index d021a69486..ed1eb7c6f6 100644 --- a/packages/techdocs-container/techdocs-core/src/core.py +++ b/packages/techdocs-container/techdocs-core/src/core.py @@ -14,10 +14,11 @@ * limitations under the License. """ -from mkdocs.plugins import BasePlugin, PluginCollection +from mkdocs.plugins import BasePlugin from mkdocs.theme import Theme from mkdocs.contrib.search import SearchPlugin from mkdocs_monorepo_plugin.plugin import MonorepoPlugin +from pymdownx.emoji import to_svg import tempfile import os @@ -53,37 +54,33 @@ class TechDocsCore(BasePlugin): # Markdown Extensions config["markdown_extensions"].append("admonition") - config["markdown_extensions"].append("abbr") - config["markdown_extensions"].append("attr_list") - config["markdown_extensions"].append("def_list") - config["markdown_extensions"].append("codehilite") - config["mdx_configs"]["codehilite"] = { - "linenums": True, - "guess_lang": False, - "pygments_style": "friendly", - } config["markdown_extensions"].append("toc") config["mdx_configs"]["toc"] = { "permalink": True, } - config["markdown_extensions"].append("footnotes") - config["markdown_extensions"].append("markdown.extensions.tables") - config["markdown_extensions"].append("pymdownx.betterem") - config["mdx_configs"]["pymdownx.betterem"] = { - "smart_enable": "all", - } + config["markdown_extensions"].append("pymdownx.caret") config["markdown_extensions"].append("pymdownx.critic") config["markdown_extensions"].append("pymdownx.details") config["markdown_extensions"].append("pymdownx.emoji") - config["mdx_configs"]["pymdownx.emoji"] = { - "emoji_generator": "!!python/name:pymdownx.emoji.to_svg", - } + config["mdx_configs"]["pymdownx.emoji"] = {"emoji_generator": to_svg} config["markdown_extensions"].append("pymdownx.inlinehilite") config["markdown_extensions"].append("pymdownx.magiclink") config["markdown_extensions"].append("pymdownx.mark") config["markdown_extensions"].append("pymdownx.smartsymbols") config["markdown_extensions"].append("pymdownx.superfences") + config["mdx_configs"]["pymdownx.superfences"] = { + "legacy_tab_classes": True, + } + config["markdown_extensions"].append("pymdownx.highlight") + config["mdx_configs"]["pymdownx.highlight"] = { + "linenums": True, + } + config["markdown_extensions"].append("pymdownx.extra") + config["mdx_configs"]["pymdownx.betterem"] = { + "smart_enable": "all", + } + config["markdown_extensions"].append("pymdownx.tabbed") config["markdown_extensions"].append("pymdownx.tasklist") config["mdx_configs"]["pymdownx.tasklist"] = { "custom_checkbox": True, diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index 8439bee779..e30a1e9409 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils-core", "description": "Utilities to test Backstage core", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 8567af8c14..2ca946e028 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.21", - "@backstage/core-api": "^0.1.1-alpha.21", - "@backstage/test-utils-core": "^0.1.1-alpha.21", - "@backstage/theme": "^0.1.1-alpha.21", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/core-api": "^0.1.1-alpha.23", + "@backstage/test-utils-core": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/packages/theme/package.json b/packages/theme/package.json index 436455633b..bd8705933f 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "@material-ui/core": "^4.11.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.21" + "@backstage/cli": "^0.1.1-alpha.23" }, "files": [ "dist" diff --git a/plugins/api-docs/README.md b/plugins/api-docs/README.md index db78b60373..17f354e997 100644 --- a/plugins/api-docs/README.md +++ b/plugins/api-docs/README.md @@ -14,8 +14,9 @@ The plugin provides a standalone list of APIs, as well as an integration into th Right now, the following API formats are supported: -- [OpenAPI](https://swagger.io/specification/) 2 & 3, -- [AsyncAPI](https://www.asyncapi.com/docs/specifications/latest/), +- [OpenAPI](https://swagger.io/specification/) 2 & 3 +- [AsyncAPI](https://www.asyncapi.com/docs/specifications/latest/) +- [GraphQL](https://graphql.org/learn/schema/) Other formats are displayed as plain text, but this can easily be extented. diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index a913316925..bc384bf796 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,15 +20,17 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.21", - "@backstage/core": "^0.1.1-alpha.21", - "@backstage/plugin-catalog": "^0.1.1-alpha.21", - "@backstage/theme": "^0.1.1-alpha.21", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/plugin-catalog": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@kyma-project/asyncapi-react": "^0.11.0", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", + "graphiql": "^1.0.0-alpha.10", + "graphql": "^15.3.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", @@ -37,9 +39,9 @@ "swagger-ui-react": "^3.31.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.21", - "@backstage/dev-utils": "^0.1.1-alpha.21", - "@backstage/test-utils": "^0.1.1-alpha.21", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/api-docs/src/catalog/EntityPageApi/EntityPageApi.tsx b/plugins/api-docs/src/catalog/EntityPageApi/EntityPageApi.tsx index da2cf71ac4..e2148184b7 100644 --- a/plugins/api-docs/src/catalog/EntityPageApi/EntityPageApi.tsx +++ b/plugins/api-docs/src/catalog/EntityPageApi/EntityPageApi.tsx @@ -16,15 +16,19 @@ import { ComponentEntity, Entity } from '@backstage/catalog-model'; import { Progress } from '@backstage/core'; -import React, { FC } from 'react'; import { Grid } from '@material-ui/core'; +import React from 'react'; import { ApiDefinitionCard, useComponentApiEntities, useComponentApiNames, } from '../../components'; -export const EntityPageApi: FC<{ entity: Entity }> = ({ entity }) => { +type Props = { + entity: Entity; +}; + +export const EntityPageApi = ({ entity }: Props) => { const apiNames = useComponentApiNames(entity as ComponentEntity); const { apiEntities, loading } = useComponentApiEntities({ @@ -39,7 +43,7 @@ export const EntityPageApi: FC<{ entity: Entity }> = ({ entity }) => { {apiNames.map(api => ( - + ))} diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx index c4a366c4d9..49b8a69318 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx @@ -15,31 +15,93 @@ */ import { ApiEntity } from '@backstage/catalog-model'; -import { InfoCard } from '@backstage/core'; -import React from 'react'; -import { ApiDefinitionWidget } from '../ApiDefinitionWidget'; +import { CardTab, useApi, TabbedCard } from '@backstage/core'; import { Alert } from '@material-ui/lab'; +import React from 'react'; +import { apiDocsConfigRef } from '../../config'; +import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget'; +import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget'; +import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget'; +import { GraphQlDefinitionWidget } from '../GraphQlDefinitionWidget'; + +export type ApiDefinitionWidget = { + type: string; + title: string; + component: (definition: string) => React.ReactElement; + rawLanguage?: string; +}; + +export function defaultDefinitionWidgets(): ApiDefinitionWidget[] { + return [ + { + type: 'openapi', + title: 'OpenAPI', + rawLanguage: 'yaml', + component: definition => ( + + ), + }, + { + type: 'asyncapi', + title: 'AsyncAPI', + rawLanguage: 'yaml', + component: definition => ( + + ), + }, + { + type: 'graphql', + title: 'GraphQL', + rawLanguage: 'graphql', + component: definition => ( + + ), + }, + ]; +} type Props = { - title?: string; apiEntity?: ApiEntity; }; -export const ApiDefinitionCard = ({ title, apiEntity }: Props) => { +export const ApiDefinitionCard = ({ apiEntity }: Props) => { + const config = useApi(apiDocsConfigRef); + const { getApiDefinitionWidget } = config; + if (!apiEntity) { + return Could not fetch the API; + } + + const definitionWidget = getApiDefinitionWidget(apiEntity); + + if (definitionWidget) { return ( - - Could not fetch the API - + + + {definitionWidget.component(apiEntity.spec.definition)} + + + + + ); } return ( - - - + + + , + ]} + /> ); }; diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/index.ts b/plugins/api-docs/src/components/ApiDefinitionCard/index.ts index b2a2f3af62..de7856f70c 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/index.ts +++ b/plugins/api-docs/src/components/ApiDefinitionCard/index.ts @@ -14,4 +14,8 @@ * limitations under the License. */ -export { ApiDefinitionCard } from './ApiDefinitionCard'; +export type { ApiDefinitionWidget } from './ApiDefinitionCard'; +export { + ApiDefinitionCard, + defaultDefinitionWidgets, +} from './ApiDefinitionCard'; diff --git a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx index aa0df3f20f..b7e9996cc0 100644 --- a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx +++ b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx @@ -34,6 +34,7 @@ import { useAsync } from 'react-use'; import { ApiDefinitionCard } from '../ApiDefinitionCard'; const REDIRECT_DELAY = 1000; + function headerProps( kind: string, namespace: string | undefined, diff --git a/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogLayout.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx similarity index 84% rename from plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogLayout.tsx rename to plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx index d846e2b0e2..a111007278 100644 --- a/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogLayout.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx @@ -21,17 +21,15 @@ type Props = { children?: React.ReactNode; }; -const ApiCatalogLayout = ({ children }: Props) => { +export const ApiExplorerLayout = ({ children }: Props) => { return (
{children} ); }; - -export default ApiCatalogLayout; diff --git a/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx similarity index 85% rename from plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.test.tsx rename to plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx index 78afa51937..e386f281e8 100644 --- a/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx @@ -20,7 +20,8 @@ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import React from 'react'; -import { ApiCatalogPage } from './ApiCatalogPage'; +import { apiDocsConfigRef } from '../../config'; +import { ApiExplorerPage } from './ApiExplorerPage'; describe('ApiCatalogPage', () => { const catalogApi: Partial = { @@ -32,6 +33,7 @@ describe('ApiCatalogPage', () => { metadata: { name: 'Entity1', }, + spec: { type: 'openapi' }, }, { apiVersion: 'backstage.io/v1alpha1', @@ -39,12 +41,17 @@ describe('ApiCatalogPage', () => { metadata: { name: 'Entity2', }, + spec: { type: 'openapi' }, }, ] as Entity[]), getLocationByEntity: () => Promise.resolve({ id: 'id', type: 'github', target: 'url' }), }; + const apiDocsConfig = { + getApiDefinitionWidget: () => undefined, + }; + const renderWrapped = (children: React.ReactNode) => render( wrapInTestApp( @@ -52,6 +59,7 @@ describe('ApiCatalogPage', () => { apis={ApiRegistry.from([ [catalogApiRef, catalogApi], [storageApiRef, MockStorageApi.create()], + [apiDocsConfigRef, apiDocsConfig], ])} > {children} @@ -63,7 +71,7 @@ describe('ApiCatalogPage', () => { // related to some theme issues in mui-table // https://github.com/mbrn/material-table/issues/1293 it('should render', async () => { - const { findByText } = renderWrapped(); + const { findByText } = renderWrapped(); expect(await findByText(/APIs \(2\)/)).toBeInTheDocument(); }); }); diff --git a/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx similarity index 59% rename from plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.tsx rename to plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx index cfeb71f7b7..b6440edc3a 100644 --- a/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx @@ -14,31 +14,42 @@ * limitations under the License. */ -import { Content, useApi } from '@backstage/core'; +import { Content, ContentHeader, SupportButton, useApi } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog'; +import { Button } from '@material-ui/core'; import React from 'react'; +import { Link as RouterLink } from 'react-router-dom'; import { useAsync } from 'react-use'; -import { ApiCatalogTable } from '../ApiCatalogTable'; -import ApiCatalogLayout from './ApiCatalogLayout'; +import { ApiExplorerTable } from '../ApiExplorerTable'; +import { ApiExplorerLayout } from './ApiExplorerLayout'; -const CatalogPageContents = () => { +export const ApiExplorerPage = () => { const catalogApi = useApi(catalogApiRef); const { loading, error, value: matchingEntities } = useAsync(() => { return catalogApi.getEntities({ kind: 'API' }); }, [catalogApi]); return ( - + - + + All your APIs + + - + ); }; - -export const ApiCatalogPage = () => ; diff --git a/plugins/api-docs/src/components/ApiCatalogTable/index.ts b/plugins/api-docs/src/components/ApiExplorerPage/index.ts similarity index 91% rename from plugins/api-docs/src/components/ApiCatalogTable/index.ts rename to plugins/api-docs/src/components/ApiExplorerPage/index.ts index 14129b2258..67f672f9a9 100644 --- a/plugins/api-docs/src/components/ApiCatalogTable/index.ts +++ b/plugins/api-docs/src/components/ApiExplorerPage/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { ApiCatalogTable } from './ApiCatalogTable'; +export { ApiExplorerPage } from './ApiExplorerPage'; diff --git a/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.test.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx similarity index 70% rename from plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.test.tsx rename to plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx index 67f6562a56..c679c2201f 100644 --- a/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx @@ -15,39 +15,50 @@ */ import { Entity } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import * as React from 'react'; -import { ApiCatalogTable } from './ApiCatalogTable'; +import { apiDocsConfigRef } from '../../config'; +import { ApiExplorerTable } from './ApiExplorerTable'; const entites: Entity[] = [ { apiVersion: 'backstage.io/v1alpha1', kind: 'API', metadata: { name: 'api1' }, + spec: { type: 'openapi' }, }, { apiVersion: 'backstage.io/v1alpha1', kind: 'API', metadata: { name: 'api2' }, + spec: { type: 'openapi' }, }, { apiVersion: 'backstage.io/v1alpha1', kind: 'API', metadata: { name: 'api3' }, + spec: { type: 'grpc' }, }, ]; +const apiRegistry = ApiRegistry.with(apiDocsConfigRef, { + getApiDefinitionWidget: () => undefined, +}); + describe('ApiCatalogTable component', () => { it('should render error message when error is passed in props', async () => { const rendered = render( wrapInTestApp( - , + + + , ), ); const errorMessage = await rendered.findByText( @@ -59,11 +70,13 @@ describe('ApiCatalogTable component', () => { it('should display entity names when loading has finished and no error occurred', async () => { const rendered = render( wrapInTestApp( - , + + + , ), ); expect(rendered.getByText(/APIs \(3\)/)).toBeInTheDocument(); diff --git a/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx similarity index 63% rename from plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.tsx rename to plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx index 3b954b5a86..485fcc216d 100644 --- a/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx @@ -14,14 +14,23 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; -import { Table, TableColumn } from '@backstage/core'; -import { Link } from '@material-ui/core'; +import { ApiEntityV1alpha1, Entity } from '@backstage/catalog-model'; +import { Table, TableColumn, useApi } from '@backstage/core'; +import { Chip, Link } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React from 'react'; import { generatePath, Link as RouterLink } from 'react-router-dom'; +import { apiDocsConfigRef } from '../../config'; import { entityRoute } from '../../routes'; +const ApiTypeTitle = ({ apiEntity }: { apiEntity: ApiEntityV1alpha1 }) => { + const config = useApi(apiDocsConfigRef); + const definition = config.getApiDefinitionWidget(apiEntity); + const type = definition ? definition.title : apiEntity.spec.type; + + return {type}; +}; + const columns: TableColumn[] = [ { title: 'Name', @@ -45,25 +54,55 @@ const columns: TableColumn[] = [ ), }, + { + title: 'Owner', + field: 'spec.owner', + }, + { + title: 'Lifecycle', + field: 'spec.lifecycle', + }, + { + title: 'Type', + field: 'spec.type', + render: (entity: Entity) => ( + + ), + }, { title: 'Description', field: 'metadata.description', }, + { + title: 'Tags', + field: 'metadata.tags', + cellStyle: { + padding: '0px 16px 0px 20px', + }, + render: (entity: Entity) => ( + <> + {entity.metadata.tags && + entity.metadata.tags.map(t => ( + + ))} + + ), + }, ]; -type CatalogTableProps = { +type ExplorerTableProps = { entities: Entity[]; titlePreamble: string; loading: boolean; error?: any; }; -export const ApiCatalogTable = ({ +export const ApiExplorerTable = ({ entities, loading, error, titlePreamble, -}: CatalogTableProps) => { +}: ExplorerTableProps) => { if (error) { return (
diff --git a/plugins/api-docs/src/components/ApiExplorerTable/index.ts b/plugins/api-docs/src/components/ApiExplorerTable/index.ts new file mode 100644 index 0000000000..a9c79861e8 --- /dev/null +++ b/plugins/api-docs/src/components/ApiExplorerTable/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { ApiExplorerTable } from './ApiExplorerTable'; diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx index 2f64c4b2ab..ac5dedfd9c 100644 --- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx @@ -136,7 +136,7 @@ const useStyles = makeStyles(theme => ({ })); type Props = { - definition: any; + definition: string; }; export const AsyncApiDefinitionWidget = ({ definition }: Props) => { diff --git a/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx new file mode 100644 index 0000000000..7e01df1460 --- /dev/null +++ b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx @@ -0,0 +1,66 @@ +/* + * 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 React, { Suspense } from 'react'; +import { buildSchema } from 'graphql'; +import { makeStyles } from '@material-ui/core/styles'; +import { Progress } from '@backstage/core'; +import { BackstageTheme } from '@backstage/theme'; + +const GraphiQL = React.lazy(() => import('graphiql')); + +const useStyles = makeStyles(() => ({ + root: { + height: '100%', + display: 'flex', + flexFlow: 'column nowrap', + }, + graphiQlWrapper: { + flex: 1, + '@global': { + '.graphiql-container': { + boxSizing: 'initial', + height: '100%', + minHeight: '600px', + flex: '1 1 auto', + }, + }, + }, +})); + +type Props = { + definition: any; +}; + +export const GraphQlDefinitionWidget = ({ definition }: Props) => { + const classes = useStyles(); + const schema = buildSchema(definition); + + return ( + }> +
+
+ Promise.resolve(null) as any} + schema={schema} + docExplorerOpen + defaultSecondaryEditorOpen={false} + /> +
+
+
+ ); +}; diff --git a/plugins/api-docs/src/components/GraphQlDefinitionWidget/index.ts b/plugins/api-docs/src/components/GraphQlDefinitionWidget/index.ts new file mode 100644 index 0000000000..b60545de15 --- /dev/null +++ b/plugins/api-docs/src/components/GraphQlDefinitionWidget/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { GraphQlDefinitionWidget } from './GraphQlDefinitionWidget'; diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx index 61f5cb2b75..b96f50166e 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx @@ -66,7 +66,7 @@ const useStyles = makeStyles(theme => ({ })); type Props = { - definition: any; + definition: string; }; export const OpenApiDefinitionWidget = ({ definition }: Props) => { diff --git a/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.tsx b/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.tsx index 46631c5a4b..0b2ebb4ca4 100644 --- a/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.tsx @@ -23,5 +23,7 @@ type Props = { }; export const PlainApiDefinitionWidget = ({ definition, language }: Props) => { - return ; + return ( + + ); }; diff --git a/plugins/api-docs/src/components/index.ts b/plugins/api-docs/src/components/index.ts index d49303c24a..cf9b189091 100644 --- a/plugins/api-docs/src/components/index.ts +++ b/plugins/api-docs/src/components/index.ts @@ -14,6 +14,13 @@ * limitations under the License. */ -export { ApiDefinitionCard } from './ApiDefinitionCard'; +export type { ApiDefinitionWidget } from './ApiDefinitionCard'; +export { + ApiDefinitionCard, + defaultDefinitionWidgets, +} from './ApiDefinitionCard'; +export { AsyncApiDefinitionWidget } from './AsyncApiDefinitionWidget'; +export { OpenApiDefinitionWidget } from './OpenApiDefinitionWidget'; +export { PlainApiDefinitionWidget } from './PlainApiDefinitionWidget'; export { useComponentApiNames } from './useComponentApiNames'; export { useComponentApiEntities } from './useComponentApiEntities'; diff --git a/plugins/api-docs/src/config.ts b/plugins/api-docs/src/config.ts new file mode 100644 index 0000000000..a90189a037 --- /dev/null +++ b/plugins/api-docs/src/config.ts @@ -0,0 +1,30 @@ +/* + * 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 { ApiEntity } from '@backstage/catalog-model'; +import { createApiRef } from '@backstage/core'; +import { ApiDefinitionWidget } from './components'; + +export const apiDocsConfigRef = createApiRef({ + id: 'plugin.api-docs.config', + description: 'Used to configure api-docs widgets', +}); + +export interface ApiDocsConfig { + getApiDefinitionWidget: ( + apiEntity: ApiEntity, + ) => ApiDefinitionWidget | undefined; +} diff --git a/plugins/api-docs/src/plugin.ts b/plugins/api-docs/src/plugin.ts index db58538b67..3fcbe8a1f4 100644 --- a/plugins/api-docs/src/plugin.ts +++ b/plugins/api-docs/src/plugin.ts @@ -14,15 +14,32 @@ * limitations under the License. */ -import { createPlugin } from '@backstage/core'; -import { ApiCatalogPage } from './components/ApiCatalogPage/ApiCatalogPage'; +import { ApiEntity } from '@backstage/catalog-model'; +import { createApiFactory, createPlugin } from '@backstage/core'; +import { ApiExplorerPage } from './components/ApiExplorerPage/ApiExplorerPage'; +import { defaultDefinitionWidgets } from './components/ApiDefinitionCard'; import { ApiEntityPage } from './components/ApiEntityPage/ApiEntityPage'; import { entityRoute, rootRoute } from './routes'; +import { apiDocsConfigRef } from './config'; export const plugin = createPlugin({ id: 'api-docs', + apis: [ + createApiFactory({ + api: apiDocsConfigRef, + deps: {}, + factory: () => { + const definitionWidgets = defaultDefinitionWidgets(); + return { + getApiDefinitionWidget: (apiEntity: ApiEntity) => { + return definitionWidgets.find(d => d.type === apiEntity.spec.type); + }, + }; + }, + }), + ], register({ router }) { - router.addRoute(rootRoute, ApiCatalogPage); + router.addRoute(rootRoute, ApiExplorerPage); router.addRoute(entityRoute, ApiEntityPage); }, }); diff --git a/plugins/api-docs/src/routes.ts b/plugins/api-docs/src/routes.ts index eea911dd62..d2d8ef10c5 100644 --- a/plugins/api-docs/src/routes.ts +++ b/plugins/api-docs/src/routes.ts @@ -23,11 +23,13 @@ export const rootRoute = createRouteRef({ path: '/api-docs', title: 'APIs', }); + export const entityRoute = createRouteRef({ icon: NoIcon, path: '/api-docs/:optionalNamespaceAndName/', title: 'API', }); + export const catalogRoute = createRouteRef({ icon: NoIcon, path: '', diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index f4080abc42..8d7fb67171 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.21", - "@backstage/config-loader": "^0.1.1-alpha.21", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/config-loader": "^0.1.1-alpha.23", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", @@ -30,7 +30,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/cli": "^0.1.1-alpha.23", "@types/supertest": "^2.0.8", "msw": "^0.19.5", "supertest": "^4.0.2" diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 2336adb860..d1c563229c 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.21", - "@backstage/config": "^0.1.1-alpha.21", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", "@types/express": "^4.17.6", "compression": "^1.7.4", "cookie-parser": "^1.4.5", @@ -49,7 +49,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/cli": "^0.1.1-alpha.23", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/jwt-decode": "2.2.1", diff --git a/plugins/catalog-backend/README.md b/plugins/catalog-backend/README.md index 7c52dfad72..1b4a653f4a 100644 --- a/plugins/catalog-backend/README.md +++ b/plugins/catalog-backend/README.md @@ -21,12 +21,9 @@ To evaluate the catalog and have a greater amount of functionality available, in # in one terminal window, run this from from the very root of the Backstage project cd packages/backend yarn start - -# open another terminal window, and run the following from the very root of the Backstage project -yarn lerna run mock-data ``` -This will launch the full example backend and populate its catalog with some mock entities. +This will launch the full example backend, populated some example entities. ## Links diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index c1b20ed7bb..7e3134a012 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.1.1-alpha.21", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -17,18 +17,17 @@ "test": "backstage-cli test", "prepack": "backstage-cli prepack", "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean", - "mock-data": "./scripts/mock-data.sh", - "mock-data:local": "./scripts/mock-data-local.sh" + "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.21", - "@backstage/catalog-model": "^0.1.1-alpha.21", - "@backstage/config": "^0.1.1-alpha.21", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", + "git-url-parse": "^11.2.0", "knex": "^0.21.1", "lodash": "^4.17.15", "morgan": "^1.10.0", @@ -41,7 +40,8 @@ "yup": "^0.29.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/cli": "^0.1.1-alpha.23", + "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", "@types/node-fetch": "^2.5.7", "@types/supertest": "^2.0.8", diff --git a/plugins/catalog-backend/scripts/mock-data-local.sh b/plugins/catalog-backend/scripts/mock-data-local.sh deleted file mode 100755 index 3bea819319..0000000000 --- a/plugins/catalog-backend/scripts/mock-data-local.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -for FILE in \ - ../../packages/catalog-model/examples/*.yaml \ -; do \ - curl \ - --location \ - --request POST 'localhost:7000/catalog/locations' \ - --header 'Content-Type: application/json' \ - --data-raw "{\"type\": \"file\", \"target\": \"../catalog-model/${FILE}\"}" - echo -done diff --git a/plugins/catalog-backend/scripts/mock-data.sh b/plugins/catalog-backend/scripts/mock-data.sh deleted file mode 100755 index 92ec647281..0000000000 --- a/plugins/catalog-backend/scripts/mock-data.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -for URL in \ - 'artist-lookup-component.yaml' \ - 'playback-order-component.yaml' \ - 'podcast-api-component.yaml' \ - 'queue-proxy-component.yaml' \ - 'searcher-component.yaml' \ - 'playback-lib-component.yaml' \ - 'www-artist-component.yaml' \ - 'shuffle-api-component.yaml' \ - 'petstore-api.yaml' \ - 'streetlights-api.yaml' \ -; do \ - curl \ - --location \ - --request POST 'localhost:7000/catalog/locations' \ - --header 'Content-Type: application/json' \ - --data-raw "{\"type\": \"github\", \"target\": \"https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/${URL}\"}" - echo -done diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts index 0c56ca4370..4ad5362fb6 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.ts @@ -343,7 +343,14 @@ export class CommonDatabase implements Database { entityName?: string, message?: string, ): Promise { - return this.database( + // Remove log entries older than a day + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - 1); + await this.database('location_update_log') + .where('created_at', '<', cutoff.toISOString()) + .del(); + + await this.database( 'location_update_log', ).insert({ status, diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index a86e073af9..8da6e20ab3 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -27,7 +27,6 @@ import { AnnotateLocationEntityProcessor } from './processors/AnnotateLocationEn import { EntityPolicyProcessor } from './processors/EntityPolicyProcessor'; import { FileReaderProcessor } from './processors/FileReaderProcessor'; import { GithubReaderProcessor } from './processors/GithubReaderProcessor'; -import { GithubApiReaderProcessor } from './processors/GithubApiReaderProcessor'; import { GitlabApiReaderProcessor } from './processors/GitlabApiReaderProcessor'; import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor'; import { BitbucketApiReaderProcessor } from './processors/BitbucketApiReaderProcessor'; @@ -67,18 +66,19 @@ export class LocationReaders implements LocationReader { private readonly rulesEnforcer: CatalogRulesEnforcer; static defaultProcessors(options: { + logger: Logger; config?: Config; entityPolicy?: EntityPolicy; }): LocationProcessor[] { const { + logger, config = new ConfigReader({}, 'missing-config'), entityPolicy = new EntityPolicies(), } = options; return [ StaticLocationProcessor.fromConfig(config), new FileReaderProcessor(), - new GithubReaderProcessor(config), - new GithubApiReaderProcessor(config), + GithubReaderProcessor.fromConfig(config, logger), new GitlabApiReaderProcessor(config), new GitlabReaderProcessor(), new BitbucketApiReaderProcessor(config), @@ -94,7 +94,7 @@ export class LocationReaders implements LocationReader { constructor({ logger = getVoidLogger(), config, - processors = LocationReaders.defaultProcessors({ config }), + processors = LocationReaders.defaultProcessors({ logger, config }), }: Options) { this.logger = logger; this.processors = processors; diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.test.ts deleted file mode 100644 index 51c3cf4cd0..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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 { GithubApiReaderProcessor } from './GithubApiReaderProcessor'; -import { ConfigReader } from '@backstage/config'; - -describe('GithubApiReaderProcessor', () => { - const createConfig = (token: string | undefined) => - ConfigReader.fromConfigs([ - { - context: '', - data: { - catalog: { - processors: { - githubApi: { - privateToken: token, - }, - }, - }, - }, - }, - ]); - - it('should build raw api', () => { - const processor = new GithubApiReaderProcessor(createConfig(undefined)); - - const tests = [ - { - target: 'https://github.com/a/b/blob/master/path/to/c.yaml', - url: new URL( - 'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=master', - ), - err: undefined, - }, - { - target: 'https://api.com/a/b/blob/master/path/to/c.yaml', - url: null, - err: - 'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong GitHub URL or Invalid file path', - }, - { - target: 'com/a/b/blob/master/path/to/c.yaml', - url: null, - err: - 'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml', - }, - { - target: - 'https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-order-component.yaml', - url: new URL( - 'https://api.github.com/repos/spotify/backstage/contents/packages/catalog-model/examples/playback-order-component.yaml?ref=master', - ), - err: undefined, - }, - ]; - - for (const test of tests) { - if (test.err) { - expect(() => processor.buildRawUrl(test.target)).toThrowError(test.err); - } else if (test.url) { - expect(processor.buildRawUrl(test.target).toString()).toEqual( - test.url.toString(), - ); - } else { - throw new Error( - 'This should not have happened. Either err or url should have matched.', - ); - } - } - }); - - it('should return request options', () => { - const tests = [ - { - token: '0123456789', - expect: { - headers: { - Accept: 'application/vnd.github.v3.raw', - Authorization: 'token 0123456789', - }, - }, - }, - { - token: '', - err: - "Invalid type in config for key 'catalog.processors.githubApi.privateToken' in '', got empty-string, wanted string", - expect: { - headers: { - Accept: 'application/vnd.github.v3.raw', - }, - }, - }, - { - token: undefined, - expect: { - headers: { - Accept: 'application/vnd.github.v3.raw', - }, - }, - }, - ]; - - for (const test of tests) { - if (test.err) { - expect( - () => new GithubApiReaderProcessor(createConfig(test.token)), - ).toThrowError(test.err); - } else { - const processor = new GithubApiReaderProcessor( - createConfig(test.token), - ); - expect(processor.getRequestOptions()).toEqual(test.expect); - } - } - }); -}); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.ts deleted file mode 100644 index f8d1d6caf8..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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 { LocationSpec } from '@backstage/catalog-model'; -import fetch, { RequestInit, HeadersInit } from 'node-fetch'; -import * as result from './results'; -import { LocationProcessor, LocationProcessorEmit } from './types'; -import { Config } from '@backstage/config'; - -export class GithubApiReaderProcessor implements LocationProcessor { - private privateToken: string; - - constructor(config: Config) { - this.privateToken = - config.getOptionalString('catalog.processors.githubApi.privateToken') ?? - ''; - } - - getRequestOptions(): RequestInit { - const headers: HeadersInit = { - Accept: 'application/vnd.github.v3.raw', - }; - - if (this.privateToken !== '') { - headers.Authorization = `token ${this.privateToken}`; - } - - const requestOptions: RequestInit = { - headers, - }; - - return requestOptions; - } - - async readLocation( - location: LocationSpec, - optional: boolean, - emit: LocationProcessorEmit, - ): Promise { - if (location.type !== 'github/api') { - return false; - } - - try { - const url = this.buildRawUrl(location.target); - - const response = await fetch(url.toString(), this.getRequestOptions()); - - if (response.ok) { - const data = await response.buffer(); - emit(result.data(location, data)); - } else { - const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`; - if (response.status === 404) { - if (!optional) { - emit(result.notFoundError(location, message)); - } - } else { - emit(result.generalError(location, message)); - } - } - } catch (e) { - const message = `Unable to read ${location.type} ${location.target}, ${e}`; - emit(result.generalError(location, message)); - } - - return true; - } - - // Converts - // from: https://github.com/a/b/blob/master/path/to/c.yaml - // to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=master - buildRawUrl(target: string): URL { - try { - const url = new URL(target); - - const [ - empty, - userOrOrg, - repoName, - blobKeyword, - ref, - ...restOfPath - ] = url.pathname.split('/'); - - if ( - url.hostname !== 'github.com' || - empty !== '' || - userOrOrg === '' || - repoName === '' || - blobKeyword !== 'blob' || - !restOfPath.join('/').match(/\.yaml$/) - ) { - throw new Error('Wrong GitHub URL or Invalid file path'); - } - - // transform to api - url.pathname = [ - empty, - 'repos', - userOrOrg, - repoName, - 'contents', - ...restOfPath, - ].join('/'); - url.hostname = 'api.github.com'; - url.protocol = 'https'; - url.search = `ref=${ref}`; - - return url; - } catch (e) { - throw new Error(`Incorrect url: ${target}, ${e}`); - } - } -} diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.test.ts new file mode 100644 index 0000000000..3ab24541e7 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.test.ts @@ -0,0 +1,269 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { LocationSpec } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { + getApiRequestOptions, + getApiUrl, + getRawRequestOptions, + getRawUrl, + GithubReaderProcessor, + ProviderConfig, + readConfig, +} from './GithubReaderProcessor'; + +describe('GithubReaderProcessor', () => { + describe('getApiRequestOptions', () => { + it('sets the correct API version', () => { + const config: ProviderConfig = { target: '', apiBaseUrl: '' }; + expect((getApiRequestOptions(config).headers as any).Accept).toEqual( + 'application/vnd.github.v3.raw', + ); + }); + + it('inserts a token when needed', () => { + const withToken: ProviderConfig = { + target: '', + apiBaseUrl: '', + token: 'A', + }; + const withoutToken: ProviderConfig = { + target: '', + apiBaseUrl: '', + }; + expect( + (getApiRequestOptions(withToken).headers as any).Authorization, + ).toEqual('token A'); + expect( + (getApiRequestOptions(withoutToken).headers as any).Authorization, + ).toBeUndefined(); + }); + }); + + describe('getRawRequestOptions', () => { + it('inserts a token when needed', () => { + const withToken: ProviderConfig = { + target: '', + rawBaseUrl: '', + token: 'A', + }; + const withoutToken: ProviderConfig = { + target: '', + rawBaseUrl: '', + }; + expect( + (getRawRequestOptions(withToken).headers as any).Authorization, + ).toEqual('token A'); + expect( + (getRawRequestOptions(withoutToken).headers as any).Authorization, + ).toBeUndefined(); + }); + }); + + describe('getApiUrl', () => { + it('rejects targets that do not look like URLs', () => { + const config: ProviderConfig = { target: '', apiBaseUrl: '' }; + expect(() => getApiUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/); + }); + + it('happy path for github', () => { + const config: ProviderConfig = { + target: 'https://github.com', + apiBaseUrl: 'https://api.github.com', + }; + expect( + getApiUrl( + 'https://github.com/a/b/blob/branchname/path/to/c.yaml', + config, + ), + ).toEqual( + new URL( + 'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname', + ), + ); + expect( + getApiUrl( + 'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml', + config, + ), + ).toEqual( + new URL( + 'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname', + ), + ); + }); + + it('happy path for ghe', () => { + const config: ProviderConfig = { + target: 'https://ghe.mycompany.net', + apiBaseUrl: 'https://ghe.mycompany.net/api/v3', + }; + expect( + getApiUrl( + 'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml', + config, + ), + ).toEqual( + new URL( + 'https://ghe.mycompany.net/api/v3/repos/a/b/contents/path/to/c.yaml?ref=branchname', + ), + ); + }); + }); + + describe('getRawUrl', () => { + it('rejects targets that do not look like URLs', () => { + const config: ProviderConfig = { target: '', apiBaseUrl: '' }; + expect(() => getRawUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/); + }); + + it('happy path for github', () => { + const config: ProviderConfig = { + target: 'https://github.com', + rawBaseUrl: 'https://raw.githubusercontent.com', + }; + expect( + getRawUrl( + 'https://github.com/a/b/blob/branchname/path/to/c.yaml', + config, + ), + ).toEqual( + new URL( + 'https://raw.githubusercontent.com/a/b/branchname/path/to/c.yaml', + ), + ); + }); + + it('happy path for ghe', () => { + const config: ProviderConfig = { + target: 'https://ghe.mycompany.net', + rawBaseUrl: 'https://ghe.mycompany.net/raw', + }; + expect( + getRawUrl( + 'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml', + config, + ), + ).toEqual( + new URL('https://ghe.mycompany.net/raw/a/b/branchname/path/to/c.yaml'), + ); + }); + }); + + describe('readConfig', () => { + function config( + providers: { target: string; apiBaseUrl?: string; token?: string }[], + ) { + return ConfigReader.fromConfigs([ + { + context: '', + data: { + catalog: { processors: { github: { providers } } }, + }, + }, + ]); + } + + it('adds a default GitHub entry when missing', () => { + const output = readConfig(config([]), getVoidLogger()); + expect(output).toEqual([ + { + target: 'https://github.com', + apiBaseUrl: 'https://api.github.com', + rawBaseUrl: 'https://raw.githubusercontent.com', + }, + ]); + }); + + it('injects the correct GitHub API base URL when missing', () => { + const output = readConfig( + config([{ target: 'https://github.com' }]), + getVoidLogger(), + ); + expect(output).toEqual([ + { + target: 'https://github.com', + apiBaseUrl: 'https://api.github.com', + rawBaseUrl: 'https://raw.githubusercontent.com', + }, + ]); + }); + + it('rejects custom targets with no base URLs', () => { + expect(() => + readConfig( + config([{ target: 'https://ghe.company.com' }]), + getVoidLogger(), + ), + ).toThrow( + 'Provider at https://ghe.company.com must configure an explicit apiBaseUrl or rawBaseUrl', + ); + }); + + it('rejects funky configs', () => { + expect(() => + readConfig(config([{ target: 7 } as any]), getVoidLogger()), + ).toThrow(/target/); + expect(() => + readConfig(config([{ noTarget: '7' } as any]), getVoidLogger()), + ).toThrow(/target/); + expect(() => + readConfig( + config([{ target: 'https://github.com', apiBaseUrl: 7 } as any]), + getVoidLogger(), + ), + ).toThrow(/apiBaseUrl/); + expect(() => + readConfig( + config([{ target: 'https://github.com', token: 7 } as any]), + getVoidLogger(), + ), + ).toThrow(/token/); + }); + }); + + describe('implementation', () => { + it('rejects unknown types', async () => { + const processor = new GithubReaderProcessor([ + { target: 'https://github.com', apiBaseUrl: 'https://api.github.com' }, + ]); + const location: LocationSpec = { + type: 'not-github/api', + target: 'https://github.com', + }; + await expect( + processor.readLocation(location, false, () => {}), + ).resolves.toBeFalsy(); + }); + + it('rejects unknown targets', async () => { + const processor = new GithubReaderProcessor([ + { target: 'https://github.com', apiBaseUrl: 'https://api.github.com' }, + ]); + const location: LocationSpec = { + type: 'github/api', + target: 'https://not.github.com/apa', + }; + await expect( + processor.readLocation(location, false, () => {}), + ).rejects.toThrow( + /There is no GitHub provider that matches https:\/\/not.github.com\/apa/, + ); + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts index 9f38d782fe..4f0c66148f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts @@ -15,33 +15,207 @@ */ import { LocationSpec } from '@backstage/catalog-model'; -import fetch, { RequestInit, HeadersInit } from 'node-fetch'; +import { Config } from '@backstage/config'; +import parseGitUri from 'git-url-parse'; +import fetch, { HeadersInit, RequestInit } from 'node-fetch'; +import { Logger } from 'winston'; import * as result from './results'; import { LocationProcessor, LocationProcessorEmit } from './types'; -import { Config } from '@backstage/config'; -export class GithubReaderProcessor implements LocationProcessor { - private privateToken: string; +/** + * The configuration parameters for a single GitHub API provider. + */ +export type ProviderConfig = { + /** + * The prefix of the target that this matches on, e.g. "https://github.com", + * with no trailing slash. + */ + target: string; - constructor(config?: Config) { - this.privateToken = - config?.getOptionalString('catalog.processors.github.privateToken') ?? ''; + /** + * The base URL of the API of this provider, e.g. "https://api.github.com", + * with no trailing slash. + * + * May be omitted specifically for GitHub; then it will be deduced. + * + * The API will always be preferred if both its base URL and a token are + * present. + */ + apiBaseUrl?: string; + + /** + * The base URL of the raw fetch endpoint of this provider, e.g. + * "https://raw.githubusercontent.com", with no trailing slash. + * + * May be omitted specifically for GitHub; then it will be deduced. + * + * The API will always be preferred if both its base URL and a token are + * present. + */ + rawBaseUrl?: string; + + /** + * The authorization token to use for requests to this provider. + * + * If no token is specified, anonymous access is used. + */ + token?: string; +}; + +export function getApiRequestOptions(provider: ProviderConfig): RequestInit { + const headers: HeadersInit = { + Accept: 'application/vnd.github.v3.raw', + }; + + if (provider.token) { + headers.Authorization = `token ${provider.token}`; } - getRequestOptions(): RequestInit { - const headers: HeadersInit = { - Accept: 'application/vnd.github.v3.raw', - }; + return { + headers, + }; +} - if (this.privateToken !== '') { - headers.Authorization = `token ${this.privateToken}`; +export function getRawRequestOptions(provider: ProviderConfig): RequestInit { + const headers: HeadersInit = {}; + + if (provider.token) { + headers.Authorization = `token ${provider.token}`; + } + + return { + headers, + }; +} + +// Converts for example +// from: https://github.com/a/b/blob/branchname/path/to/c.yaml +// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname +export function getApiUrl(target: string, provider: ProviderConfig): URL { + try { + const { owner, name, ref, filepathtype, filepath } = parseGitUri(target); + + if ( + !owner || + !name || + !ref || + (filepathtype !== 'blob' && filepathtype !== 'raw') || + !filepath?.match(/\.ya?ml$/) + ) { + throw new Error('Wrong URL or invalid file path'); } - const requestOptions: RequestInit = { - headers, - }; + const pathWithoutSlash = filepath.replace(/^\//, ''); + return new URL( + `${provider.apiBaseUrl}/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`, + ); + } catch (e) { + throw new Error(`Incorrect URL: ${target}, ${e}`); + } +} - return requestOptions; +// Converts for example +// from: https://github.com/a/b/blob/branchname/c.yaml +// to: https://raw.githubusercontent.com/a/b/branchname/c.yaml +export function getRawUrl(target: string, provider: ProviderConfig): URL { + try { + const { owner, name, ref, filepathtype, filepath } = parseGitUri(target); + + if ( + !owner || + !name || + !ref || + (filepathtype !== 'blob' && filepathtype !== 'raw') || + !filepath?.match(/\.ya?ml$/) + ) { + throw new Error('Wrong URL or invalid file path'); + } + + const pathWithoutSlash = filepath.replace(/^\//, ''); + return new URL( + `${provider.rawBaseUrl}/${owner}/${name}/${ref}/${pathWithoutSlash}`, + ); + } catch (e) { + throw new Error(`Incorrect URL: ${target}, ${e}`); + } +} + +export function readConfig(config: Config, logger: Logger): ProviderConfig[] { + const providers: ProviderConfig[] = []; + + // TODO(freben): Deprecate the old config root entirely in a later release + if (config.has('catalog.processors.githubApi')) { + logger.warn( + 'The catalog.processors.githubApi configuration key has been deprecated, please use catalog.processors.github instead', + ); + } + + // In a previous version of the configuration, we only supported github, + // and the "privateToken" key held the token to use for it. The new + // configuration method is to use the "providers" key instead. + const providerConfigs = + config.getOptionalConfigArray('catalog.processors.github.providers') ?? + config.getOptionalConfigArray('catalog.processors.githubApi.providers') ?? + []; + const legacyToken = + config.getOptionalString('catalog.processors.github.privateToken') ?? + config.getOptionalString('catalog.processors.githubApi.privateToken'); + + // First read all the explicit providers + for (const providerConfig of providerConfigs) { + const target = providerConfig.getString('target').replace(/\/+$/, ''); + let apiBaseUrl = providerConfig.getOptionalString('apiBaseUrl'); + let rawBaseUrl = providerConfig.getOptionalString('rawBaseUrl'); + const token = providerConfig.getOptionalString('token'); + + if (apiBaseUrl) { + apiBaseUrl = apiBaseUrl.replace(/\/+$/, ''); + } else if (target === 'https://github.com') { + apiBaseUrl = 'https://api.github.com'; + } + + if (rawBaseUrl) { + rawBaseUrl = rawBaseUrl.replace(/\/+$/, ''); + } else if (target === 'https://github.com') { + rawBaseUrl = 'https://raw.githubusercontent.com'; + } + + if (!apiBaseUrl && !rawBaseUrl) { + throw new Error( + `Provider at ${target} must configure an explicit apiBaseUrl or rawBaseUrl`, + ); + } + + providers.push({ target, apiBaseUrl, rawBaseUrl, token }); + } + + // If no explicit github.com provider was added, put one in the list as + // a convenience + if (!providers.some(p => p.target === 'https://github.com')) { + providers.push({ + target: 'https://github.com', + apiBaseUrl: 'https://api.github.com', + rawBaseUrl: 'https://raw.githubusercontent.com', + token: legacyToken, + }); + } + + return providers; +} + +/** + * A processor that adds the ability to read files from GitHub v3 APIs, such as + * the one exposed by GitHub itself. + */ +export class GithubReaderProcessor implements LocationProcessor { + private providers: ProviderConfig[]; + + static fromConfig(config: Config, logger: Logger) { + return new GithubReaderProcessor(readConfig(config, logger)); + } + + constructor(providers: ProviderConfig[]) { + this.providers = providers; } async readLocation( @@ -49,16 +223,30 @@ export class GithubReaderProcessor implements LocationProcessor { optional: boolean, emit: LocationProcessorEmit, ): Promise { - if (location.type !== 'github') { + // The github/api type is for backward compatibility + if (location.type !== 'github' && location.type !== 'github/api') { return false; } - try { - const url = this.buildRawUrl(location.target); + const provider = this.providers.find(p => + location.target.startsWith(`${p.target}/`), + ); + if (!provider) { + throw new Error( + `There is no GitHub provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.github.providers.`, + ); + } - // TODO(freben): Should "hard" errors thrown by this line be treated as - // notFound instead of fatal? - const response = await fetch(url.toString(), this.getRequestOptions()); + try { + const useApi = + provider.apiBaseUrl && (provider.token || !provider.rawBaseUrl); + const url = useApi + ? getApiUrl(location.target, provider) + : getRawUrl(location.target, provider); + const options = useApi + ? getApiRequestOptions(provider) + : getRawRequestOptions(provider); + const response = await fetch(url.toString(), options); if (response.ok) { const data = await response.buffer(); @@ -80,41 +268,4 @@ export class GithubReaderProcessor implements LocationProcessor { return true; } - - // Converts - // from: https://github.com/a/b/blob/master/c.yaml - // to: https://raw.githubusercontent.com/a/b/master/c.yaml - private buildRawUrl(target: string): URL { - try { - const url = new URL(target); - - const [ - empty, - userOrOrg, - repoName, - blobKeyword, - ...restOfPath - ] = url.pathname.split('/'); - - if ( - url.hostname !== 'github.com' || - empty !== '' || - userOrOrg === '' || - repoName === '' || - blobKeyword !== 'blob' || - !restOfPath.join('/').match(/\.yaml$/) - ) { - throw new Error('Wrong GitHub URL'); - } - - // Removing the "blob" part - url.pathname = [empty, userOrOrg, repoName, ...restOfPath].join('/'); - url.hostname = 'raw.githubusercontent.com'; - url.protocol = 'https'; - - return url; - } catch (e) { - throw new Error(`Incorrect url: ${target}, ${e}`); - } - } } diff --git a/plugins/catalog-graphql/.eslintrc.js b/plugins/catalog-graphql/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/catalog-graphql/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/catalog-graphql/README.md b/plugins/catalog-graphql/README.md new file mode 100644 index 0000000000..911d4a401c --- /dev/null +++ b/plugins/catalog-graphql/README.md @@ -0,0 +1,11 @@ +# Catalog GraphQL Plugin + +## Getting Started + +This is the Catalog GraphQL plugin. + +It provides the `catalog` part of the GraphQL schema. + +To register it with the GraphQL backend, be sure to follow the [Getting Started](../graphql/README.md#getting-started) guide of the GraphQL plugin. + +