@@ -100,7 +100,7 @@ const app = createApp({
|
||||
},
|
||||
/* highlight-add-end */
|
||||
// ..
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page to also work smoothly for local development.
|
||||
|
||||
@@ -104,13 +104,13 @@ installed in `packages/app/src/App.tsx` like this:
|
||||
import { ProxiedSignInPage } from '@backstage/core-components';
|
||||
|
||||
const app = createApp({
|
||||
/* highlight-add-start */
|
||||
/* highlight-add-start */
|
||||
components: {
|
||||
SignInPage: props => <ProxiedSignInPage {...props} provider="gcp-iap" />,
|
||||
},
|
||||
/* highlight-add-end */
|
||||
// ..
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
See the [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) section for more information.
|
||||
|
||||
+10
-7
@@ -121,12 +121,15 @@ const app = createApp({
|
||||
SignInPage: props => (
|
||||
<SignInPage
|
||||
{...props}
|
||||
providers={['guest', {
|
||||
id: 'github-auth-provider',
|
||||
title: 'GitHub',
|
||||
message: 'Sign in using GitHub',
|
||||
apiRef: githubAuthApiRef,
|
||||
}]}
|
||||
providers={[
|
||||
'guest',
|
||||
{
|
||||
id: 'github-auth-provider',
|
||||
title: 'GitHub',
|
||||
message: 'Sign in using GitHub',
|
||||
apiRef: githubAuthApiRef,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -314,7 +317,7 @@ import {
|
||||
export const apis: AnyApiFactory[] = [
|
||||
/* highlight-add-next-line */
|
||||
ScmAuth.createDefaultApiFactory(),
|
||||
// ...
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
|
||||
@@ -67,11 +67,13 @@ import { ProxiedSignInPage } from '@backstage/core-components';
|
||||
const app = createApp({
|
||||
/* highlight-add-start */
|
||||
components: {
|
||||
SignInPage: props => <ProxiedSignInPage {...props} provider="azure-easyauth" />,
|
||||
SignInPage: props => (
|
||||
<ProxiedSignInPage {...props} provider="azure-easyauth" />
|
||||
),
|
||||
},
|
||||
/* highlight-add-end */
|
||||
// ..
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
See the [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) section for more information.
|
||||
|
||||
@@ -70,11 +70,13 @@ import { ProxiedSignInPage } from '@backstage/core-components';
|
||||
const app = createApp({
|
||||
/* highlight-add-start */
|
||||
components: {
|
||||
SignInPage: props => <ProxiedSignInPage {...props} provider="oauth2Proxy" />,
|
||||
SignInPage: props => (
|
||||
<ProxiedSignInPage {...props} provider="oauth2Proxy" />
|
||||
),
|
||||
},
|
||||
/* highlight-add-end */
|
||||
// ..
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page to also work smoothly for local development.
|
||||
|
||||
+14
-19
@@ -89,28 +89,23 @@ export const apis: AnyApiFactory[] = [
|
||||
discoveryApi: discoveryApiRef,
|
||||
oauthRequestApi: oauthRequestApiRef,
|
||||
configApi: configApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
|
||||
OAuth2.create({
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
provider: {
|
||||
id: 'my-auth-provider',
|
||||
title: 'My custom auth provider',
|
||||
icon: () => null,
|
||||
},
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
defaultScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
],
|
||||
}),
|
||||
},
|
||||
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
|
||||
OAuth2.create({
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
provider: {
|
||||
id: 'my-auth-provider',
|
||||
title: 'My custom auth provider',
|
||||
icon: () => null,
|
||||
},
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
defaultScopes: ['openid', 'profile', 'email'],
|
||||
}),
|
||||
}),
|
||||
/* highlight-add-end */
|
||||
// ..
|
||||
]
|
||||
|
||||
];
|
||||
```
|
||||
|
||||
Please note we're importing the `OAuth2` class from `@backstage/core-app-api` effectively
|
||||
|
||||
@@ -134,7 +134,10 @@ import { createBackend } from '@backstage/backend-defaults';
|
||||
/* highlight-remove-next-line */
|
||||
import { legacyPlugin } from '@backstage/backend-common';
|
||||
/* highlight-add-start */
|
||||
import { makeLegacyPlugin, loggerToWinstonLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
makeLegacyPlugin,
|
||||
loggerToWinstonLogger,
|
||||
} from '@backstage/backend-common';
|
||||
import { coreServices } from '@backstage/backend-plugin-api';
|
||||
/* highlight-add-end */
|
||||
|
||||
@@ -187,16 +190,16 @@ In this example, we'll assume that your added environment field is named
|
||||
/* highlight-add-next-line */
|
||||
import { exampleServiceRef } from '<somewhere>'; // if the definition is elsewhere
|
||||
|
||||
const legacyPlugin = makeLegacyPlugin(
|
||||
{
|
||||
// ... the above core services still go here
|
||||
/* highlight-add-next-line */
|
||||
example: exampleServiceRef
|
||||
},
|
||||
{
|
||||
logger: log => loggerToWinstonLogger(log),
|
||||
},
|
||||
);
|
||||
const legacyPlugin = makeLegacyPlugin(
|
||||
{
|
||||
// ... the above core services still go here
|
||||
/* highlight-add-next-line */
|
||||
example: exampleServiceRef,
|
||||
},
|
||||
{
|
||||
logger: log => loggerToWinstonLogger(log),
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
After this, your backend will know how to instantiate your thing on demand and
|
||||
@@ -282,7 +285,7 @@ import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
|
||||
/* highlight-add-start */
|
||||
const catalogModuleCustomExtensions = createBackendModule({
|
||||
pluginId: 'catalog', // name of the plugin that the module is targeting
|
||||
pluginId: 'catalog', // name of the plugin that the module is targeting
|
||||
moduleId: 'customExtensions',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
@@ -294,7 +297,7 @@ const catalogModuleCustomExtensions = createBackendModule({
|
||||
// Here you have the opportunity to interact with the extension
|
||||
// point before the plugin itself gets instantiated
|
||||
catalog.addEntityProvider(new MyEntityProvider()); // just an example
|
||||
catalog.addProcessor(new MyProcessor()); // just an example
|
||||
catalog.addProcessor(new MyProcessor()); // just an example
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -352,7 +355,7 @@ import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
|
||||
/* highlight-add-start */
|
||||
const eventsModuleCustomExtensions = createBackendModule({
|
||||
pluginId: 'events', // name of the plugin that the module is targeting
|
||||
pluginId: 'events', // name of the plugin that the module is targeting
|
||||
moduleId: 'customExtensions',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
@@ -420,7 +423,7 @@ import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
|
||||
/* highlight-add-start */
|
||||
const scaffolderModuleCustomExtensions = createBackendModule({
|
||||
pluginId: 'scaffolder', // name of the plugin that the module is targeting
|
||||
pluginId: 'scaffolder', // name of the plugin that the module is targeting
|
||||
moduleId: 'customExtensions',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
|
||||
@@ -38,7 +38,7 @@ const serviceEntityPage = (
|
||||
</EntityLayout.Route>
|
||||
{/* highlight-add-end */}
|
||||
</EntityLayout>
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
|
||||
@@ -20,30 +20,30 @@ to do that in two steps.
|
||||
[interface](https://github.com/backstage/backstage/blob/db2666b980853c281b8fe77905d7639c5d255f13/plugins/search/src/apis.ts#L31)
|
||||
according to your needs.
|
||||
|
||||
```typescript
|
||||
export class SearchClient implements SearchApi {
|
||||
// your implementation
|
||||
}
|
||||
```
|
||||
```typescript
|
||||
export class SearchClient implements SearchApi {
|
||||
// your implementation
|
||||
}
|
||||
```
|
||||
|
||||
2. Override the API ref `searchApiRef` with your new implemented API in the
|
||||
`App.tsx` using `ApiFactories`.
|
||||
[Read more about App APIs](https://backstage.io/docs/api/utility-apis#app-apis).
|
||||
|
||||
```typescript
|
||||
const app = createApp({
|
||||
apis: [
|
||||
// SearchApi
|
||||
createApiFactory({
|
||||
api: searchApiRef,
|
||||
deps: { discovery: discoveryApiRef },
|
||||
factory({ discovery }) {
|
||||
return new SearchClient({ discoveryApi: discovery });
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
```typescript
|
||||
const app = createApp({
|
||||
apis: [
|
||||
// SearchApi
|
||||
createApiFactory({
|
||||
api: searchApiRef,
|
||||
deps: { discovery: discoveryApiRef },
|
||||
factory({ discovery }) {
|
||||
return new SearchClient({ discoveryApi: discovery });
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## How to index TechDocs documents
|
||||
|
||||
@@ -63,35 +63,35 @@ getting started guide.
|
||||
1. Import the `DefaultTechDocsCollatorFactory` from
|
||||
`@backstage/plugin-techdocs-backend`.
|
||||
|
||||
```typescript
|
||||
import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
|
||||
```
|
||||
```typescript
|
||||
import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
|
||||
```
|
||||
|
||||
2. If there isn't an existing schedule you'd like to run the collator on, be
|
||||
sure to create it first. Something like...
|
||||
|
||||
```typescript
|
||||
import { Duration } from 'luxon';
|
||||
```typescript
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({
|
||||
frequency: Duration.fromObject({ seconds: 600 }),
|
||||
timeout: Duration.fromObject({ seconds: 900 }),
|
||||
initialDelay: Duration.fromObject({ seconds: 3 }),
|
||||
});
|
||||
```
|
||||
const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({
|
||||
frequency: Duration.fromObject({ seconds: 600 }),
|
||||
timeout: Duration.fromObject({ seconds: 900 }),
|
||||
initialDelay: Duration.fromObject({ seconds: 3 }),
|
||||
});
|
||||
```
|
||||
|
||||
3. Register the `DefaultTechDocsCollatorFactory` with the IndexBuilder.
|
||||
|
||||
```typescript
|
||||
indexBuilder.addCollator({
|
||||
schedule: every10MinutesSchedule,
|
||||
factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, {
|
||||
discovery: env.discovery,
|
||||
logger: env.logger,
|
||||
tokenManager: env.tokenManager,
|
||||
}),
|
||||
});
|
||||
```
|
||||
```typescript
|
||||
indexBuilder.addCollator({
|
||||
schedule: every10MinutesSchedule,
|
||||
factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, {
|
||||
discovery: env.discovery,
|
||||
logger: env.logger,
|
||||
tokenManager: env.tokenManager,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
You should now have your TechDocs documents indexed to your search engine of
|
||||
choice!
|
||||
@@ -125,7 +125,9 @@ You can either just simply amend default behaviour, or even to write completely
|
||||
> `authorization` and `location` cannot be modified via a `entityTransformer`, `location` can be modified only through `locationTemplate`.
|
||||
|
||||
```ts title="packages/backend/src/plugins/search.ts"
|
||||
const entityTransformer: CatalogCollatorEntityTransformer = (entity: Entity) => {
|
||||
const entityTransformer: CatalogCollatorEntityTransformer = (
|
||||
entity: Entity,
|
||||
) => {
|
||||
if (entity.kind === 'SomeKind') {
|
||||
return {
|
||||
// customize here output for 'SomeKind' kind
|
||||
@@ -173,7 +175,7 @@ indexBuilder.addCollator({
|
||||
tokenManager: env.tokenManager,
|
||||
/* highlight-add-start */
|
||||
filter: {
|
||||
kind: ['API', 'Component', 'Domain', 'Group', 'System', 'User'],
|
||||
kind: ['API', 'Component', 'Domain', 'Group', 'System', 'User'],
|
||||
},
|
||||
/* highlight-add-end */
|
||||
}),
|
||||
|
||||
@@ -254,7 +254,9 @@ export default async function createPlugin(
|
||||
/* highlight-add-start */
|
||||
await env.scheduler.scheduleTask({
|
||||
id: 'run_frobs_refresh',
|
||||
fn: async () => { await frobs.run(); },
|
||||
fn: async () => {
|
||||
await frobs.run();
|
||||
},
|
||||
frequency: { minutes: 30 },
|
||||
timeout: { minutes: 10 },
|
||||
});
|
||||
|
||||
@@ -46,7 +46,6 @@ The `ScaffolderPage` router has a completely different export for the `scaffolde
|
||||
import { ScaffolderPage } from '@backstage/plugin-scaffolder';
|
||||
/* highlight-add-next-line */
|
||||
import { NextScaffolderPage } from '@backstage/plugin-scaffolder/alpha';
|
||||
|
||||
```
|
||||
|
||||
And this API should be the exact same as the previous Router, so you should be able to make a change like the following further down in this file:
|
||||
@@ -136,7 +135,7 @@ export const EntityNamePicker = (
|
||||
placeholder,
|
||||
} = props;
|
||||
// ..
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
There's another `/alpha` export that you need to replace `FieldExtensionComponentProps` with which is the `NextFieldExtensionComponentProps`.
|
||||
@@ -167,7 +166,7 @@ export const EntityNamePicker = (
|
||||
placeholder,
|
||||
} = props;
|
||||
// ..
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
You'll notice that there's an additional change here, which is that we're now defaulting the `uiSchema` to an empty object. This is because the `uiSchema` is now optional, and if you don't provide it, it will be `undefined` instead of an empty object. There's more around this in the [breaking changes](#breaking-changes) section.
|
||||
@@ -218,20 +217,20 @@ Once we fully release the code that is in the `/alpha` exports right now onto th
|
||||
Later releases of `react-jsonschema-form` have made the `uiSchema` optional, and if you don't provide it, it will be `undefined` instead of an empty object. This means that you will need to make sure that you're defaulting the `uiSchema` to an empty object if you're using it in your code.
|
||||
|
||||
```tsx
|
||||
const {
|
||||
onChange,
|
||||
required,
|
||||
schema: { title = 'Name', description = 'Unique name of the component' },
|
||||
rawErrors,
|
||||
formData,
|
||||
/* highlight-remove-next-line */
|
||||
uiSchema: { 'ui:autofocus': autoFocus },
|
||||
/* highlight-add-next-line */
|
||||
uiSchema: { 'ui:autofocus': autoFocus } = {},
|
||||
idSchema,
|
||||
placeholder,
|
||||
} = props;
|
||||
// ..
|
||||
const {
|
||||
onChange,
|
||||
required,
|
||||
schema: { title = 'Name', description = 'Unique name of the component' },
|
||||
rawErrors,
|
||||
formData,
|
||||
/* highlight-remove-next-line */
|
||||
uiSchema: { 'ui:autofocus': autoFocus },
|
||||
/* highlight-add-next-line */
|
||||
uiSchema: { 'ui:autofocus': autoFocus } = {},
|
||||
idSchema,
|
||||
placeholder,
|
||||
} = props;
|
||||
// ..
|
||||
```
|
||||
|
||||
### `formData` can also be `undefined`
|
||||
@@ -239,18 +238,18 @@ Later releases of `react-jsonschema-form` have made the `uiSchema` optional, and
|
||||
If you were using the `formData` and assuming that it was set to an empty object when building `Field Extensions` that return objects, then this will be `undefined` now due to a change in the `react-jsonschema-form` library.
|
||||
|
||||
```tsx
|
||||
const {
|
||||
onChange,
|
||||
required,
|
||||
schema: { title = 'Name', description = 'Unique name of the component' },
|
||||
rawErrors,
|
||||
/* highlight-remove-next-line */
|
||||
formData,
|
||||
/* highlight-add-next-line */
|
||||
formData = {}, // or maybe some other default value that you would prefer
|
||||
uiSchema: { 'ui:autofocus': autoFocus } = {},
|
||||
idSchema,
|
||||
placeholder,
|
||||
} = props;
|
||||
// ..
|
||||
const {
|
||||
onChange,
|
||||
required,
|
||||
schema: { title = 'Name', description = 'Unique name of the component' },
|
||||
rawErrors,
|
||||
/* highlight-remove-next-line */
|
||||
formData,
|
||||
/* highlight-add-next-line */
|
||||
formData = {}, // or maybe some other default value that you would prefer
|
||||
uiSchema: { 'ui:autofocus': autoFocus } = {},
|
||||
idSchema,
|
||||
placeholder,
|
||||
} = props;
|
||||
// ..
|
||||
```
|
||||
|
||||
@@ -346,19 +346,19 @@ You can add more icons, if the [default icons](https://github.com/backstage/back
|
||||
2. Then you want to import your icon, add this to the rest of your imports: `import AlarmIcon from '@material-ui/icons/Alarm';`
|
||||
3. Next you want to add the icon like this to your `createApp`:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
const app = createApp({
|
||||
apis: ...,
|
||||
plugins: ...,
|
||||
/* highlight-add-start */
|
||||
icons: {
|
||||
alert: AlarmIcon,
|
||||
},
|
||||
/* highlight-add-end */
|
||||
themes: ...,
|
||||
components: ...,
|
||||
});
|
||||
```
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
const app = createApp({
|
||||
apis: ...,
|
||||
plugins: ...,
|
||||
/* highlight-add-start */
|
||||
icons: {
|
||||
alert: AlarmIcon,
|
||||
},
|
||||
/* highlight-add-end */
|
||||
themes: ...,
|
||||
components: ...,
|
||||
});
|
||||
```
|
||||
|
||||
4. Now we can reference `alert` for our icon in our entity links like this:
|
||||
|
||||
@@ -413,71 +413,71 @@ For this example we'll show you how you can expand the sidebar with a sub-menu:
|
||||
|
||||
3. Then update the `@backstage/core-components` import like this:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Sidebar,
|
||||
sidebarConfig,
|
||||
SidebarDivider,
|
||||
SidebarGroup,
|
||||
SidebarItem,
|
||||
SidebarPage,
|
||||
SidebarScrollWrapper,
|
||||
SidebarSpace,
|
||||
useSidebarOpenState,
|
||||
Link,
|
||||
/* highlight-add-start */
|
||||
GroupIcon,
|
||||
SidebarSubmenu,
|
||||
SidebarSubmenuItem,
|
||||
/* highlight-add-end */
|
||||
} from '@backstage/core-components';
|
||||
```
|
||||
```tsx
|
||||
import {
|
||||
Sidebar,
|
||||
sidebarConfig,
|
||||
SidebarDivider,
|
||||
SidebarGroup,
|
||||
SidebarItem,
|
||||
SidebarPage,
|
||||
SidebarScrollWrapper,
|
||||
SidebarSpace,
|
||||
useSidebarOpenState,
|
||||
Link,
|
||||
/* highlight-add-start */
|
||||
GroupIcon,
|
||||
SidebarSubmenu,
|
||||
SidebarSubmenuItem,
|
||||
/* highlight-add-end */
|
||||
} from '@backstage/core-components';
|
||||
```
|
||||
|
||||
4. Finally replace `<SidebarItem icon={HomeIcon} to="catalog" text="Home" />` with this:
|
||||
|
||||
```tsx
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home">
|
||||
<SidebarSubmenu title="Catalog">
|
||||
<SidebarSubmenuItem
|
||||
title="Domains"
|
||||
to="catalog?filters[kind]=domain"
|
||||
icon={DomainIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Systems"
|
||||
to="catalog?filters[kind]=system"
|
||||
icon={SystemIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Components"
|
||||
to="catalog?filters[kind]=component"
|
||||
icon={ComponentIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="APIs"
|
||||
to="catalog?filters[kind]=api"
|
||||
icon={ApiIcon}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
<SidebarSubmenuItem
|
||||
title="Resources"
|
||||
to="catalog?filters[kind]=resource"
|
||||
icon={ResourceIcon}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
<SidebarSubmenuItem
|
||||
title="Groups"
|
||||
to="catalog?filters[kind]=group"
|
||||
icon={GroupIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Users"
|
||||
to="catalog?filters[kind]=user"
|
||||
icon={UserIcon}
|
||||
/>
|
||||
</SidebarSubmenu>
|
||||
</SidebarItem>
|
||||
```
|
||||
```tsx
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home">
|
||||
<SidebarSubmenu title="Catalog">
|
||||
<SidebarSubmenuItem
|
||||
title="Domains"
|
||||
to="catalog?filters[kind]=domain"
|
||||
icon={DomainIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Systems"
|
||||
to="catalog?filters[kind]=system"
|
||||
icon={SystemIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Components"
|
||||
to="catalog?filters[kind]=component"
|
||||
icon={ComponentIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="APIs"
|
||||
to="catalog?filters[kind]=api"
|
||||
icon={ApiIcon}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
<SidebarSubmenuItem
|
||||
title="Resources"
|
||||
to="catalog?filters[kind]=resource"
|
||||
icon={ResourceIcon}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
<SidebarSubmenuItem
|
||||
title="Groups"
|
||||
to="catalog?filters[kind]=group"
|
||||
icon={GroupIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Users"
|
||||
to="catalog?filters[kind]=user"
|
||||
icon={UserIcon}
|
||||
/>
|
||||
</SidebarSubmenu>
|
||||
</SidebarItem>
|
||||
```
|
||||
|
||||
When you startup your Backstage app and hover over the Home option on the sidebar you'll now see a nice sub-menu appear with links to the various Kinds in your Catalog. It would look like this:
|
||||
|
||||
|
||||
@@ -33,25 +33,25 @@ to an entity in the software catalog.
|
||||
|
||||
2. Add the `EntityCircleCIContent` extension to the entity pages in the app:
|
||||
|
||||
```tsx title="packages/app/src/components/catalog/EntityPage.tsx"
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
EntityCircleCIContent,
|
||||
isCircleCIAvailable,
|
||||
} from '@backstage/plugin-circleci';
|
||||
/* highlight-add-end */
|
||||
```tsx title="packages/app/src/components/catalog/EntityPage.tsx"
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
EntityCircleCIContent,
|
||||
isCircleCIAvailable,
|
||||
} from '@backstage/plugin-circleci';
|
||||
/* highlight-add-end */
|
||||
|
||||
const cicdContent = (
|
||||
<EntitySwitch>
|
||||
{/* ... */}
|
||||
{/* highlight-add-next-line */}
|
||||
<EntitySwitch.Case if={isCircleCIAvailable}>
|
||||
<EntityCircleCIContent />
|
||||
</EntitySwitch.Case>;
|
||||
{/* highlight-add-end */}
|
||||
</EntitySwitch>
|
||||
);
|
||||
```
|
||||
const cicdContent = (
|
||||
<EntitySwitch>
|
||||
{/* ... */}
|
||||
{/* highlight-add-next-line */}
|
||||
<EntitySwitch.Case if={isCircleCIAvailable}>
|
||||
<EntityCircleCIContent />
|
||||
</EntitySwitch.Case>
|
||||
;{/* highlight-add-end */}
|
||||
</EntitySwitch>
|
||||
);
|
||||
```
|
||||
|
||||
This is just one example, but each Backstage instance may integrate content or
|
||||
cards to suit their needs on different pages, tabs, etc. In addition, while some
|
||||
|
||||
@@ -58,7 +58,7 @@ const routes = (
|
||||
<Navigate key="/" to="catalog" />
|
||||
{/* ... */}
|
||||
</FlatRoutes>
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
Let's replace the `<Navigate>` line and use the new component we created in the previous step as the new homepage.
|
||||
@@ -80,7 +80,7 @@ const routes = (
|
||||
{/* highlight-add-end */}
|
||||
{/* ... */}
|
||||
</FlatRoutes>
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
#### 4. Update sidebar items
|
||||
@@ -122,7 +122,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
|
||||
</SidebarGroup>
|
||||
</Sidebar>
|
||||
</SidebarPage>
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
That's it! You should now have _(although slightly boring)_ a homepage!
|
||||
|
||||
@@ -128,7 +128,11 @@ export default async function createPlugin(
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/* highlight-add-next-line */
|
||||
builder.addProcessor(AzureDevOpsDiscoveryProcessor.fromConfig(env.config, { logger: env.logger }));
|
||||
builder.addProcessor(
|
||||
AzureDevOpsDiscoveryProcessor.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
}),
|
||||
);
|
||||
|
||||
// ..
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ For large organizations, this plugin can take a long time, so be careful setting
|
||||
import { MicrosoftGraphOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-msgraph';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
|
||||
@@ -55,7 +55,7 @@ export default async function createPlugin(
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { hours: 1 },
|
||||
timeout: { minutes: 50 },
|
||||
initialDelay: { seconds: 15}
|
||||
initialDelay: { seconds: 15 },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -105,8 +105,8 @@ export default async function createCatalogEventBasedProviders(
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
return providers.flat();
|
||||
}
|
||||
return providers.flat();
|
||||
}
|
||||
```
|
||||
|
||||
**Attention:**
|
||||
|
||||
@@ -44,7 +44,7 @@ export default async function createPlugin(
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 30 },
|
||||
timeout: { minutes: 3 },
|
||||
}),
|
||||
}),
|
||||
// optional: alternatively, use schedule
|
||||
scheduler: env.scheduler,
|
||||
}),
|
||||
@@ -88,14 +88,14 @@ export default async function createCatalogEventBasedProviders(
|
||||
/* highlight-add-start */
|
||||
providers.push(
|
||||
GithubEntityProvider.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
// optional: alternatively, use scheduler with schedule defined in app-config.yaml
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 30 },
|
||||
timeout: { minutes: 3 },
|
||||
}),
|
||||
// optional: alternatively, use schedule
|
||||
scheduler: env.scheduler,
|
||||
logger: env.logger,
|
||||
// optional: alternatively, use scheduler with schedule defined in app-config.yaml
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 30 },
|
||||
timeout: { minutes: 3 },
|
||||
}),
|
||||
// optional: alternatively, use schedule
|
||||
scheduler: env.scheduler,
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
@@ -269,7 +269,7 @@ import {
|
||||
} from '@backstage/plugin-catalog-backend-module-github';
|
||||
import {
|
||||
ScmIntegrations,
|
||||
DefaultGithubCredentialsProvider
|
||||
DefaultGithubCredentialsProvider,
|
||||
} from '@backstage/integration';
|
||||
/* highlight-add-end */
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ import { PluginEnvironment } from '../types';
|
||||
export default async function createCatalogEventBasedProviders(
|
||||
/* highlight-remove-next-line */
|
||||
_: PluginEnvironment,
|
||||
/* highlight-add-next-line */
|
||||
/* highlight-add-next-line */
|
||||
env: PluginEnvironment,
|
||||
): Promise<Array<EntityProvider & EventSubscriber>> {
|
||||
const providers: Array<
|
||||
|
||||
@@ -99,7 +99,7 @@ export default async function createPlugin(
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/* highlight-add-start */
|
||||
builder.addProcessor(
|
||||
GitLabDiscoveryProcessor.fromConfig(env.config, { logger: env.logger })
|
||||
GitLabDiscoveryProcessor.fromConfig(env.config, { logger: env.logger }),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
|
||||
@@ -46,67 +46,67 @@ The permissions framework uses a new `permission-backend` plugin to accept autho
|
||||
|
||||
1. Add `@backstage/plugin-permission-backend` as a dependency of your Backstage backend:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @backstage/plugin-permission-backend
|
||||
```
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @backstage/plugin-permission-backend
|
||||
```
|
||||
|
||||
2. Add the following to a new file, `packages/backend/src/plugins/permission.ts`. This adds the permission-backend router, and configures it with a policy which allows everything.
|
||||
|
||||
```typescript title="packages/backend/src/plugins/permission.ts"
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
```typescript title="packages/backend/src/plugins/permission.ts"
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
config: env.config,
|
||||
logger: env.logger,
|
||||
discovery: env.discovery,
|
||||
policy: new TestPermissionPolicy(),
|
||||
identity: env.identity,
|
||||
});
|
||||
}
|
||||
```
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
config: env.config,
|
||||
logger: env.logger,
|
||||
discovery: env.discovery,
|
||||
policy: new TestPermissionPolicy(),
|
||||
identity: env.identity,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. Wire up the permission policy in `packages/backend/src/index.ts`. [The index in the example backend](https://github.com/backstage/backstage/blob/master/packages/backend/src/index.ts) shows how to do this. You’ll need to import the module from the previous step, create a plugin environment, and add the router to the express app:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import proxy from './plugins/proxy';
|
||||
import techdocs from './plugins/techdocs';
|
||||
import search from './plugins/search';
|
||||
/* highlight-add-next-line */
|
||||
import permission from './plugins/permission';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import proxy from './plugins/proxy';
|
||||
import techdocs from './plugins/techdocs';
|
||||
import search from './plugins/search';
|
||||
/* highlight-add-next-line */
|
||||
import permission from './plugins/permission';
|
||||
|
||||
async function main() {
|
||||
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
/* highlight-add-next-line */
|
||||
const permissionEnv = useHotMemoize(module, () => createEnv('permission'));
|
||||
// ..
|
||||
async function main() {
|
||||
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
/* highlight-add-next-line */
|
||||
const permissionEnv = useHotMemoize(module, () => createEnv('permission'));
|
||||
// ..
|
||||
|
||||
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
/* highlight-add-next-line */
|
||||
apiRouter.use('/permission', await permission(permissionEnv));
|
||||
// ..
|
||||
}
|
||||
```
|
||||
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
/* highlight-add-next-line */
|
||||
apiRouter.use('/permission', await permission(permissionEnv));
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Enable and test the permissions system
|
||||
|
||||
@@ -114,46 +114,46 @@ Now that the permission backend is running, it’s time to enable the permission
|
||||
|
||||
1. Set the property `permission.enabled` to `true` in `app-config.yaml`.
|
||||
|
||||
```yaml title="app-config.yaml"
|
||||
permission:
|
||||
enabled: true
|
||||
```
|
||||
```yaml title="app-config.yaml"
|
||||
permission:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
2. Update the PermissionPolicy in `packages/backend/src/plugins/permission.ts` to disable a permission that’s easy for us to test. This policy rejects any attempt to delete a catalog entity:
|
||||
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
/* highlight-remove-next-line */
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
PermissionPolicy,
|
||||
PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-end */
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
/* highlight-remove-next-line */
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
PermissionPolicy,
|
||||
PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-end */
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
/* highlight-remove-next-line */
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
/* highlight-add-start */
|
||||
async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
if (request.permission.name === 'catalog.entity.delete') {
|
||||
return {
|
||||
result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
/* highlight-add-end */
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
/* highlight-remove-next-line */
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
/* highlight-add-start */
|
||||
async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
if (request.permission.name === 'catalog.entity.delete') {
|
||||
return {
|
||||
result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
```
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Now that you’ve made this change, you should find that the unregister entity menu option on the catalog entity page is disabled.
|
||||
|
||||
|
||||
@@ -20,99 +20,99 @@ The source code is available here:
|
||||
|
||||
1. Copy-paste the three folders into the plugins folder of your backstage application repository (removing the `example-` prefix from each folder) or run the following script from the root of your backstage application:
|
||||
|
||||
```bash
|
||||
$ cd $(mktemp -d)
|
||||
git clone --depth 1 --quiet --no-checkout --filter=blob:none https://github.com/backstage/backstage.git .
|
||||
git checkout master -- plugins/example-todo-list/
|
||||
git checkout master -- plugins/example-todo-list-backend/
|
||||
git checkout master -- plugins/example-todo-list-common/
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-backend/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-common/package.json
|
||||
for file in plugins/*; do mv "$file" "$OLDPWD/${file/example-todo/todo}"; done
|
||||
cd -
|
||||
```
|
||||
```bash
|
||||
$ cd $(mktemp -d)
|
||||
git clone --depth 1 --quiet --no-checkout --filter=blob:none https://github.com/backstage/backstage.git .
|
||||
git checkout master -- plugins/example-todo-list/
|
||||
git checkout master -- plugins/example-todo-list-backend/
|
||||
git checkout master -- plugins/example-todo-list-common/
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-backend/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-common/package.json
|
||||
for file in plugins/*; do mv "$file" "$OLDPWD/${file/example-todo/todo}"; done
|
||||
cd -
|
||||
```
|
||||
|
||||
The `plugins` directory of your project should now include `todo-list`, `todo-list-backend`, and `todo-list-common`.
|
||||
The `plugins` directory of your project should now include `todo-list`, `todo-list-backend`, and `todo-list-common`.
|
||||
|
||||
**Important**: if you are on **Windows**, make sure you have WSL and git installed on your machine before executing the script above.
|
||||
**Important**: if you are on **Windows**, make sure you have WSL and git installed on your machine before executing the script above.
|
||||
|
||||
2. Add these packages as dependencies for your Backstage app:
|
||||
|
||||
```sh
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @internal/plugin-todo-list-backend @internal/plugin-todo-list-common
|
||||
$ yarn add --cwd packages/app @internal/plugin-todo-list
|
||||
```
|
||||
```sh
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @internal/plugin-todo-list-backend @internal/plugin-todo-list-common
|
||||
$ yarn add --cwd packages/app @internal/plugin-todo-list
|
||||
```
|
||||
|
||||
3. Include the backend and frontend plugin in your application:
|
||||
|
||||
Create a new `packages/backend/src/plugins/todolist.ts` with the following content:
|
||||
Create a new `packages/backend/src/plugins/todolist.ts` with the following content:
|
||||
|
||||
```typescript title="packages/backend/src/plugins/todolist.ts"
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@internal/plugin-todo-list-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
```typescript title="packages/backend/src/plugins/todolist.ts"
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@internal/plugin-todo-list-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
discovery,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
});
|
||||
}
|
||||
```
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
discovery,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Apply the following changes to `packages/backend/src/index.ts`:
|
||||
Apply the following changes to `packages/backend/src/index.ts`:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import techdocs from './plugins/techdocs';
|
||||
/* highlight-add-next-line */
|
||||
import todoList from './plugins/todolist';
|
||||
import search from './plugins/search';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import techdocs from './plugins/techdocs';
|
||||
/* highlight-add-next-line */
|
||||
import todoList from './plugins/todolist';
|
||||
import search from './plugins/search';
|
||||
|
||||
async function main() {
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
/* highlight-add-next-line */
|
||||
const todoListEnv = useHotMemoize(module, () => createEnv('todolist'));
|
||||
// ..
|
||||
async function main() {
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
/* highlight-add-next-line */
|
||||
const todoListEnv = useHotMemoize(module, () => createEnv('todolist'));
|
||||
// ..
|
||||
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
apiRouter.use('/permission', await permission(permissionEnv));
|
||||
/* highlight-add-next-line */
|
||||
apiRouter.use('/todolist', await todoList(todoListEnv));
|
||||
// Add backends ABOVE this line; this 404 handler is the catch-all fallback
|
||||
apiRouter.use(notFoundHandler());
|
||||
// ..
|
||||
}
|
||||
```
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
apiRouter.use('/permission', await permission(permissionEnv));
|
||||
/* highlight-add-next-line */
|
||||
apiRouter.use('/todolist', await todoList(todoListEnv));
|
||||
// Add backends ABOVE this line; this 404 handler is the catch-all fallback
|
||||
apiRouter.use(notFoundHandler());
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
Apply the following changes to `packages/app/src/App.tsx`:
|
||||
Apply the following changes to `packages/app/src/App.tsx`:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { TodoListPage } from '@internal/plugin-todo-list';
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { TodoListPage } from '@internal/plugin-todo-list';
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Route path="/search" element={<SearchPage />}>
|
||||
{searchPage}
|
||||
</Route>
|
||||
<Route path="/settings" element={<UserSettingsPage />} />
|
||||
{/* highlight-add-next-line */}
|
||||
<Route path="/todo-list" element={<TodoListPage />} />
|
||||
{/* ... */}
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Route path="/search" element={<SearchPage />}>
|
||||
{searchPage}
|
||||
</Route>
|
||||
<Route path="/settings" element={<UserSettingsPage />} />
|
||||
{/* highlight-add-next-line */}
|
||||
<Route path="/todo-list" element={<TodoListPage />} />
|
||||
{/* ... */}
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
Now if you start your application you should be able to reach the `/todo-list` page:
|
||||
|
||||
|
||||
@@ -32,7 +32,10 @@ export const todoListUpdatePermission = createPermission({
|
||||
/* highlight-remove-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission];
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
|
||||
export const todoListPermissions = [
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
];
|
||||
```
|
||||
|
||||
Notice that unlike `todoListCreatePermission`, the `todoListUpdatePermission` permission contains a `resourceType` field. This field indicates to the permission framework that this permission is intended to be authorized in the context of a resource with type `'todo-item'`. You can use whatever string you like as the resource type, as long as you use the same value consistently for each type of resource.
|
||||
@@ -45,7 +48,10 @@ To start, let's edit `plugins/todo-list-backend/src/service/router.ts` in the sa
|
||||
/* highlight-remove-next-line */
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-next-line */
|
||||
import { todoListCreatePermission, todoListUpdatePermission } from '@internal/plugin-todo-list-common';
|
||||
import {
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
} from '@internal/plugin-todo-list-common';
|
||||
|
||||
router.put('/todos', async (req, res) => {
|
||||
/* highlight-add-start */
|
||||
|
||||
@@ -14,11 +14,14 @@ router.get('/todos', async (req, res) => {
|
||||
const token = IdentityClient.getBearerToken(req.header('authorization'));
|
||||
|
||||
/* highlight-remove-next-line */
|
||||
res.json(getAll())
|
||||
res.json(getAll());
|
||||
/* highlight-add-start */
|
||||
const items = getAll();
|
||||
const decisions = await permissions.authorize(
|
||||
items.map(({ id }) => ({ permission: todoListReadPermission, resourceRef: id })),
|
||||
items.map(({ id }) => ({
|
||||
permission: todoListReadPermission,
|
||||
resourceRef: id,
|
||||
})),
|
||||
);
|
||||
|
||||
const filteredItems = decisions.filter(
|
||||
@@ -64,9 +67,16 @@ export const todoListReadPermission = createPermission({
|
||||
/* highlight-add-end */
|
||||
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
|
||||
export const todoListPermissions = [
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
];
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission, todoListReadPermission];
|
||||
export const todoListPermissions = [
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
todoListReadPermission,
|
||||
];
|
||||
```
|
||||
|
||||
## Using conditional policy decisions
|
||||
|
||||
@@ -36,7 +36,9 @@ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
/* highlight-add-next-line */
|
||||
const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
|
||||
const { loading: loadingPermission, allowed: canAddTodo } = usePermission({
|
||||
permission: todoListCreatePermission,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -78,7 +80,6 @@ Here we are using the [`usePermission` hook](https://backstage.io/docs/reference
|
||||
It's really that simple! Let's change our policy to test the disabled button:
|
||||
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
/* highlight-remove-next-line */
|
||||
@@ -117,10 +118,7 @@ export const TodoListPage = () => {
|
||||
</Grid>
|
||||
{/* highlight-remove-end */}
|
||||
{/* highlight-add-start */}
|
||||
<RequirePermission
|
||||
permission={todoListCreatePermission}
|
||||
errorPage={<></>}
|
||||
>
|
||||
<RequirePermission permission={todoListCreatePermission} errorPage={<></>}>
|
||||
<Grid item>
|
||||
<AddTodo onAdd={handleAdd} />
|
||||
</Grid>
|
||||
@@ -129,13 +127,15 @@ export const TodoListPage = () => {
|
||||
<Grid item>
|
||||
<TodoList key={key} onEdit={setEdit} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
</Grid>;
|
||||
};
|
||||
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
/* highlight-remove-next-line */
|
||||
const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
|
||||
const { loading: loadingPermission, allowed: canAddTodo } = usePermission({
|
||||
permission: todoListCreatePermission,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -181,10 +181,14 @@ It's crucial that you update to `RequirePermission` at the same time as you upda
|
||||
When migrating over to React Router v6 stable, you might also see browser console warnings for the `Navigate` component. This will need to be wrapped up in a `Route` component with the `Navigate` component in the `element` prop.
|
||||
|
||||
```tsx
|
||||
{/* highlight-remove-next-line */}
|
||||
<Navigate key="/" to="catalog" />
|
||||
{/* highlight-add-next-line */}
|
||||
<Route path="/" element={<Navigate to="catalog" />} />
|
||||
{
|
||||
/* highlight-remove-next-line */
|
||||
}
|
||||
<Navigate key="/" to="catalog" />;
|
||||
{
|
||||
/* highlight-add-next-line */
|
||||
}
|
||||
<Route path="/" element={<Navigate to="catalog" />} />;
|
||||
```
|
||||
|
||||
### `NavLink`
|
||||
|
||||
Reference in New Issue
Block a user