Merge branch 'backstage:master' into master

This commit is contained in:
Daniel Johansson
2021-06-17 18:14:23 +02:00
committed by GitHub
153 changed files with 8868 additions and 1354 deletions
+115
View File
@@ -0,0 +1,115 @@
## API Report File for "@backstage/plugin-api-docs"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiEntity } from '@backstage/catalog-model';
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { CatalogTableRow } from '@backstage/plugin-catalog';
import { Entity } from '@backstage/catalog-model';
import { ExternalRouteRef } from '@backstage/core';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core';
import { TableColumn } from '@backstage/core';
import { UserListFilterKind } from '@backstage/plugin-catalog-react';
// @public (undocumented)
export const ApiDefinitionCard: (_: Props) => JSX.Element;
// @public (undocumented)
export type ApiDefinitionWidget = {
type: string;
title: string;
component: (definition: string) => React_2.ReactElement;
rawLanguage?: string;
};
// @public (undocumented)
export const apiDocsConfigRef: ApiRef<ApiDocsConfig>;
// @public (undocumented)
const apiDocsPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
}, {
createComponent: ExternalRouteRef<undefined, true>;
}>;
export { apiDocsPlugin }
export { apiDocsPlugin as plugin }
// @public (undocumented)
export const ApiExplorerPage: ({ initiallySelectedFilter, columns, }: ApiExplorerPageProps) => JSX.Element;
// @public (undocumented)
export const ApiTypeTitle: ({ apiEntity }: {
apiEntity: ApiEntity;
}) => JSX.Element;
// @public (undocumented)
export const AsyncApiDefinitionWidget: ({ definition }: Props_5) => JSX.Element;
// @public (undocumented)
export const ConsumedApisCard: ({ variant }: Props_2) => JSX.Element;
// @public (undocumented)
export const ConsumingComponentsCard: ({ variant }: Props_6) => JSX.Element;
// @public (undocumented)
export function defaultDefinitionWidgets(): ApiDefinitionWidget[];
// @public (undocumented)
export const EntityApiDefinitionCard: (_: {
apiEntity?: ApiEntity | undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityConsumedApisCard: ({ variant }: {
entity?: Entity| undefined;
variant?: "gridItem" | undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityConsumingComponentsCard: ({ variant }: {
entity?: Entity| undefined;
variant?: "gridItem" | undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityHasApisCard: ({ variant }: {
variant?: "gridItem" | undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityProvidedApisCard: ({ variant }: {
entity?: Entity| undefined;
variant?: "gridItem" | undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityProvidingComponentsCard: ({ variant }: {
entity?: Entity| undefined;
variant?: "gridItem" | undefined;
}) => JSX.Element;
// @public (undocumented)
export const HasApisCard: ({ variant }: Props_3) => JSX.Element;
// @public (undocumented)
export const OpenApiDefinitionWidget: ({ definition }: Props_8) => JSX.Element;
// @public (undocumented)
export const PlainApiDefinitionWidget: ({ definition, language }: Props_9) => JSX.Element;
// @public (undocumented)
export const ProvidedApisCard: ({ variant }: Props_4) => JSX.Element;
// @public (undocumented)
export const ProvidingComponentsCard: ({ variant }: Props_7) => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+28
View File
@@ -0,0 +1,28 @@
## API Report File for "@backstage/plugin-app-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Config } from '@backstage/config';
import express from 'express';
import { Logger } from 'winston';
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public (undocumented)
export interface RouterOptions {
appPackageName: string;
// (undocumented)
config: Config;
disableConfigInjection?: boolean;
// (undocumented)
logger: Logger;
staticFallbackHandler?: express.Handler;
}
// (No @packageDocumentation comment for this package)
```
+211
View File
@@ -0,0 +1,211 @@
## API Report File for "@backstage/plugin-auth-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import express from 'express';
import { JSONWebKey } from 'jose';
import { Logger } from 'winston';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { Profile } from 'passport';
// @public (undocumented)
export type AuthProviderFactory = (options: AuthProviderFactoryOptions) => AuthProviderRouteHandlers;
// @public (undocumented)
export type AuthProviderFactoryOptions = {
providerId: string;
globalConfig: AuthProviderConfig;
config: Config;
logger: Logger;
tokenIssuer: TokenIssuer;
discovery: PluginEndpointDiscovery;
catalogApi: CatalogApi;
identityResolver?: ExperimentalIdentityResolver;
};
// @public
export interface AuthProviderRouteHandlers {
frameHandler(req: express.Request, res: express.Response): Promise<void>;
logout?(req: express.Request, res: express.Response): Promise<void>;
refresh?(req: express.Request, res: express.Response): Promise<void>;
start(req: express.Request, res: express.Response): Promise<void>;
}
// @public (undocumented)
export type AuthResponse<ProviderInfo> = {
providerInfo: ProviderInfo;
profile: ProfileInfo;
backstageIdentity?: BackstageIdentity;
};
// @public (undocumented)
export type BackstageIdentity = {
id: string;
idToken?: string;
};
// @public (undocumented)
export function createRouter({ logger, config, discovery, database, providerFactories, }: RouterOptions): Promise<express.Router>;
// @public (undocumented)
export const defaultAuthProviderFactories: {
[providerId: string]: AuthProviderFactory;
};
// @public (undocumented)
export const encodeState: (state: OAuthState) => string;
// @public (undocumented)
export const ensuresXRequestedWith: (req: express.Request) => boolean;
// @public
export class IdentityClient {
constructor(options: {
discovery: PluginEndpointDiscovery;
issuer: string;
});
authenticate(token: string | undefined): Promise<BackstageIdentity>;
static getBearerToken(authorizationHeader: string | undefined): string | undefined;
listPublicKeys(): Promise<{
keys: JSONWebKey[];
}>;
}
// @public (undocumented)
export class OAuthAdapter implements AuthProviderRouteHandlers {
constructor(handlers: OAuthHandlers, options: Options);
// (undocumented)
frameHandler(req: express.Request, res: express.Response): Promise<void>;
// (undocumented)
static fromConfig(config: AuthProviderConfig, handlers: OAuthHandlers, options: Pick<Options, 'providerId' | 'persistScopes' | 'disableRefresh' | 'tokenIssuer'>): OAuthAdapter;
// (undocumented)
logout(req: express.Request, res: express.Response): Promise<void>;
// (undocumented)
refresh(req: express.Request, res: express.Response): Promise<void>;
// (undocumented)
start(req: express.Request, res: express.Response): Promise<void>;
}
// @public (undocumented)
export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers {
constructor(handlers: Map<string, AuthProviderRouteHandlers>);
// (undocumented)
frameHandler(req: express.Request, res: express.Response): Promise<void>;
// (undocumented)
logout(req: express.Request, res: express.Response): Promise<void>;
// (undocumented)
static mapConfig(config: Config, factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers): OAuthEnvironmentHandler;
// (undocumented)
refresh(req: express.Request, res: express.Response): Promise<void>;
// (undocumented)
start(req: express.Request, res: express.Response): Promise<void>;
}
// @public
export interface OAuthHandlers {
handler(req: express.Request): Promise<{
response: AuthResponse<OAuthProviderInfo>;
refreshToken?: string;
}>;
logout?(): Promise<void>;
refresh?(req: OAuthRefreshRequest): Promise<AuthResponse<OAuthProviderInfo>>;
start(req: OAuthStartRequest): Promise<RedirectInfo>;
}
// @public (undocumented)
export type OAuthProviderInfo = {
accessToken: string;
idToken?: string;
expiresInSeconds?: number;
scope: string;
refreshToken?: string;
};
// @public
export type OAuthProviderOptions = {
clientId: string;
clientSecret: string;
callbackUrl: string;
};
// @public (undocumented)
export type OAuthRefreshRequest = express.Request<{}> & {
scope: string;
refreshToken: string;
};
// @public (undocumented)
export type OAuthResponse = AuthResponse<OAuthProviderInfo>;
// @public (undocumented)
export type OAuthResult = {
fullProfile: Profile;
params: {
id_token?: string;
scope: string;
expires_in: number;
};
accessToken: string;
refreshToken?: string;
};
// @public (undocumented)
export type OAuthStartRequest = express.Request<{}> & {
scope: string;
state: OAuthState;
};
// @public (undocumented)
export type OAuthState = {
nonce: string;
env: string;
};
// @public (undocumented)
export const postMessageResponse: (res: express.Response, appOrigin: string, response: WebMessageResponse) => void;
// @public
export type ProfileInfo = {
email?: string;
displayName?: string;
picture?: string;
};
// @public (undocumented)
export const readState: (stateString: string) => OAuthState;
// @public (undocumented)
export interface RouterOptions {
// (undocumented)
config: Config;
// (undocumented)
database: PluginDatabaseManager;
// (undocumented)
discovery: PluginEndpointDiscovery;
// (undocumented)
logger: Logger;
// (undocumented)
providerFactories?: ProviderFactories;
}
// @public (undocumented)
export const verifyNonce: (req: express.Request, providerId: string) => void;
// @public
export type WebMessageResponse = {
type: 'authorization_response';
response: AuthResponse<unknown>;
} | {
type: 'authorization_response';
error: Error;
};
// (No @packageDocumentation comment for this package)
```
@@ -67,13 +67,14 @@ export class TokenFactory implements TokenIssuer {
const iss = this.issuer;
const sub = params.claims.sub;
const ent = params.claims.ent;
const aud = 'backstage';
const iat = Math.floor(Date.now() / MS_IN_S);
const exp = iat + this.keyDurationSeconds;
this.logger.info(`Issuing token for ${sub}`);
this.logger.info(`Issuing token for ${sub}, with entities ${ent ?? []}`);
return JWS.sign({ iss, sub, aud, iat, exp }, key, {
return JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
alg: key.alg,
kid: key.kid,
});
@@ -28,6 +28,8 @@ export type TokenParams = {
claims: {
/** The token subject, i.e. User ID */
sub: string;
/** A list of entity references that the user claims ownership through */
ent?: string[];
};
};
@@ -16,6 +16,7 @@
import { CatalogApi } from '@backstage/catalog-client';
import { UserEntity } from '@backstage/catalog-model';
import { TokenIssuer } from '../../identity';
import { CatalogIdentityClient } from './CatalogIdentityClient';
describe('CatalogIdentityClient', () => {
@@ -29,25 +30,36 @@ describe('CatalogIdentityClient', () => {
getLocationByEntity: jest.fn(),
removeEntityByUid: jest.fn(),
};
const tokenIssuer: jest.Mocked<TokenIssuer> = {
issueToken: jest.fn(),
listPublicKeys: jest.fn(),
};
afterEach(() => jest.resetAllMocks());
it('passes through the correct search params', async () => {
catalogApi.getEntities.mockResolvedValueOnce({ items: [{} as UserEntity] });
tokenIssuer.issueToken.mockResolvedValue('my-token');
const client = new CatalogIdentityClient({
catalogApi: catalogApi as CatalogApi,
catalogApi: catalogApi,
tokenIssuer: tokenIssuer,
});
client.findUser({ annotations: { key: 'value' } });
await client.findUser({ annotations: { key: 'value' } });
expect(catalogApi.getEntities).toBeCalledWith(
expect(catalogApi.getEntities).toHaveBeenCalledWith(
{
filter: {
kind: 'user',
'metadata.annotations.key': 'value',
},
},
undefined,
{ token: 'my-token' },
);
expect(tokenIssuer.issueToken).toHaveBeenCalledWith({
claims: {
sub: 'backstage.io/auth-backend',
},
});
});
});
@@ -17,6 +17,7 @@
import { ConflictError, NotFoundError } from '@backstage/errors';
import { CatalogApi } from '@backstage/catalog-client';
import { UserEntity } from '@backstage/catalog-model';
import { TokenIssuer } from '../../identity';
type UserQuery = {
annotations: Record<string, string>;
@@ -27,9 +28,11 @@ type UserQuery = {
*/
export class CatalogIdentityClient {
private readonly catalogApi: CatalogApi;
private readonly tokenIssuer: TokenIssuer;
constructor(options: { catalogApi: CatalogApi }) {
constructor(options: { catalogApi: CatalogApi; tokenIssuer: TokenIssuer }) {
this.catalogApi = options.catalogApi;
this.tokenIssuer = options.tokenIssuer;
}
/**
@@ -37,10 +40,7 @@ export class CatalogIdentityClient {
*
* Throws a NotFoundError or ConflictError if 0 or multiple users are found.
*/
async findUser(
query: UserQuery,
options?: { token?: string },
): Promise<UserEntity> {
async findUser(query: UserQuery): Promise<UserEntity> {
const filter: Record<string, string> = {
kind: 'user',
};
@@ -48,7 +48,11 @@ export class CatalogIdentityClient {
filter[`metadata.annotations.${key}`] = value;
}
const { items } = await this.catalogApi.getEntities({ filter }, options);
// TODO(Rugvip): cache the token
const token = await this.tokenIssuer.issueToken({
claims: { sub: 'backstage.io/auth-backend' },
});
const { items } = await this.catalogApi.getEntities({ filter }, { token });
if (items.length !== 1) {
if (items.length > 1) {
@@ -0,0 +1,40 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
RELATION_MEMBER_OF,
stringifyEntityRef,
UserEntity,
} from '@backstage/catalog-model';
import { TokenParams } from '../../identity';
export function getEntityClaims(entity: UserEntity): TokenParams['claims'] {
const userRef = stringifyEntityRef(entity);
const membershipRefs =
entity.relations
?.filter(
r =>
r.type === RELATION_MEMBER_OF &&
r.target.kind.toLocaleLowerCase('en-US') === 'group',
)
.map(r => stringifyEntityRef(r.target)) ?? [];
return {
sub: userRef,
ent: [userRef, ...membershipRefs],
};
}
@@ -15,3 +15,4 @@
*/
export { CatalogIdentityClient } from './CatalogIdentityClient';
export { getEntityClaims } from './helpers';
@@ -14,5 +14,9 @@
* limitations under the License.
*/
export { createGoogleProvider } from './provider';
export {
createGoogleProvider,
googleDefaultSignInResolver,
googleEmailSignInResolver,
} from './provider';
export type { GoogleProviderOptions } from './provider';
@@ -0,0 +1,91 @@
/*
* 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 { GoogleAuthProvider } from './provider';
import * as helpers from '../../lib/passport/PassportStrategyHelper';
import { OAuthResult } from '../../lib/oauth';
import { getVoidLogger } from '@backstage/backend-common';
import { TokenIssuer } from '../../identity/types';
import { CatalogIdentityClient } from '../../lib/catalog';
const mockFrameHandler = (jest.spyOn(
helpers,
'executeFrameHandlerStrategy',
) as unknown) as jest.MockedFunction<
() => Promise<{ result: OAuthResult; privateInfo: any }>
>;
describe('createGoogleProvider', () => {
it('should auth', async () => {
const tokenIssuer = {
issueToken: jest.fn(),
listPublicKeys: jest.fn(),
};
const catalogIdentityClient = {
findUser: jest.fn(),
};
const provider = new GoogleAuthProvider({
logger: getVoidLogger(),
catalogIdentityClient: (catalogIdentityClient as unknown) as CatalogIdentityClient,
tokenIssuer: (tokenIssuer as unknown) as TokenIssuer,
authHandler: async ({ fullProfile }) => ({
profile: {
email: fullProfile.emails![0]!.value,
displayName: fullProfile.displayName,
picture: 'http://google.com/lols',
},
}),
clientId: 'mock',
clientSecret: 'mock',
callbackUrl: 'mock',
});
mockFrameHandler.mockResolvedValueOnce({
result: {
fullProfile: {
emails: [{ value: 'conrad@example.com' }],
displayName: 'Conrad',
id: 'conrad',
provider: 'google',
},
params: {
id_token: 'idToken',
scope: 'scope',
expires_in: 123,
},
accessToken: 'accessToken',
},
privateInfo: {
refreshToken: 'wacka',
},
});
const { response } = await provider.handler({} as any);
expect(response).toEqual({
providerInfo: {
accessToken: 'accessToken',
expiresInSeconds: 123,
idToken: 'idToken',
scope: 'scope',
},
profile: {
email: 'conrad@example.com',
displayName: 'Conrad',
picture: 'http://google.com/lols',
},
});
});
});
@@ -17,8 +17,8 @@
import express from 'express';
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import { Logger } from 'winston';
import { CatalogIdentityClient } from '../../lib/catalog';
import { TokenIssuer } from '../../identity/types';
import { CatalogIdentityClient, getEntityClaims } from '../../lib/catalog';
import {
encodeState,
OAuthAdapter,
@@ -27,8 +27,8 @@ import {
OAuthProviderOptions,
OAuthRefreshRequest,
OAuthResponse,
OAuthStartRequest,
OAuthResult,
OAuthStartRequest,
} from '../../lib/oauth';
import {
executeFetchUserProfileStrategy,
@@ -38,30 +38,40 @@ import {
makeProfileInfo,
PassportDoneCallback,
} from '../../lib/passport';
import { AuthProviderFactory, RedirectInfo } from '../types';
import { TokenIssuer } from '../../identity/types';
import {
AuthProviderFactory,
AuthHandler,
RedirectInfo,
SignInResolver,
} from '../types';
import { Logger } from 'winston';
type PrivateInfo = {
refreshToken: string;
};
type Options = OAuthProviderOptions & {
logger: Logger;
identityClient: CatalogIdentityClient;
signInResolver?: SignInResolver<OAuthResult>;
authHandler: AuthHandler<OAuthResult>;
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger;
};
export class GoogleAuthProvider implements OAuthHandlers {
private readonly _strategy: GoogleStrategy;
private readonly logger: Logger;
private readonly identityClient: CatalogIdentityClient;
private readonly signInResolver?: SignInResolver<OAuthResult>;
private readonly authHandler: AuthHandler<OAuthResult>;
private readonly tokenIssuer: TokenIssuer;
private readonly catalogIdentityClient: CatalogIdentityClient;
private readonly logger: Logger;
constructor(options: Options) {
this.logger = options.logger;
this.identityClient = options.identityClient;
this.signInResolver = options.signInResolver;
this.authHandler = options.authHandler;
this.tokenIssuer = options.tokenIssuer;
// TODO: throw error if env variables not set?
this.catalogIdentityClient = options.catalogIdentityClient;
this.logger = options.logger;
this._strategy = new GoogleStrategy(
{
clientID: options.clientId,
@@ -111,18 +121,8 @@ export class GoogleAuthProvider implements OAuthHandlers {
PrivateInfo
>(req, this._strategy);
const profile = makeProfileInfo(result.fullProfile, result.params.id_token);
return {
response: await this.populateIdentity({
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
profile,
}),
response: await this.handleResult(result),
refreshToken: privateInfo.refreshToken,
};
}
@@ -133,89 +133,170 @@ export class GoogleAuthProvider implements OAuthHandlers {
req.refreshToken,
req.scope,
);
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
const profile = makeProfileInfo(fullProfile, params.id_token);
return this.populateIdentity({
providerInfo: {
accessToken,
idToken: params.id_token,
expiresInSeconds: params.expires_in,
scope: params.scope,
},
profile,
return this.handleResult({
fullProfile,
params,
accessToken,
refreshToken: req.refreshToken,
});
}
private async populateIdentity(
response: OAuthResponse,
): Promise<OAuthResponse> {
const { profile } = response;
private async handleResult(result: OAuthResult) {
const { profile } = await this.authHandler(result);
if (!profile.email) {
throw new Error('Google profile contained no email');
}
const response: OAuthResponse = {
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
profile,
};
try {
const token = await this.tokenIssuer.issueToken({
claims: { sub: 'backstage.io/auth-backend' },
});
const user = await this.identityClient.findUser(
if (this.signInResolver) {
response.backstageIdentity = await this.signInResolver(
{
annotations: {
'google.com/email': profile.email,
},
result,
profile,
},
{ token },
);
return {
...response,
backstageIdentity: {
id: user.metadata.name,
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
};
} catch (error) {
this.logger.warn(
`Failed to look up user, ${error}, falling back to allowing login based on email pattern, this will probably break in the future`,
);
return {
...response,
backstageIdentity: { id: profile.email.split('@')[0] },
};
}
return response;
}
}
export type GoogleProviderOptions = {};
export const googleEmailSignInResolver: SignInResolver<OAuthResult> = async (
info,
ctx,
) => {
const { profile } = info;
if (!profile.email) {
throw new Error('Google profile contained no email');
}
const entity = await ctx.catalogIdentityClient.findUser({
annotations: {
'google.com/email': profile.email,
},
});
const claims = getEntityClaims(entity);
const token = await ctx.tokenIssuer.issueToken({ claims });
return { id: entity.metadata.name, entity, token };
};
export const googleDefaultSignInResolver: SignInResolver<OAuthResult> = async (
info,
ctx,
) => {
const { profile } = info;
if (!profile.email) {
throw new Error('Google profile contained no email');
}
let userId: string;
try {
const entity = await ctx.catalogIdentityClient.findUser({
annotations: {
'google.com/email': profile.email,
},
});
userId = entity.metadata.name;
} catch (error) {
ctx.logger.warn(
`Failed to look up user, ${error}, falling back to allowing login based on email pattern, this will probably break in the future`,
);
userId = profile.email.split('@')[0];
}
const token = await ctx.tokenIssuer.issueToken({
claims: { sub: userId, ent: [`user:default/${userId}`] },
});
return { id: userId, token };
};
export type GoogleProviderOptions = {
/**
* The profile transformation function used to verify and convert the auth response
* into the profile that will be presented to the user.
*/
authHandler?: AuthHandler<OAuthResult>;
/**
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
*/
/**
* Maps an auth result to a Backstage identity for the user.
*
* Set to `'email'` to use the default email-based sign in resolver, which will search
* the catalog for a single user entity that has a matching `google.com/email` annotation.
*/
signIn?: {
resolver?: SignInResolver<OAuthResult>;
};
};
export const createGoogleProvider = (
_options?: GoogleProviderOptions,
options?: GoogleProviderOptions,
): AuthProviderFactory => {
return ({
providerId,
globalConfig,
config,
logger,
tokenIssuer,
catalogApi,
logger,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
const catalogIdentityClient = new CatalogIdentityClient({
catalogApi,
tokenIssuer,
});
const authHandler: AuthHandler<OAuthResult> = options?.authHandler
? options.authHandler
: async ({ fullProfile, params }) => ({
profile: makeProfileInfo(fullProfile, params.id_token),
});
const signInResolverFn =
options?.signIn?.resolver ?? googleDefaultSignInResolver;
const signInResolver: SignInResolver<OAuthResult> = info =>
signInResolverFn(info, {
catalogIdentityClient,
tokenIssuer,
logger,
});
const provider = new GoogleAuthProvider({
clientId,
clientSecret,
callbackUrl,
logger,
signInResolver,
authHandler,
tokenIssuer,
identityClient: new CatalogIdentityClient({ catalogApi }),
catalogIdentityClient,
logger,
});
return OAuthAdapter.fromConfig(globalConfig, provider, {
@@ -14,6 +14,7 @@
* limitations under the License.
*/
export * from './google';
export { factories as defaultAuthProviderFactories } from './factories';
// Export the minimal interface required for implementing a
+55 -2
View File
@@ -16,10 +16,12 @@
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import express from 'express';
import { Logger } from 'winston';
import { TokenIssuer } from '../identity/types';
import { CatalogIdentityClient } from '../lib/catalog';
export type AuthProviderConfig = {
/**
@@ -148,14 +150,30 @@ export type AuthResponse<ProviderInfo> = {
export type BackstageIdentity = {
/**
* The backstage user ID.
* An opaque ID that uniquely identifies the user within Backstage.
*
* This is typically the same as the user entity `metadata.name`.
*/
id: string;
/**
* An ID token that can be used to authenticate the user within Backstage.
* This is deprecated, use `token` instead.
* @deprecated
*/
idToken?: string;
/**
* The token used to authenticate the user within Backstage.
*/
token?: string;
/**
* The entity that the user is represented by within Backstage.
*
* This entity may or may not exist within the Catalog, and it can be used
* to read and store additional metadata about the user.
*/
entity?: Entity;
};
/**
@@ -179,3 +197,38 @@ export type ProfileInfo = {
*/
picture?: string;
};
export type SignInInfo<AuthResult> = {
/**
* The simple profile passed down for use in the frontend.
*/
profile: ProfileInfo;
/**
* The authentication result that was received from the authentication provider.
*/
result: AuthResult;
};
export type SignInResolver<AuthResult> = (
info: SignInInfo<AuthResult>,
context: {
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger;
},
) => Promise<BackstageIdentity>;
export type AuthHandlerResult = { profile: ProfileInfo };
/**
* The AuthHandler function is called every time the user authenticates using the provider.
*
* The handler should return a profile that represents the session for the user in the frontend.
*
* Throwing an error in the function will cause the authentication to fail, making it
* possible to use this function as a way to limit access to a certain group of users.
*/
export type AuthHandler<AuthResult> = (
input: AuthResult,
) => Promise<AuthHandlerResult>;
+113
View File
@@ -0,0 +1,113 @@
## API Report File for "@backstage/plugin-badges-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import express from 'express';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
// @public (undocumented)
export interface Badge {
color?: string;
description?: string;
kind?: 'entity';
label: string;
labelColor?: string;
link?: string;
message: string;
style?: BadgeStyle;
}
// @public (undocumented)
export const BADGE_STYLES: readonly ["plastic", "flat", "flat-square", "for-the-badge", "social"];
// @public (undocumented)
export type BadgeBuilder = {
getBadges(): Promise<BadgeInfo[]>;
createBadgeJson(options: BadgeOptions): Promise<BadgeSpec>;
createBadgeSvg(options: BadgeOptions): Promise<string>;
};
// @public (undocumented)
export interface BadgeContext {
// (undocumented)
badgeUrl: string;
// (undocumented)
config: Config;
// (undocumented)
entity?: Entity;
}
// @public (undocumented)
export interface BadgeFactories {
// (undocumented)
[id: string]: BadgeFactory;
}
// @public (undocumented)
export interface BadgeFactory {
// (undocumented)
createBadge(context: BadgeContext): Badge;
}
// @public (undocumented)
export type BadgeInfo = {
id: string;
};
// @public (undocumented)
export type BadgeOptions = {
badgeInfo: BadgeInfo;
context: BadgeContext;
};
// @public (undocumented)
export type BadgeSpec = {
id: string;
badge: Badge;
url: string;
markdown: string;
};
// @public (undocumented)
export type BadgeStyle = typeof BADGE_STYLES[number];
// @public (undocumented)
export const createDefaultBadgeFactories: () => BadgeFactories;
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public (undocumented)
export class DefaultBadgeBuilder implements BadgeBuilder {
constructor(factories: BadgeFactories);
// (undocumented)
createBadgeJson(options: BadgeOptions): Promise<BadgeSpec>;
// (undocumented)
createBadgeSvg(options: BadgeOptions): Promise<string>;
// (undocumented)
getBadges(): Promise<BadgeInfo[]>;
}
// @public (undocumented)
export interface RouterOptions {
// (undocumented)
badgeBuilder?: BadgeBuilder;
// (undocumented)
badgeFactories?: BadgeFactories;
// (undocumented)
catalog?: CatalogApi;
// (undocumented)
config: Config;
// (undocumented)
discovery: PluginEndpointDiscovery;
}
// (No @packageDocumentation comment for this package)
```
+21
View File
@@ -0,0 +1,21 @@
## API Report File for "@backstage/plugin-badges"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackstagePlugin } from '@backstage/core';
// @public (undocumented)
export const badgesPlugin: BackstagePlugin<{}, {}>;
// @public (undocumented)
export const EntityBadgesDialog: ({ open, onClose }: {
open: boolean;
onClose?: (() => any) | undefined;
}) => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+22
View File
@@ -0,0 +1,22 @@
## API Report File for "@backstage/plugin-bitrise"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackstagePlugin } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
// @public (undocumented)
export const bitrisePlugin: BackstagePlugin<{}, {}>;
// @public (undocumented)
export const EntityBitriseContent: () => JSX.Element;
// @public (undocumented)
export const isBitriseAvailable: (entity: Entity) => boolean;
// (No @packageDocumentation comment for this package)
```
+74 -14
View File
@@ -1,21 +1,80 @@
# Catalog Backend
This is the backend part of the default catalog plugin.
This is the backend for the default Backstage [software
catalog](http://backstage.io/docs/features/software-catalog/software-catalog-overview).
This provides an API for consumers such as the frontend [catalog
plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog).
It comes with a builtin database backed implementation of the catalog, that can store
and serve your catalog for you.
It comes with a builtin database-backed implementation of the catalog that can
store and serve your catalog for you.
It can also act as a bridge to your existing catalog solutions, either ingesting their
data to store in the database, or by effectively proxying calls to an external catalog
service.
It can also act as a bridge to your existing catalog solutions, either ingesting
data to store in the database, or by effectively proxying calls to an
external catalog service.
## Getting Started
## Installation
This backend plugin can be started in a standalone mode from directly in this package
with `yarn start`. However, it will have limited functionality and that process is
most convenient when developing the catalog backend plugin itself.
This `@backstage/plugin-catalog-backend` package comes installed by default in
any Backstage application created with `npx @backstage/create-app`, so
installation is not usually required.
To evaluate the catalog and have a greater amount of functionality available, instead do
To check if you already have the package, look under
`packages/backend/package.json`, in the `dependencies` block, for
`@backstage/plugin-catalog-backend`. The instructions below walk through
restoring the plugin, if you previously removed it.
### Install the package
```bash
# From your Backstage root directory
cd packages/backend
yarn add @backstage/plugin-catalog-backend
```
### Adding the plugin to your `packages/backend`
You'll need to add the plugin to the router in your `backend` package. You can
do this by creating a file called `packages/backend/src/plugins/catalog.ts` with
contents matching [catalog.ts in the create-app
template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts).
With the `catalog.ts` router setup in place, add the router to
`packages/backend/src/index.ts`:
```diff
+import catalog from './plugins/catalog';
async function main() {
...
const createEnv = makeCreateEnv(config);
+ const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const apiRouter = Router();
+ apiRouter.use('/catalog', await catalog(catalogEnv));
...
apiRouter.use(notFoundHandler());
```
### Adding catalog entities
At this point the `catalog-backend` is installed in your backend package, but
you will not have any catalog entities loaded. See [Catalog
Configuration](https://backstage.io/docs/features/software-catalog/configuration)
for how to add locations, or copy the catalog locations from the [create-app
template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs)
to get up and running quickly.
## Development
This backend plugin can be started in a standalone mode from directly in this
package with `yarn start`. However, it will have limited functionality and that
process is most convenient when developing the catalog backend plugin itself.
To evaluate the catalog and have a greater amount of functionality available,
run the entire Backstage example application from the root folder:
```bash
# in one terminal window, run this from from the very root of the Backstage project
@@ -23,9 +82,10 @@ cd packages/backend
yarn start
```
This will launch the full example backend, populated some example entities.
This will launch both frontend and backend in the same window, populated with
some example entities.
## Links
- [Frontend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog)
- [The Backstage homepage](https://backstage.io)
- [catalog](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend)
is the frontend interface for this plugin.
+25
View File
@@ -0,0 +1,25 @@
## API Report File for "@backstage/plugin-catalog-graphql"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Config } from '@backstage/config';
import { GraphQLModule } from '@graphql-modules/core';
import { Logger } from 'winston';
// @public (undocumented)
export function createModule(options: ModuleOptions): Promise<GraphQLModule>;
// @public (undocumented)
export interface ModuleOptions {
// (undocumented)
config: Config;
// (undocumented)
logger: Logger;
}
// (No @packageDocumentation comment for this package)
```
+130
View File
@@ -0,0 +1,130 @@
## API Report File for "@backstage/plugin-catalog-import"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { CatalogApi } from '@backstage/catalog-client';
import { ConfigApi } from '@backstage/core';
import { Control } from 'react-hook-form';
import { DiscoveryApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { EntityName } from '@backstage/catalog-model';
import { FieldErrors } from 'react-hook-form';
import { IdentityApi } from '@backstage/core';
import { InfoCardVariants } from '@backstage/core';
import { OAuthApi } from '@backstage/core';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { SubmitHandler } from 'react-hook-form';
import { TextFieldProps } from '@material-ui/core/TextField/TextField';
import { UnpackNestedValue } from 'react-hook-form';
import { UseControllerOptions } from 'react-hook-form';
import { UseFormMethods } from 'react-hook-form';
import { UseFormOptions } from 'react-hook-form';
// @public (undocumented)
export type AnalyzeResult = {
type: 'locations';
locations: Array<{
target: string;
entities: EntityName[];
}>;
} | {
type: 'repository';
url: string;
integrationType: string;
generatedEntities: PartialEntity[];
};
// @public (undocumented)
export const AutocompleteTextField: <TFieldValue extends string>({ name, options, required, control, errors, rules, loading, loadingText, helperText, errorHelperText, textFieldProps, }: Props_4<TFieldValue>) => JSX.Element;
// @public (undocumented)
export interface CatalogImportApi {
// (undocumented)
analyzeUrl(url: string): Promise<AnalyzeResult>;
// (undocumented)
submitPullRequest(options: {
repositoryUrl: string;
fileContent: string;
title: string;
body: string;
}): Promise<{
link: string;
location: string;
}>;
}
// @public (undocumented)
export const catalogImportApiRef: ApiRef<CatalogImportApi>;
// @public (undocumented)
export class CatalogImportClient implements CatalogImportApi {
constructor(options: {
discoveryApi: DiscoveryApi;
githubAuthApi: OAuthApi;
identityApi: IdentityApi;
scmIntegrationsApi: ScmIntegrationRegistry;
catalogApi: CatalogApi;
});
// (undocumented)
analyzeUrl(url: string): Promise<AnalyzeResult>;
// (undocumented)
submitPullRequest({ repositoryUrl, fileContent, title, body, }: {
repositoryUrl: string;
fileContent: string;
title: string;
body: string;
}): Promise<{
link: string;
location: string;
}>;
}
// @public (undocumented)
export const CatalogImportPage: (opts: StepperProviderOpts) => JSX.Element;
// @public (undocumented)
const catalogImportPlugin: BackstagePlugin<{
importPage: RouteRef<undefined>;
}, {}>;
export { catalogImportPlugin }
export { catalogImportPlugin as plugin }
// @public
export function defaultGenerateStepper(flow: ImportFlows, defaults: StepperProvider): StepperProvider;
// @public (undocumented)
export const EntityListComponent: ({ locations, collapsed, locationListItemIcon, onItemClick, firstListItem, withLinks, }: Props_2) => JSX.Element;
// @public (undocumented)
export const ImportStepper: ({ initialUrl, generateStepper, variant, opts, }: Props) => JSX.Element;
// @public
export const PreparePullRequestForm: <TFieldValues extends Record<string, any>>({ defaultValues, onSubmit, render, }: Props_5<TFieldValues>) => JSX.Element;
// @public (undocumented)
export const PreviewCatalogInfoComponent: ({ repositoryUrl, entities, classes, }: Props_6) => JSX.Element;
// @public (undocumented)
export const PreviewPullRequestComponent: ({ title, description, classes, }: Props_7) => JSX.Element;
// @public (undocumented)
export const Router: (opts: StepperProviderOpts) => JSX.Element;
// @public
export const StepInitAnalyzeUrl: ({ onAnalysis, analysisUrl, disablePullRequest, }: Props_3) => JSX.Element;
// @public (undocumented)
export const StepPrepareCreatePullRequest: ({ analyzeResult, onPrepare, onGoBack, renderFormFields, defaultTitle, defaultBody, }: Props_8) => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+93 -12
View File
@@ -1,26 +1,107 @@
# Backstage Catalog Frontend
This is the frontend part of the default catalog plugin.
This is the React frontend for the default Backstage [software
catalog](http://backstage.io/docs/features/software-catalog/software-catalog-overview).
This package supplies interfaces related to listing catalog entities or showing
more information about them on entity pages.
It will implement the core API for handling your catalog of software, and
supply the base views to show and manage them.
## Installation
## Getting Started
This `@backstage/plugin-catalog` package comes installed by default in any
Backstage application created with `npx @backstage/create-app`, so installation
is not usually required.
This frontend plugin can be started in a standalone mode from directly in this package
with `yarn start`. However, it will have limited functionality and that process is
most convenient when developing the catalog frontend plugin itself.
To check if you already have the package, look under
`packages/app/package.json`, in the `dependencies` block, for
`@backstage/plugin-catalog`. The instructions below walk through restoring the
plugin, if you previously removed it.
To evaluate the catalog and have a greater amount of functionality available, from the main
Backstage root folder, instead do:
### Install the package
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-catalog
```
### Add the plugin to your `packages/app`
Add the two pages that the catalog plugin provides to your app. You can choose
any name for these routes, but we recommend the following:
```diff
// packages/app/src/App.tsx
import {
CatalogIndexPage,
CatalogEntityPage,
} from '@backstage/plugin-catalog';
import { entityPage } from './components/catalog/EntityPage';
<FlatRoutes>
+ <Route path="/catalog" element={<CatalogIndexPage />} />
+ <Route path="/catalog/:namespace/:kind/:name" element={<CatalogEntityPage />}>
+ {/*
+ This is the root of the custom entity pages for your app, refer to the example app
+ in the main repo or the output of @backstage/create-app for an example
+ */}
+ {entityPage}
+ </Route>
...
</FlatRoutes>
```
The catalog plugin also has one external route that needs to be bound for it to
function: the `createComponent` route which should link to the page where the
user can create components. In a typical setup the create component route will
be linked to the scaffolder plugin's template index page:
```diff
// packages/app/src/App.tsx
+import { catalogPlugin } from '@backstage/plugin-catalog';
+import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
const app = createApp({
// ...
bindRoutes({ bind }) {
+ bind(catalogPlugin.externalRoutes, {
+ createComponent: scaffolderPlugin.routes.root,
+ });
},
});
```
You may also want to add a link to the catalog index page to your application
sidebar:
```diff
// packages/app/src/components/Root/Root.tsx
+import HomeIcon from '@material-ui/icons/Home';
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
+ <SidebarItem icon={HomeIcon} to="/catalog" text="Home" />
...
</Sidebar>
```
## Development
This frontend plugin can be started in a standalone mode from directly in this
package with `yarn start`. However, it will have limited functionality and that
process is most convenient when developing the catalog frontend plugin itself.
To evaluate the catalog and have a greater amount of functionality available,
run the entire Backstage example application from the root folder:
```bash
yarn dev
```
This will launch both frontend and backend in the same window, populated with some example entities.
This will launch both frontend and backend in the same window, populated with
some example entities.
## Links
- [Backend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend)
- [The Backstage homepage](https://backstage.io)
- [catalog-backend](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend)
provides the backend API for this frontend.
+2
View File
@@ -33,6 +33,7 @@
"@backstage/catalog-client": "^0.3.13",
"@backstage/catalog-model": "^0.8.3",
"@backstage/core": "^0.7.13",
"@backstage/core-plugin-api": "^0.1.2",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.6",
"@backstage/integration-react": "^0.1.3",
@@ -54,6 +55,7 @@
},
"devDependencies": {
"@backstage/cli": "^0.7.1",
"@backstage/core-app-api": "^0.1.2",
"@backstage/dev-utils": "^0.1.17",
"@backstage/test-utils": "^0.1.13",
"@testing-library/jest-dom": "^5.10.1",
@@ -29,6 +29,7 @@ import {
Progress,
RoutedTabs,
} from '@backstage/core';
import { useElementFilter } from '@backstage/core-plugin-api';
import {
EntityContext,
EntityRefLinks,
@@ -37,14 +38,7 @@ import {
} from '@backstage/plugin-catalog-react';
import { Box, TabProps } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import {
Children,
default as React,
Fragment,
isValidElement,
useContext,
useState,
} from 'react';
import React, { useContext, useState } from 'react';
import { useNavigate } from 'react-router';
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity';
@@ -58,46 +52,14 @@ type SubRoute = {
tabProps?: TabProps<React.ElementType, { component?: React.ElementType }>;
};
const dataKey = 'plugin.catalog.entityLayoutRoute';
const Route: (props: SubRoute) => null = () => null;
attachComponentData(Route, dataKey, true);
// This causes all mount points that are discovered within this route to use the path of the route itself
attachComponentData(Route, 'core.gatherMountPoints', true);
function createSubRoutesFromChildren(
childrenProps: React.ReactNode,
entity: Entity | undefined,
): SubRoute[] {
// Directly comparing child.type with Route will not work with in
// combination with react-hot-loader in storybook
// https://github.com/gaearon/react-hot-loader/issues/304
const routeType = (
<Route path="" title="">
<div />
</Route>
).type;
return Children.toArray(childrenProps).flatMap(child => {
if (!isValidElement(child)) {
return [];
}
if (child.type === Fragment) {
return createSubRoutesFromChildren(child.props.children, entity);
}
if (child.type !== routeType) {
throw new Error('Child of EntityLayout must be an EntityLayout.Route');
}
const { path, title, children, if: condition, tabProps } = child.props;
if (condition && entity && !condition(entity)) {
return [];
}
return [{ path, title, children, tabProps }];
});
}
const EntityLayoutTitle = ({
entity,
title,
@@ -195,7 +157,29 @@ export const EntityLayout = ({
const { kind, namespace, name } = useEntityCompoundName();
const { entity, loading, error } = useContext(EntityContext);
const routes = createSubRoutesFromChildren(children, entity);
const routes = useElementFilter(children, elements =>
elements
.selectByComponentData({
key: dataKey,
withStrictError: 'Child of EntityLayout must be an EntityLayout.Route',
})
.getElements<SubRoute>() // all nodes, element data, maintain structure or not?
.flatMap(({ props }) => {
if (props.if && entity && !props.if(entity)) {
return [];
}
return [
{
path: props.path,
title: props.title,
children: props.children,
tabProps: props.tabProps,
},
];
}),
);
const { headerTitle, headerType } = headerProps(
kind,
namespace,
@@ -20,6 +20,19 @@ import { render } from '@testing-library/react';
import React from 'react';
import { isKind } from './conditions';
import { EntitySwitch } from './EntitySwitch';
import { featureFlagsApiRef } from '@backstage/core-plugin-api';
import {
LocalStorageFeatureFlags,
ApiProvider,
ApiRegistry,
} from '@backstage/core-app-api';
const mockFeatureFlagsApi = new LocalStorageFeatureFlags();
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={ApiRegistry.with(featureFlagsApiRef, mockFeatureFlagsApi)}>
{children}
</ApiProvider>
);
describe('EntitySwitch', () => {
it('should switch child when entity switches', () => {
@@ -32,15 +45,17 @@ describe('EntitySwitch', () => {
);
const rendered = render(
<EntityContext.Provider
value={{
entity: { kind: 'component' } as Entity,
loading: false,
error: undefined,
}}
>
{content}
</EntityContext.Provider>,
<Wrapper>
<EntityContext.Provider
value={{
entity: { kind: 'component' } as Entity,
loading: false,
error: undefined,
}}
>
{content}
</EntityContext.Provider>
</Wrapper>,
);
expect(rendered.queryByText('A')).toBeInTheDocument();
@@ -48,15 +63,17 @@ describe('EntitySwitch', () => {
expect(rendered.queryByText('C')).not.toBeInTheDocument();
rendered.rerender(
<EntityContext.Provider
value={{
entity: { kind: 'template' } as Entity,
loading: false,
error: undefined,
}}
>
{content}
</EntityContext.Provider>,
<Wrapper>
<EntityContext.Provider
value={{
entity: { kind: 'template' } as Entity,
loading: false,
error: undefined,
}}
>
{content}
</EntityContext.Provider>
</Wrapper>,
);
expect(rendered.queryByText('A')).not.toBeInTheDocument();
@@ -64,15 +81,17 @@ describe('EntitySwitch', () => {
expect(rendered.queryByText('C')).not.toBeInTheDocument();
rendered.rerender(
<EntityContext.Provider
value={{
entity: { kind: 'derp' } as Entity,
loading: false,
error: undefined,
}}
>
{content}
</EntityContext.Provider>,
<Wrapper>
<EntityContext.Provider
value={{
entity: { kind: 'derp' } as Entity,
loading: false,
error: undefined,
}}
>
{content}
</EntityContext.Provider>
</Wrapper>,
);
expect(rendered.queryByText('A')).not.toBeInTheDocument();
@@ -88,24 +107,28 @@ describe('EntitySwitch', () => {
};
const rendered = render(
<EntityContext.Provider value={entityContextValue}>
<EntitySwitch>
<EntitySwitch.Case if={isKind('component')} children="A" />
<EntitySwitch.Case children="B" />
</EntitySwitch>
</EntityContext.Provider>,
<Wrapper>
<EntityContext.Provider value={entityContextValue}>
<EntitySwitch>
<EntitySwitch.Case if={isKind('component')} children="A" />
<EntitySwitch.Case children="B" />
</EntitySwitch>
</EntityContext.Provider>
</Wrapper>,
);
expect(rendered.queryByText('A')).toBeInTheDocument();
expect(rendered.queryByText('B')).not.toBeInTheDocument();
rendered.rerender(
<EntityContext.Provider value={entityContextValue}>
<EntitySwitch>
<EntitySwitch.Case if={isKind('template')} children="A" />
<EntitySwitch.Case children="B" />
</EntitySwitch>
</EntityContext.Provider>,
<Wrapper>
<EntityContext.Provider value={entityContextValue}>
<EntitySwitch>
<EntitySwitch.Case if={isKind('template')} children="A" />
<EntitySwitch.Case children="B" />
</EntitySwitch>
</EntityContext.Provider>
</Wrapper>,
);
expect(rendered.queryByText('A')).not.toBeInTheDocument();
@@ -16,49 +16,40 @@
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { PropsWithChildren, ReactNode } from 'react';
import {
Children,
Fragment,
isValidElement,
PropsWithChildren,
ReactNode,
useMemo,
} from 'react';
attachComponentData,
useElementFilter,
} from '@backstage/core-plugin-api';
const ENTITY_SWITCH_KEY = 'core.backstage.entitySwitch';
const EntitySwitchCase = (_: {
if?: (entity: Entity) => boolean;
children: ReactNode;
}) => null;
attachComponentData(EntitySwitchCase, ENTITY_SWITCH_KEY, true);
type SwitchCase = {
if?: (entity: Entity) => boolean;
children: JSX.Element;
};
function createSwitchCasesFromChildren(childrenNode: ReactNode): SwitchCase[] {
return Children.toArray(childrenNode).flatMap(child => {
if (!isValidElement(child)) {
return [];
}
if (child.type === Fragment) {
return createSwitchCasesFromChildren(child.props.children);
}
if (child.type !== EntitySwitchCase) {
throw new Error(`Child of EntitySwitch is not an EntitySwitch.Case`);
}
const { if: condition, children } = child.props;
return [{ if: condition, children }];
});
}
export const EntitySwitch = ({ children }: PropsWithChildren<{}>) => {
const { entity } = useEntity();
const switchCases = useMemo(() => createSwitchCasesFromChildren(children), [
children,
]);
const switchCases = useElementFilter(children, collection =>
collection
.selectByComponentData({
key: ENTITY_SWITCH_KEY,
withStrictError: 'Child of EntitySwitch is not an EntitySwitch.Case',
})
.getElements()
.flatMap<SwitchCase>((element: React.ReactElement) => {
const { if: condition, children: elementsChildren } = element.props;
return [{ if: condition, children: elementsChildren }];
}),
);
const matchingCase = switchCases.find(switchCase =>
switchCase.if ? switchCase.if(entity) : true,
+81
View File
@@ -0,0 +1,81 @@
## API Report File for "@backstage/plugin-circleci"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { BuildStepAction } from 'circleci-api';
import { BuildSummary } from 'circleci-api';
import { BuildSummaryResponse } from 'circleci-api';
import { BuildWithSteps } from 'circleci-api';
import { CircleCIOptions } from 'circleci-api';
import { DiscoveryApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { GitType } from 'circleci-api';
import { Me } from 'circleci-api';
import { RouteRef } from '@backstage/core';
export { BuildStepAction }
export { BuildSummary }
export { BuildWithSteps }
// @public (undocumented)
export const CIRCLECI_ANNOTATION = "circleci.com/project-slug";
// @public (undocumented)
export class CircleCIApi {
constructor(options: Options);
// (undocumented)
getBuild(buildNumber: number, options: Partial<CircleCIOptions>): Promise<BuildWithSteps>;
// (undocumented)
getBuilds({ limit, offset }: {
limit: number;
offset: number;
}, options: Partial<CircleCIOptions>): Promise<BuildSummaryResponse>;
// (undocumented)
getUser(options: Partial<CircleCIOptions>): Promise<Me>;
// (undocumented)
retry(buildNumber: number, options: Partial<CircleCIOptions>): Promise<BuildSummary>;
}
// @public (undocumented)
export const circleCIApiRef: ApiRef<CircleCIApi>;
// @public (undocumented)
export const circleCIBuildRouteRef: RouteRef<undefined>;
// @public (undocumented)
const circleCIPlugin: BackstagePlugin<{}, {}>;
export { circleCIPlugin }
export { circleCIPlugin as plugin }
// @public (undocumented)
export const circleCIRouteRef: RouteRef<undefined>;
// @public (undocumented)
export const EntityCircleCIContent: (_props: {
entity?: Entity| undefined;
}) => JSX.Element;
export { GitType }
// @public (undocumented)
const isCircleCIAvailable: (entity: Entity) => boolean;
export { isCircleCIAvailable }
export { isCircleCIAvailable as isPluginApplicableToEntity }
// @public (undocumented)
export const Router: (_props: Props) => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+283
View File
@@ -0,0 +1,283 @@
## API Report File for "@backstage/plugin-cloudbuild"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { OAuthApi } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export type ActionsGetWorkflowResponseData = {
id: string;
status: string;
source: Source;
createTime: string;
startTime: string;
steps: Step[];
timeout: string;
projectId: string;
logsBucket: string;
sourceProvenance: SourceProvenance;
buildTriggerId: string;
options: Options;
logUrl: string;
substitutions: Substitutions;
tags: string[];
queueTtl: string;
name: string;
finishTime: any;
results: Results;
timing: Timing2;
};
// @public (undocumented)
export interface ActionsListWorkflowRunsForRepoResponseData {
// (undocumented)
builds: ActionsGetWorkflowResponseData[];
}
// @public (undocumented)
export interface BUILD {
// (undocumented)
endTime: string;
// (undocumented)
startTime: string;
}
// @public (undocumented)
export const CLOUDBUILD_ANNOTATION = "google.com/cloudbuild-project-slug";
// @public (undocumented)
export type CloudbuildApi = {
listWorkflowRuns: (request: {
projectId: string;
}) => Promise<ActionsListWorkflowRunsForRepoResponseData>;
getWorkflow: ({ projectId, id, }: {
projectId: string;
id: string;
}) => Promise<ActionsGetWorkflowResponseData>;
getWorkflowRun: ({ projectId, id, }: {
projectId: string;
id: string;
}) => Promise<ActionsGetWorkflowResponseData>;
reRunWorkflow: ({ projectId, runId, }: {
projectId: string;
runId: string;
}) => Promise<any>;
};
// @public (undocumented)
export const cloudbuildApiRef: ApiRef<CloudbuildApi>;
// @public (undocumented)
export class CloudbuildClient implements CloudbuildApi {
constructor(googleAuthApi: OAuthApi);
// (undocumented)
getToken(): Promise<string>;
// (undocumented)
getWorkflow({ projectId, id, }: {
projectId: string;
id: string;
}): Promise<ActionsGetWorkflowResponseData>;
// (undocumented)
getWorkflowRun({ projectId, id, }: {
projectId: string;
id: string;
}): Promise<ActionsGetWorkflowResponseData>;
// (undocumented)
listWorkflowRuns({ projectId, }: {
projectId: string;
}): Promise<ActionsListWorkflowRunsForRepoResponseData>;
// (undocumented)
reRunWorkflow({ projectId, runId, }: {
projectId: string;
runId: string;
}): Promise<void>;
}
// @public (undocumented)
const cloudbuildPlugin: BackstagePlugin<{
entityContent: RouteRef<undefined>;
}, {}>;
export { cloudbuildPlugin }
export { cloudbuildPlugin as plugin }
// @public (undocumented)
export const EntityCloudbuildContent: (_props: {
entity?: Entity| undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityLatestCloudbuildRunCard: ({ branch, }: {
entity?: Entity| undefined;
branch: string;
}) => JSX.Element;
// @public (undocumented)
export const EntityLatestCloudbuildsForBranchCard: ({ branch, }: {
entity?: Entity| undefined;
branch: string;
}) => JSX.Element;
// @public (undocumented)
export interface FETCHSOURCE {
// (undocumented)
endTime: string;
// (undocumented)
startTime: string;
}
// @public (undocumented)
const isCloudbuildAvailable: (entity: Entity) => boolean;
export { isCloudbuildAvailable }
export { isCloudbuildAvailable as isPluginApplicableToEntity }
// @public (undocumented)
export const LatestWorkflowRunCard: ({ branch, }: {
entity?: Entity | undefined;
branch: string;
}) => JSX.Element;
// @public (undocumented)
export const LatestWorkflowsForBranchCard: ({ branch, }: {
entity?: Entity | undefined;
branch: string;
}) => JSX.Element;
// @public (undocumented)
export interface Options {
// (undocumented)
dynamicSubstitutions: boolean;
// (undocumented)
logging: string;
// (undocumented)
machineType: string;
// (undocumented)
substitutionOption: string;
}
// @public (undocumented)
export interface PullTiming {
// (undocumented)
endTime: string;
// (undocumented)
startTime: string;
}
// @public (undocumented)
export interface ResolvedStorageSource {
// (undocumented)
bucket: string;
// (undocumented)
generation: string;
// (undocumented)
object: string;
}
// @public (undocumented)
export interface Results {
// (undocumented)
buildStepImages: string[];
// (undocumented)
buildStepOutputs: string[];
}
// @public (undocumented)
export const Router: (_props: Props) => JSX.Element;
// @public (undocumented)
export interface Source {
// (undocumented)
storageSource: StorageSource;
}
// @public (undocumented)
export interface SourceProvenance {
// (undocumented)
fileHashes: {};
// (undocumented)
resolvedStorageSource: {};
}
// @public (undocumented)
export interface Step {
// (undocumented)
args: string[];
// (undocumented)
dir: string;
// (undocumented)
entrypoint: string;
// (undocumented)
id: string;
// (undocumented)
name: string;
// (undocumented)
pullTiming: PullTiming;
// (undocumented)
status: string;
// (undocumented)
timing: Timing;
// (undocumented)
volumes: Volume[];
// (undocumented)
waitFor: string[];
}
// @public (undocumented)
export interface StorageSource {
// (undocumented)
bucket: string;
// (undocumented)
object: string;
}
// @public (undocumented)
export interface Substitutions {
// (undocumented)
BRANCH_NAME: string;
// (undocumented)
COMMIT_SHA: string;
// (undocumented)
REPO_NAME: string;
// (undocumented)
REVISION_ID: string;
// (undocumented)
SHORT_SHA: string;
}
// @public (undocumented)
export interface Timing {
// (undocumented)
endTime: string;
// (undocumented)
startTime: string;
}
// @public (undocumented)
export interface Timing2 {
// (undocumented)
BUILD: BUILD;
// (undocumented)
FETCHSOURCE: FETCHSOURCE;
}
// @public (undocumented)
export interface Volume {
// (undocumented)
name: string;
// (undocumented)
path: string;
}
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,43 @@
## API Report File for "@backstage/plugin-code-coverage-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Config } from '@backstage/config';
import express from 'express';
import { Logger } from 'winston';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { UrlReader } from '@backstage/backend-common';
// @public (undocumented)
export interface CodeCoverageApi {
// (undocumented)
name: string;
}
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public (undocumented)
export const makeRouter: (options: RouterOptions) => Promise<express.Router>;
// @public (undocumented)
export interface RouterOptions {
// (undocumented)
config: Config;
// (undocumented)
database: PluginDatabaseManager;
// (undocumented)
discovery: PluginEndpointDiscovery;
// (undocumented)
logger: Logger;
// (undocumented)
urlReader: UrlReader;
}
// (No @packageDocumentation comment for this package)
```
+32
View File
@@ -0,0 +1,32 @@
## API Report File for "@backstage/plugin-code-coverage"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackstagePlugin } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const codeCoveragePlugin: BackstagePlugin<{
root: RouteRef<undefined>;
}, {}>;
// @public (undocumented)
export const EntityCodeCoverageContent: () => JSX.Element;
// @public (undocumented)
const isCodeCoverageAvailable: (entity: Entity) => boolean;
export { isCodeCoverageAvailable }
export { isCodeCoverageAvailable as isPluginApplicableToEntity }
// @public (undocumented)
export const Router: () => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+42
View File
@@ -0,0 +1,42 @@
## API Report File for "@backstage/plugin-config-schema"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { Observable } from '@backstage/core';
import { RouteRef } from '@backstage/core';
import { Schema } from 'jsonschema';
// @public (undocumented)
export interface ConfigSchemaApi {
// (undocumented)
schema$(): Observable<ConfigSchemaResult>;
}
// @public (undocumented)
export const configSchemaApiRef: ApiRef<ConfigSchemaApi>;
// @public (undocumented)
export const ConfigSchemaPage: () => JSX.Element;
// @public (undocumented)
export const configSchemaPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
}, {}>;
// @public
export class StaticSchemaLoader implements ConfigSchemaApi {
constructor({ url }?: {
url?: string;
});
// (undocumented)
schema$(): Observable<ConfigSchemaResult>;
}
// (No @packageDocumentation comment for this package)
```
+621
View File
@@ -0,0 +1,621 @@
## API Report File for "@backstage/plugin-cost-insights"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePalette } from '@backstage/theme';
import { BackstagePlugin } from '@backstage/core';
import { BackstageTheme } from '@backstage/theme';
import { ContentRenderer } from 'recharts';
import { Dispatch } from 'react';
import { ForwardRefExoticComponent } from 'react';
import { PaletteOptions } from '@material-ui/core/styles/createPalette';
import { PropsWithChildren } from 'react';
import { ReactNode } from 'react';
import { RechartsFunction } from 'recharts';
import { RefAttributes } from 'react';
import { RouteRef } from '@backstage/core';
import { SetStateAction } from 'react';
import { TooltipProps } from 'recharts';
import { TypographyProps } from '@material-ui/core';
// @public
export type Alert = {
title: string | JSX.Element;
subtitle: string | JSX.Element;
element?: JSX.Element;
status?: AlertStatus;
url?: string;
buttonText?: string;
SnoozeForm?: Maybe<AlertForm>;
AcceptForm?: Maybe<AlertForm>;
DismissForm?: Maybe<AlertForm>;
onSnoozed?(options: AlertOptions): Promise<Alert[]>;
onAccepted?(options: AlertOptions): Promise<Alert[]>;
onDismissed?(options: AlertOptions): Promise<Alert[]>;
};
// @public (undocumented)
export interface AlertCost {
// (undocumented)
aggregation: [number, number];
// (undocumented)
id: string;
}
// @public (undocumented)
export interface AlertDismissFormData {
// (undocumented)
feedback: Maybe<string>;
// (undocumented)
other: Maybe<string>;
// (undocumented)
reason: AlertDismissReason;
}
// @public (undocumented)
export interface AlertDismissOption {
// (undocumented)
label: string;
// (undocumented)
reason: string;
}
// @public (undocumented)
export const AlertDismissOptions: AlertDismissOption[];
// @public (undocumented)
export enum AlertDismissReason {
// (undocumented)
Expected = "expected",
// (undocumented)
Migration = "migration",
// (undocumented)
NotApplicable = "not-applicable",
// (undocumented)
Other = "other",
// (undocumented)
Resolved = "resolved",
// (undocumented)
Seasonal = "seasonal"
}
// @public (undocumented)
export type AlertForm<A extends Alert = any, Data = any> = ForwardRefExoticComponent<AlertFormProps<A, Data> & RefAttributes<HTMLFormElement>>;
// @public (undocumented)
export type AlertFormProps<A extends Alert, FormData = {}> = {
alert: A;
onSubmit: (data: FormData) => void;
disableSubmit: (isDisabled: boolean) => void;
};
// @public (undocumented)
export interface AlertOptions<T = any> {
// (undocumented)
data: T;
// (undocumented)
group: string;
}
// @public
export interface AlertSnoozeFormData {
// (undocumented)
intervals: string;
}
// @public (undocumented)
export type AlertSnoozeOption = {
label: string;
duration: Duration;
};
// @public (undocumented)
export const AlertSnoozeOptions: AlertSnoozeOption[];
// @public (undocumented)
export enum AlertStatus {
// (undocumented)
Accepted = "accepted",
// (undocumented)
Dismissed = "dismissed",
// (undocumented)
Snoozed = "snoozed"
}
// @public (undocumented)
export const BarChart: ({ resources, responsive, displayAmount, options, tooltip, onClick, onMouseMove, }: BarChartProps) => JSX.Element;
// @public
export interface BarChartData extends BarChartOptions {
}
// @public (undocumented)
export const BarChartLegend: ({ costStart, costEnd, options, children, }: PropsWithChildren<BarChartLegendProps>) => JSX.Element;
// @public (undocumented)
export type BarChartLegendOptions = {
previousName: string;
previousFill: string;
currentName: string;
currentFill: string;
hideMarker?: boolean;
};
// @public (undocumented)
export type BarChartLegendProps = {
costStart: number;
costEnd: number;
options?: Partial<BarChartLegendOptions>;
};
// @public (undocumented)
export interface BarChartOptions {
// (undocumented)
currentFill: string;
// (undocumented)
currentName: string;
// (undocumented)
previousFill: string;
// (undocumented)
previousName: string;
}
// @public (undocumented)
export type BarChartProps = {
resources: ResourceData[];
responsive?: boolean;
displayAmount?: number;
options?: Partial<BarChartData>;
tooltip?: ContentRenderer<TooltipProps>;
onClick?: RechartsFunction;
onMouseMove?: RechartsFunction;
};
// @public (undocumented)
export const BarChartTooltip: ({ title, content, subtitle, topRight, actions, children, }: PropsWithChildren<BarChartTooltipProps>) => JSX.Element;
// @public (undocumented)
export const BarChartTooltipItem: ({ item }: BarChartTooltipItemProps) => JSX.Element;
// @public (undocumented)
export type BarChartTooltipItemProps = {
item: TooltipItem;
};
// @public (undocumented)
export type BarChartTooltipProps = {
title: string;
content?: ReactNode | string;
subtitle?: ReactNode;
topRight?: ReactNode;
actions?: ReactNode;
};
// @public (undocumented)
export interface ChangeStatistic {
// (undocumented)
amount: number;
// (undocumented)
ratio?: number;
}
// @public (undocumented)
export enum ChangeThreshold {
// (undocumented)
lower = -0.05,
// (undocumented)
upper = 0.05
}
// @public (undocumented)
export type ChartData = {
date: number;
trend: number;
dailyCost: number;
[key: string]: number;
};
// @public (undocumented)
export interface Cost {
// (undocumented)
aggregation: DateAggregation[];
// (undocumented)
change?: ChangeStatistic;
// (undocumented)
groupedCosts?: Record<string, Cost[]>;
// (undocumented)
id: string;
// (undocumented)
trendline?: Trendline;
}
// @public (undocumented)
export const CostGrowth: ({ change, duration }: CostGrowthProps) => JSX.Element;
// @public (undocumented)
export const CostGrowthIndicator: ({ change, formatter, className, ...props }: CostGrowthIndicatorProps) => JSX.Element;
// @public (undocumented)
export type CostGrowthIndicatorProps = TypographyProps & {
change: ChangeStatistic;
formatter?: (change: ChangeStatistic) => Maybe<string>;
};
// @public (undocumented)
export type CostGrowthProps = {
change: ChangeStatistic;
duration: Duration;
};
// @public (undocumented)
export type CostInsightsApi = {
getLastCompleteBillingDate(): Promise<string>;
getUserGroups(userId: string): Promise<Group[]>;
getGroupProjects(group: string): Promise<Project[]>;
getGroupDailyCost(group: string, intervals: string): Promise<Cost>;
getProjectDailyCost(project: string, intervals: string): Promise<Cost>;
getDailyMetricData(metric: string, intervals: string): Promise<MetricData>;
getProductInsights(options: ProductInsightsOptions): Promise<Entity>;
getAlerts(group: string): Promise<Alert[]>;
};
// @public (undocumented)
export const costInsightsApiRef: ApiRef<CostInsightsApi>;
// @public (undocumented)
export const CostInsightsLabelDataflowInstructionsPage: () => JSX.Element;
// @public (undocumented)
export const CostInsightsPage: () => JSX.Element;
// @public (undocumented)
export type CostInsightsPalette = BackstagePalette & CostInsightsPaletteAdditions;
// @public (undocumented)
export type CostInsightsPaletteOptions = PaletteOptions & CostInsightsPaletteAdditions;
// @public (undocumented)
const costInsightsPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
growthAlerts: RouteRef<undefined>;
unlabeledDataflowAlerts: RouteRef<undefined>;
}, {}>;
export { costInsightsPlugin }
export { costInsightsPlugin as plugin }
// @public (undocumented)
export const CostInsightsProjectGrowthInstructionsPage: () => JSX.Element;
// @public (undocumented)
export interface CostInsightsTheme extends BackstageTheme {
// (undocumented)
palette: CostInsightsPalette;
}
// @public (undocumented)
export interface CostInsightsThemeOptions extends PaletteOptions {
// (undocumented)
palette: CostInsightsPaletteOptions;
}
// @public (undocumented)
export interface Currency {
// (undocumented)
kind: string | null;
// (undocumented)
label: string;
// (undocumented)
prefix?: string;
// (undocumented)
rate?: number;
// (undocumented)
unit: string;
}
// @public (undocumented)
export enum CurrencyType {
// (undocumented)
Beers = "BEERS",
// (undocumented)
CarbonOffsetTons = "CARBON_OFFSET_TONS",
// (undocumented)
IceCream = "PINTS_OF_ICE_CREAM",
// (undocumented)
USD = "USD"
}
// @public (undocumented)
export enum DataKey {
// (undocumented)
Current = "current",
// (undocumented)
Name = "name",
// (undocumented)
Previous = "previous"
}
// @public (undocumented)
export type DateAggregation = {
date: string;
amount: number;
};
// @public (undocumented)
export const DEFAULT_DATE_FORMAT = "YYYY-MM-DD";
// @public
export enum Duration {
// (undocumented)
P30D = "P30D",
// (undocumented)
P3M = "P3M",
// (undocumented)
P7D = "P7D",
// (undocumented)
P90D = "P90D"
}
// @public (undocumented)
export const EngineerThreshold = 0.5;
// @public (undocumented)
export interface Entity {
// (undocumented)
aggregation: [number, number];
// (undocumented)
change: ChangeStatistic;
// (undocumented)
entities: Record<string, Entity[]>;
// (undocumented)
id: Maybe<string>;
}
// @public (undocumented)
export class ExampleCostInsightsClient implements CostInsightsApi {
// (undocumented)
getAlerts(group: string): Promise<Alert[]>;
// (undocumented)
getDailyMetricData(metric: string, intervals: string): Promise<MetricData>;
// (undocumented)
getGroupDailyCost(group: string, intervals: string): Promise<Cost>;
// (undocumented)
getGroupProjects(group: string): Promise<Project[]>;
// (undocumented)
getLastCompleteBillingDate(): Promise<string>;
// (undocumented)
getProductInsights(options: ProductInsightsOptions): Promise<Entity>;
// (undocumented)
getProjectDailyCost(project: string, intervals: string): Promise<Cost>;
// (undocumented)
getUserGroups(userId: string): Promise<Group[]>;
}
// @public (undocumented)
export type Group = {
id: string;
};
// @public (undocumented)
export enum GrowthType {
// (undocumented)
Excess = 2,
// (undocumented)
Negligible = 0,
// (undocumented)
Savings = 1
}
// @public (undocumented)
export type Icon = {
kind: string;
component: JSX.Element;
};
// @public (undocumented)
export enum IconType {
// (undocumented)
Compute = "compute",
// (undocumented)
Data = "data",
// (undocumented)
Database = "database",
// (undocumented)
ML = "ml",
// (undocumented)
Search = "search",
// (undocumented)
Storage = "storage"
}
// @public (undocumented)
export const LegendItem: ({ title, tooltipText, markerColor, children, }: PropsWithChildren<LegendItemProps>) => JSX.Element;
// @public (undocumented)
export type LegendItemProps = {
title: string;
tooltipText?: string;
markerColor?: string;
};
// @public (undocumented)
export type Loading = Record<string, boolean>;
// @public (undocumented)
export type Maybe<T> = T | null;
// @public (undocumented)
export type Metric = {
kind: string;
name: string;
default: boolean;
};
// @public (undocumented)
export interface MetricData {
// (undocumented)
aggregation: DateAggregation[];
// (undocumented)
change: ChangeStatistic;
// (undocumented)
format: 'number' | 'currency';
// (undocumented)
id: string;
}
// @public (undocumented)
export const MockConfigProvider: ({ children, ...context }: MockConfigProviderProps) => JSX.Element;
// @public (undocumented)
export const MockCurrencyProvider: ({ children, ...context }: MockCurrencyProviderProps) => JSX.Element;
// @public (undocumented)
export interface PageFilters {
// (undocumented)
duration: Duration;
// (undocumented)
group: Maybe<string>;
// (undocumented)
metric: string | null;
// (undocumented)
project: Maybe<string>;
}
// @public (undocumented)
export interface Product {
// (undocumented)
kind: string;
// (undocumented)
name: string;
}
// @public (undocumented)
export type ProductFilters = Array<ProductPeriod>;
// @public (undocumented)
export type ProductInsightsOptions = {
product: string;
group: string;
intervals: string;
project: Maybe<string>;
};
// @public (undocumented)
export interface ProductPeriod {
// (undocumented)
duration: Duration;
// (undocumented)
productType: string;
}
// @public (undocumented)
export interface Project {
// (undocumented)
id: string;
// (undocumented)
name?: string;
}
// @public
export class ProjectGrowthAlert implements Alert {
constructor(data: ProjectGrowthData);
// (undocumented)
data: ProjectGrowthData;
// (undocumented)
get element(): JSX.Element;
// (undocumented)
get subtitle(): string;
// (undocumented)
get title(): string;
// (undocumented)
get url(): string;
}
// @public (undocumented)
export interface ProjectGrowthData {
// (undocumented)
aggregation: [number, number];
// (undocumented)
change: ChangeStatistic;
// (undocumented)
periodEnd: string;
// (undocumented)
periodStart: string;
// (undocumented)
products: Array<AlertCost>;
// (undocumented)
project: string;
}
// @public (undocumented)
export interface ResourceData {
// (undocumented)
current: number;
// (undocumented)
name: Maybe<string>;
// (undocumented)
previous: number;
}
// @public (undocumented)
export type TooltipItem = {
fill: string;
label: string;
value: string;
};
// @public (undocumented)
export type Trendline = {
slope: number;
intercept: number;
};
// @public
export class UnlabeledDataflowAlert implements Alert {
constructor(data: UnlabeledDataflowData);
// (undocumented)
data: UnlabeledDataflowData;
// (undocumented)
get element(): JSX.Element;
// (undocumented)
status?: AlertStatus;
// (undocumented)
get subtitle(): string;
// (undocumented)
get title(): string;
// (undocumented)
get url(): string;
}
// @public (undocumented)
export interface UnlabeledDataflowAlertProject {
// (undocumented)
id: string;
// (undocumented)
labeledCost: number;
// (undocumented)
unlabeledCost: number;
}
// @public (undocumented)
export interface UnlabeledDataflowData {
// (undocumented)
labeledCost: number;
// (undocumented)
periodEnd: string;
// (undocumented)
periodStart: string;
// (undocumented)
projects: Array<UnlabeledDataflowAlertProject>;
// (undocumented)
unlabeledCost: number;
}
// (No @packageDocumentation comment for this package)
```
+31
View File
@@ -0,0 +1,31 @@
## API Report File for "@backstage/plugin-explore-react"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
// @public (undocumented)
export type ExploreTool = {
title: string;
description?: string;
url: string;
image: string;
tags?: string[];
lifecycle?: string;
};
// @public (undocumented)
export interface ExploreToolsConfig {
// (undocumented)
getTools: () => Promise<ExploreTool[]>;
}
// @public (undocumented)
export const exploreToolsConfigRef: ApiRef<ExploreToolsConfig>;
// (No @packageDocumentation comment for this package)
```
+46
View File
@@ -33,3 +33,49 @@ import LayersIcon from '@material-ui/icons/Layers';
<SidebarItem icon={LayersIcon} to="explore" text="Explore" />
```
## Customization
Create a custom explore page in `packages/app/src/components/explore/ExplorePage.tsx`.
```tsx
import {
DomainExplorerContent,
ExploreLayout,
} from '@backstage/plugin-explore';
import React from 'react';
import { InnserSourceExplorerContent } from './InnserSourceExplorerContent';
export const ExplorePage = () => {
return (
<ExploreLayout
title="Explore the ACME corp ecosystem"
subtitle="Browse our ecosystem"
>
<ExploreLayout.Route path="domains" title="Domains">
<DomainExplorerContent />
</ExploreLayout.Route>
<ExploreLayout.Route path="inner-source" title="InnerSource">
<AcmeInnserSourceExplorerContent />
</ExploreLayout.Route>
</ExploreLayout>
);
};
export const explorePage = <ExplorePage />;
```
Now register the new explore page in `packages/app/src/App.tsx`.
```diff
+ import { explorePage } from './components/explore/ExplorePage';
const routes = (
<FlatRoutes>
- <Route path="/explore" element={<ExplorePage />} />
+ <Route path="/explore" element={<ExplorePage />}>
+ {explorePage}
+ </Route>
</FlatRoutes>
);
```
+38
View File
@@ -0,0 +1,38 @@
## API Report File for "@backstage/plugin-explore"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackstagePlugin } from '@backstage/core';
import { ExternalRouteRef } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const catalogEntityRouteRef: ExternalRouteRef<{
name: string;
kind: string;
namespace: string;
}, false>;
// @public (undocumented)
export const ExplorePage: () => JSX.Element;
// @public (undocumented)
export const explorePlugin: BackstagePlugin<{
explore: RouteRef<undefined>;
}, {
catalogEntity: ExternalRouteRef<{
name: string;
kind: string;
namespace: string;
}, false>;
}>;
// @public (undocumented)
export const exploreRouteRef: RouteRef<undefined>;
// (No @packageDocumentation comment for this package)
```
+2
View File
@@ -38,9 +38,11 @@
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@types/react": "^16.9",
"classnames": "^2.2.6",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^17.2.4"
},
@@ -0,0 +1,63 @@
/*
* 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 { ApiProvider, ApiRegistry } from '@backstage/core';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor, getByText } from '@testing-library/react';
import React from 'react';
import { DefaultExplorePage } from './DefaultExplorePage';
describe('<DefaultExplorePage />', () => {
const catalogApi: jest.Mocked<typeof catalogApiRef.T> = {
addLocation: jest.fn(_a => new Promise(() => {})),
getEntities: jest.fn(),
getOriginLocationByEntity: jest.fn(),
getLocationByEntity: jest.fn(),
getLocationById: jest.fn(),
removeLocationById: jest.fn(),
removeEntityByUid: jest.fn(),
getEntityByName: jest.fn(),
};
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={ApiRegistry.with(catalogApiRef, catalogApi)}>
{children}
</ApiProvider>
);
beforeEach(() => {
jest.resetAllMocks();
});
it('renders the default explore page', async () => {
catalogApi.getEntities.mockResolvedValue({ items: [] });
const { getAllByRole } = await renderInTestApp(
<Wrapper>
<DefaultExplorePage />
</Wrapper>,
);
await waitFor(() => {
const elements = getAllByRole('tab');
expect(elements.length).toBe(3);
expect(getByText(elements[0], 'Domains')).toBeInTheDocument();
expect(getByText(elements[1], 'Groups')).toBeInTheDocument();
expect(getByText(elements[2], 'Tools')).toBeInTheDocument();
});
});
});
@@ -0,0 +1,45 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { configApiRef, useApi } from '@backstage/core';
import { DomainExplorerContent } from '../DomainExplorerContent';
import { ExploreLayout } from '../ExploreLayout';
import { GroupsExplorerContent } from '../GroupsExplorerContent';
import { ToolExplorerContent } from '../ToolExplorerContent';
export const DefaultExplorePage = () => {
const configApi = useApi(configApiRef);
const organizationName =
configApi.getOptionalString('organization.name') ?? 'Backstage';
return (
<ExploreLayout
title={`Explore the ${organizationName} ecosystem`}
subtitle="Discover solutions available in your ecosystem"
>
<ExploreLayout.Route path="domains" title="Domains">
<DomainExplorerContent />
</ExploreLayout.Route>
<ExploreLayout.Route path="groups" title="Groups">
<GroupsExplorerContent />
</ExploreLayout.Route>
<ExploreLayout.Route path="tools" title="Tools">
<ToolExplorerContent />
</ExploreLayout.Route>
</ExploreLayout>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { DefaultExplorePage } from './DefaultExplorePage';
@@ -41,6 +41,12 @@ describe('<DomainExplorerContent />', () => {
</ApiProvider>
);
const mountedRoutes = {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': catalogEntityRouteRef,
},
};
beforeEach(() => {
jest.resetAllMocks();
});
@@ -74,11 +80,7 @@ describe('<DomainExplorerContent />', () => {
<Wrapper>
<DomainExplorerContent />
</Wrapper>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': catalogEntityRouteRef,
},
},
mountedRoutes,
);
await waitFor(() => {
@@ -87,6 +89,19 @@ describe('<DomainExplorerContent />', () => {
});
});
it('renders a custom title', async () => {
catalogApi.getEntities.mockResolvedValue({ items: [] });
const { getByText } = await renderInTestApp(
<Wrapper>
<DomainExplorerContent title="Our Areas" />
</Wrapper>,
mountedRoutes,
);
await waitFor(() => expect(getByText('Our Areas')).toBeInTheDocument());
});
it('renders empty state', async () => {
catalogApi.getEntities.mockResolvedValue({ items: [] });
@@ -94,11 +109,7 @@ describe('<DomainExplorerContent />', () => {
<Wrapper>
<DomainExplorerContent />
</Wrapper>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': catalogEntityRouteRef,
},
},
mountedRoutes,
);
await waitFor(() =>
@@ -114,11 +125,7 @@ describe('<DomainExplorerContent />', () => {
<Wrapper>
<DomainExplorerContent />
</Wrapper>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': catalogEntityRouteRef,
},
},
mountedRoutes,
);
await waitFor(() =>
@@ -79,10 +79,16 @@ const Body = () => {
);
};
export const DomainExplorerContent = () => {
type DomainExplorerContentProps = {
title?: string;
};
export const DomainExplorerContent = ({
title,
}: DomainExplorerContentProps) => {
return (
<Content noPadding>
<ContentHeader title="Domains">
<ContentHeader title={title ?? 'Domains'}>
<SupportButton>Discover the domains in your ecosystem.</SupportButton>
</ContentHeader>
<Body />
@@ -0,0 +1,81 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
import { ExploreLayout } from './ExploreLayout';
describe('<ExploreLayout />', () => {
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<>{children}</>
);
beforeEach(() => {
jest.resetAllMocks();
});
it('renders an explore tabbed layout page with defaults', async () => {
const { getByText } = await renderInTestApp(
<Wrapper>
<ExploreLayout>
<ExploreLayout.Route path="/tools" title="Tools">
<div>Tools Content</div>
</ExploreLayout.Route>
</ExploreLayout>
</Wrapper>,
);
await waitFor(() => {
expect(getByText('Explore our ecosystem')).toBeInTheDocument();
expect(
getByText('Discover solutions available in our ecosystem'),
).toBeInTheDocument();
});
});
it('renders a custom page title', async () => {
const { getByText } = await renderInTestApp(
<Wrapper>
<ExploreLayout title="Explore our universe">
<ExploreLayout.Route path="/tools" title="Tools">
<div>Tools Content</div>
</ExploreLayout.Route>
</ExploreLayout>
</Wrapper>,
);
await waitFor(() =>
expect(getByText('Explore our universe')).toBeInTheDocument(),
);
});
it('renders a custom page subtitle', async () => {
const { getByText } = await renderInTestApp(
<Wrapper>
<ExploreLayout subtitle="Browse the ACME Corp ecosystem">
<ExploreLayout.Route path="/tools" title="Tools">
<div>Tools Content</div>
</ExploreLayout.Route>
</ExploreLayout>
</Wrapper>,
);
await waitFor(() =>
expect(getByText('Browse the ACME Corp ecosystem')).toBeInTheDocument(),
);
});
});
@@ -0,0 +1,102 @@
/*
* 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 { attachComponentData, Header, Page, RoutedTabs } from '@backstage/core';
import { TabProps } from '@material-ui/core';
import { Children, default as React, Fragment, isValidElement } from 'react';
// TODO: This layout could be a shared based component if it was possible to create custom TabbedLayouts
// A generalized version of createSubRoutesFromChildren, etc. would be required
type SubRoute = {
path: string;
title: string;
children: JSX.Element;
tabProps?: TabProps<React.ElementType, { component?: React.ElementType }>;
};
const Route: (props: SubRoute) => null = () => null;
// This causes all mount points that are discovered within this route to use the path of the route itself
attachComponentData(Route, 'core.gatherMountPoints', true);
function createSubRoutesFromChildren(
childrenProps: React.ReactNode,
): SubRoute[] {
// Directly comparing child.type with Route will not work with in
// combination with react-hot-loader in storybook
// https://github.com/gaearon/react-hot-loader/issues/304
const routeType = (
<Route path="" title="">
<div />
</Route>
).type;
return Children.toArray(childrenProps).flatMap(child => {
if (!isValidElement(child)) {
return [];
}
if (child.type === Fragment) {
return createSubRoutesFromChildren(child.props.children);
}
if (child.type !== routeType) {
throw new Error('Child of ExploreLayout must be an ExploreLayout.Route');
}
const { path, title, children, tabProps } = child.props;
return [{ path, title, children, tabProps }];
});
}
type ExploreLayoutProps = {
title?: string;
subtitle?: string;
children?: React.ReactNode;
};
/**
* Explore is a compound component, which allows you to define a custom layout
*
* @example
* ```jsx
* <ExploreLayout title="Explore ACME's ecosystem">
* <ExploreLayout.Route path="/example" title="Example tab">
* <div>This is rendered under /example/anything-here route</div>
* </ExploreLayout.Route>
* </ExploreLayout>
* ```
*/
export const ExploreLayout = ({
title,
subtitle,
children,
}: ExploreLayoutProps) => {
const routes = createSubRoutesFromChildren(children);
return (
<Page themeId="home">
<Header
title={title ?? 'Explore our ecosystem'}
subtitle={subtitle ?? 'Discover solutions available in our ecosystem'}
/>
<RoutedTabs routes={routes} />
</Page>
);
};
ExploreLayout.Route = Route;
@@ -0,0 +1,17 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ExploreLayout } from './ExploreLayout';
@@ -0,0 +1,48 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { useOutlet } from 'react-router';
import { ExplorePage } from './ExplorePage';
jest.mock('react-router', () => ({
...jest.requireActual('react-router'),
useLocation: jest.fn().mockReturnValue({
search: '',
}),
useOutlet: jest.fn().mockReturnValue('Route Children'),
}));
jest.mock('../DefaultExplorePage', () => ({
...jest.requireActual('../DefaultExplorePage'),
DefaultExplorePage: jest.fn().mockReturnValue('DefaultExplorePageMock'),
}));
describe('ExplorePage', () => {
it('renders provided router element', async () => {
const { getByText } = await renderInTestApp(<ExplorePage />);
expect(getByText('Route Children')).toBeInTheDocument();
});
it('renders default explorer page when no router children are provided', async () => {
(useOutlet as jest.Mock).mockReturnValueOnce(null);
const { getByText } = await renderInTestApp(<ExplorePage />);
expect(getByText('DefaultExplorePageMock')).toBeInTheDocument();
});
});
@@ -13,22 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { configApiRef, Header, Page, useApi } from '@backstage/core';
import React from 'react';
import { ExploreTabs } from './ExploreTabs';
import { useOutlet } from 'react-router';
import { DefaultExplorePage } from '../DefaultExplorePage';
export const ExplorePage = () => {
const configApi = useApi(configApiRef);
const organizationName =
configApi.getOptionalString('organization.name') ?? 'Backstage';
return (
<Page themeId="home">
<Header
title={`Explore the ${organizationName} ecosystem`}
subtitle="Discover solutions available in your ecosystem"
/>
const outlet = useOutlet();
<ExploreTabs />
</Page>
);
return <>{outlet || <DefaultExplorePage />}</>;
};
@@ -1,34 +0,0 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TabbedLayout } from '@backstage/core';
import React from 'react';
import { DomainExplorerContent } from '../DomainExplorerContent';
import { GroupsExplorerContent } from '../GroupsExplorerContent';
import { ToolExplorerContent } from '../ToolExplorerContent';
export const ExploreTabs = () => (
<TabbedLayout>
<TabbedLayout.Route path="domains" title="Domains">
<DomainExplorerContent />
</TabbedLayout.Route>
<TabbedLayout.Route path="groups" title="Groups">
<GroupsExplorerContent />
</TabbedLayout.Route>
<TabbedLayout.Route path="tools" title="Tools">
<ToolExplorerContent />
</TabbedLayout.Route>
</TabbedLayout>
);
@@ -40,6 +40,12 @@ describe('<GroupsExplorerContent />', () => {
</ApiProvider>
);
const mountedRoutes = {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
};
beforeEach(() => {
jest.resetAllMocks();
@@ -69,11 +75,7 @@ describe('<GroupsExplorerContent />', () => {
<Wrapper>
<GroupsExplorerContent />
</Wrapper>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
mountedRoutes,
);
await waitFor(() => {
@@ -81,6 +83,19 @@ describe('<GroupsExplorerContent />', () => {
});
});
it('renders a custom title', async () => {
catalogApi.getEntities.mockResolvedValue({ items: [] });
const { getByText } = await renderInTestApp(
<Wrapper>
<GroupsExplorerContent title="Our Teams" />
</Wrapper>,
mountedRoutes,
);
await waitFor(() => expect(getByText('Our Teams')).toBeInTheDocument());
});
it('renders a friendly error if it cannot collect domains', async () => {
const catalogError = new Error('Network timeout');
catalogApi.getEntities.mockRejectedValueOnce(catalogError);
@@ -89,11 +104,7 @@ describe('<GroupsExplorerContent />', () => {
<Wrapper>
<GroupsExplorerContent />
</Wrapper>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
mountedRoutes,
);
await waitFor(() =>
@@ -13,14 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Content, ContentHeader, SupportButton } from '@backstage/core';
import React from 'react';
import { GroupsDiagram } from './GroupsDiagram';
export const GroupsExplorerContent = () => {
type GroupsExplorerContentProps = {
title?: string;
};
export const GroupsExplorerContent = ({
title,
}: GroupsExplorerContentProps) => {
return (
<Content noPadding>
<ContentHeader title="Groups">
<ContentHeader title={title ?? 'Groups'}>
<SupportButton>Explore your groups.</SupportButton>
</ContentHeader>
@@ -80,6 +80,18 @@ describe('<ToolExplorerContent />', () => {
});
});
it('renders a custom title', async () => {
exploreToolsConfigApi.getTools.mockResolvedValue([]);
const { getByText } = await renderInTestApp(
<Wrapper>
<ToolExplorerContent title="Our Tools" />
</Wrapper>,
);
await waitFor(() => expect(getByText('Our Tools')).toBeInTheDocument());
});
it('renders empty state', async () => {
exploreToolsConfigApi.getTools.mockResolvedValue([]);
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Content,
ContentHeader,
@@ -61,9 +62,13 @@ const Body = () => {
);
};
export const ToolExplorerContent = () => (
type ToolExplorerContentProps = {
title?: string;
};
export const ToolExplorerContent = ({ title }: ToolExplorerContentProps) => (
<Content noPadding>
<ContentHeader title="Tools">
<ContentHeader title={title ?? 'Tools'}>
<SupportButton>Discover the tools in your ecosystem.</SupportButton>
</ContentHeader>
<Body />
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ExploreLayout } from './ExploreLayout';
+37 -1
View File
@@ -14,7 +14,10 @@
* limitations under the License.
*/
import { createRoutableExtension } from '@backstage/core';
import {
createComponentExtension,
createRoutableExtension,
} from '@backstage/core';
import { explorePlugin } from './plugin';
import { exploreRouteRef } from './routes';
@@ -25,3 +28,36 @@ export const ExplorePage = explorePlugin.provide(
mountPoint: exploreRouteRef,
}),
);
export const DomainExplorerContent = explorePlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/DomainExplorerContent').then(
m => m.DomainExplorerContent,
),
},
}),
);
export const GroupsExplorerContent = explorePlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/GroupsExplorerContent').then(
m => m.GroupsExplorerContent,
),
},
}),
);
export const ToolExplorerContent = explorePlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/ToolExplorerContent').then(
m => m.ToolExplorerContent,
),
},
}),
);
+2 -1
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
export { ExploreLayout } from './components';
export * from './extensions';
export { explorePlugin } from './plugin';
export { explorePlugin, explorePlugin as plugin } from './plugin';
export * from './routes';
+27
View File
@@ -0,0 +1,27 @@
## API Report File for "@backstage/plugin-fossa"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackstagePlugin } from '@backstage/core';
import { InfoCardVariants } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const EntityFossaCard: ({ variant }: {
variant?: InfoCardVariants| undefined;
}) => JSX.Element;
// @public (undocumented)
export const FossaPage: () => JSX.Element;
// @public (undocumented)
export const fossaPlugin: BackstagePlugin<{
fossaOverview: RouteRef<undefined>;
}, {}>;
// (No @packageDocumentation comment for this package)
```
+86
View File
@@ -0,0 +1,86 @@
## API Report File for "@backstage/plugin-gcp-projects"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { OAuthApi } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export type GcpApi = {
listProjects(): Promise<Project[]>;
getProject(projectId: string): Promise<Project>;
createProject(options: {
projectId: string;
projectName: string;
}): Promise<Operation>;
};
// @public (undocumented)
export const gcpApiRef: ApiRef<GcpApi>;
// @public (undocumented)
export class GcpClient implements GcpApi {
constructor(googleAuthApi: OAuthApi);
// (undocumented)
createProject(options: {
projectId: string;
projectName: string;
}): Promise<Operation>;
// (undocumented)
getProject(projectId: string): Promise<Project>;
// (undocumented)
getToken(): Promise<string>;
// (undocumented)
listProjects(): Promise<Project[]>;
}
// @public (undocumented)
export const GcpProjectsPage: () => JSX.Element;
// @public (undocumented)
const gcpProjectsPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
}, {}>;
export { gcpProjectsPlugin }
export { gcpProjectsPlugin as plugin }
// @public (undocumented)
export type Operation = {
name: string;
metadata: string;
done: boolean;
error: Status;
response: string;
};
// @public (undocumented)
export type Project = {
name: string;
projectNumber?: string;
projectId: string;
lifecycleState?: string;
createTime?: string;
};
// @public (undocumented)
export type ProjectDetails = {
details: string;
};
// @public (undocumented)
export type Status = {
code: number;
message: string;
details: string[];
};
// (No @packageDocumentation comment for this package)
```
+25
View File
@@ -0,0 +1,25 @@
## API Report File for "@backstage/plugin-git-release-manager"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const gitReleaseManagerApiRef: ApiRef<GitReleaseApi>;
// @public (undocumented)
export const GitReleaseManagerPage: GitReleaseManager;
// @public (undocumented)
export const gitReleaseManagerPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
}, {}>;
// (No @packageDocumentation comment for this package)
```
+213
View File
@@ -0,0 +1,213 @@
## API Report File for "@backstage/plugin-github-actions"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { ConfigApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { InfoCardVariants } from '@backstage/core';
import { OAuthApi } from '@backstage/core';
import { RestEndpointMethodTypes } from '@octokit/rest';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export enum BuildStatus {
// (undocumented)
'failure' = 1,
// (undocumented)
'pending' = 2,
// (undocumented)
'running' = 3,
// (undocumented)
'success' = 0
}
// @public (undocumented)
export const EntityGithubActionsContent: (_props: {
entity?: Entity| undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityLatestGithubActionRunCard: ({ branch, variant, }: {
entity?: Entity| undefined;
branch: string;
variant?: InfoCardVariants| undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityLatestGithubActionsForBranchCard: ({ branch, variant, }: {
entity?: Entity| undefined;
branch: string;
variant?: InfoCardVariants| undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityRecentGithubActionsRunsCard: ({ branch, dense, limit, variant, }: Props) => JSX.Element;
// @public (undocumented)
export const GITHUB_ACTIONS_ANNOTATION = "github.com/project-slug";
// @public (undocumented)
export type GithubActionsApi = {
listWorkflowRuns: ({ hostname, owner, repo, pageSize, page, branch, }: {
hostname?: string;
owner: string;
repo: string;
pageSize?: number;
page?: number;
branch?: string;
}) => Promise<RestEndpointMethodTypes['actions']['listWorkflowRuns']['response']['data']>;
getWorkflow: ({ hostname, owner, repo, id, }: {
hostname?: string;
owner: string;
repo: string;
id: number;
}) => Promise<RestEndpointMethodTypes['actions']['getWorkflow']['response']['data']>;
getWorkflowRun: ({ hostname, owner, repo, id, }: {
hostname?: string;
owner: string;
repo: string;
id: number;
}) => Promise<RestEndpointMethodTypes['actions']['getWorkflowRun']['response']['data']>;
reRunWorkflow: ({ hostname, owner, repo, runId, }: {
hostname?: string;
owner: string;
repo: string;
runId: number;
}) => Promise<any>;
listJobsForWorkflowRun: ({ hostname, owner, repo, id, pageSize, page, }: {
hostname?: string;
owner: string;
repo: string;
id: number;
pageSize?: number;
page?: number;
}) => Promise<RestEndpointMethodTypes['actions']['listJobsForWorkflowRun']['response']['data']>;
downloadJobLogsForWorkflowRun: ({ hostname, owner, repo, runId, }: {
hostname?: string;
owner: string;
repo: string;
runId: number;
}) => Promise<RestEndpointMethodTypes['actions']['downloadJobLogsForWorkflowRun']['response']['data']>;
};
// @public (undocumented)
export const githubActionsApiRef: ApiRef<GithubActionsApi>;
// @public (undocumented)
export class GithubActionsClient implements GithubActionsApi {
constructor(options: {
configApi: ConfigApi;
githubAuthApi: OAuthApi;
});
// (undocumented)
downloadJobLogsForWorkflowRun({ hostname, owner, repo, runId, }: {
hostname?: string;
owner: string;
repo: string;
runId: number;
}): Promise<RestEndpointMethodTypes['actions']['downloadJobLogsForWorkflowRun']['response']['data']>;
// (undocumented)
getWorkflow({ hostname, owner, repo, id, }: {
hostname?: string;
owner: string;
repo: string;
id: number;
}): Promise<RestEndpointMethodTypes['actions']['getWorkflow']['response']['data']>;
// (undocumented)
getWorkflowRun({ hostname, owner, repo, id, }: {
hostname?: string;
owner: string;
repo: string;
id: number;
}): Promise<RestEndpointMethodTypes['actions']['getWorkflowRun']['response']['data']>;
// (undocumented)
listJobsForWorkflowRun({ hostname, owner, repo, id, pageSize, page, }: {
hostname?: string;
owner: string;
repo: string;
id: number;
pageSize?: number;
page?: number;
}): Promise<RestEndpointMethodTypes['actions']['listJobsForWorkflowRun']['response']['data']>;
// (undocumented)
listWorkflowRuns({ hostname, owner, repo, pageSize, page, branch, }: {
hostname?: string;
owner: string;
repo: string;
pageSize?: number;
page?: number;
branch?: string;
}): Promise<RestEndpointMethodTypes['actions']['listWorkflowRuns']['response']['data']>;
// (undocumented)
reRunWorkflow({ hostname, owner, repo, runId, }: {
hostname?: string;
owner: string;
repo: string;
runId: number;
}): Promise<any>;
}
// @public (undocumented)
const githubActionsPlugin: BackstagePlugin<{
entityContent: RouteRef<undefined>;
}, {}>;
export { githubActionsPlugin }
export { githubActionsPlugin as plugin }
// @public (undocumented)
const isGithubActionsAvailable: (entity: Entity) => boolean;
export { isGithubActionsAvailable }
export { isGithubActionsAvailable as isPluginApplicableToEntity }
// @public (undocumented)
export type Job = {
html_url: string;
status: string;
conclusion: string;
started_at: string;
completed_at: string;
id: number;
name: string;
steps: Step[];
};
// @public (undocumented)
export type Jobs = {
total_count: number;
jobs: Job[];
};
// @public (undocumented)
export const LatestWorkflowRunCard: ({ branch, variant, }: Props_3) => JSX.Element;
// @public (undocumented)
export const LatestWorkflowsForBranchCard: ({ branch, variant, }: Props_3) => JSX.Element;
// @public (undocumented)
export const RecentWorkflowRunsCard: ({ branch, dense, limit, variant, }: Props) => JSX.Element;
// @public (undocumented)
export const Router: (_props: Props_2) => JSX.Element;
// @public (undocumented)
export type Step = {
name: string;
status: string;
conclusion?: string;
number: number;
started_at: string;
completed_at: string;
};
// (No @packageDocumentation comment for this package)
```
+197
View File
@@ -0,0 +1,197 @@
## API Report File for "@backstage/plugin-gitops-profiles"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export interface ApplyProfileRequest {
// (undocumented)
gitHubToken: string;
// (undocumented)
gitHubUser: string;
// (undocumented)
profiles: string[];
// (undocumented)
targetOrg: string;
// (undocumented)
targetRepo: string;
}
// @public (undocumented)
export interface ChangeClusterStateRequest {
// (undocumented)
clusterState: 'present' | 'absent';
// (undocumented)
gitHubToken: string;
// (undocumented)
gitHubUser: string;
// (undocumented)
targetOrg: string;
// (undocumented)
targetRepo: string;
}
// @public (undocumented)
export interface CloneFromTemplateRequest {
// (undocumented)
gitHubToken: string;
// (undocumented)
gitHubUser: string;
// (undocumented)
secrets: {
awsAccessKeyId: string;
awsSecretAccessKey: string;
};
// (undocumented)
targetOrg: string;
// (undocumented)
targetRepo: string;
// (undocumented)
templateRepository: string;
}
// @public (undocumented)
export interface ClusterStatus {
// (undocumented)
conclusion: string;
// (undocumented)
link: string;
// (undocumented)
name: string;
// (undocumented)
runStatus: Status[];
// (undocumented)
status: string;
}
// @public (undocumented)
export class FetchError extends Error {
// (undocumented)
static forResponse(resp: Response): Promise<FetchError>;
// (undocumented)
get name(): string;
}
// @public (undocumented)
export interface GithubUserInfoRequest {
// (undocumented)
accessToken: string;
}
// @public (undocumented)
export interface GithubUserInfoResponse {
// (undocumented)
login: string;
}
// @public (undocumented)
export type GitOpsApi = {
url: string;
fetchLog(req: PollLogRequest): Promise<StatusResponse>;
changeClusterState(req: ChangeClusterStateRequest): Promise<any>;
cloneClusterFromTemplate(req: CloneFromTemplateRequest): Promise<any>;
applyProfiles(req: ApplyProfileRequest): Promise<any>;
listClusters(req: ListClusterRequest): Promise<ListClusterStatusesResponse>;
fetchUserInfo(req: GithubUserInfoRequest): Promise<GithubUserInfoResponse>;
};
// @public (undocumented)
export const gitOpsApiRef: ApiRef<GitOpsApi>;
// @public (undocumented)
export const GitopsProfilesClusterListPage: () => JSX.Element;
// @public (undocumented)
export const GitopsProfilesClusterPage: () => JSX.Element;
// @public (undocumented)
export const GitopsProfilesCreatePage: () => JSX.Element;
// @public (undocumented)
const gitopsProfilesPlugin: BackstagePlugin<{
listPage: RouteRef<undefined>;
detailsPage: RouteRef<{
owner: string;
repo: string;
}>;
createPage: RouteRef<undefined>;
}, {}>;
export { gitopsProfilesPlugin }
export { gitopsProfilesPlugin as plugin }
// @public (undocumented)
export class GitOpsRestApi implements GitOpsApi {
constructor(url?: string);
// (undocumented)
applyProfiles(req: ApplyProfileRequest): Promise<any>;
// (undocumented)
changeClusterState(req: ChangeClusterStateRequest): Promise<any>;
// (undocumented)
cloneClusterFromTemplate(req: CloneFromTemplateRequest): Promise<any>;
// (undocumented)
fetchLog(req: PollLogRequest): Promise<StatusResponse>;
// (undocumented)
fetchUserInfo(req: GithubUserInfoRequest): Promise<GithubUserInfoResponse>;
// (undocumented)
listClusters(req: ListClusterRequest): Promise<ListClusterStatusesResponse>;
// (undocumented)
url: string;
}
// @public (undocumented)
export interface ListClusterRequest {
// (undocumented)
gitHubToken: string;
// (undocumented)
gitHubUser: string;
}
// @public (undocumented)
export interface ListClusterStatusesResponse {
// (undocumented)
result: ClusterStatus[];
}
// @public (undocumented)
export interface PollLogRequest {
// (undocumented)
gitHubToken: string;
// (undocumented)
gitHubUser: string;
// (undocumented)
targetOrg: string;
// (undocumented)
targetRepo: string;
}
// @public (undocumented)
export interface Status {
// (undocumented)
conclusion: string;
// (undocumented)
message: string;
// (undocumented)
status: string;
}
// @public (undocumented)
export interface StatusResponse {
// (undocumented)
link: string;
// (undocumented)
result: Status[];
// (undocumented)
status: string;
}
// (No @packageDocumentation comment for this package)
```
+82
View File
@@ -0,0 +1,82 @@
## API Report File for "@backstage/plugin-graphiql"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { ErrorApi } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core';
import { OAuthApi } from '@backstage/core-plugin-api';
import { RouteRef } from '@backstage/core-plugin-api';
// @public (undocumented)
export type EndpointConfig = {
id: string;
title: string;
url: string;
method?: 'POST';
headers?: {
[name in string]: string;
};
};
// @public (undocumented)
export type GithubEndpointConfig = {
id: string;
title: string;
url?: string;
errorApi?: ErrorApi;
githubAuthApi: OAuthApi;
};
// @public (undocumented)
export const GraphiQLIcon: IconComponent;
// @public (undocumented)
export const GraphiQLPage: () => JSX.Element;
// @public (undocumented)
const graphiqlPlugin: BackstagePlugin<{}, {}>;
export { graphiqlPlugin }
export { graphiqlPlugin as plugin }
// @public (undocumented)
export const graphiQLRouteRef: RouteRef<undefined>;
// @public (undocumented)
export type GraphQLBrowseApi = {
getEndpoints(): Promise<GraphQLEndpoint[]>;
};
// @public (undocumented)
export const graphQlBrowseApiRef: ApiRef<GraphQLBrowseApi>;
// @public (undocumented)
export type GraphQLEndpoint = {
id: string;
title: string;
fetcher: (body: any) => Promise<any>;
};
// @public (undocumented)
export class GraphQLEndpoints implements GraphQLBrowseApi {
// (undocumented)
static create(config: EndpointConfig): GraphQLEndpoint;
// (undocumented)
static from(endpoints: GraphQLEndpoint[]): GraphQLEndpoints;
// (undocumented)
getEndpoints(): Promise<GraphQLEndpoint[]>;
static github(config: GithubEndpointConfig): GraphQLEndpoint;
}
// @public (undocumented)
export const Router: () => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+25
View File
@@ -0,0 +1,25 @@
## API Report File for "@backstage/plugin-graphql-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Config } from '@backstage/config';
import express from 'express';
import { Logger } from 'winston';
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public (undocumented)
export interface RouterOptions {
// (undocumented)
config: Config;
// (undocumented)
logger: Logger;
}
// (No @packageDocumentation comment for this package)
```
+205
View File
@@ -0,0 +1,205 @@
## API Report File for "@backstage/plugin-ilert"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { ConfigApi } from '@backstage/core';
import { DiscoveryApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { IconComponent } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const EntityILertCard: () => JSX.Element;
// @public (undocumented)
export type GetIncidentsCountOpts = {
states?: IncidentStatus[];
};
// @public (undocumented)
export type GetIncidentsOpts = {
maxResults?: number;
startIndex?: number;
states?: IncidentStatus[];
alertSources?: number[];
};
// @public (undocumented)
export interface ILertApi {
// (undocumented)
acceptIncident(incident: Incident, userName: string): Promise<Incident>;
// (undocumented)
addImmediateMaintenance(alertSourceId: number, minutes: number): Promise<void>;
// (undocumented)
assignIncident(incident: Incident, responder: IncidentResponder): Promise<Incident>;
// (undocumented)
createIncident(eventRequest: EventRequest): Promise<boolean>;
// (undocumented)
disableAlertSource(alertSource: AlertSource): Promise<AlertSource>;
// (undocumented)
enableAlertSource(alertSource: AlertSource): Promise<AlertSource>;
// (undocumented)
fetchAlertSource(idOrIntegrationKey: number | string): Promise<AlertSource>;
// (undocumented)
fetchAlertSourceOnCalls(alertSource: AlertSource): Promise<OnCall[]>;
// (undocumented)
fetchAlertSources(): Promise<AlertSource[]>;
// (undocumented)
fetchIncident(id: number): Promise<Incident>;
// (undocumented)
fetchIncidentActions(incident: Incident): Promise<IncidentAction[]>;
// (undocumented)
fetchIncidentResponders(incident: Incident): Promise<IncidentResponder[]>;
// (undocumented)
fetchIncidents(opts?: GetIncidentsOpts): Promise<Incident[]>;
// (undocumented)
fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise<number>;
// (undocumented)
fetchOnCallSchedules(): Promise<Schedule[]>;
// (undocumented)
fetchUptimeMonitor(id: number): Promise<UptimeMonitor>;
// (undocumented)
fetchUptimeMonitors(): Promise<UptimeMonitor[]>;
// (undocumented)
fetchUsers(): Promise<User[]>;
// (undocumented)
getAlertSourceDetailsURL(alertSource: AlertSource | null): string;
// (undocumented)
getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string;
// (undocumented)
getIncidentDetailsURL(incident: Incident): string;
// (undocumented)
getScheduleDetailsURL(schedule: Schedule): string;
// (undocumented)
getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string;
// (undocumented)
getUserInitials(user: User | null): string;
// (undocumented)
getUserPhoneNumber(user: User | null): string;
// (undocumented)
overrideShift(scheduleId: number, userId: number, start: string, end: string): Promise<Schedule>;
// (undocumented)
pauseUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise<UptimeMonitor>;
// (undocumented)
resolveIncident(incident: Incident, userName: string): Promise<Incident>;
// (undocumented)
resumeUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise<UptimeMonitor>;
// (undocumented)
triggerIncidentAction(incident: Incident, action: IncidentAction): Promise<void>;
}
// @public (undocumented)
export const ilertApiRef: ApiRef<ILertApi>;
// @public (undocumented)
export const ILertCard: () => JSX.Element;
// @public (undocumented)
export class ILertClient implements ILertApi {
constructor(opts: Options);
// (undocumented)
acceptIncident(incident: Incident, userName: string): Promise<Incident>;
// (undocumented)
addImmediateMaintenance(alertSourceId: number, minutes: number): Promise<void>;
// (undocumented)
assignIncident(incident: Incident, responder: IncidentResponder): Promise<Incident>;
// (undocumented)
createIncident(eventRequest: EventRequest): Promise<boolean>;
// (undocumented)
disableAlertSource(alertSource: AlertSource): Promise<AlertSource>;
// (undocumented)
enableAlertSource(alertSource: AlertSource): Promise<AlertSource>;
// (undocumented)
fetchAlertSource(idOrIntegrationKey: number | string): Promise<AlertSource>;
// (undocumented)
fetchAlertSourceOnCalls(alertSource: AlertSource): Promise<OnCall[]>;
// (undocumented)
fetchAlertSources(): Promise<AlertSource[]>;
// (undocumented)
fetchIncident(id: number): Promise<Incident>;
// (undocumented)
fetchIncidentActions(incident: Incident): Promise<IncidentAction[]>;
// (undocumented)
fetchIncidentResponders(incident: Incident): Promise<IncidentResponder[]>;
// (undocumented)
fetchIncidents(opts?: GetIncidentsOpts): Promise<Incident[]>;
// (undocumented)
fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise<number>;
// (undocumented)
fetchOnCallSchedules(): Promise<Schedule[]>;
// (undocumented)
fetchUptimeMonitor(id: number): Promise<UptimeMonitor>;
// (undocumented)
fetchUptimeMonitors(): Promise<UptimeMonitor[]>;
// (undocumented)
fetchUsers(): Promise<User[]>;
// (undocumented)
static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi): ILertClient;
// (undocumented)
getAlertSourceDetailsURL(alertSource: AlertSource | null): string;
// (undocumented)
getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string;
// (undocumented)
getIncidentDetailsURL(incident: Incident): string;
// (undocumented)
getScheduleDetailsURL(schedule: Schedule): string;
// (undocumented)
getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string;
// (undocumented)
getUserInitials(user: User | null): string;
// (undocumented)
getUserPhoneNumber(user: User | null): string;
// (undocumented)
overrideShift(scheduleId: number, userId: number, start: string, end: string): Promise<Schedule>;
// (undocumented)
pauseUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise<UptimeMonitor>;
// (undocumented)
resolveIncident(incident: Incident, userName: string): Promise<Incident>;
// (undocumented)
resumeUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise<UptimeMonitor>;
// (undocumented)
triggerIncidentAction(incident: Incident, action: IncidentAction): Promise<void>;
}
// @public (undocumented)
export const ILertIcon: IconComponent;
// @public (undocumented)
export const ILertPage: () => JSX.Element;
// @public (undocumented)
const ilertPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
}, {}>;
export { ilertPlugin }
export { ilertPlugin as plugin }
// @public (undocumented)
export const iLertRouteRef: RouteRef<undefined>;
// @public (undocumented)
const isPluginApplicableToEntity: (entity: Entity) => boolean;
export { isPluginApplicableToEntity as isILertAvailable }
export { isPluginApplicableToEntity }
// @public (undocumented)
export const Router: () => JSX.Element;
// @public (undocumented)
export type TableState = {
page: number;
pageSize: number;
};
// (No @packageDocumentation comment for this package)
```
+83
View File
@@ -0,0 +1,83 @@
## API Report File for "@backstage/plugin-jenkins"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { DiscoveryApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { InfoCardVariants } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const EntityJenkinsContent: (_props: {
entity?: Entity| undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityLatestJenkinsRunCard: ({ branch, variant, }: {
branch: string;
variant?: InfoCardVariants| undefined;
}) => JSX.Element;
// @public (undocumented)
const isJenkinsAvailable: (entity: Entity) => boolean;
export { isJenkinsAvailable }
export { isJenkinsAvailable as isPluginApplicableToEntity }
// @public (undocumented)
export const JENKINS_ANNOTATION = "jenkins.io/github-folder";
// @public (undocumented)
export class JenkinsApi {
constructor(options: Options);
// (undocumented)
extractJobDetailsFromBuildName(buildName: string): {
jobName: string;
buildNumber: number;
};
// (undocumented)
extractScmDetailsFromJob(jobDetails: any): any | undefined;
// (undocumented)
getBuild(buildName: string): Promise<any>;
// (undocumented)
getFolder(folderName: string): Promise<CITableBuildInfo[]>;
// (undocumented)
getJob(jobName: string): Promise<any>;
// (undocumented)
getLastBuild(jobName: string): Promise<any>;
// (undocumented)
mapJenkinsBuildToCITable(jenkinsResult: any, jobScmInfo?: any): CITableBuildInfo;
// (undocumented)
retry(buildName: string): Promise<any>;
}
// @public (undocumented)
export const jenkinsApiRef: ApiRef<JenkinsApi>;
// @public (undocumented)
const jenkinsPlugin: BackstagePlugin<{
entityContent: RouteRef<undefined>;
}, {}>;
export { jenkinsPlugin }
export { jenkinsPlugin as plugin }
// @public (undocumented)
export const LatestRunCard: ({ branch, variant, }: {
branch: string;
variant?: InfoCardVariants | undefined;
}) => JSX.Element;
// @public (undocumented)
export const Router: (_props: Props) => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+17
View File
@@ -0,0 +1,17 @@
## API Report File for "@backstage/plugin-kafka-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Config } from '@backstage/config';
import express from 'express';
import { Logger } from 'winston';
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// (No @packageDocumentation comment for this package)
```
+41
View File
@@ -0,0 +1,41 @@
## API Report File for "@backstage/plugin-kafka"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackstagePlugin } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const EntityKafkaContent: (_props: {
entity?: Entity| undefined;
}) => JSX.Element;
// @public (undocumented)
const isPluginApplicableToEntity: (entity: Entity) => boolean;
export { isPluginApplicableToEntity as isKafkaAvailable }
export { isPluginApplicableToEntity }
// @public (undocumented)
export const KAFKA_CONSUMER_GROUP_ANNOTATION = "kafka.apache.org/consumer-groups";
// @public (undocumented)
const kafkaPlugin: BackstagePlugin<{
entityContent: RouteRef<undefined>;
}, {}>;
export { kafkaPlugin }
export { kafkaPlugin as plugin }
// @public (undocumented)
export const Router: (_props: Props) => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+103
View File
@@ -0,0 +1,103 @@
## API Report File for "@backstage/plugin-kubernetes-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Config } from '@backstage/config';
import express from 'express';
import { FetchResponse } from '@backstage/plugin-kubernetes-common';
import { KubernetesFetchError } from '@backstage/plugin-kubernetes-common';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
import { Logger } from 'winston';
// @public (undocumented)
export interface ClusterDetails {
// (undocumented)
authProvider: string;
// (undocumented)
name: string;
// (undocumented)
serviceAccountToken?: string | undefined;
// (undocumented)
skipTLSVerify?: boolean;
// (undocumented)
url: string;
}
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public (undocumented)
export interface CustomResource {
// (undocumented)
apiVersion: string;
// (undocumented)
group: string;
// (undocumented)
plural: string;
}
// @public (undocumented)
export interface FetchResponseWrapper {
// (undocumented)
errors: KubernetesFetchError[];
// (undocumented)
responses: FetchResponse[];
}
// @public (undocumented)
export interface KubernetesClustersSupplier {
// (undocumented)
getClusters(): Promise<ClusterDetails[]>;
}
// @public (undocumented)
export interface KubernetesFetcher {
// (undocumented)
fetchObjectsForService(params: ObjectFetchParams): Promise<FetchResponseWrapper>;
}
// @public (undocumented)
export type KubernetesObjectTypes = 'pods' | 'services' | 'configmaps' | 'deployments' | 'replicasets' | 'horizontalpodautoscalers' | 'ingresses' | 'customresources';
// @public (undocumented)
export interface KubernetesServiceLocator {
// (undocumented)
getClustersByServiceId(serviceId: string): Promise<ClusterDetails[]>;
}
// @public (undocumented)
export const makeRouter: (logger: Logger, kubernetesFanOutHandler: KubernetesFanOutHandler, clusterDetails: ClusterDetails[]) => express.Router;
// @public (undocumented)
export interface ObjectFetchParams {
// (undocumented)
clusterDetails: ClusterDetails;
// (undocumented)
customResources: CustomResource[];
// (undocumented)
labelSelector: string;
// (undocumented)
objectTypesToFetch: Set<KubernetesObjectTypes>;
// (undocumented)
serviceId: string;
}
// @public (undocumented)
export interface RouterOptions {
// (undocumented)
clusterSupplier?: KubernetesClustersSupplier;
// (undocumented)
config: Config;
// (undocumented)
logger: Logger;
}
// @public (undocumented)
export type ServiceLocatorMethod = 'multiTenant' | 'http';
// (No @packageDocumentation comment for this package)
```
+130
View File
@@ -0,0 +1,130 @@
## API Report File for "@backstage/plugin-kubernetes-common"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Entity } from '@backstage/catalog-model';
import { ExtensionsV1beta1Ingress } from '@kubernetes/client-node';
import { V1ConfigMap } from '@kubernetes/client-node';
import { V1Deployment } from '@kubernetes/client-node';
import { V1HorizontalPodAutoscaler } from '@kubernetes/client-node';
import { V1Pod } from '@kubernetes/client-node';
import { V1ReplicaSet } from '@kubernetes/client-node';
import { V1Service } from '@kubernetes/client-node';
// @public (undocumented)
export type AuthProviderType = 'google' | 'serviceAccount' | 'aws';
// @public (undocumented)
export interface ClusterObjects {
// (undocumented)
cluster: {
name: string;
};
// (undocumented)
errors: KubernetesFetchError[];
// (undocumented)
resources: FetchResponse[];
}
// @public (undocumented)
export interface ConfigMapFetchResponse {
// (undocumented)
resources: Array<V1ConfigMap>;
// (undocumented)
type: 'configmaps';
}
// @public (undocumented)
export interface CustomResourceFetchResponse {
// (undocumented)
resources: Array<any>;
// (undocumented)
type: 'customresources';
}
// @public (undocumented)
export interface DeploymentFetchResponse {
// (undocumented)
resources: Array<V1Deployment>;
// (undocumented)
type: 'deployments';
}
// @public (undocumented)
export type FetchResponse = PodFetchResponse | ServiceFetchResponse | ConfigMapFetchResponse | DeploymentFetchResponse | ReplicaSetsFetchResponse | HorizontalPodAutoscalersFetchResponse | IngressesFetchResponse | CustomResourceFetchResponse;
// @public (undocumented)
export interface HorizontalPodAutoscalersFetchResponse {
// (undocumented)
resources: Array<V1HorizontalPodAutoscaler>;
// (undocumented)
type: 'horizontalpodautoscalers';
}
// @public (undocumented)
export interface IngressesFetchResponse {
// (undocumented)
resources: Array<ExtensionsV1beta1Ingress>;
// (undocumented)
type: 'ingresses';
}
// @public (undocumented)
export type KubernetesErrorTypes = 'BAD_REQUEST' | 'UNAUTHORIZED_ERROR' | 'SYSTEM_ERROR' | 'UNKNOWN_ERROR';
// @public (undocumented)
export interface KubernetesFetchError {
// (undocumented)
errorType: KubernetesErrorTypes;
// (undocumented)
resourcePath?: string;
// (undocumented)
statusCode?: number;
}
// @public (undocumented)
export interface KubernetesRequestBody {
// (undocumented)
auth?: {
google?: string;
};
// (undocumented)
entity: Entity;
}
// @public (undocumented)
export interface ObjectsByEntityResponse {
// (undocumented)
items: ClusterObjects[];
}
// @public (undocumented)
export interface PodFetchResponse {
// (undocumented)
resources: Array<V1Pod>;
// (undocumented)
type: 'pods';
}
// @public (undocumented)
export interface ReplicaSetsFetchResponse {
// (undocumented)
resources: Array<V1ReplicaSet>;
// (undocumented)
type: 'replicasets';
}
// @public (undocumented)
export interface ServiceFetchResponse {
// (undocumented)
resources: Array<V1Service>;
// (undocumented)
type: 'services';
}
// (No @packageDocumentation comment for this package)
```
+46
View File
@@ -0,0 +1,46 @@
## API Report File for "@backstage/plugin-kubernetes"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
import { OAuthApi } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const EntityKubernetesContent: (_props: {
entity?: Entity| undefined;
}) => JSX.Element;
// @public (undocumented)
export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
constructor(options: {
googleAuthApi: OAuthApi;
});
// (undocumented)
decorateRequestBodyForAuth(authProvider: string, requestBody: KubernetesRequestBody): Promise<KubernetesRequestBody>;
}
// @public (undocumented)
export const kubernetesAuthProvidersApiRef: ApiRef<KubernetesAuthProvidersApi>;
// @public (undocumented)
const kubernetesPlugin: BackstagePlugin<{
entityContent: RouteRef<undefined>;
}, {}>;
export { kubernetesPlugin }
export { kubernetesPlugin as plugin }
// @public (undocumented)
export const Router: (_props: Props) => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+186
View File
@@ -0,0 +1,186 @@
## API Report File for "@backstage/plugin-lighthouse"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import { InfoCardVariants } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export type Audit = AuditRunning | AuditFailed | AuditCompleted;
// @public (undocumented)
export interface AuditCompleted extends AuditBase {
// (undocumented)
categories: Record<LighthouseCategoryId, LighthouseCategoryAbbr>;
// (undocumented)
report: Object;
// (undocumented)
status: 'COMPLETED';
// (undocumented)
timeCompleted: string;
}
// @public (undocumented)
export interface AuditFailed extends AuditBase {
// (undocumented)
status: 'FAILED';
// (undocumented)
timeCompleted: string;
}
// @public (undocumented)
export interface AuditRunning extends AuditBase {
// (undocumented)
status: 'RUNNING';
}
// @public (undocumented)
export const EmbeddedRouter: (_props: Props) => JSX.Element;
// @public (undocumented)
export const EntityLastLighthouseAuditCard: ({ dense, variant, }: {
dense?: boolean | undefined;
variant?: InfoCardVariants| undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityLighthouseContent: (_props: {
entity?: Entity| undefined;
}) => JSX.Element;
// @public (undocumented)
export class FetchError extends Error {
// (undocumented)
static forResponse(resp: Response): Promise<FetchError>;
// (undocumented)
get name(): string;
}
// @public (undocumented)
const isLighthouseAvailable: (entity: Entity) => boolean;
export { isLighthouseAvailable }
export { isLighthouseAvailable as isPluginApplicableToEntity }
// @public (undocumented)
export interface LASListRequest {
// (undocumented)
limit?: number;
// (undocumented)
offset?: number;
}
// @public (undocumented)
export interface LASListResponse<Item> {
// (undocumented)
items: Item[];
// (undocumented)
limit: number;
// (undocumented)
offset: number;
// (undocumented)
total: number;
}
// @public (undocumented)
export const LastLighthouseAuditCard: ({ dense, variant, }: {
dense?: boolean | undefined;
variant?: InfoCardVariants | undefined;
}) => JSX.Element;
// @public (undocumented)
export type LighthouseApi = {
url: string;
getWebsiteList: (listOptions: LASListRequest) => Promise<WebsiteListResponse>;
getWebsiteForAuditId: (auditId: string) => Promise<Website>;
triggerAudit: (payload: TriggerAuditPayload) => Promise<Audit>;
getWebsiteByUrl: (websiteUrl: string) => Promise<Website>;
};
// @public (undocumented)
export const lighthouseApiRef: ApiRef<LighthouseApi>;
// @public (undocumented)
export interface LighthouseCategoryAbbr {
// (undocumented)
id: LighthouseCategoryId;
// (undocumented)
score: number;
// (undocumented)
title: string;
}
// @public (undocumented)
export type LighthouseCategoryId = 'pwa' | 'seo' | 'performance' | 'accessibility' | 'best-practices';
// @public (undocumented)
export const LighthousePage: () => JSX.Element;
// @public (undocumented)
const lighthousePlugin: BackstagePlugin<{
root: RouteRef<undefined>;
entityContent: RouteRef<undefined>;
}, {}>;
export { lighthousePlugin }
export { lighthousePlugin as plugin }
// @public (undocumented)
export class LighthouseRestApi implements LighthouseApi {
constructor(url: string);
// (undocumented)
static fromConfig(config: Config): LighthouseRestApi;
// (undocumented)
getWebsiteByUrl(websiteUrl: string): Promise<Website>;
// (undocumented)
getWebsiteForAuditId(auditId: string): Promise<Website>;
// (undocumented)
getWebsiteList({ limit, offset, }?: LASListRequest): Promise<WebsiteListResponse>;
// (undocumented)
triggerAudit(payload: TriggerAuditPayload): Promise<Audit>;
// (undocumented)
url: string;
}
// @public (undocumented)
export const Router: () => JSX.Element;
// @public (undocumented)
export interface TriggerAuditPayload {
// (undocumented)
options: {
lighthouseConfig: {
settings: {
emulatedFormFactor: string;
};
};
};
// (undocumented)
url: string;
}
// @public (undocumented)
export interface Website {
// (undocumented)
audits: Audit[];
// (undocumented)
lastAudit: Audit;
// (undocumented)
url: string;
}
// @public (undocumented)
export type WebsiteListResponse = LASListResponse<Website>;
// (No @packageDocumentation comment for this package)
```
+25
View File
@@ -0,0 +1,25 @@
## API Report File for "@backstage/plugin-newrelic"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackstagePlugin } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const NewRelicPage: () => JSX.Element;
// @public (undocumented)
const newRelicPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
}, {}>;
export { newRelicPlugin }
export { newRelicPlugin as plugin }
// (No @packageDocumentation comment for this package)
```
+69
View File
@@ -0,0 +1,69 @@
## API Report File for "@backstage/plugin-org"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackstagePlugin } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { GroupEntity } from '@backstage/catalog-model';
import { InfoCardVariants } from '@backstage/core';
import { UserEntity } from '@backstage/catalog-model';
// @public (undocumented)
export const EntityGroupProfileCard: ({ variant, }: {
entity?: GroupEntity| undefined;
variant?: InfoCardVariants| undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityMembersListCard: (_props: {
entity?: GroupEntity| undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityOwnershipCard: ({ variant, }: {
entity?: Entity| undefined;
variant?: InfoCardVariants| undefined;
}) => JSX.Element;
// @public (undocumented)
export const EntityUserProfileCard: ({ variant, }: {
entity?: UserEntity| undefined;
variant?: InfoCardVariants| undefined;
}) => JSX.Element;
// @public (undocumented)
export const GroupProfileCard: ({ variant, }: {
entity?: GroupEntity | undefined;
variant?: InfoCardVariants | undefined;
}) => JSX.Element;
// @public (undocumented)
export const MembersListCard: (_props: {
entity?: GroupEntity;
}) => JSX.Element;
// @public (undocumented)
const orgPlugin: BackstagePlugin<{}, {}>;
export { orgPlugin }
export { orgPlugin as plugin }
// @public (undocumented)
export const OwnershipCard: ({ variant, }: {
entity?: Entity | undefined;
variant?: InfoCardVariants | undefined;
}) => JSX.Element;
// @public (undocumented)
export const UserProfileCard: ({ variant, }: {
entity?: UserEntity | undefined;
variant?: InfoCardVariants | undefined;
}) => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+62
View File
@@ -0,0 +1,62 @@
## API Report File for "@backstage/plugin-pagerduty"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { ConfigApi } from '@backstage/core';
import { DiscoveryApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { PropsWithChildren } from 'react';
// @public (undocumented)
export const EntityPagerDutyCard: () => JSX.Element;
// @public (undocumented)
const isPluginApplicableToEntity: (entity: Entity) => boolean;
export { isPluginApplicableToEntity as isPagerDutyAvailable }
export { isPluginApplicableToEntity }
// @public (undocumented)
export const pagerDutyApiRef: ApiRef<PagerDutyApi>;
// @public (undocumented)
export const PagerDutyCard: () => JSX.Element;
// @public (undocumented)
export class PagerDutyClient implements PagerDutyApi {
constructor(config: ClientApiConfig);
// (undocumented)
static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi): PagerDutyClient;
// (undocumented)
getIncidentsByServiceId(serviceId: string): Promise<Incident[]>;
// (undocumented)
getOnCallByPolicyId(policyId: string): Promise<OnCall[]>;
// (undocumented)
getServiceByIntegrationKey(integrationKey: string): Promise<Service[]>;
// (undocumented)
triggerAlarm({ integrationKey, source, description, userName, }: TriggerAlarmRequest): Promise<Response>;
}
// @public (undocumented)
const pagerDutyPlugin: BackstagePlugin<{}, {}>;
export { pagerDutyPlugin }
export { pagerDutyPlugin as plugin }
// @public (undocumented)
export function TriggerButton({ children, }: PropsWithChildren<TriggerButtonProps>): JSX.Element;
// @public (undocumented)
export class UnauthorizedError extends Error {
}
// (No @packageDocumentation comment for this package)
```
+18
View File
@@ -0,0 +1,18 @@
## API Report File for "@backstage/plugin-proxy-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Config } from '@backstage/config';
import express from 'express';
import { Logger } from 'winston';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// (No @packageDocumentation comment for this package)
```
+32
View File
@@ -0,0 +1,32 @@
## API Report File for "@backstage/plugin-register-component"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackstagePlugin } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const RegisterComponentPage: ({ catalogRouteRef, }: {
catalogRouteRef: RouteRef<any>;
}) => JSX.Element;
// @public (undocumented)
const registerComponentPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
}, {}>;
export { registerComponentPlugin as plugin }
export { registerComponentPlugin }
// @public @deprecated
export const Router: ({ catalogRouteRef }: {
catalogRouteRef: RouteRef;
}) => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+60
View File
@@ -0,0 +1,60 @@
## API Report File for "@backstage/plugin-rollbar-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Config } from '@backstage/config';
import express from 'express';
import { Logger } from 'winston';
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public (undocumented)
export function getRequestHeaders(token: string): {
headers: {
'X-Rollbar-Access-Token': string;
};
};
// @public (undocumented)
export class RollbarApi {
constructor(accessToken: string, logger: Logger);
// (undocumented)
getActivatedCounts(projectName: string, options?: {
environment: string;
item_id?: number;
}): Promise<RollbarItemCount[]>;
// (undocumented)
getAllProjects(): Promise<RollbarProject[]>;
// (undocumented)
getOccuranceCounts(projectName: string, options?: {
environment: string;
item_id?: number;
}): Promise<RollbarItemCount[]>;
// (undocumented)
getProject(projectName: string): Promise<RollbarProject>;
// (undocumented)
getProjectItems(projectName: string): Promise<RollbarItemsResponse>;
// (undocumented)
getTopActiveItems(projectName: string, options?: {
hours: number;
environment: string;
}): Promise<RollbarTopActiveItem[]>;
}
// @public (undocumented)
export interface RouterOptions {
// (undocumented)
config: Config;
// (undocumented)
logger: Logger;
// (undocumented)
rollbarApi?: RollbarApi;
}
// (No @packageDocumentation comment for this package)
```
+78
View File
@@ -0,0 +1,78 @@
## API Report File for "@backstage/plugin-rollbar"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { DiscoveryApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { IdentityApi } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const EntityPageRollbar: (_props: Props) => JSX.Element;
// @public (undocumented)
export const EntityRollbarContent: (_props: {
entity?: Entity| undefined;
}) => JSX.Element;
// @public (undocumented)
const isPluginApplicableToEntity: (entity: Entity) => boolean;
export { isPluginApplicableToEntity }
export { isPluginApplicableToEntity as isRollbarAvailable }
// @public (undocumented)
export const ROLLBAR_ANNOTATION = "rollbar.com/project-slug";
// @public (undocumented)
export interface RollbarApi {
// (undocumented)
getAllProjects(): Promise<RollbarProject[]>;
// (undocumented)
getProject(projectName: string): Promise<RollbarProject>;
// (undocumented)
getProjectItems(project: string): Promise<RollbarItemsResponse>;
// (undocumented)
getTopActiveItems(project: string, hours?: number): Promise<RollbarTopActiveItem[]>;
}
// @public (undocumented)
export const rollbarApiRef: ApiRef<RollbarApi>;
// @public (undocumented)
export class RollbarClient implements RollbarApi {
constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
});
// (undocumented)
getAllProjects(): Promise<RollbarProject[]>;
// (undocumented)
getProject(projectName: string): Promise<RollbarProject>;
// (undocumented)
getProjectItems(project: string): Promise<RollbarItemsResponse>;
// (undocumented)
getTopActiveItems(project: string, hours?: number, environment?: string): Promise<RollbarTopActiveItem[]>;
}
// @public (undocumented)
const rollbarPlugin: BackstagePlugin<{
entityContent: RouteRef<undefined>;
}, {}>;
export { rollbarPlugin as plugin }
export { rollbarPlugin }
// @public (undocumented)
export const Router: (_props: Props_2) => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+6
View File
@@ -1,5 +1,11 @@
# @backstage/plugin-scaffolder-backend
## 0.12.2
### Patch Changes
- b49222176: Keep the empty string as empty string in `input` rather than replacing with `undefined` to make empty values ok for `cookiecutter`
## 0.12.1
### Patch Changes
+53 -14
View File
@@ -1,25 +1,64 @@
# Scaffolder Backend
Welcome to the scaffolder plugin!
This is the backend for the default Backstage [software
templates](https://backstage.io/docs/features/software-templates/software-templates-index).
This provides the API for the frontend [scaffolder
plugin](https://github.com/backstage/backstage/tree/master/plugins/scaffolder),
as well as the built-in template actions, tasks and stages.
## Jobs
## Installation
Documentation for `Jobs` here
This `@backstage/plugin-scaffolder-backend` package comes installed by default
in any Backstage application created with `npx @backstage/create-app`, so
installation is not usually required.
## Stages
To check if you already have the package, look under
`packages/backend/package.json`, in the `dependencies` block, for
`@backstage/plugin-scaffolder-backend`. The instructions below walk through
restoring the plugin, if you previously removed it.
Documentation for `Stages` here
### Install the package
## Tasks
```bash
# From your Backstage root directory
cd packages/backend
yarn add @backstage/plugin-scaffolder-backend
```
Documentation for `Tasks` here
### Adding the plugin to your `packages/backend`
## Actions
You'll need to add the plugin to the router in your `backend` package. You can
do this by creating a file called `packages/backend/src/plugins/scaffolder.ts`
with contents matching [scaffolder.ts in the create-app
template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts).
### Built-in:
With the `scaffolder.ts` router setup in place, add the router to
`packages/backend/src/index.ts`:
- #### GitHub Pull Request
- Minimum permissions required for GitHub App for creating a Pull Request with the built-in action:
- Read and Write permissions for `Contents`.
- Read and write permissions for `Pull Requests` and `Issues`.
- Read permissions on `Metadata`.
```diff
+import scaffolder from './plugins/scaffolder';
async function main() {
...
const createEnv = makeCreateEnv(config);
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
+ const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const apiRouter = Router();
+ apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
...
apiRouter.use(notFoundHandler());
```
### Adding templates
At this point the scaffolder backend is installed in your backend package, but
you will not have any templates available to use. These need to be [added to the
software
catalog](https://backstage.io/docs/features/software-templates/adding-templates).
To get up and running and try out some templates quickly, you can or copy the
catalog locations from the [create-app
template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs).
+518
View File
@@ -0,0 +1,518 @@
## API Report File for "@backstage/plugin-scaffolder-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AzureIntegrationConfig } from '@backstage/integration';
import { BitbucketIntegrationConfig } from '@backstage/integration';
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { ContainerRunner } from '@backstage/backend-common';
import { createPullRequest } from 'octokit-plugin-create-pull-request';
import express from 'express';
import { GithubCredentialsProvider } from '@backstage/integration';
import { GitHubIntegrationConfig } from '@backstage/integration';
import { Gitlab } from '@gitbeaker/core';
import { GitLabIntegrationConfig } from '@backstage/integration';
import gitUrlParse from 'git-url-parse';
import { JsonObject } from '@backstage/config';
import { JsonValue } from '@backstage/config';
import { Logger } from 'winston';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { Schema } from 'jsonschema';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { ScmIntegrations } from '@backstage/integration';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { TemplateEntityV1beta2 } from '@backstage/catalog-model';
import { UrlReader } from '@backstage/backend-common';
import { Writable } from 'stream';
// @public (undocumented)
export type ActionContext<Input extends InputBase> = {
baseUrl?: string;
logger: Logger;
logStream: Writable;
token?: string | undefined;
workspacePath: string;
input: Input;
output(name: string, value: JsonValue): void;
createTemporaryDirectory(): Promise<string>;
};
// @public (undocumented)
export class AzurePreparer implements PreparerBase {
constructor(config: {
token?: string;
});
// (undocumented)
static fromConfig(config: AzureIntegrationConfig): AzurePreparer;
// (undocumented)
prepare({ url, workspacePath, logger }: PreparerOptions): Promise<void>;
}
// @public (undocumented)
export class AzurePublisher implements PublisherBase {
constructor(config: {
token: string;
});
// (undocumented)
static fromConfig(config: AzureIntegrationConfig): Promise<AzurePublisher | undefined>;
// (undocumented)
publish({ values, workspacePath, logger, }: PublisherOptions): Promise<PublisherResult>;
}
// @public (undocumented)
export class BitbucketPreparer implements PreparerBase {
constructor(config: {
username?: string;
token?: string;
appPassword?: string;
});
// (undocumented)
static fromConfig(config: BitbucketIntegrationConfig): BitbucketPreparer;
// (undocumented)
prepare({ url, workspacePath, logger }: PreparerOptions): Promise<void>;
}
// @public (undocumented)
export class BitbucketPublisher implements PublisherBase {
constructor(config: {
host: string;
token?: string;
appPassword?: string;
username?: string;
apiBaseUrl?: string;
repoVisibility: RepoVisibilityOptions_2;
});
// (undocumented)
static fromConfig(config: BitbucketIntegrationConfig, { repoVisibility }: {
repoVisibility: RepoVisibilityOptions_2;
}): Promise<BitbucketPublisher>;
// (undocumented)
publish({ values, workspacePath, logger, }: PublisherOptions): Promise<PublisherResult>;
}
// @public
export class CatalogEntityClient {
constructor(catalogClient: CatalogApi);
findTemplate(templateName: string, options?: {
token?: string;
}): Promise<TemplateEntityV1alpha1 | TemplateEntityV1beta2>;
}
// @public (undocumented)
export class CookieCutter implements TemplaterBase {
constructor({ containerRunner }: {
containerRunner: ContainerRunner;
});
// (undocumented)
run({ workspacePath, values, logStream, }: TemplaterRunOptions): Promise<void>;
}
// @public (undocumented)
export const createBuiltinActions: (options: {
reader: UrlReader;
integrations: ScmIntegrations;
catalogClient: CatalogApi;
templaters: TemplaterBuilder;
}) => TemplateAction<any>[];
// @public (undocumented)
export function createCatalogRegisterAction(options: {
catalogClient: CatalogApi;
integrations: ScmIntegrations;
}): TemplateAction<any>;
// @public
export function createDebugLogAction(): TemplateAction<any>;
// @public (undocumented)
export function createFetchCookiecutterAction(options: {
reader: UrlReader;
integrations: ScmIntegrations;
templaters: TemplaterBuilder;
}): TemplateAction<any>;
// @public (undocumented)
export function createFetchPlainAction(options: {
reader: UrlReader;
integrations: ScmIntegrations;
}): TemplateAction<any>;
// @public (undocumented)
export function createLegacyActions(options: Options): TemplateAction<any>[];
// @public (undocumented)
export function createPublishAzureAction(options: {
integrations: ScmIntegrationRegistry;
}): TemplateAction<any>;
// @public (undocumented)
export function createPublishBitbucketAction(options: {
integrations: ScmIntegrationRegistry;
}): TemplateAction<any>;
// @public
export function createPublishFileAction(): TemplateAction<any>;
// @public (undocumented)
export function createPublishGithubAction(options: {
integrations: ScmIntegrationRegistry;
}): TemplateAction<any>;
// @public (undocumented)
export const createPublishGithubPullRequestAction: ({ integrations, clientFactory, }: CreateGithubPullRequestActionOptions) => TemplateAction<any>;
// @public (undocumented)
export function createPublishGitlabAction(options: {
integrations: ScmIntegrationRegistry;
}): TemplateAction<any>;
// @public (undocumented)
export class CreateReactAppTemplater implements TemplaterBase {
constructor({ containerRunner }: {
containerRunner: ContainerRunner;
});
// (undocumented)
run({ workspacePath, values, logStream, }: TemplaterRunOptions): Promise<void>;
}
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public (undocumented)
export const createTemplateAction: <Input extends Partial<{
[name: string]: JsonValue| Partial<JsonObject> | undefined;
}>>(templateAction: TemplateAction<Input>) => TemplateAction<any>;
// @public (undocumented)
export class FilePreparer implements PreparerBase {
// (undocumented)
prepare({ url, workspacePath }: PreparerOptions): Promise<void>;
}
// @public
export const getTemplaterKey: (entity: TemplateEntityV1alpha1) => string;
// @public (undocumented)
export class GithubPreparer implements PreparerBase {
constructor(config: {
credentialsProvider: GithubCredentialsProvider;
});
// (undocumented)
static fromConfig(config: GitHubIntegrationConfig): GithubPreparer;
// (undocumented)
prepare({ url, workspacePath, logger }: PreparerOptions): Promise<void>;
}
// @public @deprecated (undocumented)
export class GithubPublisher implements PublisherBase {
constructor(config: {
credentialsProvider: GithubCredentialsProvider;
repoVisibility: RepoVisibilityOptions;
apiBaseUrl: string | undefined;
});
// (undocumented)
static fromConfig(config: GitHubIntegrationConfig, { repoVisibility }: {
repoVisibility: RepoVisibilityOptions;
}): Promise<GithubPublisher | undefined>;
// (undocumented)
publish({ values, workspacePath, logger, }: PublisherOptions): Promise<PublisherResult>;
}
// @public (undocumented)
export class GitlabPreparer implements PreparerBase {
constructor(config: {
token?: string;
});
// (undocumented)
static fromConfig(config: GitLabIntegrationConfig): GitlabPreparer;
// (undocumented)
prepare({ url, workspacePath, logger }: PreparerOptions): Promise<void>;
}
// @public (undocumented)
export class GitlabPublisher implements PublisherBase {
constructor(config: {
token: string;
client: Gitlab;
repoVisibility: RepoVisibilityOptions_3;
});
// (undocumented)
static fromConfig(config: GitLabIntegrationConfig, { repoVisibility }: {
repoVisibility: RepoVisibilityOptions_3;
}): Promise<GitlabPublisher | undefined>;
// (undocumented)
publish({ values, workspacePath, logger, }: PublisherOptions): Promise<PublisherResult>;
}
// @public (undocumented)
export type Job = {
id: string;
context: StageContext;
status: ProcessorStatus;
stages: StageResult[];
error?: Error;
};
// @public (undocumented)
export type JobAndDirectoryTuple = {
job: Job;
directory: string;
};
// @public (undocumented)
export class JobProcessor implements Processor {
constructor(workingDirectory: string);
// (undocumented)
create({ entity, values, stages, }: {
entity: TemplateEntityV1alpha1;
values: TemplaterValues;
stages: StageInput[];
}): Job;
// (undocumented)
static fromConfig({ config, logger, }: {
config: Config;
logger: Logger;
}): Promise<JobProcessor>;
// (undocumented)
get(id: string): Job | undefined;
// (undocumented)
run(job: Job): Promise<void>;
}
// @public (undocumented)
export function joinGitUrlPath(repoUrl: string, path?: string): string;
// @public (undocumented)
export type ParsedLocationAnnotation = {
protocol: 'file' | 'url';
location: string;
};
// @public (undocumented)
export const parseLocationAnnotation: (entity: TemplateEntityV1alpha1) => ParsedLocationAnnotation;
// @public (undocumented)
export interface PreparerBase {
prepare(opts: PreparerOptions): Promise<void>;
}
// @public (undocumented)
export type PreparerBuilder = {
register(host: string, preparer: PreparerBase): void;
get(url: string): PreparerBase;
};
// @public (undocumented)
export type PreparerOptions = {
url: string;
workspacePath: string;
logger: Logger;
};
// @public (undocumented)
export class Preparers implements PreparerBuilder {
// (undocumented)
static fromConfig(config: Config, _: {
logger: Logger;
}): Promise<PreparerBuilder>;
// (undocumented)
get(url: string): PreparerBase;
// (undocumented)
register(host: string, preparer: PreparerBase): void;
}
// @public (undocumented)
export type Processor = {
create({ entity, values, stages, }: {
entity: TemplateEntityV1alpha1;
values: TemplaterValues;
stages: StageInput[];
}): Job;
get(id: string): Job | undefined;
run(job: Job): Promise<void>;
};
// @public (undocumented)
export type ProcessorStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED';
// @public
export type PublisherBase = {
publish(opts: PublisherOptions): Promise<PublisherResult>;
};
// @public (undocumented)
export type PublisherBuilder = {
register(host: string, publisher: PublisherBase): void;
get(storePath: string): PublisherBase;
};
// @public (undocumented)
export type PublisherOptions = {
values: TemplaterValues;
workspacePath: string;
logger: Logger;
};
// @public (undocumented)
export type PublisherResult = {
remoteUrl: string;
catalogInfoUrl?: string;
};
// @public (undocumented)
export class Publishers implements PublisherBuilder {
// (undocumented)
static fromConfig(config: Config, _options: {
logger: Logger;
}): Promise<PublisherBuilder>;
// (undocumented)
get(url: string): PublisherBase;
// (undocumented)
register(host: string, preparer: PublisherBase | undefined): void;
}
// @public (undocumented)
export type RepoVisibilityOptions = 'private' | 'internal' | 'public';
// @public
export type RequiredTemplateValues = {
owner: string;
storePath: string;
destination?: {
git?: gitUrlParse.GitUrl;
};
};
// @public (undocumented)
export interface RouterOptions {
// (undocumented)
actions?: TemplateAction<any>[];
// (undocumented)
catalogClient: CatalogApi;
// (undocumented)
config: Config;
// (undocumented)
database: PluginDatabaseManager;
// (undocumented)
logger: Logger;
// (undocumented)
preparers: PreparerBuilder;
// (undocumented)
publishers: PublisherBuilder;
// (undocumented)
reader: UrlReader;
// (undocumented)
taskWorkers?: number;
// (undocumented)
templaters: TemplaterBuilder;
}
// @public (undocumented)
export const runCommand: ({ command, args, logStream, }: RunCommandOptions) => Promise<void>;
// @public (undocumented)
export type RunCommandOptions = {
command: string;
args: string[];
logStream?: Writable;
};
// @public (undocumented)
export type StageContext<T = {}> = {
values: TemplaterValues;
entity: TemplateEntityV1alpha1;
logger: Logger;
logStream: Writable;
workspacePath: string;
} & T;
// @public (undocumented)
export interface StageInput<T = {}> {
// (undocumented)
handler(ctx: StageContext<T>): Promise<void | object>;
// (undocumented)
name: string;
}
// @public (undocumented)
export interface StageResult extends StageInput {
// (undocumented)
endedAt?: number;
// (undocumented)
log: string[];
// (undocumented)
startedAt?: number;
// (undocumented)
status: ProcessorStatus;
}
// @public
export type SupportedTemplatingKey = 'cookiecutter' | string;
// @public (undocumented)
export type TemplateAction<Input extends InputBase> = {
id: string;
description?: string;
schema?: {
input?: Schema;
output?: Schema;
};
handler: (ctx: ActionContext<Input>) => Promise<void>;
};
// @public (undocumented)
export class TemplateActionRegistry {
// (undocumented)
get(actionId: string): TemplateAction<any>;
// (undocumented)
list(): TemplateAction<any>[];
// (undocumented)
register<Parameters extends InputBase>(action: TemplateAction<Parameters>): void;
}
// @public (undocumented)
export type TemplaterBase = {
run(opts: TemplaterRunOptions): Promise<void>;
};
// @public
export type TemplaterBuilder = {
register(protocol: SupportedTemplatingKey, templater: TemplaterBase): void;
get(templater: string): TemplaterBase;
};
// @public (undocumented)
export type TemplaterConfig = {
templater?: TemplaterBase;
};
// @public
export type TemplaterRunOptions = {
workspacePath: string;
values: TemplaterValues;
logStream?: Writable;
};
// @public
export type TemplaterRunResult = {
resultDir: string;
};
// @public (undocumented)
export class Templaters implements TemplaterBuilder {
// (undocumented)
get(templaterId: string): TemplaterBase;
// (undocumented)
register(templaterKey: SupportedTemplatingKey, templater: TemplaterBase): void;
}
// @public (undocumented)
export type TemplaterValues = RequiredTemplateValues & Record<string, any>;
// (No @packageDocumentation comment for this package)
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-scaffolder-backend",
"version": "0.12.1",
"version": "0.12.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -170,11 +170,6 @@ export class TaskWorker {
preventIndent: true,
})(templateCtx);
// If it's just an empty string, treat it as undefined
if (templated === '') {
return undefined;
}
// If it smells like a JSON object then give it a parse as an object and if it fails return the string
if (
(templated.startsWith('"') && templated.endsWith('"')) ||
@@ -213,6 +208,10 @@ export class TaskWorker {
// Keep track of all tmp dirs that are created by the action so we can remove them after
const tmpDirs = new Array<string>();
this.options.logger.debug(`Running ${action.id} with input`, {
input: JSON.stringify(input, null, 2),
});
await action.handler({
baseUrl: task.spec.baseUrl,
logger: taskLogger,
+80 -4
View File
@@ -1,10 +1,86 @@
# Scaffolder Frontend
WORK IN PROGRESS
This is the React frontend for the default Backstage [software
templates](https://backstage.io/docs/features/software-templates/software-templates-index).
This package supplies interfaces related to showing available templates in the
Backstage catalog and the workflow to create software using those templates.
This is the frontend part of the default scaffolder plugin.
## Installation
This `@backstage/plugin-scaffolder` package comes installed by default in any
Backstage application created with `npx @backstage/create-app`, so installation
is not usually required.
To check if you already have the package, look under
`packages/app/package.json`, in the `dependencies` block, for
`@backstage/plugin-scaffolder`. The instructions below walk through restoring
the plugin, if you previously removed it.
### Install the package
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-scaffolder
```
### Add the plugin to your `packages/app`
Add the root page that the scaffolder plugin provides to your app. You can
choose any path for the route, but we recommend the following:
```diff
// packages/app/src/App.tsx
+import { ScaffolderPage } from '@backstage/plugin-scaffolder';
<FlatRoutes>
<Route path="/catalog" element={<CatalogIndexPage />} />
<Route path="/catalog/:namespace/:kind/:name" element={<CatalogEntityPage />}>
{entityPage}
</Route>
+ <Route path="/create" element={<ScaffolderPage />} />;
...
</FlatRoutes>
```
The scaffolder plugin also has one external route that needs to be bound for it
to function: the `registerComponent` route which should link to the page where
the user can register existing software component. In a typical setup, the
register component route will be linked to the `catalog-import` plugin's import
page:
```diff
// packages/app/src/App.tsx
+import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
+import { catalogImportPlugin } from '@backstage/plugin-catalog-import';
const app = createApp({
// ...
bindRoutes({ bind }) {
+ bind(scaffolderPlugin.externalRoutes, {
+ registerComponent: catalogImportPlugin.routes.importPage,
+ });
},
});
```
You may also want to add a link to the scaffolder page to your application
sidebar:
```diff
// packages/app/src/components/Root/Root.tsx
+import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
+ <SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />;
...
</Sidebar>
```
## Links
- [Backend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend)
- [The Backstage homepage](https://backstage.io)
- [scaffolder-backend](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend)
provides the backend API for this frontend.
+112
View File
@@ -0,0 +1,112 @@
## API Report File for "@backstage/plugin-scaffolder"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { DiscoveryApi } from '@backstage/core';
import { EntityName } from '@backstage/catalog-model';
import { Extension } from '@backstage/core';
import { ExternalRouteRef } from '@backstage/core';
import { FieldProps } from '@rjsf/core';
import { FieldValidation } from '@rjsf/core';
import { IdentityApi } from '@backstage/core';
import { JsonObject } from '@backstage/config';
import { JSONSchema } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/config';
import { Observable } from '@backstage/core';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core';
import { ScmIntegrationRegistry } from '@backstage/integration';
// @public (undocumented)
export function createScaffolderFieldExtension<T = any>(options: FieldExtensionOptions<T>): Extension<() => null>;
// @public (undocumented)
export const EntityPickerFieldExtension: () => null;
// @public (undocumented)
export const OwnerPickerFieldExtension: () => null;
// @public (undocumented)
export const RepoUrlPickerFieldExtension: () => null;
// @public (undocumented)
export interface ScaffolderApi {
// (undocumented)
getIntegrationsList(options: {
allowedHosts: string[];
}): Promise<{
type: string;
title: string;
host: string;
}[]>;
// (undocumented)
getTask(taskId: string): Promise<ScaffolderTask>;
// (undocumented)
getTemplateParameterSchema(templateName: EntityName): Promise<TemplateParameterSchema>;
// (undocumented)
listActions(): Promise<ListActionsResponse>;
scaffold(templateName: string, values: Record<string, any>): Promise<string>;
// (undocumented)
streamLogs({ taskId, after, }: {
taskId: string;
after?: number;
}): Observable<LogEvent>;
}
// @public (undocumented)
export const scaffolderApiRef: ApiRef<ScaffolderApi>;
// @public (undocumented)
export class ScaffolderClient implements ScaffolderApi {
constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
scmIntegrationsApi: ScmIntegrationRegistry;
});
// (undocumented)
getIntegrationsList(options: {
allowedHosts: string[];
}): Promise<{
type: string;
title: string;
host: string;
}[]>;
// (undocumented)
getTask(taskId: string): Promise<any>;
// (undocumented)
getTemplateParameterSchema(templateName: EntityName): Promise<TemplateParameterSchema>;
// (undocumented)
listActions(): Promise<ListActionsResponse>;
scaffold(templateName: string, values: Record<string, any>): Promise<string>;
// (undocumented)
streamLogs({ taskId, after, }: {
taskId: string;
after?: number;
}): Observable<LogEvent>;
}
// @public (undocumented)
export const ScaffolderFieldExtensions: React_2.ComponentType;
// @public (undocumented)
export const ScaffolderPage: () => JSX.Element;
// @public (undocumented)
const scaffolderPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
}, {
registerComponent: ExternalRouteRef<undefined, true>;
}>;
export { scaffolderPlugin as plugin }
export { scaffolderPlugin }
// (No @packageDocumentation comment for this package)
```
+4 -3
View File
@@ -33,11 +33,12 @@
"@backstage/catalog-client": "^0.3.13",
"@backstage/catalog-model": "^0.8.2",
"@backstage/config": "^0.1.5",
"@backstage/errors": "^0.1.1",
"@backstage/core": "^0.7.12",
"@backstage/integration": "^0.5.6",
"@backstage/integration-react": "^0.1.3",
"@backstage/plugin-catalog-react": "^0.2.2",
"@backstage/core-plugin-api": "^0.1.2",
"@backstage/errors": "^0.1.1",
"@backstage/theme": "^0.2.8",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -46,10 +47,10 @@
"@rjsf/material-ui": "^2.4.0",
"@types/react": "^16.9",
"classnames": "^2.2.6",
"json-schema": "^0.3.0",
"git-url-parse": "^11.4.4",
"humanize-duration": "^3.25.1",
"immer": "^9.0.1",
"json-schema": "^0.3.0",
"luxon": "^1.25.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
@@ -66,9 +67,9 @@
"@backstage/test-utils": "^0.1.13",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/react-hooks": "^3.3.0",
"@testing-library/user-event": "^13.1.8",
"@types/humanize-duration": "^3.18.1",
"@testing-library/react-hooks": "^3.3.0",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"cross-fetch": "^3.0.6",
+14 -11
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { useMemo } from 'react';
import React from 'react';
import { Routes, Route, useOutlet } from 'react-router';
import { ScaffolderPage } from './ScaffolderPage';
import { TemplatePage } from './TemplatePage';
@@ -27,21 +27,24 @@ import {
FIELD_EXTENSION_KEY,
DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS,
} from '../extensions';
import { collectComponentData, collectChildren } from '../extensions/helpers';
import { useElementFilter } from '@backstage/core-plugin-api';
export const Router = () => {
const outlet = useOutlet();
const fieldExtensions = useMemo(() => {
const registeredExtensions = collectComponentData<FieldExtensionOptions>(
collectChildren(outlet, FIELD_EXTENSION_WRAPPER_KEY).flat(),
FIELD_EXTENSION_KEY,
);
const foundExtensions = useElementFilter(outlet, elements =>
elements
.selectByComponentData({
key: FIELD_EXTENSION_WRAPPER_KEY,
})
.findComponentData<FieldExtensionOptions>({
key: FIELD_EXTENSION_KEY,
}),
);
return registeredExtensions.length
? registeredExtensions
: DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS;
}, [outlet]);
const fieldExtensions = foundExtensions.length
? foundExtensions
: DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS;
return (
<Routes>
@@ -1,107 +0,0 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { collectComponentData, collectChildren } from './helpers';
import { attachComponentData } from '@backstage/core';
describe('Extension Helpers', () => {
const createElementWithComponentData = ({
type,
data,
}: {
type: string;
data: any;
}) => {
const element: React.ComponentType = () => null;
attachComponentData(element, type, data);
return element;
};
describe('collectChildren', () => {
it('should return the children of the component which has the correct componentData flag', () => {
const SearchElement = createElementWithComponentData({
type: 'find.me',
data: {},
});
const DontCareAboutme = createElementWithComponentData({
type: 'dont.find.me',
data: {},
});
const child1 = (
<div>
<b>hello</b>
</div>
);
const child2 = (
<div>
<p>Hello2</p>
</div>
);
const testCase = (
<div>
<SearchElement>
{child1}
{child1}
</SearchElement>
<SearchElement>{child2}</SearchElement>
<DontCareAboutme>
<p>Hello!</p>
<SearchElement>{child1}</SearchElement>
</DontCareAboutme>
</div>
);
const children = collectChildren(testCase, 'find.me');
expect(children).toEqual([[child1, child1], child2, child1]);
});
});
describe('collectComponentData', () => {
it('should return the componentData for particular nodes', () => {
const componentData1 = { help: 'im something' };
const componentData2 = { help: 'im something else' };
const FirstElement = createElementWithComponentData({
type: 'find.me',
data: componentData1,
});
const SecondElement = createElementWithComponentData({
type: 'dont.find.me',
data: componentData2,
});
const testCase = [
<FirstElement />,
<FirstElement />,
<SecondElement />,
<FirstElement />,
];
const returnedData = collectComponentData(testCase, 'find.me');
expect(returnedData).toEqual([
componentData1,
componentData1,
componentData1,
]);
});
});
});
@@ -1,73 +0,0 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { getComponentData } from '@backstage/core';
export const collectComponentData = <T>(
children: React.ReactNode,
componentDataKey: string,
) => {
const stack = [children];
const found: T[] = [];
while (stack.length) {
const current: React.ReactNode = stack.pop()!;
React.Children.forEach(current, child => {
if (!React.isValidElement(child)) {
return;
}
const data = getComponentData<T>(child, componentDataKey);
if (data) {
found.push(data);
}
if (child.props.children) {
stack.push(child.props.children);
}
});
}
return found;
};
export const collectChildren = (
component: React.ReactNode,
componentDataKey: string,
) => {
const stack = [component];
const found: React.ReactNode[] = [];
while (stack.length) {
const current: React.ReactNode = stack.pop()!;
React.Children.forEach(current, child => {
if (!React.isValidElement(child)) {
return;
}
if (child.props.children) {
if (getComponentData(child, componentDataKey)) {
found.push(child.props.children);
}
stack.push(child.props.children);
}
});
}
return found;
};
+68
View File
@@ -0,0 +1,68 @@
## API Report File for "@backstage/plugin-search-backend-node"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { DocumentCollator } from '@backstage/search-common';
import { DocumentDecorator } from '@backstage/search-common';
import { IndexableDocument } from '@backstage/search-common';
import { Logger } from 'winston';
import { default as lunr_2 } from 'lunr';
import { SearchQuery } from '@backstage/search-common';
import { SearchResultSet } from '@backstage/search-common';
// @public (undocumented)
export class IndexBuilder {
constructor({ logger, searchEngine }: IndexBuilderOptions);
addCollator({ collator, defaultRefreshIntervalSeconds, }: RegisterCollatorParameters): void;
addDecorator({ decorator }: RegisterDecoratorParameters): void;
build(): Promise<{
scheduler: Scheduler;
}>;
// (undocumented)
getSearchEngine(): SearchEngine;
}
// @public (undocumented)
export class LunrSearchEngine implements SearchEngine {
constructor({ logger }: {
logger: Logger;
});
// (undocumented)
protected docStore: Record<string, IndexableDocument>;
// (undocumented)
index(type: string, documents: IndexableDocument[]): void;
// (undocumented)
protected logger: Logger;
// (undocumented)
protected lunrIndices: Record<string, lunr_2.Index>;
// (undocumented)
query(query: SearchQuery): Promise<SearchResultSet>;
// (undocumented)
setTranslator(translator: LunrQueryTranslator): void;
// (undocumented)
protected translator: QueryTranslator;
}
// @public
export class Scheduler {
constructor({ logger }: {
logger: Logger;
});
addToSchedule(task: Function, interval: number): void;
start(): void;
stop(): void;
}
// @public
export interface SearchEngine {
index(type: string, documents: IndexableDocument[]): void;
query(query: SearchQuery): Promise<SearchResultSet>;
setTranslator(translator: QueryTranslator): void;
}
// (No @packageDocumentation comment for this package)
```
+17
View File
@@ -0,0 +1,17 @@
## API Report File for "@backstage/plugin-search-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import express from 'express';
import { Logger } from 'winston';
import { SearchEngine } from '@backstage/plugin-search-backend-node';
// @public (undocumented)
export function createRouter({ engine, logger, }: RouterOptions): Promise<express.Router>;
// (No @packageDocumentation comment for this package)
```
+102
View File
@@ -0,0 +1,102 @@
## API Report File for "@backstage/plugin-search"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { AsyncState } from 'react-use/lib/useAsync';
import { BackstagePlugin } from '@backstage/core';
import { IndexableDocument } from '@backstage/search-common';
import { JsonObject } from '@backstage/config';
import { default as React_2 } from 'react';
import { ReactElement } from 'react';
import { RouteRef } from '@backstage/core';
import { SearchQuery } from '@backstage/search-common';
import { SearchResult as SearchResult_2 } from '@backstage/search-common';
import { SearchResultSet } from '@backstage/search-common';
// @public (undocumented)
export const DefaultResultListItem: ({ result }: {
result: IndexableDocument;
}) => JSX.Element;
// @public (undocumented)
export const Filters: ({ filters, filterOptions, resetFilters, updateSelected, updateChecked, }: FiltersProps) => JSX.Element;
// @public (undocumented)
export const FiltersButton: ({ numberOfSelectedFilters, handleToggleFilters, }: FiltersButtonProps) => JSX.Element;
// @public (undocumented)
export type FiltersState = {
selected: string;
checked: Array<string>;
};
// @public (undocumented)
export const Router: () => JSX.Element;
// @public (undocumented)
export const searchApiRef: ApiRef<SearchApi>;
// @public (undocumented)
export const SearchBar: ({ className, debounceTime }: Props) => JSX.Element;
// @public @deprecated (undocumented)
export const SearchBarNext: ({ className, debounceTime }: {
className?: string | undefined;
debounceTime?: number | undefined;
}) => JSX.Element;
// @public (undocumented)
export const SearchContextProvider: ({ initialState, children, }: React_2.PropsWithChildren<{
initialState?: SettableSearchContext | undefined;
}>) => JSX.Element;
// @public (undocumented)
export const SearchFilter: {
({ component: Element, ...props }: Props_2): JSX.Element;
Checkbox(props: Omit<Props_2, 'component'> & Component): JSX.Element;
Select(props: Omit<Props_2, 'component'> & Component): JSX.Element;
};
// @public @deprecated (undocumented)
export const SearchFilterNext: {
({ component: Element, ...props }: Props_2): JSX.Element;
Checkbox(props: Omit<Props_2, 'component'> & Component): JSX.Element;
Select(props: Omit<Props_2, 'component'> & Component): JSX.Element;
};
// @public (undocumented)
export const SearchPage: () => JSX.Element;
// @public @deprecated (undocumented)
export const SearchPageNext: () => JSX.Element;
// @public (undocumented)
const searchPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
nextRoot: RouteRef<undefined>;
}, {}>;
export { searchPlugin as plugin }
export { searchPlugin }
// @public (undocumented)
export const SearchResult: ({ children }: {
children: (results: {
results: SearchResult_2[];
}) => JSX.Element;
}) => JSX.Element;
// @public (undocumented)
export const SidebarSearch: () => JSX.Element;
// @public (undocumented)
export const useSearch: () => SearchContextValue;
// (No @packageDocumentation comment for this package)
```
+100
View File
@@ -0,0 +1,100 @@
## API Report File for "@backstage/plugin-sentry"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { DiscoveryApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { InfoCardVariants } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const EntitySentryCard: () => JSX.Element;
// @public (undocumented)
export const EntitySentryContent: () => JSX.Element;
// @public (undocumented)
export class MockSentryApi implements SentryApi {
// (undocumented)
fetchIssues(): Promise<SentryIssue[]>;
}
// @public (undocumented)
export class ProductionSentryApi implements SentryApi {
constructor(discoveryApi: DiscoveryApi, organization: string);
// (undocumented)
fetchIssues(project: string, statsFor: string): Promise<SentryIssue[]>;
}
// @public (undocumented)
export const Router: ({ entity }: {
entity: Entity;
}) => JSX.Element;
// @public (undocumented)
export interface SentryApi {
// (undocumented)
fetchIssues(project: string, statsFor: string): Promise<SentryIssue[]>;
}
// @public (undocumented)
export const sentryApiRef: ApiRef<SentryApi>;
// @public (undocumented)
export type SentryIssue = {
platform: SentryPlatform;
lastSeen: string;
numComments: number;
userCount: number;
stats: {
'24h'?: EventPoint[];
'12h'?: EventPoint[];
};
culprit: string;
title: string;
id: string;
assignedTo: any;
logger: any;
type: string;
annotations: any[];
metadata: SentryIssueMetadata;
status: string;
subscriptionDetails: any;
isPublic: boolean;
hasSeen: boolean;
shortId: string;
shareId: string | null;
firstSeen: string;
count: string;
permalink: string;
level: string;
isSubscribed: boolean;
isBookmarked: boolean;
project: SentryProject;
statusDetails: any;
};
// @public (undocumented)
export const SentryIssuesWidget: ({ entity, statsFor, variant, }: {
entity: Entity;
statsFor?: "12h" | "24h" | undefined;
variant?: InfoCardVariants | undefined;
}) => JSX.Element;
// @public (undocumented)
const sentryPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
}, {}>;
export { sentryPlugin as plugin }
export { sentryPlugin }
// (No @packageDocumentation comment for this package)
```
+56
View File
@@ -0,0 +1,56 @@
## API Report File for "@backstage/plugin-shortcuts"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { Observable } from '@backstage/core';
import ObservableImpl from 'zen-observable';
import { StorageApi } from '@backstage/core';
// @public
export class LocalStoredShortcuts implements ShortcutApi {
constructor(storageApi: StorageApi);
// (undocumented)
add(shortcut: Omit<Shortcut, 'id'>): Promise<void>;
// (undocumented)
getColor(url: string): string;
// (undocumented)
remove(id: string): Promise<void>;
// (undocumented)
shortcut$(): ObservableImpl<Shortcut[]>;
// (undocumented)
update(shortcut: Shortcut): Promise<void>;
}
// @public (undocumented)
export type Shortcut = {
id: string;
url: string;
title: string;
};
// @public (undocumented)
export interface ShortcutApi {
add(shortcut: Omit<Shortcut, 'id'>): Promise<void>;
getColor(url: string): string;
remove(id: string): Promise<void>;
shortcut$(): Observable<Shortcut[]>;
update(shortcut: Shortcut): Promise<void>;
}
// @public (undocumented)
export const Shortcuts: () => JSX.Element;
// @public (undocumented)
export const shortcutsApiRef: ApiRef<ShortcutApi>;
// @public (undocumented)
export const shortcutsPlugin: BackstagePlugin<{}, {}>;
// (No @packageDocumentation comment for this package)
```
+41
View File
@@ -0,0 +1,41 @@
## API Report File for "@backstage/plugin-sonarqube"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackstagePlugin } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { InfoCardVariants } from '@backstage/core';
// @public (undocumented)
export const EntitySonarQubeCard: ({ variant, duplicationRatings, }: {
entity?: Entity| undefined;
variant?: InfoCardVariants| undefined;
duplicationRatings?: {
greaterThan: number;
rating: "1.0" | "2.0" | "3.0" | "4.0" | "5.0";
}[] | undefined;
}) => JSX.Element;
// @public (undocumented)
export const isSonarQubeAvailable: (entity: Entity) => boolean;
// @public (undocumented)
export const SonarQubeCard: ({ variant, duplicationRatings, }: {
entity?: Entity | undefined;
variant?: InfoCardVariants | undefined;
duplicationRatings?: DuplicationRating[] | undefined;
}) => JSX.Element;
// @public (undocumented)
const sonarQubePlugin: BackstagePlugin<{}, {}>;
export { sonarQubePlugin as plugin }
export { sonarQubePlugin }
// (No @packageDocumentation comment for this package)
```
+68
View File
@@ -0,0 +1,68 @@
## API Report File for "@backstage/plugin-splunk-on-call"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { ConfigApi } from '@backstage/core';
import { DiscoveryApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const EntitySplunkOnCallCard: () => JSX.Element;
// @public (undocumented)
export const isSplunkOnCallAvailable: (entity: Entity) => boolean;
// @public (undocumented)
export const splunkOnCallApiRef: ApiRef<SplunkOnCallApi>;
// @public (undocumented)
export class SplunkOnCallClient implements SplunkOnCallApi {
constructor(config: ClientApiConfig);
// (undocumented)
static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi): SplunkOnCallClient;
// (undocumented)
getEscalationPolicies(): Promise<EscalationPolicyInfo[]>;
// (undocumented)
getIncidents(): Promise<Incident[]>;
// (undocumented)
getOnCallUsers(): Promise<OnCall[]>;
// (undocumented)
getTeams(): Promise<Team[]>;
// (undocumented)
getUsers(): Promise<User[]>;
// (undocumented)
incidentAction({ routingKey, incidentType, incidentId, incidentDisplayName, incidentMessage, incidentStartTime, }: TriggerAlarmRequest): Promise<Response>;
}
// @public (undocumented)
export const SplunkOnCallPage: {
({ title, subtitle, pageTitle, }: SplunkOnCallPageProps): JSX.Element;
defaultProps: {
title: string;
subtitle: string;
pageTitle: string;
};
};
// @public (undocumented)
const splunkOnCallPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
}, {}>;
export { splunkOnCallPlugin as plugin }
export { splunkOnCallPlugin }
// @public (undocumented)
export class UnauthorizedError extends Error {
}
// (No @packageDocumentation comment for this package)
```
+123
View File
@@ -0,0 +1,123 @@
## API Report File for "@backstage/plugin-tech-radar"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export interface RadarEntry {
// (undocumented)
description?: string;
// (undocumented)
id: string;
// (undocumented)
key: string;
// (undocumented)
quadrant: string;
// (undocumented)
timeline: Array<RadarEntrySnapshot>;
// (undocumented)
title: string;
// (undocumented)
url: string;
}
// @public (undocumented)
export interface RadarEntrySnapshot {
// (undocumented)
date: Date;
// (undocumented)
description?: string;
// (undocumented)
moved?: MovedState;
// (undocumented)
ringId: string;
}
// @public (undocumented)
export interface RadarQuadrant {
// (undocumented)
id: string;
// (undocumented)
name: string;
}
// @public
export interface RadarRing {
// (undocumented)
color: string;
// (undocumented)
id: string;
// (undocumented)
name: string;
}
// @public (undocumented)
export const Router: {
({ title, subtitle, pageTitle, ...props }: TechRadarPageProps): JSX.Element;
defaultProps: {
title: string;
subtitle: string;
pageTitle: string;
};
};
// @public (undocumented)
export interface TechRadarApi {
// (undocumented)
load: () => Promise<TechRadarLoaderResponse>;
}
// @public (undocumented)
export const techRadarApiRef: ApiRef<TechRadarApi>;
// @public (undocumented)
export const TechRadarComponent: (props: TechRadarComponentProps) => JSX.Element;
// @public
export interface TechRadarComponentProps {
// (undocumented)
height: number;
// (undocumented)
svgProps?: object;
// (undocumented)
width: number;
}
// @public
export interface TechRadarLoaderResponse {
// (undocumented)
entries: RadarEntry[];
// (undocumented)
quadrants: RadarQuadrant[];
// (undocumented)
rings: RadarRing[];
}
// @public (undocumented)
export const TechRadarPage: {
({ title, subtitle, pageTitle, ...props }: TechRadarPageProps): JSX.Element;
defaultProps: {
title: string;
subtitle: string;
pageTitle: string;
};
};
// @public (undocumented)
const techRadarPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
}, {}>;
export { techRadarPlugin as plugin }
export { techRadarPlugin }
// (No @packageDocumentation comment for this package)
```
+24
View File
@@ -0,0 +1,24 @@
## API Report File for "@backstage/plugin-techdocs-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Config } from '@backstage/config';
import express from 'express';
import { GeneratorBuilder } from '@backstage/techdocs-common';
import { Knex } from 'knex';
import { Logger } from 'winston';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { PreparerBuilder } from '@backstage/techdocs-common';
import { PublisherBase } from '@backstage/techdocs-common';
// @public (undocumented)
export function createRouter({ preparers, generators, publisher, config, logger, discovery, }: RouterOptions): Promise<express.Router>;
export * from "@backstage/techdocs-common";
// (No @packageDocumentation comment for this package)
```
@@ -68,7 +68,11 @@ export class DocsBuilder {
this.config = config;
}
public async build(): Promise<void> {
/**
* Build the docs and return whether they have been newly generated or have been cached
* @returns true, if the docs have been built. false, if the cached docs are still up-to-date.
*/
public async build(): Promise<boolean> {
if (!this.entity.metadata.uid) {
throw new Error(
'Trying to build documentation for entity not in service catalog',
@@ -125,7 +129,7 @@ export class DocsBuilder {
this.entity,
)} are unmodified. Using cache, skipping generate and prepare`,
);
return;
return false;
}
throw new Error(err.message);
}
@@ -205,5 +209,7 @@ export class DocsBuilder {
// Update the last check time for the entity
new BuildMetadataStorage(this.entity.metadata.uid).setLastUpdated();
return true;
}
}
+11 -3
View File
@@ -16,7 +16,7 @@
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { NotFoundError } from '@backstage/errors';
import { NotFoundError, NotModifiedError } from '@backstage/errors';
import {
GeneratorBuilder,
getLocationForEntity,
@@ -172,10 +172,15 @@ export async function createRouter({
case 'awsS3':
case 'azureBlobStorage':
case 'openStackSwift':
case 'googleGcs':
case 'googleGcs': {
// This block should be valid for all storage implementations. So no need to duplicate in future,
// add the publisher type in the list here.
await docsBuilder.build();
const updated = await docsBuilder.build();
if (!updated) {
throw new NotModifiedError();
}
// With a maximum of ~5 seconds wait, check if the files got published and if docs will be fetched
// on the user's page. If not, respond with a message asking them to check back later.
// The delay here is to make sure GCS/AWS/etc. registers newly uploaded files which is usually <1 second
@@ -194,10 +199,13 @@ export async function createRouter({
'Sorry! It took too long for the generated docs to show up in storage. Check back later.',
);
}
res
.status(201)
.json({ message: 'Docs updated or did not need updating' });
break;
}
default:
throw new NotFoundError(
`Publisher type ${publisherType} is not supported by techdocs-backend docs builder.`,
+146
View File
@@ -0,0 +1,146 @@
## API Report File for "@backstage/plugin-techdocs"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiRef } from '@backstage/core';
import { BackstagePlugin } from '@backstage/core';
import { Config } from '@backstage/config';
import { CSSProperties } from '@material-ui/styles';
import { DiscoveryApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { EntityName } from '@backstage/catalog-model';
import { IdentityApi } from '@backstage/core';
import { Location as Location_2 } from '@backstage/catalog-model';
import { RouteRef } from '@backstage/core';
// @public (undocumented)
export const DocsCardGrid: ({ entities, }: {
entities: Entity[] | undefined;
}) => JSX.Element | null;
// @public (undocumented)
export const DocsTable: ({ entities, title, }: {
entities: Entity[] | undefined;
title?: string | undefined;
}) => JSX.Element | null;
// @public (undocumented)
export const EmbeddedDocsRouter: (_props: Props) => JSX.Element;
// @public (undocumented)
export const EntityTechdocsContent: (_props: {
entity?: Entity| undefined;
}) => JSX.Element;
// @public (undocumented)
export type PanelType = 'DocsCardGrid' | 'DocsTable';
// @public (undocumented)
export const Reader: ({ entityId, onReady }: Props_2) => JSX.Element;
// @public (undocumented)
export const Router: () => JSX.Element;
// @public (undocumented)
export interface TechDocsApi {
// (undocumented)
getApiOrigin(): Promise<string>;
// (undocumented)
getEntityMetadata(entityId: EntityName): Promise<TechDocsEntityMetadata>;
// (undocumented)
getTechDocsMetadata(entityId: EntityName): Promise<TechDocsMetadata>;
}
// @public (undocumented)
export const techdocsApiRef: ApiRef<TechDocsApi>;
// @public
export class TechDocsClient implements TechDocsApi {
constructor({ configApi, discoveryApi, identityApi, }: {
configApi: Config;
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
});
// (undocumented)
configApi: Config;
// (undocumented)
discoveryApi: DiscoveryApi;
// (undocumented)
getApiOrigin(): Promise<string>;
getEntityMetadata(entityId: EntityName): Promise<TechDocsEntityMetadata>;
getTechDocsMetadata(entityId: EntityName): Promise<TechDocsMetadata>;
// (undocumented)
identityApi: IdentityApi;
}
// @public (undocumented)
export const TechDocsCustomHome: ({ tabsConfig, }: {
tabsConfig: TabsConfig;
}) => JSX.Element;
// @public (undocumented)
export const TechdocsPage: () => JSX.Element;
// @public (undocumented)
const techdocsPlugin: BackstagePlugin<{
root: RouteRef<undefined>;
entityContent: RouteRef<undefined>;
}, {}>;
export { techdocsPlugin as plugin }
export { techdocsPlugin }
// @public (undocumented)
export const TechDocsReaderPage: () => JSX.Element;
// @public (undocumented)
export interface TechDocsStorageApi {
// (undocumented)
getApiOrigin(): Promise<string>;
// (undocumented)
getBaseUrl(oldBaseUrl: string, entityId: EntityName, path: string): Promise<string>;
// (undocumented)
getBuilder(): Promise<string>;
// (undocumented)
getEntityDocs(entityId: EntityName, path: string): Promise<string>;
// (undocumented)
getStorageUrl(): Promise<string>;
// (undocumented)
syncEntityDocs(entityId: EntityName): Promise<boolean>;
}
// @public (undocumented)
export const techdocsStorageApiRef: ApiRef<TechDocsStorageApi>;
// @public
export class TechDocsStorageClient implements TechDocsStorageApi {
constructor({ configApi, discoveryApi, identityApi, }: {
configApi: Config;
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
});
// (undocumented)
configApi: Config;
// (undocumented)
discoveryApi: DiscoveryApi;
// (undocumented)
getApiOrigin(): Promise<string>;
// (undocumented)
getBaseUrl(oldBaseUrl: string, entityId: EntityName, path: string): Promise<string>;
// (undocumented)
getBuilder(): Promise<string>;
getEntityDocs(entityId: EntityName, path: string): Promise<string>;
// (undocumented)
getStorageUrl(): Promise<string>;
// (undocumented)
identityApi: IdentityApi;
syncEntityDocs(entityId: EntityName): Promise<boolean>;
}
// (No @packageDocumentation comment for this package)
```
-149
View File
@@ -1,149 +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 { EntityName } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { DiscoveryApi, IdentityApi } from '@backstage/core';
import { NotFoundError } from '@backstage/errors';
import { TechDocsStorageApi } from '../src/api';
export class TechDocsDevStorageApi implements TechDocsStorageApi {
public configApi: Config;
public discoveryApi: DiscoveryApi;
public identityApi: IdentityApi;
constructor({
configApi,
discoveryApi,
identityApi,
}: {
configApi: Config;
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
}) {
this.configApi = configApi;
this.discoveryApi = discoveryApi;
this.identityApi = identityApi;
}
async getApiOrigin() {
return (
this.configApi.getOptionalString('techdocs.requestUrl') ??
(await this.discoveryApi.getBaseUrl('techdocs'))
);
}
async getStorageUrl() {
return (
this.configApi.getOptionalString('techdocs.storageUrl') ??
`${await this.discoveryApi.getBaseUrl('techdocs')}/static/docs`
);
}
async getBuilder() {
return this.configApi.getString('techdocs.builder');
}
async fetchUrl(url: string) {
const token = await this.identityApi.getIdToken();
return fetch(url, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
}
async getEntityDocs(entityId: EntityName, path: string) {
const { kind, namespace, name } = entityId;
const storageUrl = await this.getStorageUrl();
const url = `${storageUrl}/${namespace}/${kind}/${name}/${path}`;
const token = await this.identityApi.getIdToken();
const request = await fetch(
`${url.endsWith('/') ? url : `${url}/`}index.html`,
{
headers: token ? { Authorization: `Bearer ${token}` } : {},
},
);
let errorMessage = '';
switch (request.status) {
case 404:
errorMessage = 'Page not found. ';
// path is empty for the home page of an entity's docs site
if (!path) {
errorMessage +=
'This could be because there is no index.md file in the root of the docs directory of this repository.';
}
throw new NotFoundError(errorMessage);
case 500:
errorMessage =
'Could not generate documentation or an error in the TechDocs backend. ';
throw new Error(errorMessage);
default:
// Do nothing
break;
}
return request.text();
}
/**
* Check if docs are the latest version and trigger rebuilds if not
*
* @param {EntityName} entityId Object containing entity data like name, namespace, etc.
* @returns {boolean} Whether documents are currently synchronized to newest version
* @throws {Error} Throws error on error from sync endpoint
*/
async syncEntityDocs(entityId: EntityName) {
const { kind, namespace, name } = entityId;
const apiOrigin = await this.getApiOrigin();
const url = `${apiOrigin}/sync/${namespace}/${kind}/${name}`;
let request;
let attempts: number = 0;
// retry if request times out, up to 5 times
// can happen due to docs taking too long to generate
while (!request || (request.status === 408 && attempts < 5)) {
attempts++;
request = await this.fetchUrl(
`${url.endsWith('/') ? url : `${url}/`}index.html`,
);
}
switch (request.status) {
case 404:
throw (await request.json()).error;
case 200:
case 201:
return true;
// for timeout and misc errors, handle without error to allow viewing older docs
// if older docs not available,
// Reader will show 404 error coming from getEntityDocs
case 408:
default:
return false;
}
}
async getBaseUrl(
oldBaseUrl: string,
entityId: EntityName,
path: string,
): Promise<string> {
const { name } = entityId;
const apiOrigin = await this.getApiOrigin();
return new URL(oldBaseUrl, `${apiOrigin}/${name}/${path}`).toString();
}
}
+182 -11
View File
@@ -14,11 +14,105 @@
* limitations under the License.
*/
import { configApiRef, discoveryApiRef, identityApiRef } from '@backstage/core';
import {
configApiRef,
discoveryApiRef,
Header,
identityApiRef,
Page,
TabbedLayout,
} from '@backstage/core';
import { createDevApp } from '@backstage/dev-utils';
import { techdocsPlugin } from '../src/plugin';
import { TechDocsDevStorageApi } from './api';
import { techdocsStorageApiRef } from '../src';
import { NotFoundError } from '@backstage/errors';
import React from 'react';
import {
Reader,
SyncResult,
TechDocsStorageApi,
techdocsStorageApiRef,
} from '../src';
// used so each route can provide it's own implementation in the constructor of the react component
let apiHolder: TechDocsStorageApi | undefined = undefined;
const apiBridge: TechDocsStorageApi = {
getApiOrigin: async () => '',
getBaseUrl: (...args) => apiHolder!.getBaseUrl(...args),
getBuilder: () => apiHolder!.getBuilder(),
getStorageUrl: () => apiHolder!.getStorageUrl(),
getEntityDocs: (...args) => apiHolder!.getEntityDocs(...args),
syncEntityDocs: (...args) => apiHolder!.syncEntityDocs(...args),
};
const mockContent = `
<h1>Hello World!</h1>
<p>This is an example content that will actually be provided by a MkDocs powered site</p>
`;
function createPage({
entityDocs,
syncDocs,
syncDocsDelay,
}: {
entityDocs?: (props: {
called: number;
content: string;
}) => string | Promise<string>;
syncDocs: () => SyncResult;
syncDocsDelay?: number;
}) {
class Api implements TechDocsStorageApi {
private entityDocsCallCount: number = 0;
getApiOrigin = async () => '';
getBaseUrl = async () => '';
getBuilder = async () => 'local';
getStorageUrl = async () => '';
async getEntityDocs() {
await new Promise(resolve => setTimeout(resolve, 500));
if (!entityDocs) {
return mockContent;
}
return entityDocs({
called: this.entityDocsCallCount++,
content: mockContent,
});
}
async syncEntityDocs() {
if (syncDocsDelay) {
await new Promise(resolve => setTimeout(resolve, syncDocsDelay));
}
return syncDocs();
}
}
class Component extends React.Component {
constructor(props: {}) {
super(props);
apiHolder = new Api();
}
render() {
return (
<Reader
entityId={{
kind: 'Component',
namespace: 'default',
name: 'my-docs',
}}
/>
);
}
}
return <Component />;
}
createDevApp()
.registerApi({
@@ -28,12 +122,89 @@ createDevApp()
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, discoveryApi, identityApi }) =>
new TechDocsDevStorageApi({
configApi,
discoveryApi,
identityApi,
}),
factory: () => apiBridge,
})
.addPage({
title: 'TechDocs',
element: (
<Page themeId="home">
<Header title="TechDocs" />
<TabbedLayout>
<TabbedLayout.Route path="/fresh" title="Fresh">
{createPage({
syncDocs: () => 'cached',
})}
</TabbedLayout.Route>
<TabbedLayout.Route path="/stale" title="Stale">
{createPage({
syncDocs: () => 'updated',
syncDocsDelay: 2000,
})}
</TabbedLayout.Route>
<TabbedLayout.Route path="/initial" title="Initial Build">
{createPage({
entityDocs: ({ called, content }) => {
if (called < 1) {
throw new NotFoundError();
}
return content;
},
syncDocs: () => 'updated',
syncDocsDelay: 10000,
})}
</TabbedLayout.Route>
<TabbedLayout.Route path="/not-found" title="Not Found">
{createPage({
entityDocs: () => {
throw new NotFoundError('Not found, some error message...');
},
syncDocs: () => 'cached',
})}
</TabbedLayout.Route>
<TabbedLayout.Route path="/error" title="Error">
{createPage({
entityDocs: () => {
throw new Error('Another more critical error');
},
syncDocs: () => 'cached',
})}
</TabbedLayout.Route>
<TabbedLayout.Route path="/serror" title="Sync Error">
{createPage({
syncDocs: () => {
throw new Error('Some random error');
},
syncDocsDelay: 2000,
})}
</TabbedLayout.Route>
<TabbedLayout.Route path="/berror" title="Both Error">
{createPage({
entityDocs: () => {
throw new Error('Some random error');
},
syncDocs: () => {
throw new Error('Some random error');
},
syncDocsDelay: 2000,
})}
</TabbedLayout.Route>
<TabbedLayout.Route path="/timeout" title="Sync Timeout">
{createPage({
syncDocs: () => 'timeout',
syncDocsDelay: 2000,
})}
</TabbedLayout.Route>
</TabbedLayout>
</Page>
),
})
.registerPlugin(techdocsPlugin)
.render();

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