Merge branch 'backstage:master' into master
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -29,7 +29,9 @@ auth:
|
||||
jwtHeader: x-custom-header # Optional: Only if you are using a custom header for the IAP JWT
|
||||
```
|
||||
|
||||
You can find the project number and service ID in the Google Cloud Console.
|
||||
The full `audience` value can be obtained by visiting your [Identity-Aware Proxy Google Cloud console](https://console.cloud.google.com/security/iap), selecting your project, finding your Backend Service to proxy, clicking the 3 vertical dots then "Get JWT Audience Code", and copying from the resulting popup, which will look similar to the following:
|
||||
|
||||

|
||||
|
||||
This config section must be in place for the provider to load at all. Now let's
|
||||
add the provider itself.
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
export const examplePlugin = createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -53,7 +53,7 @@ export interface ExamplePluginOptions {
|
||||
|
||||
export const examplePlugin = createBackendPlugin(
|
||||
(options?: ExamplePluginOptions) => ({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
|
||||
@@ -44,7 +44,7 @@ class ActionsExtension implements ScaffolderActionsExtensionPoint {
|
||||
|
||||
export const scaffolderPlugin = createBackendPlugin(
|
||||
{
|
||||
id: 'scaffolder',
|
||||
pluginId: 'scaffolder',
|
||||
register(env) {
|
||||
const actionsExtensions = new ActionsExtension();
|
||||
env.registerExtensionPoint(
|
||||
|
||||
@@ -21,7 +21,7 @@ Example:
|
||||
|
||||
```ts
|
||||
export const catalogPlugin = createBackendPlugin({
|
||||
id: 'catalog',
|
||||
pluginId: 'catalog',
|
||||
...
|
||||
})
|
||||
```
|
||||
@@ -36,7 +36,7 @@ export const catalogPlugin = createBackendPlugin({
|
||||
Example:
|
||||
|
||||
```ts
|
||||
export const catalogModuleGithubEntityProvider = createBackendPlugin({
|
||||
export const catalogModuleGithubEntityProvider = createBackendModule({
|
||||
pluginId: 'catalog',
|
||||
moduleId: 'githubEntityProvider',
|
||||
...
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
import { createExampleRouter } from './router';
|
||||
|
||||
export const examplePlugin = createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -183,7 +183,7 @@ that needs to be different across environments.
|
||||
import { coreServices } from '@backstage/backend-plugin-api';
|
||||
|
||||
export const examplePlugin = createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: { config: coreServices.config },
|
||||
@@ -216,7 +216,7 @@ export interface ExampleOptions {
|
||||
|
||||
export const examplePlugin = createBackendPlugin(
|
||||
(options?: ExampleOptions) => ({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
|
||||
@@ -46,7 +46,7 @@ import { Router } from 'express';
|
||||
import { KubernetesBuilder } from './KubernetesBuilder';
|
||||
|
||||
export const kubernetesPlugin = createBackendPlugin({
|
||||
id: 'kubernetes',
|
||||
pluginId: 'kubernetes',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -95,7 +95,7 @@ export interface KubernetesOptions {
|
||||
}
|
||||
|
||||
const kubernetesPlugin = createBackendPlugin((options: KubernetesOptions) => ({
|
||||
id: 'kubernetes',
|
||||
pluginId: 'kubernetes',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -182,7 +182,7 @@ class ClusterSupplier implements KubernetesClusterSupplierExtensionPoint {
|
||||
}
|
||||
|
||||
export const kubernetesPlugin = createBackendPlugin({
|
||||
id: 'kubernetes',
|
||||
pluginId: 'kubernetes',
|
||||
register(env) {
|
||||
const extensionPoint = new ClusterSupplier();
|
||||
// We register the extension point with the backend, which allows modules to
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import { Router } from 'express';
|
||||
|
||||
createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: { http: coreServices.httpRouter },
|
||||
@@ -86,7 +86,7 @@ import {
|
||||
import { Router } from 'express';
|
||||
|
||||
createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -157,7 +157,7 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -215,7 +215,7 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -288,7 +288,7 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -327,7 +327,7 @@ import {
|
||||
import { resolvePackagePath } from '@backstage/backend-common';
|
||||
|
||||
createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -366,7 +366,7 @@ import {
|
||||
import { fetch } from 'node-fetch';
|
||||
|
||||
createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -397,7 +397,7 @@ import {
|
||||
import { Router } from 'express';
|
||||
|
||||
createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -465,7 +465,7 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -479,10 +479,7 @@ createBackendPlugin({
|
||||
// do some other stuff.
|
||||
}, 1000);
|
||||
|
||||
lifecycle.addShutdownHook({
|
||||
fn: () => clearInterval(interval),
|
||||
logger,
|
||||
});
|
||||
lifecycle.addShutdownHook(() => clearInterval(interval));
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -499,17 +496,19 @@ The following example shows how to override the default implementation of the li
|
||||
|
||||
```ts
|
||||
class MyCustomLifecycleService implements RootLifecycleService {
|
||||
constructor(private readonly logger: LoggerService) {
|
||||
['SIGKILL', 'SIGTERM'].map(signal =>
|
||||
process.on(signal, () => this.shutdown()),
|
||||
);
|
||||
}
|
||||
constructor(private readonly logger: LoggerService) {}
|
||||
|
||||
#isCalled = false;
|
||||
#shutdownTasks: Array<LifecycleServiceShutdownHook> = [];
|
||||
#shutdownTasks: Array<{
|
||||
hook: LifecycleServiceShutdownHook;
|
||||
options?: LifecycleServiceShutdownOptions;
|
||||
}> = [];
|
||||
|
||||
addShutdownHook(options: LifecycleServiceShutdownHook): void {
|
||||
this.#shutdownTasks.push(options);
|
||||
addShutdownHook(
|
||||
hook: LifecycleServiceShutdownHook,
|
||||
options?: LifecycleServiceShutdownOptions,
|
||||
): void {
|
||||
this.#shutdownTasks.push({ hook, options });
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
@@ -520,10 +519,10 @@ class MyCustomLifecycleService implements RootLifecycleService {
|
||||
|
||||
this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`);
|
||||
await Promise.all(
|
||||
this.#shutdownTasks.map(async hook => {
|
||||
const { logger = this.logger } = hook;
|
||||
this.#shutdownTasks.map(async ({ hook, options }) => {
|
||||
const logger = options?.logger ?? this.logger;
|
||||
try {
|
||||
await hook.fn();
|
||||
await hook();
|
||||
logger.info(`Shutdown hook succeeded`);
|
||||
} catch (error) {
|
||||
logger.error(`Shutdown hook failed, ${error}`);
|
||||
@@ -564,7 +563,7 @@ import {
|
||||
import { Router } from 'express';
|
||||
|
||||
createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -613,7 +612,7 @@ import {
|
||||
import { fetch } from 'node-fetch';
|
||||
|
||||
createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
@@ -652,7 +651,7 @@ import {
|
||||
import os from 'os';
|
||||
|
||||
createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
|
||||
@@ -112,11 +112,11 @@ steps which would be rendered as different steps in the scaffolder plugin
|
||||
frontend.
|
||||
|
||||
Each `Step` is `JSONSchema` with some extra goodies for styling what it might
|
||||
look like in the frontend. For these steps we rely very heavily on this library:
|
||||
https://github.com/rjsf-team/react-jsonschema-form. They have some great docs
|
||||
too here: https://react-jsonschema-form.readthedocs.io/ and a playground where
|
||||
you can play around with some examples here
|
||||
https://rjsf-team.github.io/react-jsonschema-form.
|
||||
look like in the frontend. For these steps we rely very heavily on this
|
||||
[library](https://github.com/rjsf-team/react-jsonschema-form). They have some
|
||||
[great docs](https://rjsf-team.github.io/react-jsonschema-form/docs/) and a
|
||||
[playground](https://rjsf-team.github.io/react-jsonschema-form) where you can
|
||||
play around with some examples.
|
||||
|
||||
There's another option for that library called `uiSchema` which we've taken
|
||||
advantage of, and we've merged it with the existing `JSONSchema` that you
|
||||
|
||||
@@ -28,3 +28,5 @@ The configuration is a structure with two elements:
|
||||
|
||||
- `host`: The DevOps host; only `dev.azure.com` is supported.
|
||||
- `token` (optional): A personal access token as expected by Azure DevOps.
|
||||
|
||||
> Note: The token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API
|
||||
|
||||
@@ -22,7 +22,7 @@ catalog:
|
||||
yourProviderId:
|
||||
host: gitlab-host # Identifies one of the hosts set up in the integrations
|
||||
branch: main # Optional. Uses `master` as default
|
||||
group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole project will be scanned
|
||||
group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole instance will be scanned
|
||||
entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml`
|
||||
projectPattern: /[\s\S]*/ # Optional. Filters found projects based on provided patter. Defaults to `/[\s\S]*/`, what means to not filter anything
|
||||
schedule: # optional; same options as in TaskScheduleDefinition
|
||||
|
||||
@@ -48,7 +48,7 @@ The source code is available here:
|
||||
|
||||
Create a new `packages/backend/src/plugins/todolist.ts` with the following content:
|
||||
|
||||
```javascript
|
||||
```typescript
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@internal/plugin-todo-list-backend';
|
||||
import { Router } from 'express';
|
||||
|
||||
@@ -146,7 +146,7 @@ In order to test the logic above, the integrators of your backstage instance nee
|
||||
- async handle(): Promise<PolicyDecision> {
|
||||
+ async handle(
|
||||
+ request: PolicyQuery,
|
||||
+ user?: BackstageIdentityResponse,
|
||||
+ _user?: BackstageIdentityResponse,
|
||||
+ ): Promise<PolicyDecision> {
|
||||
+ if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
+ return {
|
||||
@@ -204,7 +204,7 @@ First we'll clean up the `plugins/todo-list-backend/src/service/router.test.ts`:
|
||||
const router = await createRouter({
|
||||
logger: getVoidLogger(),
|
||||
identity: {} as DefaultIdentityClient,
|
||||
+ permissions: toPermissionEvaluator,
|
||||
+ permissions: permissionEvaluator,
|
||||
});
|
||||
app = express().use(router);
|
||||
});
|
||||
@@ -235,7 +235,7 @@ Then we want to update the `plugins/todo-list-backend/src/service/standaloneServ
|
||||
+ ServerTokenManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
+ import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
@@ -285,4 +285,44 @@ Then we want to update the `plugins/todo-list-backend/src/service/standaloneServ
|
||||
module.hot?.accept();
|
||||
```
|
||||
|
||||
Finally, we need to update `plugins/todo-list-backend/src/plugin.ts`:
|
||||
|
||||
```diff
|
||||
import { loggerToWinstonLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createRouter } from './service/router';
|
||||
|
||||
/**
|
||||
* The example TODO list backend plugin.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const exampleTodoListPlugin = createBackendPlugin({
|
||||
pluginId: 'exampleTodoList',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
identity: coreServices.identity,
|
||||
logger: coreServices.logger,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
+ permissions: coreServices.permissions,
|
||||
},
|
||||
- async init({ identity, logger, httpRouter }) {
|
||||
+ async init({ identity, logger, httpRouter, permissions }) {
|
||||
httpRouter.use(
|
||||
await createRouter({
|
||||
identity,
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
permissions,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Now when you run `yarn tsc` you should have no more errors.
|
||||
|
||||
@@ -76,7 +76,7 @@ This enables decisions based on characteristics of the resource, but it's import
|
||||
Install the missing module:
|
||||
|
||||
```
|
||||
$ yarn workspace @internal/plugin-todo-list-backend add @backstage/plugin-permission-node
|
||||
$ yarn workspace @internal/plugin-todo-list-backend add @backstage/plugin-permission-node zod
|
||||
```
|
||||
|
||||
Create a new `plugins/todo-list-backend/src/service/rules.ts` file and append the following code:
|
||||
@@ -89,7 +89,8 @@ import { Todo, TodoFilter } from './todos';
|
||||
|
||||
export const createTodoListPermissionRule = makeCreatePermissionRule<
|
||||
Todo,
|
||||
TodoFilter
|
||||
TodoFilter,
|
||||
typeof TODO_LIST_RESOURCE_TYPE
|
||||
>();
|
||||
|
||||
export const isOwner = createTodoListPermissionRule({
|
||||
@@ -97,8 +98,8 @@ export const isOwner = createTodoListPermissionRule({
|
||||
description: 'Should allow only if the todo belongs to the user',
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
paramsSchema: z.object({
|
||||
userId: z.string().describe('User ID to match on the resource')
|
||||
})
|
||||
userId: z.string().describe('User ID to match on the resource'),
|
||||
}),
|
||||
apply: (resource: Todo, { userId }) => {
|
||||
return resource.author === userId;
|
||||
},
|
||||
@@ -187,6 +188,7 @@ Make sure `todoListConditions` and `createTodoListConditionalDecision` are expor
|
||||
```diff
|
||||
export * from './service/router';
|
||||
+ export * from './conditionExports';
|
||||
export { exampleTodoListPlugin } from './plugin';
|
||||
```
|
||||
|
||||
## Test the authorized update endpoint
|
||||
@@ -209,7 +211,6 @@ Let's go back to the permission policy's handle function and try to authorize ou
|
||||
+ import {
|
||||
+ todoListCreatePermission,
|
||||
+ todoListUpdatePermission,
|
||||
+ TODO_LIST_RESOURCE_TYPE,
|
||||
+ } from '@internal/plugin-todo-list-common';
|
||||
+ import {
|
||||
+ todoListConditions,
|
||||
@@ -217,7 +218,11 @@ Let's go back to the permission policy's handle function and try to authorize ou
|
||||
+ } from '@internal/plugin-todo-list-backend';
|
||||
|
||||
...
|
||||
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
- _user?: BackstageIdentityResponse,
|
||||
+ user?: BackstageIdentityResponse,
|
||||
): Promise<PolicyDecision> {
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
result: AuthorizeResult.ALLOW,
|
||||
@@ -236,6 +241,7 @@ Let's go back to the permission policy's handle function and try to authorize ou
|
||||
return {
|
||||
result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
For any incoming update requests, we now return a _Conditional Decision_. We are saying:
|
||||
|
||||
@@ -15,7 +15,7 @@ One possible solution may leverage the batching functionality to authorize all o
|
||||
- res.json(getAll())
|
||||
+ const items = getAll();
|
||||
+ const decisions = await permissions.authorize(
|
||||
+ items.map(({ id }) => ({ permission: todosListRead, resourceRef: id })),
|
||||
+ items.map(({ id }) => ({ permission: todoListReadPermission, resourceRef: id })),
|
||||
+ );
|
||||
|
||||
+ const filteredItems = decisions.filter(
|
||||
@@ -53,7 +53,7 @@ Let's add another permission to the plugin.
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
});
|
||||
+
|
||||
+ export const todosListRead = createPermission({
|
||||
+ export const todoListReadPermission = createPermission({
|
||||
+ name: 'todos.list.read',
|
||||
+ attributes: { action: 'read' },
|
||||
+ resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
@@ -81,10 +81,9 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev
|
||||
import {
|
||||
todosListCreate,
|
||||
todosListUpdate,
|
||||
+ todosListRead,
|
||||
+ todoListReadPermission,
|
||||
TODO_LIST_RESOURCE_TYPE,
|
||||
} from './permissions';
|
||||
+ import { rules } from './rules';
|
||||
|
||||
+ const transformConditions: ConditionTransformer<TodoFilter> = createConditionTransformer(Object.values(rules));
|
||||
|
||||
@@ -95,7 +94,7 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev
|
||||
+ );
|
||||
+
|
||||
+ const decision = (
|
||||
+ await permissions.authorizeConditional([{ permission: todosListRead }], {
|
||||
+ await permissions.authorizeConditional([{ permission: todoListReadPermission }], {
|
||||
+ token,
|
||||
+ })
|
||||
+ )[0];
|
||||
@@ -110,7 +109,6 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev
|
||||
+ } else {
|
||||
+ res.json(getAll());
|
||||
+ }
|
||||
+ }
|
||||
- res.json(getAll());
|
||||
});
|
||||
```
|
||||
@@ -121,7 +119,7 @@ Since `TodoFilter` used in our plugin matches the structure of the conditions ob
|
||||
|
||||
## Test the authorized read endpoint
|
||||
|
||||
Let's update our permission policy to return a conditional result whenever a `todosListRead` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission.
|
||||
Let's update our permission policy to return a conditional result whenever a `todoListReadPermission` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
@@ -132,7 +130,6 @@ import {
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
+ todoListReadPermission,
|
||||
TODO_LIST_RESOURCE_TYPE,
|
||||
} from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
|
||||
@@ -56,7 +56,11 @@ Let's make the following changes in `plugins/todo-list/src/components/TodoListPa
|
||||
- Add
|
||||
- </Button>
|
||||
+ {!loadingPermission && (
|
||||
+ <Button disabled={!canAddTodo} variant="contained" onClick={handleAdd}>
|
||||
+ <Button
|
||||
+ disabled={!canAddTodo}
|
||||
+ variant="contained"
|
||||
+ onClick={() => onAdd(title.current)}
|
||||
+ >
|
||||
+ Add
|
||||
+ </Button>
|
||||
+ )}
|
||||
@@ -118,7 +122,10 @@ Providing a disabled state can be a helpful signal to users, but there may be ca
|
||||
- <Grid item>
|
||||
- <AddTodo onAdd={handleAdd} />
|
||||
- </Grid>
|
||||
+ <RequirePermission permission={todoListCreatePermission} errorPage={<></>}>
|
||||
+ <RequirePermission
|
||||
+ permission={todoListCreatePermission}
|
||||
+ errorPage={<></>}
|
||||
+ >
|
||||
+ <Grid item>
|
||||
+ <AddTodo onAdd={handleAdd} />
|
||||
+ </Grid>
|
||||
@@ -149,11 +156,15 @@ Providing a disabled state can be a helpful signal to users, but there may be ca
|
||||
onChange={e => (title.current = e.target.value)}
|
||||
/>
|
||||
- {!loadingPermission && (
|
||||
- <Button disabled={!canAddTodo} variant="contained" onClick={handleAdd}>
|
||||
- <Button
|
||||
- disabled={!canAddTodo}
|
||||
- variant="contained"
|
||||
- onClick={() => onAdd(title.current)}
|
||||
- >
|
||||
- Add
|
||||
- </Button>
|
||||
- )}
|
||||
+ <Button variant="contained" onClick={handleAdd}>
|
||||
+ <Button variant="contained" onClick={() => onAdd(title.current)}>
|
||||
+ Add
|
||||
+ </Button>
|
||||
</Box>
|
||||
|
||||
@@ -84,7 +84,7 @@ import {
|
||||
// export type ExamplePluginOptions = { exampleOption: boolean };
|
||||
export const examplePlugin = createBackendPlugin({
|
||||
// unique id for the plugin
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
// It's possible to provide options to the plugin
|
||||
// register(env, options: ExamplePluginOptions) {
|
||||
register(env) {
|
||||
@@ -111,7 +111,7 @@ If we wanted our plugin to accept options as well, we'd accept the options as th
|
||||
|
||||
```ts
|
||||
export const examplePlugin = createBackendPlugin({
|
||||
id: 'example',
|
||||
pluginId: 'example',
|
||||
register(env, options?: { silent?: boolean }) {
|
||||
env.registerInit({
|
||||
deps: { logger: coreServices.logger },
|
||||
|
||||
Reference in New Issue
Block a user