Merge branch 'master' into scaffolder-improvement

This commit is contained in:
Brian Fletcher
2021-10-26 08:55:33 +01:00
committed by GitHub
201 changed files with 1740 additions and 749 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes': patch
---
The Kubernetes plugin will now re-fetch the kubernetes objects every ten seconds (not current configurable), this allows users to track the progress of deployments without refreshing the browser.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-sentry': patch
---
feature: sentry-plugin allows passing search query for listing issues
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/test-utils': patch
'@backstage/test-utils-core': patch
---
Migrates all utility methods from `test-utils-core` into `test-utils` and delete exports from the old package.
This should have no impact since this package is considered internal and have no usages outside core packages.
Notable changes are that the testing tool `msw.setupDefaultHandlers()` have been deprecated in favour of `setupRequestMockHandlers()`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Reader will now scroll to the top of the page when navigating between pages
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Surfacing all components of the ScaffolderPage outside of the plugin so you can customize the page
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/plugin-azure-devops': patch
---
Azure DevOps frontend refactoring items from issue #7641
- Remove backend setup documentation and linked to the Azure DevOps backend plugin for these instructions
- Improved documentation to be easier to expand with new features in the future
- Removed Router based on feedback from maintainers
- Added tests for `getBuildResultComponent` and `getBuildStateComponent` from the BuildTable
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-azure-devops-backend': patch
---
Added duration (startTime and finishTime) and identity (uniqueName) to the RepoBuild results. Also did a bit of refactoring to help finish up the backend items in issue #7641
+26
View File
@@ -0,0 +1,26 @@
---
'@backstage/backend-common': patch
'@backstage/catalog-model': patch
'@backstage/cli': patch
'@backstage/config-loader': patch
'@backstage/core-app-api': patch
'@backstage/errors': patch
'@backstage/search-common': patch
'@backstage/test-utils': patch
'@backstage/plugin-app-backend': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-catalog-backend-module-ldap': patch
'@backstage/plugin-catalog-graphql': patch
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-config-schema': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch
'@backstage/plugin-scaffolder-backend-module-rails': patch
'@backstage/plugin-scaffolder-backend-module-yeoman': patch
'@backstage/plugin-scaffolder-common': patch
'@backstage/plugin-search': patch
'@backstage/plugin-shortcuts': patch
---
Switch to use the json and observable types from `@backstage/types`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/techdocs-common': patch
---
Support optional bucketRootPath configuration parameter in S3 and GCS publishers
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/config': patch
'@backstage/core-app-api': patch
'@backstage/core-plugin-api': patch
---
Replace usage of test-utils-core with test-utils
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-sentry': patch
---
fix: sentry-plugin correction for allowed period values
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+8
View File
@@ -63,6 +63,10 @@ techdocs:
# (Required) Cloud Storage Bucket Name
bucketName: 'techdocs-storage'
# (Optional) Location in storage bucket to save files
# If not set, the default location will be the root of the storage bucket
bucketRootPath: '/'
# (Optional) An API key is required to write to a storage bucket.
# If missing, GOOGLE_APPLICATION_CREDENTIALS environment variable will be used.
# https://cloud.google.com/docs/authentication/production
@@ -75,6 +79,10 @@ techdocs:
# (Required) AWS S3 Bucket Name
bucketName: 'techdocs-storage'
# (Optional) Location in storage bucket to save files
# If not set, the default location will be the root of the storage bucket
bucketRootPath: '/'
# (Optional) An API key is required to write to a storage bucket.
# If not set, environment variables or aws config file will be used to authenticate.
# https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html
+81 -31
View File
@@ -4,49 +4,99 @@ title: Getting Started
description: Documentation on How to get started with Backstage
---
There are two different ways to get started with Backstage:
For most Backstage installations, installing the standalone app will bring you
the best and most streamlined experience. In this guide you will:
- **Recommended:** Create a standalone app
- **Contributors:** Clone the Backstage repository
- Deploy Backstage Standalone with npm packages
- Run Backstage Standalone with a SQLite in-memory database and demo content
Creating a standalone app makes it simpler to customize the application for your
needs and stay up to date with the project. You will depend on `@backstage`
packages from npm, making your app much smaller. This is the recommended
approach for most installations.
This guide assumes a basic understanding of working on a Linux based operating
system using tools like apt-get, npm, yarn, curl. Docker knowledge is also
helpful for making the best use of your Backstage installation.
If you want to contribute plugins or to the project in general, it's easier to
fork and clone the repository. The `@backstage` packages will be included in the
clone. That will let you stay up to date with the latest changes, and give you
an easier path to make Pull Requests.
If you are planning to contribute plugins or the project in general, we advise
you to use the
[Getting Started for Contributors](https://backstage.io/docs/getting-started/running-backstage-locally)
guide to do a repository-based installation.
### Prerequisites
- Access to a Linux-based operating system, such as Linux, MacOS or
[Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/)
- An account with elevated rights
- `curl` or `wget` installed
- Node.js Active LTS Release installed (currently v14) using one of these
methods:
- Using `nvm` (recommended)
- [Installing nvm](https://github.com/nvm-sh/nvm#install--update-script)
- [Install and change Node version with nvm](https://nodejs.org/en/download/package-manager/#nvm)
- [Binary Download](https://nodejs.org/en/download/)
- [Package manager](https://nodejs.org/en/download/package-manager/)
- [Using NodeSource packages](https://github.com/nodesource/distributions/blob/master/README.md)
- `yarn` [Installation](https://classic.yarnpkg.com/en/docs/install)
- `docker` [installation](https://docs.docker.com/engine/install/)
- `git` [installation](https://github.com/git-guides/install-git)
- If the system is not directly accessible over your network, the following
ports need to be opened: 3000, 7000
### Create your Backstage App
Backstage provides the `@backstage/create-app` package to scaffold standalone
instances of Backstage. You will need to have
[Node.js](https://nodejs.org/en/download/) Active LTS Release installed
(currently v14) 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:
To install the Backstage Standalone app, we make use of `npx`, a tool to run
Node executables straight from the registry. Running the command below will
install Backstage. The wizard will create a subdirectory inside your current
working directory.
```bash
npx @backstage/create-app
```
You will be taken through a wizard to create your app. You can read more about
this process in [Create an app](./create-an-app.md).
The wizard will ask you
### Contributing to Backstage
- The name of the app, which will also be the name of the directory
- The database type to use for the backend. For this guide, you'll be using the
SQLite option.
If you intend to make changes to the core project's packages, certain plugins,
or project documentation, then you can fork and clone
[https://github.com/backstage/backstage](https://github.com/backstage/backstage).
<p align='center'>
<img src='../assets/getting-started/wizard.png' alt='Screenshot of the wizard asking for a name for the app, and a selection menu for the database.'>
</p>
This will let you run the latest code off of the `master` branch, fix bugs or
contribute new features, run test suites, etc.
### Run the Backstage app
You can read more in our
[CONTRIBUTING](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)
guide, which can help you get setup with a Backstage development environment.
When the installation is complete you can go to the application directory and
start the app. The `yarn dev` command will run both the frontend and backend as
separate processes (named `[0]` and `[1]`) in the same window.
```bash
cd my-backstage-app
yarn dev
```
<p align='center'>
<img src='../assets/getting-started/startup.png' alt='Screenshot of the command output, with the message web pack compiled successfully.'>
</p>
It might take a little while, but as soon as the message
`[0] webpack compiled successfully` appears, you can open a browser and directly
navigate to your freshly installed Backstage portal at `http://localhost:3000`.
You can start exploring the demo immediately.
<p align='center'>
<img src='../assets/getting-started/portal.png' alt='Screenshot of the Backstage portal.'>
</p>
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/backstage/backstage/issues/new?labels=help+wanted&template=feature_template.md)
or
[plugin suggestions](https://github.com/backstage/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME),
or
[bugs](https://github.com/backstage/backstage/issues/new?labels=bug&template=bug_template.md)
you have, and feel free to
[contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)!
The most common next steps are to configure Backstage, add a plugin and moving
to a more persistent database:
- [Setting up Authentication](https://backstage.io/docs/auth/)
- [Switching from SQLite to PostgresQL](https://backstage.io/docs/tutorials/switching-sqlite-postgres)
- [Adding a plugin](https://backstage.io/docs/getting-started/configure-app-with-plugins)
+1 -1
View File
@@ -83,7 +83,7 @@
window,
document,
'script',
'https://www.datadoghq-browser-agent.com/datadog-rum.js',
'https://www.datadoghq-browser-agent.com/datadog-rum-v3.js',
'DD_RUM',
);
DD_RUM.onReady(function () {
+1 -1
View File
@@ -18,7 +18,7 @@ import { GithubCredentialsProvider } from '@backstage/integration';
import { GitHubIntegration } from '@backstage/integration';
import { GitLabIntegration } from '@backstage/integration';
import { isChildPath } from '@backstage/cli-common';
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { Knex } from 'knex';
import { Logger as Logger_2 } from 'winston';
import { MergeResult } from 'isomorphic-git';
+1
View File
@@ -34,6 +34,7 @@
"@backstage/config-loader": "^0.7.0",
"@backstage/errors": "^0.1.3",
"@backstage/integration": "^0.6.7",
"@backstage/types": "^0.1.1",
"@google-cloud/storage": "^5.8.0",
"@octokit/rest": "^18.5.3",
"@types/cors": "^2.8.6",
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { createHash } from 'crypto';
import Keyv from 'keyv';
+2 -1
View File
@@ -18,7 +18,8 @@ import { resolve as resolvePath } from 'path';
import parseArgs from 'minimist';
import { Logger } from 'winston';
import { findPaths } from '@backstage/cli-common';
import { Config, ConfigReader, JsonValue } from '@backstage/config';
import { Config, ConfigReader } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { loadConfig } from '@backstage/config-loader';
export class ObservableConfigProxy implements Config {
@@ -15,7 +15,8 @@
*/
import { Knex } from 'knex';
import { omit } from 'lodash';
import { Config, ConfigReader, JsonObject } from '@backstage/config';
import { Config, ConfigReader } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import {
createDatabaseClient,
ensureDatabaseExists,
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { Config, JsonObject } from '@backstage/config';
import { Config } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { InputError } from '@backstage/errors';
import knexFactory, { Knex } from 'knex';
import { mergeDatabaseConfig } from './config';
@@ -13,7 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader, JsonObject } from '@backstage/config';
import { ConfigReader } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { getVoidLogger } from '../logging';
import { DefaultReadTreeResponseFactory } from './tree';
import { AwsS3UrlReader } from './AwsS3UrlReader';
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { ConfigReader, JsonObject } from '@backstage/config';
import { ConfigReader } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { getVoidLogger } from '../logging';
import { DefaultReadTreeResponseFactory } from './tree';
import { GoogleGcsUrlReader } from './GoogleGcsUrlReader';
+2 -2
View File
@@ -3,9 +3,9 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { JSONSchema7 } from 'json-schema';
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { SerializedError } from '@backstage/errors';
import * as yup from 'yup';
+1
View File
@@ -32,6 +32,7 @@
"dependencies": {
"@backstage/config": "^0.1.10",
"@backstage/errors": "^0.1.3",
"@backstage/types": "^0.1.1",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.29.13",
"ajv": "^7.0.3",
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { EntityName } from '../types';
import { UNSTABLE_EntityStatus } from './EntityStatus';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import type { Entity } from '../entity/Entity';
import schema from '../schema/kinds/Template.v1beta2.schema.json';
import { ajvCompiledJsonSchemaValidator } from './util';
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { JSONSchema7 } from 'json-schema';
/**
+1
View File
@@ -32,6 +32,7 @@
"@backstage/config": "^0.1.10",
"@backstage/config-loader": "^0.7.0",
"@backstage/errors": "^0.1.3",
"@backstage/types": "^0.1.1",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^4.0.0",
"@lerna/project": "^4.0.0",
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { mergeConfigSchemas } from '@backstage/config-loader';
import { Command } from 'commander';
import { JSONSchema7 as JSONSchema } from 'json-schema';
+1 -1
View File
@@ -18,7 +18,7 @@ import { Command } from 'commander';
import { JSONSchema7 as JSONSchema } from 'json-schema';
import { stringify as stringifyYaml } from 'yaml';
import { loadCliConfig } from '../../lib/config';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { mergeConfigSchemas } from '@backstage/config-loader';
export default async (cmd: Command) => {
+1 -1
View File
@@ -15,7 +15,7 @@
*/
import { YarnInfoInspectData } from '../../lib/versioning';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
/**
* TODO: possible types
+1 -1
View File
@@ -4,7 +4,7 @@
```ts
import { AppConfig } from '@backstage/config';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { JSONSchema7 } from 'json-schema';
// @public
+1
View File
@@ -33,6 +33,7 @@
"@backstage/cli-common": "^0.1.4",
"@backstage/config": "^0.1.9",
"@backstage/errors": "^0.1.3",
"@backstage/types": "^0.1.1",
"@types/json-schema": "^7.0.6",
"ajv": "^7.0.3",
"chokidar": "^3.5.2",
+2 -1
View File
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { AppConfig, JsonObject } from '@backstage/config';
import { AppConfig } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { assertError } from '@backstage/errors';
const ENV_PREFIX = 'APP_CONFIG_';
@@ -23,7 +23,7 @@ import {
} from 'path';
import { ConfigSchemaPackageEntry } from './types';
import { getProgramFromFiles, generateSchema } from 'typescript-json-schema';
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { assertError } from '@backstage/errors';
type Item = {
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { ConfigVisibility } from './types';
import { filterByVisibility, filterErrorsByVisibility } from './filtering';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject, JsonValue } from '@backstage/config';
import { JsonObject, JsonValue } from '@backstage/types';
import {
ConfigVisibility,
DEFAULT_CONFIG_VISIBILITY,
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { AppConfig, JsonObject } from '@backstage/config';
import { AppConfig } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { compileConfigSchemas } from './compile';
import { collectConfigSchemas } from './collect';
import { filterByVisibility, filterErrorsByVisibility } from './filtering';
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { AppConfig, JsonObject } from '@backstage/config';
import { AppConfig } from '@backstage/config';
import { JsonObject } from '@backstage/types';
/**
* An sub-set of configuration schema.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject, JsonValue } from '@backstage/config';
import { JsonObject, JsonValue } from '@backstage/types';
import { assertError } from '@backstage/errors';
import { TransformFunc } from './types';
import { isObject } from './utils';
@@ -16,7 +16,7 @@
import yaml from 'yaml';
import { extname, dirname, resolve as resolvePath } from 'path';
import { JsonObject, JsonValue } from '@backstage/config';
import { JsonObject, JsonValue } from '@backstage/types';
import { isObject } from './utils';
import { TransformFunc, EnvFunc, ReadFileFunc } from './types';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { TransformFunc, EnvFunc } from './types';
/**
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonValue } from '@backstage/config';
import { JsonValue } from '@backstage/types';
export type EnvFunc = (name: string) => Promise<string | undefined>;
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonValue, JsonObject } from '@backstage/config';
import { JsonValue, JsonObject } from '@backstage/types';
export function isObject(obj: JsonValue | undefined): obj is JsonObject {
if (typeof obj !== 'object') {
+2 -2
View File
@@ -30,11 +30,11 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/types": "^0.1.0",
"@backstage/types": "^0.1.1",
"lodash": "^4.17.21"
},
"devDependencies": {
"@backstage/test-utils-core": "^0.1.2",
"@backstage/test-utils": "^0.1.19",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32"
},
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { withLogCollector } from '@backstage/test-utils-core';
import { withLogCollector } from '@backstage/test-utils';
import { ConfigReader } from './reader';
const DATA = {
+2 -3
View File
@@ -40,8 +40,7 @@ import { IconComponent } from '@backstage/core-plugin-api';
import { microsoftAuthApiRef } from '@backstage/core-plugin-api';
import { OAuthApi } from '@backstage/core-plugin-api';
import { OAuthRequestApi } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/core-plugin-api';
import { Observable as Observable_2 } from '@backstage/types';
import { Observable } from '@backstage/types';
import { oktaAuthApiRef } from '@backstage/core-plugin-api';
import { oneloginAuthApiRef } from '@backstage/core-plugin-api';
import { OpenIdConnectApi } from '@backstage/core-plugin-api';
@@ -341,7 +340,7 @@ export const defaultConfigLoader: AppConfigLoader;
export class ErrorAlerter implements ErrorApi {
constructor(alertApi: AlertApi, errorApi: ErrorApi);
// (undocumented)
error$(): Observable_2<{
error$(): Observable<{
error: {
name: string;
message: string;
+1 -1
View File
@@ -33,6 +33,7 @@
"@backstage/config": "^0.1.10",
"@backstage/core-plugin-api": "^0.1.11",
"@backstage/theme": "^0.2.11",
"@backstage/types": "^0.1.1",
"@backstage/version-bridge": "^0.1.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
@@ -47,7 +48,6 @@
"devDependencies": {
"@backstage/cli": "^0.8.0",
"@backstage/test-utils": "^0.1.19",
"@backstage/test-utils-core": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/react-hooks": "^7.0.2",
@@ -13,7 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AlertApi, AlertMessage, Observable } from '@backstage/core-plugin-api';
import { AlertApi, AlertMessage } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { PublishSubject } from '../../../lib/subjects';
/**
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { AppThemeApi, AppTheme, Observable } from '@backstage/core-plugin-api';
import { AppThemeApi, AppTheme } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { BehaviorSubject } from '../../../lib/subjects';
const STORAGE_KEY = 'theme';
@@ -13,7 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ErrorApi, ErrorContext, Observable } from '@backstage/core-plugin-api';
import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { PublishSubject } from '../../../lib/subjects';
/**
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Observable } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { BehaviorSubject } from '../../../lib/subjects';
type RequestQueueEntry<ResultType> = {
@@ -19,8 +19,8 @@ import {
PendingAuthRequest,
AuthRequester,
AuthRequesterOptions,
Observable,
} from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests';
import { BehaviorSubject } from '../../../lib/subjects';
@@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
StorageApi,
StorageValueChange,
ErrorApi,
Observable,
} from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import ObservableImpl from 'zen-observable';
const buckets = new Map<string, WebStorage>();
@@ -24,8 +24,8 @@ import {
ProfileInfo,
BackstageIdentity,
AuthRequestOptions,
Observable,
} from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import {
AuthSessionStore,
@@ -28,8 +28,8 @@ import {
SessionState,
SessionApi,
BackstageIdentityApi,
Observable,
} from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { OAuth2Session } from './types';
import { OAuthApiCreateOptions } from '../types';
@@ -25,8 +25,8 @@ import {
ProfileInfoApi,
BackstageIdentityApi,
SessionApi,
Observable,
} from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { SamlSession } from './types';
import {
AuthSessionStore,
@@ -25,7 +25,7 @@ import {
import { ApiProvider } from './ApiProvider';
import { ApiRegistry } from './ApiRegistry';
import { render } from '@testing-library/react';
import { withLogCollector } from '@backstage/test-utils-core';
import { withLogCollector } from '@backstage/test-utils';
import { useVersionedContext } from '@backstage/version-bridge';
describe('ApiProvider', () => {
+2 -1
View File
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { AppConfig, JsonObject } from '@backstage/config';
import { AppConfig } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { Button } from '@material-ui/core';
import { ErrorPage, ErrorPanel, Progress } from '@backstage/core-components';
import { darkTheme, lightTheme } from '@backstage/theme';
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { Observable, SessionState } from '@backstage/core-plugin-api';
import { SessionState } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { OptionalRefreshSessionManagerMux } from './OptionalRefreshSessionManagerMux';
import { MutableSessionManager, SessionManager } from './types';
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { Observable, SessionState } from '@backstage/core-plugin-api';
import { SessionState } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import {
SessionManager,
MutableSessionManager,
@@ -15,7 +15,8 @@
*/
import { BehaviorSubject } from '../subjects';
import { Observable, SessionState } from '@backstage/core-plugin-api';
import { SessionState } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
export class SessionStateTracker {
private readonly subject = new BehaviorSubject<SessionState>(
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { Observable, SessionState } from '@backstage/core-plugin-api';
import { SessionState } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
export type GetSessionOptions = {
optional?: boolean;
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Observable } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import ObservableImpl from 'zen-observable';
// TODO(Rugvip): These are stopgap and probably incomplete implementations of subjects.
+2 -3
View File
@@ -31,7 +31,7 @@
"dependencies": {
"@backstage/config": "^0.1.9",
"@backstage/theme": "^0.2.9",
"@backstage/types": "^0.1.0",
"@backstage/types": "^0.1.1",
"@backstage/version-bridge": "^0.1.0",
"@material-ui/core": "^4.12.2",
"@types/react": "*",
@@ -45,8 +45,7 @@
"devDependencies": {
"@backstage/cli": "^0.8.0",
"@backstage/core-app-api": "^0.1.18",
"@backstage/test-utils": "^0.1.18",
"@backstage/test-utils-core": "^0.1.3",
"@backstage/test-utils": "^0.1.19",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/react-hooks": "^7.0.2",
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { withLogCollector } from '@backstage/test-utils-core';
import { withLogCollector } from '@backstage/test-utils';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { useAnalyticsContext } from '../analytics/AnalyticsContext';
+1 -1
View File
@@ -3,7 +3,7 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
// @public
export function assertError(value: unknown): asserts value is ErrorLike;
+1 -1
View File
@@ -30,7 +30,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.9",
"@backstage/types": "^0.1.1",
"cross-fetch": "^3.0.6",
"serialize-error": "^8.0.1"
},
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import {
deserializeError as deserializeErrorInternal,
serializeError as serializeErrorInternal,
+1 -1
View File
@@ -3,7 +3,7 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
// Warning: (ae-missing-release-tag) "DocumentCollator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
+1 -1
View File
@@ -36,7 +36,7 @@
"url": "https://github.com/backstage/backstage/issues"
},
"dependencies": {
"@backstage/config": "^0.1.6"
"@backstage/types": "^0.1.1"
},
"devDependencies": {
"@backstage/cli": "^0.8.0"
+1 -1
View File
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject } from '@backstage/types';
export interface SearchQuery {
term: string;
+68 -52
View File
@@ -1,61 +1,77 @@
const path = require('path');
const WebpackPluginFailBuildOnWarning = require('./webpack-plugin-fail-build-on-warning');
module.exports = {
stories: [
'../../core-components/src/**/*.stories.tsx',
'../../../plugins/**/src/**/*.stories.tsx',
],
addons: [
'@storybook/addon-a11y',
'@storybook/addon-actions',
'@storybook/addon-links',
'@storybook/addon-storysource',
'storybook-dark-mode/register',
],
webpackFinal: async config => {
// Mirror config in packages/cli/src/lib/bundler
config.resolve.mainFields = ['browser', 'module', 'main'];
module.exports = ({ args }) => {
// Calling storybook with no args causes our default list of stories to be used.
// This set of stories are the ones that we publish to backstage.io
//
// If it's called with args, each arg should be the path to a package that we will
// show the stories from, for example `yarn storybook plugins/catalog`.
let stories;
if (args.length === 0) {
stories = [
'../../core-components/src/**/*.stories.tsx',
'../../../plugins/org/src/**/*.stories.tsx',
'../../../plugins/search/src/**/*.stories.tsx',
];
} else {
const rootDir = path.resolve(__dirname, '../../..');
stories = args.map(arg => path.join(rootDir, arg, 'src/**/*.stories.tsx'));
}
// Remove the default babel-loader for js files, we're using sucrase instead
const [jsLoader] = config.module.rules.splice(0, 1);
if (!jsLoader.use[0].loader.includes('babel-loader')) {
throw new Error(
`Unexpected loader removed from storybook config, ${jsLoader.use[0].loader}`,
return {
stories,
addons: [
'@storybook/addon-a11y',
'@storybook/addon-actions',
'@storybook/addon-links',
'@storybook/addon-storysource',
'storybook-dark-mode/register',
],
webpackFinal: async config => {
// Mirror config in packages/cli/src/lib/bundler
config.resolve.mainFields = ['browser', 'module', 'main'];
// Remove the default babel-loader for js files, we're using sucrase instead
const [jsLoader] = config.module.rules.splice(0, 1);
if (!jsLoader.use[0].loader.includes('babel-loader')) {
throw new Error(
`Unexpected loader removed from storybook config, ${jsLoader.use[0].loader}`,
);
}
config.resolve.extensions.push('.ts', '.tsx');
config.module.rules.push(
{
test: /\.(tsx?)$/,
exclude: /node_modules/,
loader: require.resolve('@sucrase/webpack-loader'),
options: {
transforms: ['typescript', 'jsx', 'react-hot-loader'],
},
},
{
test: /\.(jsx?|mjs)$/,
exclude: /node_modules/,
loader: require.resolve('@sucrase/webpack-loader'),
options: {
transforms: ['jsx', 'react-hot-loader'],
},
},
);
}
config.resolve.extensions.push('.ts', '.tsx');
// Disable ProgressPlugin which logs verbose webpack build progress. Warnings and Errors are still logged.
config.plugins = config.plugins.filter(
({ constructor }) => constructor.name !== 'ProgressPlugin',
);
config.module.rules.push(
{
test: /\.(tsx?)$/,
exclude: /node_modules/,
loader: require.resolve('@sucrase/webpack-loader'),
options: {
transforms: ['typescript', 'jsx', 'react-hot-loader'],
},
},
{
test: /\.(jsx?|mjs)$/,
exclude: /node_modules/,
loader: require.resolve('@sucrase/webpack-loader'),
options: {
transforms: ['jsx', 'react-hot-loader'],
},
},
);
// Fail storybook build on CI if there are webpack warnings.
if (process.env.CI) {
config.plugins.push(new WebpackPluginFailBuildOnWarning());
}
// Disable ProgressPlugin which logs verbose webpack build progress. Warnings and Errors are still logged.
config.plugins = config.plugins.filter(
({ constructor }) => constructor.name !== 'ProgressPlugin',
);
// Fail storybook build on CI if there are webpack warnings.
if (process.env.CI) {
config.plugins.push(new WebpackPluginFailBuildOnWarning());
}
return config;
},
return config;
},
};
};
+10
View File
@@ -2,6 +2,16 @@
This package provides a Storybook build for Backstage. See https://backstage.io/storybook/.
## Usage
To run the storybook locally, call `yarn storybook` in repo root. This will show the default set of stories that we publish to https://backstage.io/storybook
If you want to show stories other than the default set, you can pass one or more package paths as an argument when calling storybook, for example:
```sh
yarn storybook plugins/search
```
## Why is this not part of `@backstage/core-components`?
This separate storybook package exists because of dependency conflicts with `@backstage/cli`. It uses `nohoist` to avoid the conflicts, and since you can only use that in private packages it has to be separated out of `@backstage/core-components`.
@@ -43,9 +43,11 @@ const loggerErrorSpy = jest.spyOn(logger, 'error');
const createPublisherFromConfig = ({
bucketName = 'bucketName',
bucketRootPath = '/',
legacyUseCaseSensitiveTripletPaths = false,
}: {
bucketName?: string;
bucketRootPath?: string;
legacyUseCaseSensitiveTripletPaths?: boolean;
} = {}) => {
const mockConfig = new ConfigReader({
@@ -59,6 +61,7 @@ const createPublisherFromConfig = ({
secretAccessKey: 'secretAccessKey',
},
bucketName,
bucketRootPath,
},
},
legacyUseCaseSensitiveTripletPaths,
@@ -153,6 +156,21 @@ describe('AwsS3Publish', () => {
expect(await publisher.publish({ entity, directory })).toBeUndefined();
});
it('should publish a directory when root path is specified', async () => {
const publisher = createPublisherFromConfig({
bucketRootPath: 'backstage-data/techdocs',
});
expect(await publisher.publish({ entity, directory })).toBeUndefined();
});
it('should publish a directory when root path is specified and legacy casing is used', async () => {
const publisher = createPublisherFromConfig({
bucketRootPath: 'backstage-data/techdocs',
legacyUseCaseSensitiveTripletPaths: true,
});
expect(await publisher.publish({ entity, directory })).toBeUndefined();
});
it('should fail to publish a directory', async () => {
const wrongPathToGeneratedDirectory = path.join(
rootDir,
@@ -214,6 +232,23 @@ describe('AwsS3Publish', () => {
expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true);
});
it('should return true if docs has been generated if root path is specified', async () => {
const publisher = createPublisherFromConfig({
bucketRootPath: 'backstage-data/techdocs',
});
await publisher.publish({ entity, directory });
expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true);
});
it('should return true if docs has been generated if root path is specified and legacy casing is used', async () => {
const publisher = createPublisherFromConfig({
bucketRootPath: 'backstage-data/techdocs',
legacyUseCaseSensitiveTripletPaths: true,
});
await publisher.publish({ entity, directory });
expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true);
});
it('should return false if docs has not been generated', async () => {
const publisher = createPublisherFromConfig();
expect(
@@ -247,6 +282,27 @@ describe('AwsS3Publish', () => {
);
});
it('should return tech docs metadata even if root path is specified', async () => {
const publisher = createPublisherFromConfig({
bucketRootPath: 'backstage-data/techdocs',
});
await publisher.publish({ entity, directory });
expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual(
techdocsMetadata,
);
});
it('should return tech docs metadata if root path is specified and legacy casing is used', async () => {
const publisher = createPublisherFromConfig({
bucketRootPath: 'backstage-data/techdocs',
legacyUseCaseSensitiveTripletPaths: true,
});
await publisher.publish({ entity, directory });
expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual(
techdocsMetadata,
);
});
it('should return tech docs metadata when json encoded with single quotes', async () => {
const techdocsMetadataPath = path.join(
directory,
@@ -335,6 +391,47 @@ describe('AwsS3Publish', () => {
expect(jsResponse.text).toEqual('found it too');
});
it('should pass expected object path to bucket if root path is specified', async () => {
const rootPath = 'backstage-data/techdocs';
const publisher = createPublisherFromConfig({
bucketRootPath: rootPath,
});
await publisher.publish({ entity, directory });
app = express().use(publisher.docsRouter());
const pngResponse = await request(app).get(
`/${rootPath}/${entityTripletPath}/img/with%20spaces.png`,
);
expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
'found it',
);
const jsResponse = await request(app).get(
`/${rootPath}/${entityTripletPath}/some%20folder/also%20with%20spaces.js`,
);
expect(jsResponse.text).toEqual('found it too');
});
it('should pass expected object path to bucket if root path is specified and legacy case is enabled', async () => {
const rootPath = 'backstage-data/techdocs';
const publisher = createPublisherFromConfig({
bucketRootPath: rootPath,
legacyUseCaseSensitiveTripletPaths: true,
});
await publisher.publish({ entity, directory });
app = express().use(publisher.docsRouter());
const pngResponse = await request(app).get(
`/${rootPath}/${entityTripletPath}/img/with%20spaces.png`,
);
expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
'found it',
);
const jsResponse = await request(app).get(
`/${rootPath}/${entityTripletPath}/some%20folder/also%20with%20spaces.js`,
);
expect(jsResponse.text).toEqual('found it too');
});
it('should pass text/plain content-type for html', async () => {
const htmlResponse = await request(app).get(
`/${entityTripletPath}/html/unsafe.html`,
@@ -34,6 +34,7 @@ import {
getStaleFiles,
lowerCaseEntityTriplet,
lowerCaseEntityTripletInStoragePath,
normalizeExternalStorageRootPath,
} from './helpers';
import {
PublisherBase,
@@ -67,6 +68,10 @@ export class AwsS3Publish implements PublisherBase {
);
}
const bucketRootPath = normalizeExternalStorageRootPath(
config.getOptionalString('techdocs.publisher.awsS3.bucketRootPath') || '',
);
// Credentials is an optional config. If missing, the default ways of authenticating AWS SDK V2 will be used.
// 1. AWS environment variables
// https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html
@@ -112,6 +117,7 @@ export class AwsS3Publish implements PublisherBase {
bucketName,
legacyPathCasing,
logger,
bucketRootPath,
);
}
@@ -151,11 +157,13 @@ export class AwsS3Publish implements PublisherBase {
private readonly bucketName: string,
private readonly legacyPathCasing: boolean,
private readonly logger: Logger,
private readonly bucketRootPath: string,
) {
this.storageClient = storageClient;
this.bucketName = bucketName;
this.legacyPathCasing = legacyPathCasing;
this.logger = logger;
this.bucketRootPath = bucketRootPath;
}
/**
@@ -193,6 +201,8 @@ export class AwsS3Publish implements PublisherBase {
*/
async publish({ entity, directory }: PublishRequest): Promise<void> {
const useLegacyPathCasing = this.legacyPathCasing;
const bucketRootPath = this.bucketRootPath;
// First, try to retrieve a list of all individual files currently existing
let existingFiles: string[] = [];
try {
@@ -200,6 +210,7 @@ export class AwsS3Publish implements PublisherBase {
entity,
undefined,
useLegacyPathCasing,
bucketRootPath,
);
existingFiles = await this.getAllObjectsFromBucket({
prefix: remoteFolder,
@@ -230,6 +241,7 @@ export class AwsS3Publish implements PublisherBase {
entity,
relativeFilePath,
useLegacyPathCasing,
bucketRootPath,
),
Body: fileStream,
};
@@ -257,6 +269,7 @@ export class AwsS3Publish implements PublisherBase {
entity,
path.relative(directory, absoluteFilePath),
useLegacyPathCasing,
bucketRootPath,
),
);
const staleFiles = getStaleFiles(relativeFilesToUpload, existingFiles);
@@ -289,10 +302,12 @@ export class AwsS3Publish implements PublisherBase {
try {
return await new Promise<TechDocsMetadata>(async (resolve, reject) => {
const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;
const entityRootDir = this.legacyPathCasing
const entityDir = this.legacyPathCasing
? entityTriplet
: lowerCaseEntityTriplet(entityTriplet);
const entityRootDir = path.join(this.bucketRootPath, entityDir);
const stream = this.storageClient
.getObject({
Bucket: this.bucketName,
@@ -332,10 +347,17 @@ export class AwsS3Publish implements PublisherBase {
// Decode and trim the leading forward slash
const decodedUri = decodeURI(req.path.replace(/^\//, ''));
// Root path is removed from the Uri so that legacy casing can be applied
// to the entity triplet without manipulating the root path
const decodedUriNoRoot = path.relative(this.bucketRootPath, decodedUri);
// filePath example - /default/component/documented-component/index.html
const filePath = this.legacyPathCasing
? decodedUri
: lowerCaseEntityTripletInStoragePath(decodedUri);
const filePathNoRoot = this.legacyPathCasing
? decodedUriNoRoot
: lowerCaseEntityTripletInStoragePath(decodedUriNoRoot);
// Re-prepend the root path to the relative file path
const filePath = path.join(this.bucketRootPath, filePathNoRoot);
// Files with different extensions (CSS, HTML) need to be served with different headers
const fileExtension = path.extname(filePath);
@@ -370,10 +392,12 @@ export class AwsS3Publish implements PublisherBase {
async hasDocsBeenGenerated(entity: Entity): Promise<boolean> {
try {
const entityTriplet = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
const entityRootDir = this.legacyPathCasing
const entityDir = this.legacyPathCasing
? entityTriplet
: lowerCaseEntityTriplet(entityTriplet);
const entityRootDir = path.join(this.bucketRootPath, entityDir);
await this.storageClient
.headObject({
Bucket: this.bucketName,
@@ -43,9 +43,11 @@ jest.spyOn(logger, 'error').mockReturnValue(logger);
const createPublisherFromConfig = ({
bucketName = 'bucketName',
bucketRootPath = '/',
legacyUseCaseSensitiveTripletPaths = false,
}: {
bucketName?: string;
bucketRootPath?: string;
legacyUseCaseSensitiveTripletPaths?: boolean;
} = {}) => {
const config = new ConfigReader({
@@ -56,6 +58,7 @@ const createPublisherFromConfig = ({
googleGcs: {
credentials: '{}',
bucketName,
bucketRootPath,
},
},
legacyUseCaseSensitiveTripletPaths,
@@ -149,6 +152,21 @@ describe('GoogleGCSPublish', () => {
expect(await publisher.publish({ entity, directory })).toBeUndefined();
});
it('should publish a directory when root path is specified', async () => {
const publisher = createPublisherFromConfig({
bucketRootPath: 'backstage-data/techdocs',
});
expect(await publisher.publish({ entity, directory })).toBeUndefined();
});
it('should publish a directory when root path is specified and legacy casing is used', async () => {
const publisher = createPublisherFromConfig({
bucketRootPath: 'backstage-data/techdocs',
legacyUseCaseSensitiveTripletPaths: true,
});
expect(await publisher.publish({ entity, directory })).toBeUndefined();
});
it('should fail to publish a directory', async () => {
const wrongPathToGeneratedDirectory = path.join(
rootDir,
@@ -212,6 +230,23 @@ describe('GoogleGCSPublish', () => {
expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true);
});
it('should return true if docs has been generated if root path is specified', async () => {
const publisher = createPublisherFromConfig({
bucketRootPath: 'backstage-data/techdocs',
});
await publisher.publish({ entity, directory });
expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true);
});
it('should return true if docs has been generated if root path is specified and legacy casing is used', async () => {
const publisher = createPublisherFromConfig({
bucketRootPath: 'backstage-data/techdocs',
legacyUseCaseSensitiveTripletPaths: true,
});
await publisher.publish({ entity, directory });
expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true);
});
it('should return false if docs has not been generated', async () => {
const publisher = createPublisherFromConfig();
expect(
@@ -245,6 +280,27 @@ describe('GoogleGCSPublish', () => {
);
});
it('should return tech docs metadata even if root path is specified', async () => {
const publisher = createPublisherFromConfig({
bucketRootPath: 'backstage-data/techdocs',
});
await publisher.publish({ entity, directory });
expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual(
techdocsMetadata,
);
});
it('should return tech docs metadata if root path is specified and legacy casing is used', async () => {
const publisher = createPublisherFromConfig({
bucketRootPath: 'backstage-data/techdocs',
legacyUseCaseSensitiveTripletPaths: true,
});
await publisher.publish({ entity, directory });
expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual(
techdocsMetadata,
);
});
it('should return tech docs metadata when json encoded with single quotes', async () => {
const techdocsMetadataPath = path.join(
directory,
@@ -291,6 +347,7 @@ describe('GoogleGCSPublish', () => {
describe('docsRouter', () => {
const entityTripletPath = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
// const entityTripletPath =
let app: Express.Application;
@@ -334,6 +391,47 @@ describe('GoogleGCSPublish', () => {
expect(jsResponse.text).toEqual('found it too');
});
it('should pass expected object path to bucket if root path is specified', async () => {
const rootPath = 'backstage-data/techdocs';
const publisher = createPublisherFromConfig({
bucketRootPath: rootPath,
});
await publisher.publish({ entity, directory });
app = express().use(publisher.docsRouter());
const pngResponse = await request(app).get(
`/${rootPath}/${entityTripletPath}/img/with%20spaces.png`,
);
expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
'found it',
);
const jsResponse = await request(app).get(
`/${rootPath}/${entityTripletPath}/some%20folder/also%20with%20spaces.js`,
);
expect(jsResponse.text).toEqual('found it too');
});
it('should pass expected object path to bucket if root path is specified and legacy case is enabled', async () => {
const rootPath = 'backstage-data/techdocs';
const publisher = createPublisherFromConfig({
bucketRootPath: rootPath,
legacyUseCaseSensitiveTripletPaths: true,
});
await publisher.publish({ entity, directory });
app = express().use(publisher.docsRouter());
const pngResponse = await request(app).get(
`/${rootPath}/${entityTripletPath}/img/with%20spaces.png`,
);
expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
'found it',
);
const jsResponse = await request(app).get(
`/${rootPath}/${entityTripletPath}/some%20folder/also%20with%20spaces.js`,
);
expect(jsResponse.text).toEqual('found it too');
});
it('should pass text/plain content-type for html', async () => {
const htmlResponse = await request(app).get(
`/${entityTripletPath}/html/unsafe.html`,
@@ -30,6 +30,7 @@ import {
bulkStorageOperation,
getCloudPathForLocalPath,
getStaleFiles,
normalizeExternalStorageRootPath,
} from './helpers';
import { MigrateWriteStream } from './migrations';
import {
@@ -51,6 +52,11 @@ export class GoogleGCSPublish implements PublisherBase {
);
}
const bucketRootPath = normalizeExternalStorageRootPath(
config.getOptionalString('techdocs.publisher.googleGcs.bucketRootPath') ||
'',
);
// Credentials is an optional config. If missing, default GCS environment variables will be used.
// Read more here https://cloud.google.com/docs/authentication/production
const credentials = config.getOptionalString(
@@ -83,6 +89,7 @@ export class GoogleGCSPublish implements PublisherBase {
bucketName,
legacyPathCasing,
logger,
bucketRootPath,
);
}
@@ -91,11 +98,13 @@ export class GoogleGCSPublish implements PublisherBase {
private readonly bucketName: string,
private readonly legacyPathCasing: boolean,
private readonly logger: Logger,
private readonly bucketRootPath: string,
) {
this.storageClient = storageClient;
this.bucketName = bucketName;
this.legacyPathCasing = legacyPathCasing;
this.logger = logger;
this.bucketRootPath = bucketRootPath;
}
/**
@@ -133,6 +142,7 @@ export class GoogleGCSPublish implements PublisherBase {
async publish({ entity, directory }: PublishRequest): Promise<void> {
const useLegacyPathCasing = this.legacyPathCasing;
const bucket = this.storageClient.bucket(this.bucketName);
const bucketRootPath = this.bucketRootPath;
// First, try to retrieve a list of all individual files currently existing
let existingFiles: string[] = [];
@@ -141,6 +151,7 @@ export class GoogleGCSPublish implements PublisherBase {
entity,
undefined,
useLegacyPathCasing,
bucketRootPath,
);
existingFiles = await this.getFilesForFolder(remoteFolder);
} catch (e) {
@@ -166,6 +177,7 @@ export class GoogleGCSPublish implements PublisherBase {
entity,
relativeFilePath,
useLegacyPathCasing,
bucketRootPath,
),
});
},
@@ -190,6 +202,7 @@ export class GoogleGCSPublish implements PublisherBase {
entity,
path.relative(directory, absoluteFilePath),
useLegacyPathCasing,
bucketRootPath,
),
);
const staleFiles = getStaleFiles(relativeFilesToUpload, existingFiles);
@@ -214,10 +227,12 @@ export class GoogleGCSPublish implements PublisherBase {
fetchTechDocsMetadata(entityName: EntityName): Promise<TechDocsMetadata> {
return new Promise((resolve, reject) => {
const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;
const entityRootDir = this.legacyPathCasing
const entityDir = this.legacyPathCasing
? entityTriplet
: lowerCaseEntityTriplet(entityTriplet);
const entityRootDir = path.join(this.bucketRootPath, entityDir);
const fileStreamChunks: Array<any> = [];
this.storageClient
.bucket(this.bucketName)
@@ -246,10 +261,16 @@ export class GoogleGCSPublish implements PublisherBase {
// Decode and trim the leading forward slash
const decodedUri = decodeURI(req.path.replace(/^\//, ''));
// filePath example - /default/component/documented-component/index.html
const filePath = this.legacyPathCasing
? decodedUri
: lowerCaseEntityTripletInStoragePath(decodedUri);
// Root path is removed from the Uri so that legacy casing can be applied
// to the entity triplet without manipulating the root path
const decodedUriNoRoot = path.relative(this.bucketRootPath, decodedUri);
const filePathNoRoot = this.legacyPathCasing
? decodedUriNoRoot
: lowerCaseEntityTripletInStoragePath(decodedUriNoRoot);
// Re-prepend the root path to the relative file path
const filePath = path.join(this.bucketRootPath, filePathNoRoot);
// Files with different extensions (CSS, HTML) need to be served with different headers
const fileExtension = path.extname(filePath);
@@ -285,10 +306,12 @@ export class GoogleGCSPublish implements PublisherBase {
async hasDocsBeenGenerated(entity: Entity): Promise<boolean> {
return new Promise(resolve => {
const entityTriplet = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
const entityRootDir = this.legacyPathCasing
const entityDir = this.legacyPathCasing
? entityTriplet
: lowerCaseEntityTriplet(entityTriplet);
const entityRootDir = path.join(this.bucketRootPath, entityDir);
this.storageClient
.bucket(this.bucketName)
.file(`${entityRootDir}/index.html`)
@@ -25,6 +25,7 @@ import {
bulkStorageOperation,
lowerCaseEntityTriplet,
lowerCaseEntityTripletInStoragePath,
normalizeExternalStorageRootPath,
} from './helpers';
describe('getHeadersForFileExtension', () => {
@@ -107,6 +108,34 @@ describe('lowerCaseEntityTripletInStoragePath', () => {
});
});
describe('normalizeExternalStorageRootPath', () => {
it('returns an empty string when empty string provided', () => {
const originalPath = '';
const normalPath = normalizeExternalStorageRootPath(originalPath);
expect(normalPath).toBe('');
});
it('returns an empty string when only separator is provided', () => {
const originalPath = '/';
const normalPath = normalizeExternalStorageRootPath(originalPath);
expect(normalPath).toBe('');
});
it('returns normalized path from path with leading and trailing sep', () => {
const originalPath = '/backstage-data/techdocs/';
const normalPath = normalizeExternalStorageRootPath(originalPath);
expect(normalPath).toBe('backstage-data/techdocs');
});
it('returns normalized path from path without leading and trailing sep', () => {
const originalPath = 'backstage-data/techdocs';
const normalPath = normalizeExternalStorageRootPath(originalPath);
expect(normalPath).toBe('backstage-data/techdocs');
});
it('returns normalized path from path with trailing sep', () => {
const originalPath = 'backstage-data/techdocs/';
const normalPath = normalizeExternalStorageRootPath(originalPath);
expect(normalPath).toBe('backstage-data/techdocs');
});
});
describe('getStaleFiles', () => {
const defaultFiles = [
'default/Component/backstage/index.html',
@@ -172,6 +201,60 @@ describe('getCloudPathForLocalPath', () => {
it('should throw error when entity is invalid', () => {
expect(() => getCloudPathForLocalPath({} as Entity)).toThrow();
});
it('should prepend root directory to destination', () => {
const localPath = 'index/html';
const rootPath = 'backstage-data/techdocs/';
const remoteBucket = getCloudPathForLocalPath(
entity,
localPath,
false,
rootPath,
);
expect(remoteBucket).toBe(
`backstage-data/techdocs/custom/component/backstage/${localPath}`,
);
});
it('should add trailing seperator to root directory', () => {
const localPath = 'index/html';
const rootPath = 'backstage-data/techdocs';
const remoteBucket = getCloudPathForLocalPath(
entity,
localPath,
false,
rootPath,
);
expect(remoteBucket).toBe(
`backstage-data/techdocs/custom/component/backstage/${localPath}`,
);
});
it('should remove leading seperator from root directory', () => {
const localPath = 'index/html';
const rootPath = '/backstage-data/techdocs/';
const remoteBucket = getCloudPathForLocalPath(
entity,
localPath,
false,
rootPath,
);
expect(remoteBucket).toBe(
`backstage-data/techdocs/custom/component/backstage/${localPath}`,
);
});
it('should ignore seperator if root directory is explicitly defined', () => {
const localPath = 'index/html';
const rootPath = '/';
const remoteBucket = getCloudPathForLocalPath(
entity,
localPath,
false,
rootPath,
);
expect(remoteBucket).toBe(`custom/component/backstage/${localPath}`);
});
});
describe('bulkStorageOperation', () => {
@@ -140,6 +140,29 @@ export const lowerCaseEntityTripletInStoragePath = (
return lowerCaseEntityTriplet(parts.join(path.posix.sep));
};
/**
* Take a posix path and return a path without leading and trailing
* separators
*
* @example
* normalizeExternalStorageRootPath('/backstage-data/techdocs/')
* // return backstage-data/techdocs
*/
export const normalizeExternalStorageRootPath = (posixPath: string): string => {
// remove leading slash
let normalizedPath = posixPath;
if (posixPath.startsWith(path.posix.sep)) {
normalizedPath = posixPath.slice(1);
}
// remove trailing slash
if (normalizedPath.endsWith(path.posix.sep)) {
normalizedPath = normalizedPath.slice(0, normalizedPath.length - 1);
}
return normalizedPath;
};
// Only returns the files that existed previously and are not present anymore.
export const getStaleFiles = (
newFiles: string[],
@@ -157,6 +180,7 @@ export const getCloudPathForLocalPath = (
entity: Entity,
localPath = '',
useLegacyPathCasing = false,
externalStorageRootPath = '',
): string => {
// Convert destination file path to a POSIX path for uploading.
// GCS expects / as path separator and relativeFilePath will contain \\ on Windows.
@@ -169,11 +193,17 @@ export const getCloudPathForLocalPath = (
}/${entity.kind}/${entity.metadata.name}`;
const relativeFilePathTriplet = `${entityRootDir}/${relativeFilePathPosix}`;
const destination = useLegacyPathCasing
? relativeFilePathTriplet
: lowerCaseEntityTriplet(relativeFilePathTriplet);
return destination; // Remote storage file relative path
const destinationWithRoot = path.join(
...externalStorageRootPath.split(path.posix.sep),
destination,
);
return destinationWithRoot; // Remote storage file relative path
};
// Perform rate limited generic operations by passing a function and a list of arguments
+1 -109
View File
@@ -3,113 +3,5 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ReactElement } from 'react';
import { RenderResult } from '@testing-library/react';
// Warning: (ae-missing-release-tag) "AsyncLogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type AsyncLogCollector = () => Promise<void>;
// Warning: (ae-missing-release-tag) "CollectedLogs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type CollectedLogs<T extends LogFuncs> = {
[key in T]: string[];
};
// Warning: (ae-missing-release-tag) "Keyboard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class Keyboard {
constructor(
target: any,
{
debug,
}?: {
debug?: boolean | undefined;
},
);
// (undocumented)
click(): Promise<void>;
// (undocumented)
debug: boolean;
// (undocumented)
document: any;
// (undocumented)
enter(value: any): Promise<void>;
// (undocumented)
escape(): Promise<void>;
// (undocumented)
get focused(): any;
// (undocumented)
static fromReadableInput(input: any): any;
// (undocumented)
_log(message: any, ...args: any[]): void;
// (undocumented)
_pretty(element: any): string;
// (undocumented)
send(chars: any): Promise<void>;
// (undocumented)
_sendKey(key: any, charCode: any, action: any): Promise<void>;
// (undocumented)
tab(): Promise<void>;
// (undocumented)
static toReadableInput(chars: any): any;
// (undocumented)
toString(): string;
// (undocumented)
static type(target: any, input: any): Promise<void>;
// (undocumented)
type(input: any): Promise<void>;
// (undocumented)
static typeDebug(target: any, input: any): Promise<void>;
}
// Warning: (ae-missing-release-tag) "LogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type LogCollector = AsyncLogCollector | SyncLogCollector;
// Warning: (ae-missing-release-tag) "LogFuncs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type LogFuncs = 'log' | 'warn' | 'error';
// Warning: (ae-missing-release-tag) "renderWithEffects" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function renderWithEffects(nodes: ReactElement): Promise<RenderResult>;
// Warning: (ae-missing-release-tag) "SyncLogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type SyncLogCollector = () => void;
// Warning: (ae-missing-release-tag) "withLogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
// Warning: (ae-missing-release-tag) "withLogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
// Warning: (ae-missing-release-tag) "withLogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
// Warning: (ae-missing-release-tag) "withLogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function withLogCollector(
callback: AsyncLogCollector,
): Promise<CollectedLogs<LogFuncs>>;
// @public (undocumented)
export function withLogCollector(
callback: SyncLogCollector,
): CollectedLogs<LogFuncs>;
// @public (undocumented)
export function withLogCollector<T extends LogFuncs>(
logsToCollect: T[],
callback: AsyncLogCollector,
): Promise<CollectedLogs<T>>;
// @public (undocumented)
export function withLogCollector<T extends LogFuncs>(
logsToCollect: T[],
callback: SyncLogCollector,
): CollectedLogs<T>;
// (No @packageDocumentation comment for this package)
```
+2 -8
View File
@@ -23,18 +23,12 @@
"scripts": {
"build": "backstage-cli build --outputs types,esm",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"test": "backstage-cli test --passWithNoTests",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@types/react": "*",
"react": "^16.12.0",
"react-dom": "^16.12.0"
},
"dependencies": {},
"devDependencies": {
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32"
+4 -7
View File
@@ -14,10 +14,7 @@
* limitations under the License.
*/
/**
* Utilities to test Backstage core
*
* @packageDocumentation
*/
export * from './testUtils';
export {};
throw new Error(
'This module has been removed. Use @backstage/dev-utils instead',
);
@@ -1,19 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './Keyboard';
export * from './logCollector';
export * from './testingLibrary';
+121 -30
View File
@@ -9,7 +9,7 @@ import { ComponentType } from 'react';
import { ErrorApi } from '@backstage/core-plugin-api';
import { ErrorContext } from '@backstage/core-plugin-api';
import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
import { RenderResult } from '@testing-library/react';
@@ -17,9 +17,73 @@ import { RouteRef } from '@backstage/core-plugin-api';
import { StorageApi } from '@backstage/core-plugin-api';
import { StorageValueChange } from '@backstage/core-plugin-api';
// Warning: (ae-missing-release-tag) "MockAnalyticsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
export type AsyncLogCollector = () => Promise<void>;
// @public
export type CollectedLogs<T extends LogFuncs> = {
[key in T]: string[];
};
// @public
export type ErrorWithContext = {
error: Error;
context?: ErrorContext;
};
// @public @deprecated (undocumented)
export class Keyboard {
constructor(
target: any,
{
debug,
}?: {
debug?: boolean | undefined;
},
);
// (undocumented)
click(): Promise<void>;
// (undocumented)
debug: boolean;
// (undocumented)
document: any;
// (undocumented)
enter(value: any): Promise<void>;
// (undocumented)
escape(): Promise<void>;
// (undocumented)
get focused(): any;
// (undocumented)
static fromReadableInput(input: any): any;
// (undocumented)
_log(message: any, ...args: any[]): void;
// (undocumented)
_pretty(element: any): string;
// (undocumented)
send(chars: any): Promise<void>;
// (undocumented)
_sendKey(key: any, charCode: any, action: any): Promise<void>;
// (undocumented)
tab(): Promise<void>;
// (undocumented)
static toReadableInput(chars: any): any;
// (undocumented)
toString(): string;
// (undocumented)
static type(target: any, input: any): Promise<void>;
// (undocumented)
type(input: any): Promise<void>;
// (undocumented)
static typeDebug(target: any, input: any): Promise<void>;
}
// @public
export type LogCollector = AsyncLogCollector | SyncLogCollector;
// @public
export type LogFuncs = 'log' | 'warn' | 'error';
// @public
export class MockAnalyticsApi implements AnalyticsApi {
// (undocumented)
captureEvent({
@@ -33,9 +97,6 @@ export class MockAnalyticsApi implements AnalyticsApi {
getEvents(): AnalyticsEvent[];
}
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (ae-missing-release-tag) "mockBreakpoint" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export function mockBreakpoint({
matches,
@@ -43,19 +104,14 @@ export function mockBreakpoint({
matches?: boolean | undefined;
}): void;
// Warning: (ae-missing-release-tag) "MockErrorApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
export class MockErrorApi implements ErrorApi {
// Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts
constructor(options?: Options);
constructor(options?: MockErrorApiOptions);
// (undocumented)
error$(): Observable<{
error: Error;
context?: ErrorContext;
}>;
// Warning: (ae-forgotten-export) The symbol "ErrorWithContext" needs to be exported by the entry point index.d.ts
//
// (undocumented)
getErrors(): ErrorWithContext[];
// (undocumented)
@@ -64,9 +120,12 @@ export class MockErrorApi implements ErrorApi {
waitForError(pattern: RegExp, timeoutMs?: number): Promise<ErrorWithContext>;
}
// Warning: (ae-missing-release-tag) "MockStorageApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
export type MockErrorApiOptions = {
collect?: boolean;
};
// @public
export class MockStorageApi implements StorageApi {
// (undocumented)
static create(data?: MockStorageBucket): MockStorageApi;
@@ -82,16 +141,12 @@ export class MockStorageApi implements StorageApi {
set<T>(key: string, data: T): Promise<void>;
}
// Warning: (ae-missing-release-tag) "MockStorageBucket" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
export type MockStorageBucket = {
[key: string]: any;
};
// Warning: (ae-missing-release-tag) "msw" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export const msw: {
setupDefaultHandlers: (worker: {
listen: (t: any) => void;
@@ -100,22 +155,58 @@ export const msw: {
}) => void;
};
// Warning: (ae-forgotten-export) The symbol "TestAppOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "renderInTestApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export function renderInTestApp(
Component: ComponentType | ReactNode,
options?: TestAppOptions,
): Promise<RenderResult>;
// Warning: (ae-missing-release-tag) "wrapInTestApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export function renderWithEffects(nodes: ReactElement): Promise<RenderResult>;
// @public
export function setupRequestMockHandlers(worker: {
listen: (t: any) => void;
close: () => void;
resetHandlers: () => void;
}): void;
// @public
export type SyncLogCollector = () => void;
// @public
export type TestAppOptions = {
routeEntries?: string[];
mountedRoutes?: {
[path: string]: RouteRef | ExternalRouteRef;
};
};
// @public
export function withLogCollector(
callback: AsyncLogCollector,
): Promise<CollectedLogs<LogFuncs>>;
// @public
export function withLogCollector(
callback: SyncLogCollector,
): CollectedLogs<LogFuncs>;
// @public
export function withLogCollector<T extends LogFuncs>(
logsToCollect: T[],
callback: AsyncLogCollector,
): Promise<CollectedLogs<T>>;
// @public
export function withLogCollector<T extends LogFuncs>(
logsToCollect: T[],
callback: SyncLogCollector,
): CollectedLogs<T>;
// @public
export function wrapInTestApp(
Component: ComponentType | ReactNode,
options?: TestAppOptions,
): ReactElement;
export * from '@backstage/test-utils-core';
```
+1 -1
View File
@@ -31,8 +31,8 @@
"dependencies": {
"@backstage/core-app-api": "^0.1.18",
"@backstage/core-plugin-api": "^0.1.11",
"@backstage/test-utils-core": "^0.1.3",
"@backstage/theme": "^0.2.11",
"@backstage/types": "^0.1.1",
"@material-ui/core": "^4.12.2",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
-1
View File
@@ -21,4 +21,3 @@
*/
export * from './testUtils';
export * from '@backstage/test-utils-core';
@@ -23,6 +23,10 @@ const codes = {
Esc: 27,
};
/**
* @public
* @deprecated because it has no usages. Perhaps resurfaced in the future when need be.
*/
export class Keyboard {
static async type(target, input) {
await new Keyboard(target).type(input);
@@ -16,6 +16,11 @@
import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api';
/**
* Mock implementation of {@link core-plugin-api#AnalyticsApi} with helpers to ensure that events are sent correctly.
* Use getEvents in tests to verify captured events.
* @public
*/
export class MockAnalyticsApi implements AnalyticsApi {
private events: AnalyticsEvent[] = [];
@@ -14,13 +14,23 @@
* limitations under the License.
*/
import { ErrorApi, ErrorContext, Observable } from '@backstage/core-plugin-api';
import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
type Options = {
/**
* Constructor arguments for {@link MockErrorApi}
* @public
*/
export type MockErrorApiOptions = {
// Need to be true if getErrors is used in testing.
collect?: boolean;
};
type ErrorWithContext = {
/**
* ErrorWithContext contains error and ErrorContext
* @public
*/
export type ErrorWithContext = {
error: Error;
context?: ErrorContext;
};
@@ -38,11 +48,16 @@ const nullObservable = {
},
};
/**
* Mock implementation of the {@link core-plugin-api#ErrorApi} to be used in tests.
* Incudes withForError and getErrors methods for error testing.
* @public
*/
export class MockErrorApi implements ErrorApi {
private readonly errors = new Array<ErrorWithContext>();
private readonly waiters = new Set<Waiter>();
constructor(private readonly options: Options = {}) {}
constructor(private readonly options: MockErrorApiOptions = {}) {}
post(error: Error, context?: ErrorContext) {
if (this.options.collect) {
@@ -15,3 +15,4 @@
*/
export { MockErrorApi } from './MockErrorApi';
export type { MockErrorApiOptions, ErrorWithContext } from './MockErrorApi';
@@ -14,15 +14,20 @@
* limitations under the License.
*/
import {
Observable,
StorageApi,
StorageValueChange,
} from '@backstage/core-plugin-api';
import { StorageApi, StorageValueChange } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import ObservableImpl from 'zen-observable';
/**
* Type for map holding data in {@link MockStorageApi}
* @public
*/
export type MockStorageBucket = { [key: string]: any };
/**
* Mock implementation of the {@link core-plugin-api#StorageApi} to be used in tests
* @public
*/
export class MockStorageApi implements StorageApi {
private readonly namespace: string;
private readonly data: MockStorageBucket;
@@ -23,7 +23,7 @@ import {
useApi,
useRouteRef,
} from '@backstage/core-plugin-api';
import { withLogCollector } from '@backstage/test-utils-core';
import { withLogCollector } from './logCollector';
import { render } from '@testing-library/react';
import React, { useEffect } from 'react';
import { Route, Routes } from 'react-router';
@@ -27,7 +27,7 @@ import {
createRouteRef,
} from '@backstage/core-plugin-api';
import { RenderResult } from '@testing-library/react';
import { renderWithEffects } from '@backstage/test-utils-core';
import { renderWithEffects } from './testingLibrary';
import { mockApis } from './mockApis';
const ErrorBoundaryFallback = ({ error }: { error: Error }) => {
@@ -43,8 +43,9 @@ const Progress = () => <div data-testid="progress" />;
/**
* Options to customize the behavior of the test app wrapper.
* @public
*/
type TestAppOptions = {
export type TestAppOptions = {
/**
* Initial route entries to pass along as `initialEntries` to the router.
*/
@@ -56,11 +57,11 @@ type TestAppOptions = {
* used by `useRouteRef` in the rendered elements.
*
* @example
* wrapInTestApp(<MyComponent />, {
* mountedRoutes: {
* wrapInTestApp(<MyComponent />, \{
* mountedRoutes: \{
* '/my-path': myRouteRef,
* }
* })
* \}
* \})
* // ...
* const link = useRouteRef(myRouteRef)
*/
@@ -80,6 +81,7 @@ function isExternalRouteRef(
*
* @param Component - A component or react node to render inside the test app.
* @param options - Additional options for the rendering.
* @public
*/
export function wrapInTestApp(
Component: ComponentType | ReactNode,
@@ -171,6 +173,7 @@ export function wrapInTestApp(
*
* @param Component - A component or react node to render inside the test app.
* @param options - Additional options for the rendering.
* @public
*/
export async function renderInTestApp(
Component: ComponentType | ReactNode,
@@ -17,4 +17,8 @@
export * from './apis';
export { default as mockBreakpoint } from './mockBreakpoint';
export { wrapInTestApp, renderInTestApp } from './appWrappers';
export type { TestAppOptions } from './appWrappers';
export * from './msw';
export * from './Keyboard';
export * from './logCollector';
export * from './testingLibrary';

Some files were not shown because too many files have changed in this diff Show More