Merge pull request #11017 from backstage/permission-docs
Permission framework documentation
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
# todo-list-backend
|
||||
|
||||
This package provides a starting point to demonstrate how plugin authors can use the Backstage permission framework. Refer to the [documentation](https://backstage.io/docs/permission/plugin-authors/01-setup) to get started.
|
||||
@@ -0,0 +1,22 @@
|
||||
## API Report File for "@internal/plugin-todo-list-backend"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import express from 'express';
|
||||
import { IdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
// @public
|
||||
export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
|
||||
// @public
|
||||
export interface RouterOptions {
|
||||
// (undocumented)
|
||||
identity: IdentityClient;
|
||||
// (undocumented)
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@internal/plugin-todo-list-backend",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"clean": "backstage-cli package clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.13.3-next.0",
|
||||
"@backstage/config": "^1.0.0",
|
||||
"@backstage/errors": "^1.0.0",
|
||||
"@backstage/plugin-auth-node": "^0.2.1-next.0",
|
||||
"@types/express": "^4.17.6",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.17.1-next.0",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"@types/uuid": "^8.0.0",
|
||||
"msw": "^0.35.0",
|
||||
"supertest": "^6.1.6"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './service/router';
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getRootLogger } from '@backstage/backend-common';
|
||||
import yn from 'yn';
|
||||
import { startStandaloneServer } from './service/standaloneServer';
|
||||
|
||||
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007;
|
||||
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
|
||||
const logger = getRootLogger();
|
||||
|
||||
startStandaloneServer({ port, enableCors, logger }).catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('CTRL+C pressed; exiting.');
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { IdentityClient } from '@backstage/plugin-auth-node';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
|
||||
import { createRouter } from './router';
|
||||
|
||||
describe('createRouter', () => {
|
||||
let app: express.Express;
|
||||
|
||||
beforeAll(async () => {
|
||||
const router = await createRouter({
|
||||
logger: getVoidLogger(),
|
||||
identity: {} as IdentityClient,
|
||||
});
|
||||
app = express().use(router);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /health', () => {
|
||||
it('returns ok', async () => {
|
||||
const response = await request(app).get('/health');
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { errorHandler } from '@backstage/backend-common';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
IdentityClient,
|
||||
getBearerTokenFromAuthorizationHeader,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { add, getAll, update } from './todos';
|
||||
import { InputError } from '@backstage/errors';
|
||||
|
||||
/**
|
||||
* Dependencies of the todo-list router
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
identity: IdentityClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an express.Router with some endpoints
|
||||
* for creating, editing and deleting todo items.
|
||||
*
|
||||
* @public
|
||||
* @param options - the dependencies of the router
|
||||
* @returns an express.Router
|
||||
*
|
||||
*/
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const { logger, identity } = options;
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
router.get('/health', (_, response) => {
|
||||
logger.info('PONG!');
|
||||
response.send({ status: 'ok' });
|
||||
});
|
||||
|
||||
router.get('/todos', async (_req, res) => {
|
||||
res.json(getAll());
|
||||
});
|
||||
|
||||
router.post('/todos', async (req, res) => {
|
||||
const token = getBearerTokenFromAuthorizationHeader(
|
||||
req.header('authorization'),
|
||||
);
|
||||
let author: string | undefined = undefined;
|
||||
|
||||
const user = token ? await identity.authenticate(token) : undefined;
|
||||
author = user?.identity.userEntityRef;
|
||||
|
||||
if (!isTodoCreateRequest(req.body)) {
|
||||
throw new InputError('Invalid payload');
|
||||
}
|
||||
|
||||
const todo = add({ title: req.body.title, author });
|
||||
res.json(todo);
|
||||
});
|
||||
|
||||
router.put('/todos', async (req, res) => {
|
||||
if (!isTodoUpdateRequest(req.body)) {
|
||||
throw new InputError('Invalid payload');
|
||||
}
|
||||
res.json(update(req.body));
|
||||
});
|
||||
|
||||
router.use(errorHandler());
|
||||
return router;
|
||||
}
|
||||
|
||||
function isTodoCreateRequest(request: any): request is { title: string } {
|
||||
return typeof request?.title === 'string';
|
||||
}
|
||||
|
||||
function isTodoUpdateRequest(
|
||||
request: any,
|
||||
): request is { title: string; id: string } {
|
||||
return typeof request?.id === 'string' && isTodoCreateRequest(request);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
createServiceBuilder,
|
||||
loadBackendConfig,
|
||||
SingleHostDiscovery,
|
||||
} from '@backstage/backend-common';
|
||||
import { IdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
enableCors: boolean;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function startStandaloneServer(
|
||||
options: ServerOptions,
|
||||
): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'todo-list-backend' });
|
||||
logger.debug('Starting application server...');
|
||||
const config = await loadBackendConfig({ logger, argv: process.argv });
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
const router = await createRouter({
|
||||
logger,
|
||||
identity: IdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
});
|
||||
|
||||
let service = createServiceBuilder(module)
|
||||
.setPort(options.port)
|
||||
.addRouter('/todo-list', router);
|
||||
if (options.enableCors) {
|
||||
service = service.enableCors({ origin: 'http://localhost:3000' });
|
||||
}
|
||||
|
||||
return await service.start().catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.hot?.accept();
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
|
||||
export type Todo = {
|
||||
title: string;
|
||||
author?: string;
|
||||
id: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type TodoFilter = {
|
||||
property: Exclude<keyof Todo, 'timestamp'>;
|
||||
values: Array<string | undefined>;
|
||||
};
|
||||
|
||||
export type TodoFilters =
|
||||
| {
|
||||
anyOf: TodoFilters[];
|
||||
}
|
||||
| { allOf: TodoFilters[] }
|
||||
| { not: TodoFilters }
|
||||
| TodoFilter;
|
||||
|
||||
const todos: { [key: string]: Todo } = {};
|
||||
|
||||
const matches = (todo: Todo, filters?: TodoFilters): boolean => {
|
||||
if (!filters) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ('allOf' in filters) {
|
||||
return filters.allOf.every(filter => matches(todo, filter));
|
||||
}
|
||||
|
||||
if ('anyOf' in filters) {
|
||||
return filters.anyOf.some(filter => matches(todo, filter));
|
||||
}
|
||||
|
||||
if ('not' in filters) {
|
||||
return !matches(todo, filters.not);
|
||||
}
|
||||
|
||||
return filters.values.includes(todo[filters.property]);
|
||||
};
|
||||
|
||||
export function add(todo: Omit<Todo, 'id' | 'timestamp'>) {
|
||||
const id = uuid();
|
||||
|
||||
const obj: Todo = { ...todo, id, timestamp: Date.now() };
|
||||
todos[id] = obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function getTodo(id: string) {
|
||||
return todos[id];
|
||||
}
|
||||
|
||||
export function update({ id, title }: { id: string; title: string }) {
|
||||
let todo = todos[id];
|
||||
if (!todo) {
|
||||
throw new NotFoundError('Item not found');
|
||||
}
|
||||
|
||||
todo = { ...todo, title, timestamp: Date.now() };
|
||||
todos[id] = todo;
|
||||
return todo;
|
||||
}
|
||||
|
||||
export function getAll(filter?: TodoFilters) {
|
||||
return Object.values(todos)
|
||||
.filter(value => matches(value, filter))
|
||||
.sort((a, b) => b.timestamp - a.timestamp);
|
||||
}
|
||||
|
||||
// prepopulate the db
|
||||
add({ title: 'just a note' });
|
||||
add({ title: 'another note' });
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,3 @@
|
||||
# todo-list-common
|
||||
|
||||
This package provides a starting point to demonstrate how plugin authors can use the Backstage permission framework. Refer to the [documentation](https://backstage.io/docs/permission/plugin-authors/01-setup) to get started.
|
||||
@@ -0,0 +1,12 @@
|
||||
## API Report File for "@internal/plugin-todo-list-common"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BasicPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
// @public
|
||||
export const tempExamplePermission: BasicPermission;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@internal/plugin-todo-list-common",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "frontend-plugin"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/plugin-permission-common": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.17.1-next.0",
|
||||
"@backstage/core-app-api": "^1.0.1",
|
||||
"@backstage/dev-utils": "^1.0.2-next.0",
|
||||
"@backstage/test-utils": "^1.0.2-next.0",
|
||||
"@types/node": "^16.11.26",
|
||||
"msw": "^0.35.0",
|
||||
"cross-fetch": "^3.1.5"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './permissions';
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
/**
|
||||
* An example of a permission.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const tempExamplePermission = createPermission({
|
||||
name: 'temp.example.noop',
|
||||
attributes: {},
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
devDependencies: true,
|
||||
optionalDependencies: true,
|
||||
peerDependencies: true,
|
||||
bundledDependencies: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
# todo-list
|
||||
|
||||
This package provides a starting point to demonstrate how plugin authors can use the Backstage permission framework. Refer to the [documentation](https://backstage.io/docs/permission/plugin-authors/01-setup) to get started.
|
||||
@@ -0,0 +1,23 @@
|
||||
## API Report File for "@internal/plugin-todo-list"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
/// <reference types="react" />
|
||||
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
// @public
|
||||
export const TodoListPage: () => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const todoListPlugin: BackstagePlugin<
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@internal/plugin-todo-list",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"start": "backstage-cli package start",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"diff": "backstage-cli plugin:diff",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"clean": "backstage-cli package clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core-components": "^0.9.3",
|
||||
"@backstage/core-plugin-api": "^1.0.1",
|
||||
"@backstage/theme": "^0.2.15",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.17.1-next.0",
|
||||
"@backstage/core-app-api": "^1.0.1",
|
||||
"@backstage/dev-utils": "^1.0.2-next.0",
|
||||
"@backstage/test-utils": "^1.0.2-next.0",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/user-event": "^14.0.0",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^16.11.26",
|
||||
"msw": "^0.35.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Table, TableColumn, Progress } from '@backstage/core-components';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import {
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { Button } from '@material-ui/core';
|
||||
|
||||
export type Todo = {
|
||||
title: string;
|
||||
id: string;
|
||||
author?: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
type TodosTableProps = {
|
||||
todos: Todo[];
|
||||
onEdit(todo: Todo): any;
|
||||
};
|
||||
|
||||
export const TodoList = ({ onEdit }: { onEdit(todo: Todo): any }) => {
|
||||
const discoveryApi = useApi(discoveryApiRef);
|
||||
const { fetch } = useApi(fetchApiRef);
|
||||
|
||||
const { value, loading, error } = useAsync(async (): Promise<Todo[]> => {
|
||||
const response = await fetch(
|
||||
`${await discoveryApi.getBaseUrl('todolist')}/todos`,
|
||||
);
|
||||
return response.json();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (error) {
|
||||
return <Alert severity="error">{error.message}</Alert>;
|
||||
}
|
||||
|
||||
return <TodosTable todos={value || []} onEdit={onEdit} />;
|
||||
};
|
||||
|
||||
export function TodosTable({ todos, onEdit }: TodosTableProps) {
|
||||
const columns: TableColumn<Todo>[] = [
|
||||
{ title: 'Title', field: 'title' },
|
||||
{ title: 'Author', field: 'author' },
|
||||
{
|
||||
title: 'Last edit',
|
||||
field: 'timestamp',
|
||||
render: e => new Date(e.timestamp).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
render: todo => {
|
||||
return (
|
||||
<Button variant="contained" onClick={() => onEdit(todo)}>
|
||||
Edit
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
title="Todos"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={columns}
|
||||
data={todos}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { TodoList } from './TodoList';
|
||||
export type { Todo } from './TodoList';
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useReducer, useRef, useState } from 'react';
|
||||
import {
|
||||
Typography,
|
||||
Grid,
|
||||
TextField,
|
||||
Button,
|
||||
Dialog,
|
||||
Box,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
Header,
|
||||
Page,
|
||||
Content,
|
||||
ContentHeader,
|
||||
HeaderLabel,
|
||||
SupportButton,
|
||||
} from '@backstage/core-components';
|
||||
import { Todo, TodoList } from '../TodoList';
|
||||
import {
|
||||
alertApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
export const TodoListPage = () => {
|
||||
const discoveryApi = useApi(discoveryApiRef);
|
||||
const { fetch } = useApi(fetchApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const title = useRef('');
|
||||
const [key, refetchTodos] = useReducer(i => i + 1, 0);
|
||||
const [editElement, setEdit] = useState<Todo | undefined>();
|
||||
|
||||
const handleAdd = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${await discoveryApi.getBaseUrl('todolist')}/todos`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ title: title.current }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
const { error } = await response.json();
|
||||
alertApi.post({
|
||||
message: error.message,
|
||||
severity: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
refetchTodos();
|
||||
} catch (e: any) {
|
||||
alertApi.post({ message: e.message, severity: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = async (todo: Todo) => {
|
||||
setEdit(undefined);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${await discoveryApi.getBaseUrl('todolist')}/todos`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ title: todo.title, id: todo.id }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
const { error } = await response.json();
|
||||
alertApi.post({
|
||||
message: error.message,
|
||||
severity: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
refetchTodos();
|
||||
} catch (e: any) {
|
||||
alertApi.post({ message: e.message, severity: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page themeId="tool">
|
||||
<Header
|
||||
title="Welcome to todo-list!"
|
||||
subtitle="Just a CRU todo list plugin"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Todo List">
|
||||
<SupportButton>A description of your plugin goes here.</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<Typography variant="body1">Add todo</Typography>
|
||||
<Box
|
||||
component="span"
|
||||
alignItems="flex-end"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
>
|
||||
<TextField
|
||||
placeholder="Write something here..."
|
||||
onChange={e => (title.current = e.target.value)}
|
||||
/>
|
||||
<Button variant="contained" onClick={handleAdd}>
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<TodoList key={key} onEdit={setEdit} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
{!!editElement && (
|
||||
<EditModal
|
||||
todo={editElement}
|
||||
onSubmit={handleEdit}
|
||||
onCancel={() => setEdit(undefined)}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
function EditModal({
|
||||
todo,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: {
|
||||
todo?: Todo;
|
||||
onSubmit(todo: Todo): any;
|
||||
onCancel(): any;
|
||||
}) {
|
||||
const title = useRef('');
|
||||
return (
|
||||
<Dialog open>
|
||||
<DialogTitle id="form-dialog-title">Edit item</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
placeholder="Write something here..."
|
||||
defaultValue={todo?.title || ''}
|
||||
onChange={e => (title.current = e.target.value)}
|
||||
margin="dense"
|
||||
fullWidth
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button color="primary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onSubmit({ ...todo!, title: title.current })}
|
||||
color="primary"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { TodoListPage } from './TodoListPage';
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { todoListPlugin, TodoListPage } from './plugin';
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { todoListPlugin } from './plugin';
|
||||
|
||||
describe('todo-list', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(todoListPlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
createPlugin,
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import { rootRouteRef } from './routes';
|
||||
|
||||
/**
|
||||
* The todo-list plugin instance
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const todoListPlugin = createPlugin({
|
||||
id: 'todolist',
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* The Router and main entrypoint to the todo-list plugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const TodoListPage = todoListPlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'TodoListPage',
|
||||
component: () =>
|
||||
import('./components/TodoListPage').then(m => m.TodoListPage),
|
||||
mountPoint: rootRouteRef,
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'todo-list',
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
Reference in New Issue
Block a user