merge with master

Signed-off-by: Alex Eftimie <alex.eftimie@getyourguide.com>
This commit is contained in:
Alex Eftimie
2024-10-02 14:59:35 +02:00
1676 changed files with 55345 additions and 29472 deletions
+1
View File
@@ -96,6 +96,7 @@ async function main() {
// ...
/* highlight-add-next-line */
apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
}
```
That's it! The Kubernetes frontend and backend have now been added to your
+42 -7
View File
@@ -170,11 +170,14 @@ const highlightOverride = {
## How to render search results using extensions
Extensions for search results let you customize components used to render search result items, It is possible to provide your own search result item extensions or use the ones provided by plugin packages:
Extensions for search results let you customize components used to render search result items, It is possible to provide your own search result item extensions or use the ones provided by plugin packages.
### 1. Providing an extension in your plugin package
Using the example below, you can provide an extension to be used as a default result item:
> Note: You must use the `plugin.provide()` function to make a search item renderer available. Unlike rendering a list in a standard MUI Table or similar, you cannot simply provide
> a rendering function to the `<SearchResult />` component.
Using the example below, you can provide an extension to be used as a search result item:
```tsx title="plugins/your-plugin/src/plugin.ts"
import { createPlugin } from '@backstage/core-plugin-api';
@@ -224,7 +227,7 @@ export const YourSearchResultListItemExtension = plugin.provide(
);
```
Remember to export your new extension:
Remember to export your new extension via your plugin's `index.ts` so that it is available from within your app:
```tsx title="plugins/your-plugin/src/index.ts"
export { YourSearchResultListItem } from './plugin.ts';
@@ -232,9 +235,12 @@ export { YourSearchResultListItem } from './plugin.ts';
For more details, see the [createSearchResultListItemExtension](https://backstage.io/docs/reference/plugin-search-react.createsearchresultlistitemextension) API reference.
### 2. Using an extension in your Backstage app
### 2. Custom search result extension in the SearchPage
Now that you know how a search result item is provided, let's finally see how they can be used, for example, to compose a page in your application:
Once you have exposed your item renderer via the `plugin.provide()` function, you can now override the default search item renderers and tell the `<SearchResult>` component
which renderers to use. Note that the order of the renderers matters! The first one that matches via its predicate function will be used.
Here is an example of customizing your `SearchPage`:
```tsx title="packages/app/src/components/searchPage.tsx"
import React from 'react';
@@ -286,9 +292,38 @@ const SearchPage = () => (
export const searchPage = <SearchPage />;
```
> **Important**: A default result item extension should be placed as the last child, so it can be used only when no other extensions match the result being rendered. If a non-default extension is specified, the `DefaultResultListItem` component will be used.
> **Important**: A default result item extension (one that does not have a predicate) should be placed as the last child, so it can be used only when no other extensions match the result being rendered.
> If a non-default extension is specified, the `DefaultResultListItem` component will be used.
As another example, here's a search modal that renders results with extensions:
### 2. Custom search result extension in the SidebarSearchModal
You may be using the SidebarSearchModal component. In this case, you can customize the search items in this component as follows:
```tsx title="packages/app/src/components/Root/Root.tsx"
import { SidebarSearchModal } from '@backstage/plugin-search';
...
export const Root = ({ children }: PropsWithChildren<{}>) => {
const styles = useStyles();
return <SidebarPage>
<Sidebar>
...
<SidebarSearchModal resultItemComponents={[
/* Provide a custom Extension search item renderer */
<CustomSearchResultListItem icon={<CatalogIcon />} />,
/* Provide an existing search item renderer */
<TechDocsSearchResultListItem icon={<DocsIcon />} />
]} />
...
</Sidebar>
{children}
</SidebarPage>;
};
```
### 3. Custom search result extension in a custom SearchModal
Assuming you have completely customized your SearchModal, here's an example that renders results with extensions:
```tsx title="packages/app/src/components/searchModal.tsx"
import React from 'react';
@@ -31,7 +31,7 @@ catalog (`UrlReaderProcessor`), so no processor configuration is needed. This
processor _does however_ need an [integration](../../integrations/index.md) to
understand how to retrieve a given URL. For the example above, you would need to
configure the [GitHub integration](../../integrations/github/locations.md) to
read files from github.com.
read files from [github.com](https://github.com/).
The locations added through static configuration cannot be removed through the
catalog locations API. To remove these locations, you must remove them from the
@@ -527,6 +527,8 @@ spec:
system: artist-engagement-portal
dependsOn:
- resource:default/artists-db
dependencyOf:
- component:default/artist-web-lookup
providesApis:
- artist-api
```
@@ -636,6 +638,17 @@ field is optional.
| [`Component`](#kind-component) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
| [`Resource`](#kind-resource) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
### `spec.dependencyOf` [optional]
An array of [entity references](references.md#string-references) to the
components and resources that the component is a dependency of, e.g. `artist-web-lookup`.
This field is optional.
| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type |
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- |
| [`Component`](#kind-component) | Same as this entity, typically `default` | [`dependencyOf`, and reverse `dependsOn`](well-known-relations.md#dependson-and-dependencyof) |
| [`Resource`](#kind-resource) | Same as this entity, typically `default` | [`dependencyOf`, and reverse `dependsOn`](well-known-relations.md#dependson-and-dependencyof) |
## Kind: Template
The following describes the following entity kind:
@@ -77,28 +77,30 @@ yarn new --select backend-module --option id=catalog
The class will have this basic structure:
```ts
import { UrlReader } from '@backstage/backend-common';
```ts title="plugins/catalog-backend-module-frobs/src/FrobsProvider.ts"
import { Entity } from '@backstage/catalog-model';
import {
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-node';
import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api';
import {
SchedulerServiceTaskRunner,
UrlReaderService,
} from '@backstage/backend-plugin-api';
/**
* Provides entities from fictional frobs service.
*/
export class FrobsProvider implements EntityProvider {
private readonly env: string;
private readonly reader: UrlReader;
private readonly reader: UrlReaderService;
private connection?: EntityProviderConnection;
private taskRunner: SchedulerServiceTaskRunner;
/** [1] */
constructor(
env: string,
reader: UrlReader,
reader: UrlReaderService,
taskRunner: SchedulerServiceTaskRunner,
) {
this.env = env;
@@ -114,7 +116,7 @@ export class FrobsProvider implements EntityProvider {
/** [3] */
async connect(connection: EntityProviderConnection): Promise<void> {
this.connection = connection;
this.taskRunner.run({
await this.taskRunner.run({
id: this.getProviderName(),
fn: async () => {
await this.run();
@@ -131,7 +133,7 @@ export class FrobsProvider implements EntityProvider {
const response = await this.reader.readUrl(
`https://frobs-${this.env}.example.com/data`,
);
const data = JSON.parse(await response.buffer()).toString();
const data = JSON.parse((await response.buffer()).toString());
/** [5] */
const entities: Entity[] = frobsToEntities(data);
@@ -200,7 +202,7 @@ bucket is accessible.
There are two different types of mutation.
The first is `'full'`, which means to figuratively throw away the contents of
the bucket and replacing it with all of the new contents specified. Under the
the bucket and replacing it with all the new contents specified. Under the
hood, this is actually implemented through a highly efficient delta mechanism
for performance reasons, since it is common that the difference from one run to
the other is actually very small. This strategy is convenient for providers that
@@ -436,66 +438,66 @@ We create a basic entity provider as shown above. In the example below we might
import {
ANNOTATION_LOCATION,
ANNOTATION_ORIGIN_LOCATION,
} from '@backstage/catalog-model'
} from '@backstage/catalog-model';
import {
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-backend'
import { WebClient } from '@slack/web-api'
import {kebabCase} from 'lodash'
} from '@backstage/plugin-catalog-backend';
import { WebClient } from '@slack/web-api';
import { kebabCase } from 'lodash';
interface Staff {
displayName: string
slackUserId: string
jobTitle: string
photoUrl: string
address: string
email:string
displayName: string;
slackUserId: string;
jobTitle: string;
photoUrl: string;
address: string;
email: string;
}
export class UserEntityProvider implements EntityProvider {
private readonly getStaffUrl: string
protected readonly slackTeam: string
protected readonly slackToken: string
protected connection?: EntityProviderConnection
private readonly getStaffUrl: string;
protected readonly slackTeam: string;
protected readonly slackToken: string;
protected connection?: EntityProviderConnection;
static fromConfig(config: Config, options: { logger: Logger }) {
const getStaffUrl = config.getString('staff.url')
const slackToken = config.getString('slack.token')
const slackTeam = config.getString('slack.team')
const getStaffUrl = config.getString('staff.url');
const slackToken = config.getString('slack.token');
const slackTeam = config.getString('slack.team');
return new UserEntityProvider({
...options,
getStaffUrl,
slackToken,
slackTeam,
})
});
}
private constructor(options: {
getStaffUrl: string
slackToken: string
slackTeam: string
getStaffUrl: string;
slackToken: string;
slackTeam: string;
}) {
this.getStaffUrl = options.getStaffUrl
this.slackToken = options.slackToken
this.slackTeam = options.slackTeam
this.getStaffUrl = options.getStaffUrl;
this.slackToken = options.slackToken;
this.slackTeam = options.slackTeam;
}
async getAllStaff(): Promise<Staff[]>{
await return axios.get(this.getStaffUrl)
async getAllStaff(): Promise<Staff[]> {
return await axios.get(this.getStaffUrl);
}
public async connect(connection: EntityProviderConnection): Promise<void> {
this.connection = connection
this.connection = connection;
}
async run(): Promise<void> {
if (!this.connection) {
throw new Error('User Connection Not initialized')
throw new Error('User Connection Not initialized');
}
const userResources: UserEntity[] = []
const staff = await this.getAllStaff()
const userResources: UserEntity[] = [];
const staff = await this.getAllStaff();
for (const user of staff) {
// we can add any links here in this case it would be adding a slack link to the users so you can directly slack them.
@@ -508,7 +510,7 @@ export class UserEntityProvider implements EntityProvider {
icon: 'message',
},
]
: undefined
: undefined;
const userEntity: UserEntity = {
kind: 'User',
apiVersion: 'backstage.io/v1alpha1',
@@ -531,20 +533,20 @@ export class UserEntityProvider implements EntityProvider {
},
memberOf: [],
},
}
};
userResources.push(userEntity)
userResources.push(userEntity);
}
await this.connection.applyMutation({
type: 'full',
entities: userResources.map((entity) => ({
entities: userResources.map(entity => ({
entity,
locationKey: 'hr-user-https://www.hrurl.com/',
})),
})
});
}
}
```
## Custom Processors
@@ -595,7 +597,7 @@ startup. They are at the heart of all catalog logic, and have the ability to
read the contents of locations, modify in-flight entities that were read out of
a location, perform validation, and more. The catalog comes with a set of
builtin processors, that have the ability to read from a list of well known
location types, to perform the basic processing needs, etc, but more can be
location types, to perform the basic processing needs, etc., but more can be
added by the organization that adopts Backstage.
We will now show the process of creating a new processor and location type,
@@ -654,18 +656,18 @@ yarn new --select backend-module --option id=catalog
The class will have this basic structure:
```ts
import { UrlReader } from '@backstage/backend-common';
import {
processingResult,
CatalogProcessor,
CatalogProcessorEmit,
} from '@backstage/plugin-catalog-node';
import { UrlReaderService } from '@backstage/backend-plugin-api';
import { LocationSpec } from '@backstage/plugin-catalog-common';
// A processor that reads from the fictional System-X
export class SystemXReaderProcessor implements CatalogProcessor {
constructor(private readonly reader: UrlReader) {}
constructor(private readonly reader: UrlReaderService) {}
getProcessorName(): string {
return 'SystemXReaderProcessor';
@@ -784,8 +786,8 @@ locations in GitHub. This example aims to demonstrate how to add the same
behavior for `system-x` that we implemented earlier.
```ts
import { UrlReader } from '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { UrlReaderService } from '@backstage/backend-plugin-api';
import {
processingResult,
CatalogProcessor,
@@ -808,7 +810,7 @@ type CacheItem = {
};
export class SystemXReaderProcessor implements CatalogProcessor {
constructor(private readonly reader: UrlReader) {}
constructor(private readonly reader: UrlReaderService) {}
getProcessorName() {
// The processor name must be unique.
@@ -851,7 +853,7 @@ export class SystemXReaderProcessor implements CatalogProcessor {
}
// For this example the JSON payload is a single entity.
const entity: Entity = JSON.parse(response.buffer.toString());
const entity: Entity = JSON.parse((await response.buffer()).toString());
emit(processingResult.entity(location, entity));
// Update the cache with the new ETag and entity used for the next run.
@@ -922,13 +924,13 @@ const makeEntityFromCustomFormatJson = (
[ANNOTATION_LOCATION]: `${location.type}:${location.target}`,
[ANNOTATION_ORIGIN_LOCATION]: `${location.type}:${location.target}`,
},
}
},
spec: {
type: component.type,
owner: component.author,
lifecycle: 'experimental'
}
}
lifecycle: 'experimental',
},
};
};
export const customEntityDataParser: CatalogProcessorParser = async function* ({
@@ -960,9 +962,8 @@ export const customEntityDataParser: CatalogProcessorParser = async function* ({
// Is this a catalog-info.yaml file?
if (json.apiVersion) {
yield processingResult.entity(location, json as Entity);
// let's treat this like it's our custom format instead.
} else {
// let's treat this like it's our custom format instead.
yield processingResult.entity(
location,
makeEntityFromCustomFormatJson(json, location),
@@ -978,7 +979,7 @@ export const customEntityDataParser: CatalogProcessorParser = async function* ({
}
}
}
}
};
```
This is a lot of code right now, as this is a pretty niche use-case, so we don't currently provide many helpers for you to be able to provide custom implementations easier or to compose together different parsers.
@@ -1085,7 +1086,7 @@ interface Service {
}
```
These are the only 3 methods that you need to implement. `getProviderName()` is pretty self explanatory and it's identical to the `getProviderName()` method on a regular Entity Provider.
These are the only 3 methods that you need to implement. `getProviderName()` is pretty self-explanatory and it's identical to the `getProviderName()` method on a regular Entity Provider.
```ts
import { IncrementalEntityProvider } from '@backstage/plugin-catalog-backend-module-incremental-ingestion';
@@ -1178,7 +1179,7 @@ export class MyIncrementalEntityProvider
async next(
context: Context,
cursor?: Cursor = { page: 1 },
cursor: Cursor = { page: 1 },
): Promise<EntityIteratorResult<Cursor>> {
const { apiClient } = context;
+26
View File
@@ -0,0 +1,26 @@
---
id: faq
title: Catalog FAQ
sidebar_label: FAQ
description: This page answers frequently asked questions about the catalog
---
This page answers frequently asked questions about the catalog.
## Is it all that important to have users and groups in the catalog at all?
Yes. One of the most important concepts in the catalog is that it exposes your org structure and ownership properly, allowing your users to effectively understand and communicate around your systems. Having catalog entries for users and groups enables end users to navigate around Backstage and click on those owners and being presented with rich information pages around them, instead of getting 404 Not Found pages.
## Can I create users in the catalog on-demand as they sign in?
When standing up a new Backstage instance, adopters are faced with the realization that the catalog tends to have interactions with sign-in. Therefore the question often comes up, whether it's doable to have users pop up in the catalog on-demand only as they sign in.
This should really be avoided. Our general guidance is to set up a proper integration upfront with your authority for organizational data ([LDAP](../../integrations/ldap/org.md), [Azure](../../integrations/azure/org.md), bespoke HR systems, etc) and batch ingest _all_ users and groups from there into the catalog, whether they sign in or not. This tends to give the superior experience for users, with the smallest amount possible of complexity and frustration.
To give some background, [signing in](../../auth/index.md) technically only requires the `auth` backend which supports the flows that establish who the current user is. At the end of that process, a so called [sign-in resolver](../../auth/identity-resolver.md) is tasked with translating the third party established identity (for example the attributes returned for your AD entry) into a Backstage identity. This important step is made much simpler if the catalog is populated with users and groups from that same third party, because identities line up trivially and you can use the out-of-the-box provided sign-in resolvers for it. As a side note, you can also write your own resolver that does not interact with the catalog at all if you so desire, but let's assume that you do not chose this advanced option.
Doing on-demand user creation _is_ technically possible by writing custom [entity providers](./external-integrations.md). But it comes with significant problems both for technical and end user quality of life reasons.
On the technical side, this is unwanted complexity. You need to implement and maintain a custom provider, instead of what usually amounts to a very easily set-up batch ingestion schedule with providers that come out of the box. Also even if you do this, the catalog is an eventually consistent engine. The user that the provider feeds into the system is not guaranteed to appear immediately. Your experience will likely be only partially functional at bootstrapping time which may have unwanted side effects.
On the user experience side, a Backstage experience without complete organizational data is a serious hindrance to getting the full power out of the tool. Your users won't be able to click on owners and seeing who they are and what teams they belong to. They won't be able to find out what the communications paths are when they need to reach you or your managers when something goes wrong or they have a feature request. They can't get an overview of what teams own and how they relate to each other. It will be a much more barren experience. Organizational data is highly valuable to have centrally available, complete and correct.
@@ -45,13 +45,8 @@ backend.add(
// scaffolder plugin
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
{
/* highlight-add-start */
}
/* highlight-add-next-line */
backend.add(import('@backstage/plugin-scaffolder-backend-module-github'));
{
/* highlight-add-end */
}
backend.start();
```
@@ -87,16 +82,16 @@ allow most templates built for `fetch:cookiecutter` to work without any changes.
```yaml title="template.yaml"
steps:
- id: fetch-base
name: Fetch Base
# highlight-remove-next-line
action: fetch:cookiecutter
# highlight-add-next-line
action: fetch:template
input:
url: ./skeleton
# highlight-add-next-line
cookiecutterCompat: true
values:
name: Fetch Base
# highlight-remove-next-line
action: fetch:cookiecutter
# highlight-add-next-line
action: fetch:template
input:
url: ./skeleton
# highlight-add-next-line
cookiecutterCompat: true
values:
```
### Manual migration
@@ -4,7 +4,7 @@ title: Dry Run Testing
description: How to enable and implement dry run testing in actions
---
Scaffolder templates can be tested using the dry run feature of scaffolder actions. This allows you to simulate the effects of running a scaffolder action without making any actual changes to your environment, for example creating a webhook in Github. Once dry run is enabled in the scaffolder action, you can add handling to actions you use in your scaffolder templates to define how an action should operate in a dry run scenario.
Scaffolder templates can be tested using the dry run feature of scaffolder actions. This allows you to simulate the effects of running a scaffolder action without making any actual changes to your environment, for example creating a webhook in GitHub. Once dry run is enabled in the scaffolder action, you can add handling to actions you use in your scaffolder templates to define how an action should operate in a dry run scenario.
## Enabling dry run testing
+16 -6
View File
@@ -91,19 +91,29 @@ There could be situations where you would like to disable the
![Disable Button](../../assets/software-templates/disable-register-existing-component-button.png)
To do so, you will un-register / remove the `catalogImportPlugin.routes.importPage`
from `backstage/packages/app/src/App.tsx`:
To do so, you need to explicitly disable the default route binding from the `scaffolderPlugin.registerComponent` to the Catalog Import page.
This can be done in `backstage/packages/app/src/App.tsx`:
```diff
const app = createApp({
apis,
bindRoutes({ bind }) {
- bind(scaffolderPlugin.externalRoutes, {
bind(scaffolderPlugin.externalRoutes, {
+ registerComponent: false,
- registerComponent: catalogImportPlugin.routes.importPage,
- });
bind(orgPlugin.externalRoutes, {
catalogIndex: catalogPlugin.routes.catalogIndex,
viewTechDoc: techdocsPlugin.routes.docRoot,
});
})
```
OR in `app-config.yaml`:
```yaml
app:
routes:
bindings:
scaffolder.registerComponent: false
```
After the change, you should no longer see the button.
@@ -44,7 +44,7 @@ It's possible that if you have a hard dependency on any of the `@rjsf/*` librari
/* highlight-remove-next-line */
import { FieldValidation } from '@rjsf/core';
/* highlight-add-next-line */
import { FieldValidation } from '@rjsf/utils;
import { FieldValidation } from '@rjsf/utils';
```
## Escape hatch
@@ -43,6 +43,7 @@ After running the command, the CLI will create a new directory with your new sca
Let's create a simple action that adds a new file and some contents that are passed as `input` to the function. Within the generated directory, locate the file at `src/actions/example/example.ts`. Feel free to rename this file along with its generated unit test. We will replace the existing placeholder code with our custom action code as follows:
```ts title="With Zod"
import { resolveSafeChildPath } from '@backstage/backend-plugin-api';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import fs from 'fs-extra';
import { z } from 'zod';
@@ -62,7 +63,7 @@ export const createNewFileAction = () => {
async handler(ctx) {
await fs.outputFile(
`${ctx.workspacePath}/${ctx.input.filename}`,
resolveSafeChildPath(ctx.workspacePath, ctx.input.filename),
ctx.input.contents,
);
},
@@ -90,6 +91,7 @@ The `createTemplateAction` takes an object which specifies the following:
You can also choose to define your custom action using JSON schema instead of `zod`:
```ts title="With JSON Schema"
import { resolveSafeChildPath } from '@backstage/backend-plugin-api';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { writeFile } from 'fs';
@@ -118,7 +120,7 @@ export const createNewFileAction = () => {
async handler(ctx) {
const { signal } = ctx;
await writeFile(
`${ctx.workspacePath}/${ctx.input.filename}`,
resolveSafeChildPath(ctx.workspacePath, ctx.input.filename),
ctx.input.contents,
{ signal },
_ => {},
@@ -891,7 +891,7 @@ const scaffolderModuleCustomFilters = createBackendModule({
const backend = createBackend();
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
/* highlight-add-next-line */
backend.add(scaffolderModuleCustomFilters());
backend.add(scaffolderModuleCustomFilters);
```
If you still use the legacy backend system, then you will use the `createRouter()` function of the `Scaffolder plugin`
@@ -947,3 +947,19 @@ Have in mind that changes in this form will not be saved on the template and is
### Custom Field Explorer
The custom filed explorer allows you to select any custom field loaded on the backstage instance and test different values and configurations.
## Presentation
You can configure the text of the "Back", "Review", and "Create" buttons using the `spec.presentation` field of your Software Template. You might want have a Template that doesn't "Create" something but rather "Updates" it. This feature will allow you to change it as needed. Here's an example of how to use this:
```yaml
---
spec:
owner: scaffolder/maintainers
type: website
presentation:
buttonLabels:
backButtonText: 'Return'
createButtonText: 'Update'
reviewButtonText: 'Verify'
```
+62
View File
@@ -0,0 +1,62 @@
---
id: extensions
title: Using TechDocs Extensions
sidebar_label: Using TechDocs Extensions
description: How to use the built-in TechDocs extension points
---
# TechDocs Backend Extensions
The TechDocs backend plugin provides the following extension points:
- `techdocsPreparerExtensionPoint`
- Register a custom docs [PreparerBase extension](https://backstage.io/docs/reference/plugin-techdocs-node.preparerbase/)
- Ideal for when you want a custom type of docs created for a specific entity type
- `techdocsBuildsExtensionPoint`
- Allows overriding the build phase Winston log transport (by default does not log to console)
- Allows overriding the [DocsBuildStrategy](https://backstage.io/docs/reference/plugin-techdocs-node.docsbuildstrategy/)
- `techdocsPublisherExtensionPoint`
- Register a custom docs publisher
- `techdocsGeneratorExtensionPoint`
- Register a custom [TechdocsGenerator](https://backstage.io/docs/reference/plugin-techdocs-node.techdocsgenerator/)
Extension points are exported from `@backstage/plugin-techdocs-backend`.
## Examples
### Log TechDocs Build phase details to console
By default, the TechDocs build phase logs to the UI, but does not log to the console. However, the
`techdocsBuildsExtensionPoint` can be used to setup a custom Winston transport for TechDocs build logs.
Here is an example of logging to console:
```typescript jsx title="packages/backend/src/extensions/techDocsExtension.ts"
import { techdocsBuildsExtensionPoint } from '@backstage/plugin-techdocs-backend';
import { createBackendModule } from '@backstage/backend-plugin-api';
import { transports } from 'winston';
export const techDocsExtension = createBackendModule({
pluginId: 'techdocs',
moduleId: 'techdocs-build-log-transport-extension',
register(env) {
env.registerInit({
deps: {
build: techdocsBuildsExtensionPoint,
},
async init({ build }) {
// You can obviously use any custom transport here...
build.setBuildLogTransport(new transports.Console());
},
});
},
});
```
And then of course register this extension with the backend:
```typescript jsx title="packages/backend/src/index.ts"
import {techDocsExtension} from "./extensions/techDocsExtension";
...
backend.add(techDocsExtension);
```
+21 -1
View File
@@ -800,7 +800,7 @@ const techdocsCustomBuildStrategy = createBackendModule({
/* highlight-add-start */
backend.add(import('@backstage/plugin-techdocs-backend/alpha'));
backend.add(techdocsCustomBuildStrategy());
backend.add(techdocsCustomBuildStrategy);
/* highlight-add-end */
backend.start();
@@ -874,3 +874,23 @@ metadata:
TechDocs supports using the [mkdocs-redirects](https://github.com/mkdocs/mkdocs-redirects/tree/master) plugin to create a redirect map for any TechDocs site. This allows broken links from renamed or moved pages in your site to be redirected to their specified replacement.
TechDocs will notify the user that the page they are trying to access is no longer maintained. Then, they will be redirected. External site redirects are not supported. If an external redirect is provided, the user will instead be redirected to the index page of the documentation site.
## Create download links for static assets
You may want to make files available for download by your users such as PDF
documents, images, or code templates. Download links for files included in your
docs directory can be made by adding `{: download }` after a markdown link.
```
[Link text](https://example.com/foo.jpg){: download }
```
The user's browser will download the file as `download.jpg` when the link is
clicked.
Specify a file name to control the name the file will be given when it is
downloaded:
```
[Link text](https://example.com/foo.jpg){: download="foo.jpg" }
```
@@ -120,6 +120,36 @@ techdocs:
Your Backstage app is now ready to use Google Cloud Storage for TechDocs, to
store and read the static generated documentation files.
### Extending default Storage configuration
If you need a non-standard configuration of Google Cloud Storage client,
`TechdocsPublisherExtensionPoint` is something you should look at.
You can register custom `StorageOptions` that will be used to configure the client. To do so, you
need to register publisher settings inside your module init, like in the following example:
```typescript
export const gcsPublisherCustomizer = createBackendModule({
pluginId: 'techdocs',
moduleId: 'gcs-publisher-customizer',
register(reg) {
reg.registerInit({
deps: {
techdocsExtensionPoint: techdocsPublisherExtensionPoint,
},
async init({ techdocsExtensionPoint }) {
const customOptions: StorageOptions = {
userAgent: 'my-custom-user-agent',
};
techdocsExtensionPoint.registerPublisherSettings(
'googleGcs',
customOptions,
);
},
});
},
});
```
## Configuring AWS S3 Bucket with TechDocs
**1. Set `techdocs.publisher.type` config in your `app-config.yaml`**