Clean up of the contrib folder
Signed-off-by: Andre Wanlin <awanlin@spotify.com>
This commit is contained in:
@@ -1,459 +0,0 @@
|
||||
# Authenticate API requests
|
||||
|
||||
> [!CAUTION]
|
||||
> This entire guide MUST NOT BE USED by users of Backstage 1.26 and
|
||||
> newer. If you have applied the changes in this guide, you need to remove them
|
||||
> again as you upgrade to recent versions of Backstage. When [the new auth changes](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution)
|
||||
> landed backends became natively secured through the framework, and the
|
||||
> instructions outlined in here can interfere with the backend functioning
|
||||
> correctly.
|
||||
|
||||
The Backstage backend APIs are by default available without authentication. To avoid evil-doers from accessing or modifying data, one might use a network protection mechanism such as a firewall or an authenticating reverse proxy. For Backstage instances that are available on the Internet one can instead use the experimental IdentityClient as outlined below.
|
||||
|
||||
API requests from frontend plugins include an authorization header with a Backstage identity token acquired when the user logs in. By adding a middleware that verifies said token to be valid and signed by Backstage, non-authenticated requests can be blocked with a 401 Unauthorized response.
|
||||
|
||||
**NOTE**: Enabling this means that Backstage will stop working for guests, as no token is issued for them. If you have not done so already, you will also need to implement [service-to-service auth](https://backstage.io/docs/auth/service-to-service-auth).
|
||||
|
||||
As techdocs HTML pages load assets without an Authorization header the code below also sets a token cookie when the user logs in (and when the token is about to expire).
|
||||
|
||||
## Old Backend System Setup
|
||||
|
||||
Create `packages/backend/src/authMiddleware.ts`:
|
||||
|
||||
```typescript
|
||||
import type { Config } from '@backstage/config';
|
||||
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
|
||||
import { NextFunction, Request, Response, RequestHandler } from 'express';
|
||||
import { decodeJwt } from 'jose';
|
||||
import { URL } from 'url';
|
||||
import { PluginEnvironment } from './types';
|
||||
|
||||
function setTokenCookie(
|
||||
res: Response,
|
||||
options: { token: string; secure: boolean; cookieDomain: string },
|
||||
) {
|
||||
try {
|
||||
const payload = decodeJwt(options.token);
|
||||
res.cookie('token', options.token, {
|
||||
expires: new Date(payload.exp ? payload.exp * 1000 : 0),
|
||||
secure: options.secure,
|
||||
sameSite: 'lax',
|
||||
domain: options.cookieDomain,
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
});
|
||||
} catch (_err) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
export const createAuthMiddleware = async (
|
||||
config: Config,
|
||||
appEnv: PluginEnvironment,
|
||||
) => {
|
||||
const baseUrl = config.getString('backend.baseUrl');
|
||||
const secure = baseUrl.startsWith('https://');
|
||||
const cookieDomain = new URL(baseUrl).hostname;
|
||||
const authMiddleware: RequestHandler = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
try {
|
||||
const token =
|
||||
getBearerTokenFromAuthorizationHeader(req.headers.authorization) ||
|
||||
(req.cookies?.token as string | undefined);
|
||||
if (!token) {
|
||||
res.status(401).send('Unauthorized');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
req.user = await appEnv.identity.getIdentity({ request: req });
|
||||
} catch {
|
||||
await appEnv.tokenManager.authenticate(token);
|
||||
}
|
||||
if (!req.headers.authorization) {
|
||||
// Authorization header may be forwarded by plugin requests
|
||||
req.headers.authorization = `Bearer ${token}`;
|
||||
}
|
||||
if (token && token !== req.cookies?.token) {
|
||||
setTokenCookie(res, {
|
||||
token,
|
||||
secure,
|
||||
cookieDomain,
|
||||
});
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).send('Unauthorized');
|
||||
}
|
||||
};
|
||||
return authMiddleware;
|
||||
};
|
||||
```
|
||||
|
||||
Install cookie-parser:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn --cwd packages/backend add cookie-parser
|
||||
```
|
||||
|
||||
Update routes in `packages/backend/src/index.ts`:
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/index.ts from a create-app deployment
|
||||
|
||||
import { createAuthMiddleware } from './authMiddleware';
|
||||
import cookieParser from 'cookie-parser';
|
||||
|
||||
// ...
|
||||
|
||||
async function main() {
|
||||
// ...
|
||||
|
||||
const authMiddleware = await createAuthMiddleware(config, appEnv);
|
||||
|
||||
const apiRouter = Router();
|
||||
apiRouter.use(cookieParser());
|
||||
// The auth route must be publicly available as it is used during login
|
||||
apiRouter.use('/auth', await auth(authEnv));
|
||||
// Add a simple endpoint to be used when setting a token cookie
|
||||
apiRouter.use('/cookie', authMiddleware, (_req, res) => {
|
||||
res.status(200).send(`Coming right up`);
|
||||
});
|
||||
// Only authenticated requests are allowed to the routes below
|
||||
apiRouter.use('/catalog', authMiddleware, await catalog(catalogEnv));
|
||||
apiRouter.use('/techdocs', authMiddleware, await techdocs(techdocsEnv));
|
||||
apiRouter.use('/proxy', authMiddleware, await proxy(proxyEnv));
|
||||
apiRouter.use(authMiddleware, notFoundHandler());
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## New Backend System Setup
|
||||
|
||||
Create `packages/backend/src/authMiddlewareFactory.ts`:
|
||||
|
||||
```typescript
|
||||
import { HostDiscovery } from '@backstage/backend-app-api';
|
||||
import { ServerTokenManager } from '@backstage/backend-common';
|
||||
import {
|
||||
LoggerService,
|
||||
RootConfigService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
DefaultIdentityClient,
|
||||
getBearerTokenFromAuthorizationHeader,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { NextFunction, Request, RequestHandler, Response } from 'express';
|
||||
import { decodeJwt } from 'jose';
|
||||
import lzstring from 'lz-string';
|
||||
import { URL } from 'url';
|
||||
|
||||
type AuthMiddlewareFactoryOptions = {
|
||||
config: RootConfigService;
|
||||
logger: LoggerService;
|
||||
};
|
||||
|
||||
export const authMiddlewareFactory = ({
|
||||
config,
|
||||
logger,
|
||||
}: AuthMiddlewareFactoryOptions): RequestHandler => {
|
||||
const baseUrl = config.getString('backend.baseUrl');
|
||||
const discovery = HostDiscovery.fromConfig(config);
|
||||
const identity = DefaultIdentityClient.create({ discovery });
|
||||
const tokenManager = ServerTokenManager.fromConfig(config, { logger });
|
||||
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const fullPath = `${req.baseUrl}${req.path}`;
|
||||
|
||||
// Only apply auth to /api routes & skip auth for the following endpoints
|
||||
// Add any additional plugin routes you want to whitelist eg. events
|
||||
const nonAuthWhitelist = ['app', 'auth'];
|
||||
const nonAuthRegex = new RegExp(
|
||||
`^\/api\/(${nonAuthWhitelist.join('|')})(?=\/|$)\S*`,
|
||||
);
|
||||
if (!fullPath.startsWith('/api/') || nonAuthRegex.test(fullPath)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Token cookies are compressed to reduce size
|
||||
const cookieToken = lzstring.decompressFromEncodedURIComponent(
|
||||
req.cookies.token,
|
||||
);
|
||||
const token =
|
||||
getBearerTokenFromAuthorizationHeader(req.headers.authorization) ??
|
||||
cookieToken;
|
||||
|
||||
try {
|
||||
// Attempt to authenticate as a frontend request token
|
||||
await identity.authenticate(token);
|
||||
} catch (err) {
|
||||
// Attempt to authenticate as a backend request token
|
||||
await tokenManager.authenticate(token);
|
||||
}
|
||||
|
||||
if (!req.headers.authorization) {
|
||||
// Authorization header may be forwarded by plugin requests
|
||||
req.headers.authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (token !== cookieToken) {
|
||||
try {
|
||||
const payload = decodeJwt(token);
|
||||
res.cookie('token', token, {
|
||||
// Compress token to reduce cookie size
|
||||
encode: lzstring.compressToEncodedURIComponent,
|
||||
expires: new Date((payload?.exp ?? 0) * 1000),
|
||||
secure: baseUrl.startsWith('https://'),
|
||||
sameSite: 'lax',
|
||||
domain: new URL(baseUrl).hostname,
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
});
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).send(`Unauthorized`);
|
||||
}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Install cookie-parser:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn --cwd packages/backend add cookie-parser @types/cookie-parser
|
||||
```
|
||||
|
||||
Create a custom configured `rootHttpRouterService` in `packages/backend/src/customRootHttpRouterService.ts`:
|
||||
|
||||
```typescript
|
||||
import { rootHttpRouterServiceFactory } from '@backstage/backend-app-api';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import { authMiddlewareFactory } from './authMiddlewareFactory';
|
||||
|
||||
export default rootHttpRouterServiceFactory({
|
||||
configure: ({ app, config, logger, middleware, routes }) => {
|
||||
app.use(middleware.helmet());
|
||||
app.use(middleware.cors());
|
||||
app.use(middleware.compression());
|
||||
app.use(cookieParser());
|
||||
app.use(middleware.logging());
|
||||
|
||||
app.use(authMiddlewareFactory({ config, logger }));
|
||||
|
||||
// Simple handler to set auth cookie for user
|
||||
app.use('/api/cookie', (_, res) => {
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
app.use(routes);
|
||||
|
||||
app.use(middleware.notFound());
|
||||
app.use(middleware.error());
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Update `packages/backend/src/index.ts` to add the custom `rootHttpRouterService` and override the default:
|
||||
|
||||
```typescript
|
||||
// ...
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('./customRootHttpRouterService'));
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
## Frontend Setup
|
||||
|
||||
Create `packages/app/src/cookieAuth.ts`:
|
||||
|
||||
```typescript
|
||||
import type { IdentityApi } from '@backstage/core-plugin-api';
|
||||
|
||||
// Parses supplied JWT token and returns the payload
|
||||
function parseJwt(token: string): { exp: number } {
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map(
|
||||
c =>
|
||||
// eslint-disable-next-line prefer-template
|
||||
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2),
|
||||
)
|
||||
.join(''),
|
||||
);
|
||||
|
||||
return JSON.parse(jsonPayload);
|
||||
}
|
||||
|
||||
// Returns milliseconds until the supplied JWT token expires
|
||||
function msUntilExpiry(token: string): number {
|
||||
const payload = parseJwt(token);
|
||||
const remaining =
|
||||
new Date(payload.exp * 1000).getTime() - new Date().getTime();
|
||||
return remaining;
|
||||
}
|
||||
|
||||
// Calls the specified url regularly using an auth token to set a token cookie
|
||||
// to authorize regular HTTP requests when loading techdocs
|
||||
export async function setTokenCookie(url: string, identityApi: IdentityApi) {
|
||||
const { token } = await identityApi.getCredentials();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch(url, {
|
||||
mode: 'cors',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Call this function again a few minutes before the token expires
|
||||
const ms = msUntilExpiry(token) - 4 * 60 * 1000;
|
||||
setTimeout(
|
||||
() => {
|
||||
setTokenCookie(url, identityApi);
|
||||
},
|
||||
ms > 0 ? ms : 10000,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// required types and packages for example below
|
||||
|
||||
import type { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { discoveryApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
// additional packages/app/src/App.tsx from a create-app deployment
|
||||
|
||||
import { setTokenCookie } from './cookieAuth';
|
||||
|
||||
// ...
|
||||
|
||||
const app = createApp({
|
||||
// ...
|
||||
|
||||
components: {
|
||||
SignInPage: props => {
|
||||
const discoveryApi = useApi(discoveryApiRef);
|
||||
return (
|
||||
<SignInPage
|
||||
{...props}
|
||||
providers={['guest', 'custom', ...providers]}
|
||||
title="Select a sign-in method"
|
||||
align="center"
|
||||
onSignInSuccess={async (identityApi: IdentityApi) => {
|
||||
setTokenCookie(
|
||||
await discoveryApi.getBaseUrl('cookie'),
|
||||
identityApi,
|
||||
);
|
||||
|
||||
props.onSignInSuccess(identityApi);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// ...
|
||||
});
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
**NOTE**: Most Backstage frontend plugins come with the support for the `IdentityApi`.
|
||||
In case you already have a dozen of internal ones, you may need to update those too.
|
||||
Assuming you follow the common plugin structure, the changes to your front-end may look like:
|
||||
|
||||
```diff
|
||||
// plugins/internal-plugin/src/api.ts
|
||||
- import { createApiRef } from '@backstage/core-plugin-api';
|
||||
+ import { createApiRef, IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
// ...
|
||||
|
||||
type MyApiOptions = {
|
||||
configApi: Config;
|
||||
+ identityApi: IdentityApi;
|
||||
// ...
|
||||
}
|
||||
|
||||
interface MyInterface {
|
||||
getData(): Promise<MyData[]>;
|
||||
}
|
||||
|
||||
export class MyApi implements MyInterface {
|
||||
private configApi: Config;
|
||||
+ private identityApi: IdentityApi;
|
||||
// ...
|
||||
|
||||
constructor(options: MyApiOptions) {
|
||||
this.configApi = options.configApi;
|
||||
+ this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
async getMyData() {
|
||||
const backendUrl = this.configApi.getString('backend.baseUrl');
|
||||
|
||||
+ const { token } = await this.identityApi.getCredentials();
|
||||
const requestUrl = `${backendUrl}/api/data/`;
|
||||
- const response = await fetch(requestUrl);
|
||||
+ const response = await fetch(
|
||||
requestUrl,
|
||||
{ headers: { Authorization: `Bearer ${token}` } },
|
||||
);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```diff
|
||||
// plugins/internal-plugin/src/plugin.ts
|
||||
|
||||
import {
|
||||
configApiRef,
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
+ identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { myPluginPageRouteRef } from './routeRefs';
|
||||
import { MyApi, myApiRef } from './api';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'my-plugin',
|
||||
routes: {
|
||||
mainPage: myPluginPageRouteRef,
|
||||
},
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: myApiRef,
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
+ identityApi: identityApiRef,
|
||||
},
|
||||
- factory: ({ configApi }) =>
|
||||
- new MyApi({ configApi }),
|
||||
+ factory: ({ configApi, identityApi }) =>
|
||||
+ new MyApi({ configApi, identityApi }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 252 KiB |
@@ -1,115 +0,0 @@
|
||||
# Prometheus
|
||||
|
||||
> [!NOTE]
|
||||
> The Prometheus metrics have been marked as deprecated and will be removed at a later point. The recommendation is to use the OpenTelemetry metrics by following the [Setup OpenTelemetry](https://backstage.io/docs/tutorials/setup-opentelemetry) documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This is a small tutorial that goes over how to setup your Backstage instance to output metrics in a format that can be pulled in by Prometheus.
|
||||
|
||||
## How to Setup Prometheus Metrics
|
||||
|
||||
1. First we need to add the needed dependencies to the `package.json` in the `\packages\backend`:
|
||||
|
||||
```diff
|
||||
// packages/backend/package.json
|
||||
"dependencies": {
|
||||
+ "express-prom-bundle": "^7.0.0",
|
||||
+ "prom-client": "^15.0.0",
|
||||
```
|
||||
|
||||
2. Now we want to run `yarn install` from the root of the project to get those dependencies in place
|
||||
3. Then we need to add a handler for the metrics by creating a file called `metrics.ts` in the `\packages\backend\src` folder
|
||||
4. Next we add the following content to the `metrics.ts` file:
|
||||
|
||||
```ts
|
||||
// packages/backend/src/metrics.ts
|
||||
import { useHotCleanup } from '@backstage/backend-common';
|
||||
import { RequestHandler } from 'express';
|
||||
import promBundle from 'express-prom-bundle';
|
||||
import prom from 'prom-client';
|
||||
import * as url from 'url';
|
||||
|
||||
const rootRegEx = new RegExp('^/([^/]*)/.*');
|
||||
const apiRegEx = new RegExp('^/api/([^/]*)/.*');
|
||||
|
||||
export function normalizePath(req: any): string {
|
||||
const path = url.parse(req.originalUrl || req.url).pathname || '/';
|
||||
|
||||
// Capture /api/ and the plugin name
|
||||
if (apiRegEx.test(path)) {
|
||||
return path.replace(apiRegEx, '/api/$1');
|
||||
}
|
||||
|
||||
// Only the first path segment at root level
|
||||
return path.replace(rootRegEx, '/$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a /metrics endpoint, register default runtime metrics and instrument the router.
|
||||
*/
|
||||
export function metricsHandler(): RequestHandler {
|
||||
// We can only initialize the metrics once and have to clean them up between hot reloads
|
||||
useHotCleanup(module, () => prom.register.clear());
|
||||
|
||||
return promBundle({
|
||||
includeMethod: true,
|
||||
includePath: true,
|
||||
// Using includePath alone is problematic, as it will include path labels with high
|
||||
// cardinality (e.g. path params). Instead we would have to template them. However, this
|
||||
// is difficult, as every backend plugin might use different routes. Instead we only take
|
||||
// the first directory of the path, to have at least an idea how each plugin performs:
|
||||
normalizePath,
|
||||
promClient: { collectDefaultMetrics: {} },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
5. Now we will extend the router configuration with the `metricsHandler`:
|
||||
|
||||
```diff
|
||||
+import { metricsHandler } from './metrics';
|
||||
|
||||
...
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.loadConfig(config)
|
||||
.addRouter('', await healthcheck(healthcheckEnv))
|
||||
+ .addRouter('', metricsHandler())
|
||||
.addRouter('/api', apiRouter);
|
||||
```
|
||||
|
||||
6. You now have everything setup, from the `\packages\backend` folder run `yarn start` this will start up the backend
|
||||
7. Now in a browser load up `http://localhost:7007/metrics`, if everything went smoothly you should see metrics in your browser something like this:
|
||||
|
||||

|
||||
|
||||
## Metrics
|
||||
|
||||
The following sections goes over the included and experimental metrics available once you have completed this tutorial
|
||||
|
||||
## Included
|
||||
|
||||
This tutorials uses the [`express-prom-bundle`](https://github.com/jochen-schweizer/express-prom-bundle) and the [`prom-client`](https://github.com/siimon/prom-client) to make this all work. They both come with some built in metrics:
|
||||
|
||||
- `express-prom-bundle` comes with 2 metrics:
|
||||
- `up`: this normally will be just 1
|
||||
- `http_request_duration_seconds`: http latency histogram/summary labeled with `status_code`, `method` and `path`
|
||||
- `prom-client` comes with a collection of metrics around memory, CPU, processes, etc. You can see the supported metrics in the `prom-client's` [`lib/metrics`](https://github.com/siimon/prom-client/tree/master/lib/metrics) folder.
|
||||
|
||||
### Experimental
|
||||
|
||||
There are some custom metrics that have been added to Backstage will be output for you, these are currently deemed experimental and may be changed or removed in a future release. Here is a rough list, again subject to changes:
|
||||
|
||||
- `catalog_entities_count`: Total amount of entities in the catalog
|
||||
- `catalog_registered_locations_count`: Total amount of registered locations in the catalog
|
||||
- `catalog_relations_count`: Total amount of relations between entities
|
||||
- `catalog_stitched_entities_count`: Amount of entities stitched
|
||||
- `catalog_processed_entities_count`: Amount of entities processed
|
||||
- `catalog_processing_duration_seconds`: Time spent executing the full processing flow
|
||||
- `catalog_processors_duration_seconds`: Time spent executing catalog processors
|
||||
- `catalog_processing_queue_delay_seconds`: The amount of delay between being scheduled for processing, and the start of actually being processed
|
||||
- `scaffolder_task_count`: Tracks successful task runs.
|
||||
- `scaffolder_task_duration`: a histogram which tracks the duration of a task run
|
||||
- `scaffolder_step_count`: a count that tracks each step run
|
||||
- `scaffolder_step_duration`: a histogram which tracks the duration of each step run
|
||||
@@ -1,53 +0,0 @@
|
||||
### Source repo: https://github.com/johnson-jesse/simple-backstage-app-plugin
|
||||
|
||||
ExampleComponent.tsx reference
|
||||
|
||||
```tsx
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
InfoCard,
|
||||
Header,
|
||||
Page,
|
||||
Content,
|
||||
ContentHeader,
|
||||
HeaderLabel,
|
||||
SupportButton,
|
||||
} from '@backstage/core-components';
|
||||
import { ExampleFetchComponent } from '../ExampleFetchComponent';
|
||||
|
||||
export const ExampleComponent = () => {
|
||||
const identityApi = useApi(identityApiRef);
|
||||
const userId = identityApi.getUserId();
|
||||
const profile = identityApi.getProfile();
|
||||
|
||||
return (
|
||||
<Page themeId="tool">
|
||||
<Header
|
||||
title="Welcome to github-playground!"
|
||||
subtitle="Optional subtitle"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Plugin title">
|
||||
<SupportButton>A description of your plugin goes here.</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<InfoCard title={userId}>
|
||||
<Typography variant="body1">
|
||||
{`${profile.displayName} | ${profile.email}`}
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ExampleFetchComponent />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
```
|
||||
@@ -1,103 +0,0 @@
|
||||
### Source repo: https://github.com/johnson-jesse/simple-backstage-app-plugin
|
||||
|
||||
ExampleFetchComponent.tsx reference
|
||||
|
||||
```tsx
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import { githubAuthApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { Table, TableColumn, Progress } from '@backstage/core-components';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
|
||||
const query = `{
|
||||
viewer {
|
||||
repositories(first: 100) {
|
||||
totalCount
|
||||
nodes {
|
||||
name
|
||||
createdAt
|
||||
description
|
||||
diskUsage
|
||||
isFork
|
||||
}
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
type Node = {
|
||||
name: string;
|
||||
createdAt: string;
|
||||
description: string;
|
||||
diskUsage: number;
|
||||
isFork: boolean;
|
||||
};
|
||||
|
||||
type Viewer = {
|
||||
repositories: {
|
||||
totalCount: number;
|
||||
nodes: Node[];
|
||||
pageInfo: {
|
||||
endCursor: string;
|
||||
hasNextPage: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type DenseTableProps = {
|
||||
viewer: Viewer;
|
||||
};
|
||||
|
||||
export const DenseTable = ({ viewer }: DenseTableProps) => {
|
||||
const columns: TableColumn[] = [
|
||||
{ title: 'Name', field: 'name' },
|
||||
{ title: 'Created', field: 'createdAt' },
|
||||
{ title: 'Description', field: 'description' },
|
||||
{ title: 'Disk Usage', field: 'diskUsage' },
|
||||
{ title: 'Fork', field: 'isFork' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
title="List Of User's Repositories"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={columns}
|
||||
data={viewer.repositories.nodes}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExampleFetchComponent = () => {
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
|
||||
const { value, loading, error } = useAsync(async (): Promise<any> => {
|
||||
const token = await auth.getAccessToken();
|
||||
|
||||
const gqlEndpoint = graphql.defaults({
|
||||
// Uncomment baseUrl if using enterprise
|
||||
// baseUrl: 'https://github.MY-BIZ.com/api',
|
||||
headers: {
|
||||
authorization: `token ${token}`,
|
||||
},
|
||||
});
|
||||
const { viewer } = await gqlEndpoint(query);
|
||||
return viewer;
|
||||
}, []);
|
||||
|
||||
if (loading) return <Progress />;
|
||||
if (error) return <Alert severity="error">{error.message}</Alert>;
|
||||
if (value && value.repositories) return <DenseTable viewer={value} />;
|
||||
|
||||
return (
|
||||
<Table
|
||||
title="List Of User's Repositories"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={[]}
|
||||
data={[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
@@ -1,3 +1,6 @@
|
||||
# Basic Kubernetes example with Helm
|
||||
|
||||
> [!NOTE]
|
||||
> This documentation is deprecated and will be removed at a future date, please use the well maintained [Backstage Helm Charts](https://github.com/backstage/charts) for this.
|
||||
|
||||
Note that these examples aim to show a minimal setup and do not include best practices for secure Kubernetes deployments. See the [Kubernetes documentation](https://kubernetes.io/docs/concepts/security/) for more information, or resources provided by your own organization.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Confluence
|
||||
|
||||
> [!NOTE]
|
||||
> This documentation is deprecated and will be removed at a future date, please use the well maintained [`@backstage-community/plugin-search-backend-module-confluence-collator` Community Plugin](https://github.com/backstage/community-plugins/tree/main/workspaces/confluence/plugins/search-backend-module-confluence-collator) for this.
|
||||
|
||||
These files help you add Confluence as a source to the Backstage Search plugin.
|
||||
To do so, add both files in this directory under the packages/backend/src/plugins/search/ pathway in your Backstage app.
|
||||
Then, add the following code to your packages/app/src/components/search/SearchPage.tsx:
|
||||
|
||||
Reference in New Issue
Block a user