From ec2ad21d9e2a210043c0b7c978afed8d97fffb52 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 11 Feb 2022 03:43:51 +0100 Subject: [PATCH 01/30] chore: added migration for the created_by column Signed-off-by: blam --- .../migrations/20220211013100_created_by.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 plugins/scaffolder-backend/migrations/20220211013100_created_by.js diff --git a/plugins/scaffolder-backend/migrations/20220211013100_created_by.js b/plugins/scaffolder-backend/migrations/20220211013100_created_by.js new file mode 100644 index 0000000000..257cda21d2 --- /dev/null +++ b/plugins/scaffolder-backend/migrations/20220211013100_created_by.js @@ -0,0 +1,38 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + await knex.schema.alterTable('tasks', table => { + table + .text('created_by') + .nullable() + .comment('an entity ref of the user that created the task'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('tasks', table => { + table.dropColumn('created_by'); + }); +}; From 0ac523dd02bdd7d29858830da1ee19fc8dba84ea Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 11 Feb 2022 03:45:20 +0100 Subject: [PATCH 02/30] chore: added task page for my TaskSpec Signed-off-by: blam --- plugins/scaffolder/src/components/Router.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 774bc61638..d7158f81cb 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -38,6 +38,7 @@ import { scaffolderTaskRouteRef, selectedTemplateRouteRef, } from '../routes'; +import { MyTaskPage } from './MyTasksPage'; /** * The props for the entrypoint `ScaffolderPage` component the plugin. @@ -118,6 +119,7 @@ export const Router = (props: RouterProps) => { } /> + } /> } /> } /> Date: Fri, 11 Feb 2022 03:46:02 +0100 Subject: [PATCH 03/30] chore: added support for saving a createdBy field Signed-off-by: blam --- .../src/scaffolder/tasks/DatabaseTaskStore.ts | 39 ++++++++++++++++++- .../src/scaffolder/tasks/StorageTaskBroker.ts | 9 +++++ .../src/scaffolder/tasks/types.ts | 3 ++ .../scaffolder-backend/src/service/router.ts | 32 +++++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index d0a0fec0a6..6752972244 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -85,6 +85,32 @@ export class DatabaseTaskStore implements TaskStore { this.db = options.database; } + async list(options: Partial): Promise { + const results = await this.db('tasks') + .where({ + created_by: options.createdBy, + }) + .orderBy('created_at', 'desc') + .select(); + + return results.map(result => ({ + id: result.id, + spec: JSON.parse(result.spec), + status: result.status, + createdBy: result.created_by, + lastHeartbeatAt: + typeof result.last_heartbeat_at === 'string' + ? DateTime.fromSQL(result.last_heartbeat_at, { + zone: 'UTC', + }).toISO() + : result.last_heartbeat_at, + createdAt: + typeof result.created_at === 'string' + ? DateTime.fromSQL(result.created_at, { zone: 'UTC' }).toISO() + : result.created_at, + })); + } + async getTask(taskId: string): Promise { const [result] = await this.db('tasks') .where({ id: taskId }) @@ -99,8 +125,16 @@ export class DatabaseTaskStore implements TaskStore { id: result.id, spec, status: result.status, - lastHeartbeatAt: result.last_heartbeat_at, - createdAt: result.created_at, + lastHeartbeatAt: + typeof result.last_heartbeat_at === 'string' + ? DateTime.fromSQL(result.last_heartbeat_at, { + zone: 'UTC', + }).toISO() + : result.last_heartbeat_at, + createdAt: + typeof result.created_at === 'string' + ? DateTime.fromSQL(result.created_at, { zone: 'UTC' }).toISO() + : result.created_at, createdBy: result.created_by ?? undefined, secrets, }; @@ -119,6 +153,7 @@ export class DatabaseTaskStore implements TaskStore { secrets: options.secrets ? JSON.stringify(options.secrets) : undefined, created_by: options.createdBy ?? null, status: 'open', + created_by: ('createdBy' in spec && spec.createdBy) || null, }); return { taskId }; } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 17f07f702d..927bbf6842 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -150,6 +150,15 @@ export class StorageTaskBroker implements TaskBroker { private readonly storage: TaskStore, private readonly logger: Logger, ) {} + + async list(options?: Partial): Promise { + if (!options || !options.createdBy) { + throw new Error('Method not implemented.'); + } + + return await this.storage.list({ createdBy: options.createdBy }); + } + private deferredDispatch = defer(); /** diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index a3a9ef84d1..1fc7670e79 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -49,6 +49,7 @@ export type SerializedTask = { lastHeartbeatAt?: string; createdBy?: string; secrets?: TaskSecrets; + createdBy: string | null; }; /** @@ -132,6 +133,7 @@ export interface TaskBroker { after: number | undefined; }): Observable<{ events: SerializedTaskEvent[] }>; get(taskId: string): Promise; + list(options?: Partial): Promise; } /** @@ -192,6 +194,7 @@ export interface TaskStore { listStaleTasks(options: { timeoutS: number }): Promise<{ tasks: { taskId: string }[]; }>; + list(options: Partial): Promise; emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; listEvents({ diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 5453dcd234..58bb2e831a 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -129,6 +129,19 @@ export async function createRouter( actionsToRegister.forEach(action => actionRegistry.register(action)); workers.forEach(worker => worker.start()); + const getUserEntityRefFromToken = (backstageToken: string) => { + try { + const [_header, payload, _signature] = backstageToken.split('.'); + const parsedToken = JSON.parse(Buffer.from(payload, 'base64').toString()); + + return parsedToken.sub; + } catch (e) { + logger.warn('Could not parse token from request to create template'); + logger.debug(e); + return null; + } + }; + router .get( '/v2/templates/:namespace/:kind/:name/parameter-schema', @@ -207,6 +220,8 @@ export async function createRouter( const baseUrl = getEntityBaseUrl(template); + const createdBy = token && getUserEntityRefFromToken(token); + const taskSpec: TaskSpec = { apiVersion: template.apiVersion, steps: template.spec.steps.map((step, index) => ({ @@ -228,6 +243,7 @@ export async function createRouter( }), baseUrl, }, + createdBy, }; const result = await taskBroker.dispatch({ @@ -241,6 +257,22 @@ export async function createRouter( res.status(201).json({ id: result.taskId }); }) + .get('/v2/tasks', async (req, res) => { + const token = getBearerToken(req.headers.authorization); + const userEntityRef = token && getUserEntityRefFromToken(token); + + if (!userEntityRef) { + throw new InputError( + 'Could not find a valid user entity ref in the request', + ); + } + + const tasks = await taskBroker.list({ + createdBy: userEntityRef, + }); + + res.status(200).json(tasks); + }) .get('/v2/tasks/:taskId', async (req, res) => { const { taskId } = req.params; const task = await taskBroker.get(taskId); From bdf1deef18646e11c8e2b2b0fd17fb870909a716 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 11 Feb 2022 03:46:30 +0100 Subject: [PATCH 04/30] chore: added a sample frontend Signed-off-by: blam Signed-off-by: blam --- packages/app/src/components/Root/Root.tsx | 15 +- plugins/scaffolder/package.json | 1 + plugins/scaffolder/src/api.ts | 12 ++ .../src/components/MyTasksPage/MyTaskPage.tsx | 135 ++++++++++++++++++ .../src/components/MyTasksPage/index.tsx | 16 +++ yarn.lock | 19 +++ 6 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 plugins/scaffolder/src/components/MyTasksPage/MyTaskPage.tsx create mode 100644 plugins/scaffolder/src/components/MyTasksPage/index.tsx diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 30da01841c..ff8a260540 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -23,6 +23,8 @@ import MapIcon from '@material-ui/icons/MyLocation'; import LayersIcon from '@material-ui/icons/Layers'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; +import Add from '@material-ui/icons/Add'; +import List from '@material-ui/icons/List'; import SearchIcon from '@material-ui/icons/Search'; import MenuIcon from '@material-ui/icons/Menu'; import MoneyIcon from '@material-ui/icons/MonetizationOn'; @@ -46,6 +48,8 @@ import { SidebarScrollWrapper, SidebarSpace, useSidebarOpenState, + SidebarSubmenu, + SidebarSubmenuItem, } from '@backstage/core-components'; import { MyGroupsSidebarItem } from '@backstage/plugin-org'; import GroupIcon from '@material-ui/icons/People'; @@ -106,7 +110,16 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( - + + + + + + {/* End global nav */} diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index edc9185488..bb69adfe16 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -56,6 +56,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@rjsf/core": "^3.2.1", + "@material-table/core": "^4.3.29", "@rjsf/material-ui": "^3.2.1", "@types/json-schema": "^7.0.9", "@uiw/react-codemirror": "^4.7.0", diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 04103b9f8a..40c88ba68d 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -70,6 +70,18 @@ export class ScaffolderClient implements ScaffolderApi { this.useLongPollingLogs = options.useLongPollingLogs ?? false; } + async listTasks(): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); + const url = `${baseUrl}/v2/tasks`; + + const response = await this.fetchApi.fetch(url); + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return await response.json(); + } + async getIntegrationsList( options: ScaffolderGetIntegrationsListOptions, ): Promise { diff --git a/plugins/scaffolder/src/components/MyTasksPage/MyTaskPage.tsx b/plugins/scaffolder/src/components/MyTasksPage/MyTaskPage.tsx new file mode 100644 index 0000000000..36f7a68616 --- /dev/null +++ b/plugins/scaffolder/src/components/MyTasksPage/MyTaskPage.tsx @@ -0,0 +1,135 @@ +/* + * 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 { + Content, + Header, + Lifecycle, + Link, + Page, + Progress, +} from '@backstage/core-components'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { Grid } from '@material-ui/core'; +import React from 'react'; +import { scaffolderApiRef } from '../../api'; +import useAsync from 'react-use/lib/useAsync'; +import MaterialTable, { Column } from '@material-table/core'; +import { rootRouteRef } from '../../routes'; +import { Duration, Interval, DateTime } from 'luxon'; +import humanizeDuration from 'humanize-duration'; +import { + StatusError, + StatusPending, + StatusOK, +} from '@backstage/core-components'; + +const CreatedAtColumn = ({ createdAt }: { createdAt: string }) => { + const createdAtTime = DateTime.fromISO(createdAt); + const formatted = Interval.fromDateTimes(createdAtTime, DateTime.local()) + .toDuration() + .valueOf(); + + return

{humanizeDuration(formatted, { round: true })} ago

; +}; + +const TemplateTitle = ({ templateName }: { templateName?: string }) => { + const scaffolder = useApi(scaffolderApiRef); + const { value, loading, error } = useAsync( + () => + scaffolder.getTemplateParameterSchema({ + kind: 'Template', + namespace: 'default', + name: templateName, + }), + [scaffolder, templateName], + ); + + if (loading) { + return null; + } + + return

{value?.title}

; +}; + +const Status = ({ status }) => { + switch (status) { + case 'processing': + return {status}; + case 'completed': + return {status}; + case 'error': + default: + return {status}; + } +}; +export const MyTaskPage = () => { + const scaffolderApi = useApi(scaffolderApiRef); + const { value, loading, error } = useAsync( + () => scaffolderApi.listTasks(), + [scaffolderApi], + ); + + const rootLink = useRouteRef(rootRouteRef); + + return ( + +
+ All my tasks + + } + subtitle="All tasks that have been started by me" + /> + + {loading ? ( + + ) : ( + ( + {row.id} + ), + }, + { + title: 'Name', + render: row => ( + + ), + }, + { + title: 'Created', + field: 'createdAt', + render: row => , + }, + { + title: 'Status', + field: 'status', + render: row => , + }, + ]} + /> + )} + + + ); +}; diff --git a/plugins/scaffolder/src/components/MyTasksPage/index.tsx b/plugins/scaffolder/src/components/MyTasksPage/index.tsx new file mode 100644 index 0000000000..2535904349 --- /dev/null +++ b/plugins/scaffolder/src/components/MyTasksPage/index.tsx @@ -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 { MyTaskPage } from './MyTaskPage'; diff --git a/yarn.lock b/yarn.lock index 4ed8ee03bf..71c81d03e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3988,6 +3988,25 @@ react-beautiful-dnd "^13.0.0" react-double-scrollbar "0.0.15" +"@material-table/core@^4.3.29": + version "4.3.29" + resolved "https://registry.npmjs.org/@material-table/core/-/core-4.3.29.tgz#7f60fe4fc191e016d9be663fa437e00b012eae05" + integrity sha512-7vz1oc3bo1eSzk450NgpA61xQjlgCBg7q1TDGrB8ZDMyx2NvLRxTyNBtW8JEPLaQLoCWoDFK7F/VHOYRN1fKuQ== + dependencies: + "@babel/runtime" "^7.12.5" + "@date-io/date-fns" "^1.3.13" + "@material-ui/icons" "^4.11.2" + "@material-ui/pickers" "^3.2.10" + "@material-ui/styles" "^4.11.4" + classnames "^2.2.6" + date-fns "^2.16.1" + debounce "^1.2.0" + fast-deep-equal "^3.1.3" + prop-types "^15.7.2" + react-beautiful-dnd "^13.0.0" + react-double-scrollbar "0.0.15" + uuid "^3.4.0" + "@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2", "@material-ui/core@^4.9.10", "@material-ui/core@^4.9.13": version "4.12.4" resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.4.tgz#4ac17488e8fcaf55eb6a7f5efb2a131e10138a73" From 944651e021dadc058887694c668c6d2f218b374c Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 28 Apr 2022 09:15:05 -0300 Subject: [PATCH 05/30] feat: update listTasks flow Signed-off-by: Rogerio Angeliski --- .../src/scaffolder/tasks/DatabaseTaskStore.ts | 16 +++-- .../tasks/StorageTaskBroker.test.ts | 26 ++++++++ .../src/scaffolder/tasks/StorageTaskBroker.ts | 6 +- .../src/service/router.test.ts | 61 +++++++++++++++++++ .../scaffolder-backend/src/service/router.ts | 9 +-- 5 files changed, 99 insertions(+), 19 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 6752972244..1c4b9b5210 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -86,12 +86,15 @@ export class DatabaseTaskStore implements TaskStore { } async list(options: Partial): Promise { - const results = await this.db('tasks') - .where({ + const queryBuilder = this.db('tasks'); + + if (options.createdBy) { + queryBuilder.where({ created_by: options.createdBy, - }) - .orderBy('created_at', 'desc') - .select(); + }); + } + + const results = await queryBuilder.orderBy('created_at', 'desc').select(); return results.map(result => ({ id: result.id, @@ -153,7 +156,8 @@ export class DatabaseTaskStore implements TaskStore { secrets: options.secrets ? JSON.stringify(options.secrets) : undefined, created_by: options.createdBy ?? null, status: 'open', - created_by: ('createdBy' in spec && spec.createdBy) || null, + created_by: + ('createdBy' in options.spec && options.spec.createdBy) || null, }); return { taskId }; } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index bb375e5a0f..7ed2eb620a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -202,4 +202,30 @@ describe('StorageTaskBroker', () => { expect(task.done).toBe(true); }); + + it('should list all tasks', async () => { + const broker = new StorageTaskBroker(storage, logger); + const { taskId } = await broker.dispatch({ spec: {} as TaskSpec }); + + const promise = broker.list(); + await expect(promise).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: taskId, + }), + ]), + ); + }); + + it('should list only tasks createdBy a specific user', async () => { + const broker = new StorageTaskBroker(storage, logger); + const { taskId } = await broker.dispatch({ + spec: { createdBy: 'user:default/foo' } as TaskSpec, + }); + + const task = await storage.getTask(taskId); + + const promise = broker.list({ createdBy: 'user:default/foo' }); + await expect(promise).resolves.toEqual([task]); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 927bbf6842..8e3dadd844 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -152,11 +152,7 @@ export class StorageTaskBroker implements TaskBroker { ) {} async list(options?: Partial): Promise { - if (!options || !options.createdBy) { - throw new Error('Method not implemented.'); - } - - return await this.storage.list({ createdBy: options.createdBy }); + return await this.storage.list({ createdBy: options?.createdBy }); } private deferredDispatch = defer(); diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 14c543249e..52f11357a6 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -131,6 +131,7 @@ describe('createRouter', () => { jest.spyOn(taskBroker, 'dispatch'); jest.spyOn(taskBroker, 'get'); + jest.spyOn(taskBroker, 'list'); jest.spyOn(taskBroker, 'event$'); const router = await createRouter({ @@ -294,6 +295,65 @@ describe('createRouter', () => { }); }); + describe('GET /v2/tasks', () => { + it('return all tasks', async () => { + (taskBroker.list as jest.Mocked['list']).mockResolvedValue([ + { + id: 'a-random-id', + spec: {} as any, + status: 'completed', + createdAt: '', + createdBy: '', + }, + ]); + + const response = await request(app).get(`/v2/tasks`); + expect(taskBroker.list).toBeCalledWith({ + createdBy: undefined, + }); + expect(response.status).toEqual(200); + expect(response.body).toStrictEqual([ + { + id: 'a-random-id', + spec: {} as any, + status: 'completed', + createdAt: '', + createdBy: '', + }, + ]); + }); + + it('return filtered tasks', async () => { + (taskBroker.list as jest.Mocked['list']).mockResolvedValue([ + { + id: 'a-random-id', + spec: {} as any, + status: 'completed', + createdAt: '', + createdBy: 'user:default/foo', + }, + ]); + + const response = await request(app).get( + `/v2/tasks?createdBy=user:default/foo`, + ); + expect(taskBroker.list).toBeCalledWith({ + createdBy: 'user:default/foo', + }); + + expect(response.status).toEqual(200); + expect(response.body).toStrictEqual([ + { + id: 'a-random-id', + spec: {} as any, + status: 'completed', + createdAt: '', + createdBy: 'user:default/foo', + }, + ]); + }); + }); + describe('GET /v2/tasks/:taskId', () => { it('does not divulge secrets', async () => { (taskBroker.get as jest.Mocked['get']).mockResolvedValue({ @@ -302,6 +362,7 @@ describe('createRouter', () => { status: 'completed', createdAt: '', secrets: { backstageToken: 'secret' }, + createdBy: '', }); const response = await request(app).get(`/v2/tasks/a-random-id`); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 58bb2e831a..61fbe74b8b 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -258,14 +258,7 @@ export async function createRouter( res.status(201).json({ id: result.taskId }); }) .get('/v2/tasks', async (req, res) => { - const token = getBearerToken(req.headers.authorization); - const userEntityRef = token && getUserEntityRefFromToken(token); - - if (!userEntityRef) { - throw new InputError( - 'Could not find a valid user entity ref in the request', - ); - } + const userEntityRef = req.query.createdBy?.toString(); const tasks = await taskBroker.list({ createdBy: userEntityRef, From 6c02fd3c29977b64be140758dfdcf790a41a2abc Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 28 Apr 2022 09:15:41 -0300 Subject: [PATCH 06/30] feat: update tasks page Signed-off-by: Rogerio Angeliski --- plugins/scaffolder/dev/index.tsx | 16 +- plugins/scaffolder/src/api.test.ts | 80 +++++++ plugins/scaffolder/src/api.ts | 16 +- .../ActionsPage/ActionsPage.test.tsx | 1 + .../ListTasksPage/ListTaskPage.test.tsx | 226 ++++++++++++++++++ .../ListTasksPage/ListTasksPage.tsx | 144 +++++++++++ .../ListTasksPage/OwnerListPicker.test.tsx | 47 ++++ .../ListTasksPage/OwnerListPicker.tsx | 135 +++++++++++ .../columns/CreatedAtColumn.test.tsx | 34 +++ .../ListTasksPage/columns/CreatedAtColumn.tsx | 27 +++ .../columns/OwnerEntityColumn.test.tsx | 82 +++++++ .../columns/OwnerEntityColumn.tsx | 50 ++++ .../columns/TaskStatusColumn.test.tsx | 38 +++ .../columns/TaskStatusColumn.tsx | 33 +++ .../columns/TemplateTitleColumn.test.tsx | 48 ++++ .../columns/TemplateTitleColumn.tsx | 33 +++ .../components/ListTasksPage/columns/index.ts | 19 ++ .../{MyTasksPage => ListTasksPage}/index.tsx | 2 +- .../src/components/MyTasksPage/MyTaskPage.tsx | 135 ----------- plugins/scaffolder/src/components/Router.tsx | 8 +- .../TemplatePage/TemplatePage.test.tsx | 1 + plugins/scaffolder/src/plugin.ts | 5 +- plugins/scaffolder/src/routes.ts | 6 + plugins/scaffolder/src/types.ts | 9 + 24 files changed, 1051 insertions(+), 144 deletions(-) create mode 100644 plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx create mode 100644 plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx create mode 100644 plugins/scaffolder/src/components/ListTasksPage/OwnerListPicker.test.tsx create mode 100644 plugins/scaffolder/src/components/ListTasksPage/OwnerListPicker.tsx create mode 100644 plugins/scaffolder/src/components/ListTasksPage/columns/CreatedAtColumn.test.tsx create mode 100644 plugins/scaffolder/src/components/ListTasksPage/columns/CreatedAtColumn.tsx create mode 100644 plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx create mode 100644 plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx create mode 100644 plugins/scaffolder/src/components/ListTasksPage/columns/TaskStatusColumn.test.tsx create mode 100644 plugins/scaffolder/src/components/ListTasksPage/columns/TaskStatusColumn.tsx create mode 100644 plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx create mode 100644 plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.tsx create mode 100644 plugins/scaffolder/src/components/ListTasksPage/columns/index.ts rename plugins/scaffolder/src/components/{MyTasksPage => ListTasksPage}/index.tsx (92%) delete mode 100644 plugins/scaffolder/src/components/MyTasksPage/MyTaskPage.tsx diff --git a/plugins/scaffolder/dev/index.tsx b/plugins/scaffolder/dev/index.tsx index 03632f434d..83496c719d 100644 --- a/plugins/scaffolder/dev/index.tsx +++ b/plugins/scaffolder/dev/index.tsx @@ -25,7 +25,11 @@ import { import React from 'react'; import { scaffolderApiRef, ScaffolderClient } from '../src'; import { ScaffolderPage } from '../src/plugin'; -import { discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api'; +import { + discoveryApiRef, + fetchApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; import { CatalogEntityPage } from '@backstage/plugin-catalog'; createDevApp() @@ -49,9 +53,15 @@ createDevApp() discoveryApi: discoveryApiRef, fetchApi: fetchApiRef, scmIntegrationsApi: scmIntegrationsApiRef, + identityApi: identityApiRef, }, - factory: ({ discoveryApi, fetchApi, scmIntegrationsApi }) => - new ScaffolderClient({ discoveryApi, fetchApi, scmIntegrationsApi }), + factory: ({ discoveryApi, fetchApi, scmIntegrationsApi, identityApi }) => + new ScaffolderClient({ + discoveryApi, + fetchApi, + scmIntegrationsApi, + identityApi, + }), }) .addPage({ path: '/create', diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index 4e47c7f239..a5a3a3c51e 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -33,6 +33,13 @@ describe('api', () => { const discoveryApi = { getBaseUrl: async () => mockBaseUrl }; const fetchApi = new MockFetchApi(); + const identityApi = { + getBackstageIdentity: jest.fn(), + getProfileInfo: jest.fn(), + getCredentials: jest.fn(), + signOut: jest.fn(), + }; + const scmIntegrationsApi = ScmIntegrations.fromConfig( new ConfigReader({ integrations: { @@ -51,6 +58,7 @@ describe('api', () => { scmIntegrationsApi, discoveryApi, fetchApi, + identityApi, }); }); @@ -129,6 +137,7 @@ describe('api', () => { scmIntegrationsApi, discoveryApi, fetchApi, + identityApi, useLongPollingLogs: true, }); }); @@ -305,4 +314,75 @@ describe('api', () => { }); }); }); + + describe('listTasks', () => { + it('should list all tasks', async () => { + server.use( + rest.get(`${mockBaseUrl}/v2/tasks`, (req, res, ctx) => { + const createdBy = req.url.searchParams.get('createdBy'); + + if (createdBy) { + return res( + ctx.json([ + { + createdBy, + }, + ]), + ); + } + + return res( + ctx.json([ + { + createdBy: null, + }, + { + createdBy: null, + }, + ]), + ); + }), + ); + + const result = await apiClient.listTasks('all'); + expect(identityApi.getBackstageIdentity).not.toBeCalled(); + expect(result).toHaveLength(2); + }); + it('should list task using the current user as owner', async () => { + server.use( + rest.get(`${mockBaseUrl}/v2/tasks`, (req, res, ctx) => { + const createdBy = req.url.searchParams.get('createdBy'); + + if (createdBy) { + return res( + ctx.json([ + { + createdBy, + }, + ]), + ); + } + + return res( + ctx.json([ + { + createdBy: null, + }, + { + createdBy: null, + }, + ]), + ); + }), + ); + + identityApi.getBackstageIdentity.mockResolvedValueOnce({ + userEntityRef: 'user:default/foo', + }); + + const result = await apiClient.listTasks('owned'); + expect(identityApi.getBackstageIdentity).toBeCalled(); + expect(result).toHaveLength(1); + }); + }); }); diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 40c88ba68d..8f2319cb9d 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -19,6 +19,7 @@ import { createApiRef, DiscoveryApi, FetchApi, + IdentityApi, } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -36,6 +37,7 @@ import { ScaffolderGetIntegrationsListOptions, ScaffolderGetIntegrationsListResponse, ScaffolderTask, + TasksOwnerFilterKind, } from './types'; /** @@ -56,11 +58,13 @@ export class ScaffolderClient implements ScaffolderApi { private readonly discoveryApi: DiscoveryApi; private readonly scmIntegrationsApi: ScmIntegrationRegistry; private readonly fetchApi: FetchApi; + private readonly identityApi: IdentityApi; private readonly useLongPollingLogs: boolean; constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi; + identityApi: IdentityApi; scmIntegrationsApi: ScmIntegrationRegistry; useLongPollingLogs?: boolean; }) { @@ -68,11 +72,19 @@ export class ScaffolderClient implements ScaffolderApi { this.fetchApi = options.fetchApi ?? { fetch }; this.scmIntegrationsApi = options.scmIntegrationsApi; this.useLongPollingLogs = options.useLongPollingLogs ?? false; + this.identityApi = options.identityApi; } - async listTasks(): Promise { + async listTasks(createdBy: TasksOwnerFilterKind): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); - const url = `${baseUrl}/v2/tasks`; + + let query = ''; + if (createdBy === 'owned') { + const { userEntityRef } = await this.identityApi.getBackstageIdentity(); + query = `createdBy=${encodeURIComponent(userEntityRef)}`; + } + + const url = `${baseUrl}/v2/tasks?${query}`; const response = await this.fetchApi.fetch(url); if (!response.ok) { diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx index aff6681037..40683f01ba 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx @@ -28,6 +28,7 @@ const scaffolderApiMock: jest.Mocked = { getTask: jest.fn(), streamLogs: jest.fn(), listActions: jest.fn(), + listTasks: jest.fn(), }; const apis = TestApiRegistry.from([scaffolderApiRef, scaffolderApiMock]); diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx new file mode 100644 index 0000000000..fced397ad3 --- /dev/null +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx @@ -0,0 +1,226 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + CatalogApi, + catalogApiRef, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { identityApiRef } from '@backstage/core-plugin-api'; +import { ListTasksPage } from './ListTasksPage'; +import { ScaffolderApi } from '../../types'; +import { scaffolderApiRef } from '../../api'; +import { rootRouteRef } from '../../routes'; +import { act, fireEvent } from '@testing-library/react'; + +describe('', () => { + const catalogApi: jest.Mocked = { + getEntityByRef: jest.fn(), + } as any; + + const identityApi = { + getBackstageIdentity: jest.fn(), + getProfileInfo: jest.fn(), + getCredentials: jest.fn(), + signOut: jest.fn(), + }; + + const scaffolderApiMock: jest.Mocked = { + scaffold: jest.fn(), + getTemplateParameterSchema: jest.fn(), + listTasks: jest.fn(), + } as any; + + it('should render the page', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'service', + metadata: { + name: 'test', + }, + spec: { + profile: { + displayName: 'BackUser', + }, + }, + }; + catalogApi.getEntityByRef.mockResolvedValue(entity); + + scaffolderApiMock.listTasks.mockResolvedValue([]); + + const { getByText } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + '/root': rootRouteRef, + }, + }, + ); + + expect(getByText('List template tasks')).toBeInTheDocument(); + expect(getByText('All tasks that have been started')).toBeInTheDocument(); + expect(getByText('Tasks')).toBeInTheDocument(); + }); + + it('should render the task I am owner', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'User', + metadata: { + name: 'foo', + }, + spec: { + profile: { + displayName: 'BackUser', + }, + }, + }; + catalogApi.getEntityByRef.mockResolvedValue(entity); + scaffolderApiMock.listTasks.mockResolvedValue([ + { + id: 'a-random-id', + spec: { + createdBy: 'user:default/foo', + } as any, + status: 'completed', + createdAt: '', + lastHeartbeatAt: '', + }, + ]); + scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ + title: 'One Template', + steps: [], + }); + + const { getByText } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + '/root': rootRouteRef, + }, + }, + ); + + expect(scaffolderApiMock.listTasks).toBeCalledWith('owned'); + expect(getByText('List template tasks')).toBeInTheDocument(); + expect(getByText('All tasks that have been started')).toBeInTheDocument(); + expect(getByText('Tasks')).toBeInTheDocument(); + expect(getByText('One Template')).toBeInTheDocument(); + expect(getByText('BackUser')).toBeInTheDocument(); + }); + + it('should render all tasks', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'User', + metadata: { + name: 'foo', + }, + spec: { + profile: { + displayName: 'BackUser', + }, + }, + }; + catalogApi.getEntityByRef + .mockResolvedValue(entity) + .mockResolvedValue(entity) + .mockResolvedValue({ + ...entity, + spec: { + profile: { + displayName: 'OtherUser', + }, + }, + }); + scaffolderApiMock.listTasks + .mockResolvedValue([ + { + id: 'a-random-id', + spec: { + createdBy: 'user:default/foo', + } as any, + status: 'completed', + createdAt: '', + lastHeartbeatAt: '', + }, + ]) + .mockResolvedValue([ + { + id: 'b-random-id', + spec: { + createdBy: 'user:default/boo', + } as any, + status: 'completed', + createdAt: '', + lastHeartbeatAt: '', + }, + ]); + + scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ + title: 'One Template', + steps: [], + }); + + const { getByText } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + '/root': rootRouteRef, + }, + }, + ); + + await act(async () => { + const allButton = await getByText('All'); + fireEvent.click(allButton); + }); + + expect(scaffolderApiMock.listTasks).toBeCalledWith('all'); + expect(getByText('One Template')).toBeInTheDocument(); + expect(getByText('OtherUser')).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx new file mode 100644 index 0000000000..d66355b0a5 --- /dev/null +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx @@ -0,0 +1,144 @@ +/* + * 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 { + Content, + EmptyState, + ErrorPanel, + Header, + Lifecycle, + Link, + Page, + Progress, +} from '@backstage/core-components'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { CatalogFilterLayout } from '@backstage/plugin-catalog-react'; +import useAsync from 'react-use/lib/useAsync'; +import MaterialTable from '@material-table/core'; +import React, { useState } from 'react'; +import { scaffolderApiRef } from '../../api'; +import { rootRouteRef } from '../../routes'; +import { OwnerListPicker } from './OwnerListPicker'; +import { TasksOwnerFilterKind } from '../../types'; +import { + CreatedAtColumn, + OwnerEntityColumn, + TaskStatusColumn, + TemplateTitleColumn, +} from './columns'; + +export interface MyTaskPageProps { + initiallySelectedFilter?: TasksOwnerFilterKind; +} + +const ListTaskPageContent = (props: MyTaskPageProps) => { + const { initiallySelectedFilter = 'owned' } = props; + + const scaffolderApi = useApi(scaffolderApiRef); + const rootLink = useRouteRef(rootRouteRef); + + const [ownerFilter, setOwnerFilter] = useState(initiallySelectedFilter); + const { value, loading, error } = useAsync( + () => scaffolderApi.listTasks(ownerFilter), + [scaffolderApi, ownerFilter], + ); + + if (loading) { + return ; + } + + if (error) { + return ( + <> + + + + ); + } + + return ( + + + setOwnerFilter(id)} + /> + + + ( + {row.id} + ), + }, + { + title: 'Template', + render: row => ( + + ), + }, + { + title: 'Created', + field: 'createdAt', + render: row => , + }, + { + title: 'Owner', + field: 'createdBy', + render: row => ( + + ), + }, + { + title: 'Status', + field: 'status', + render: row => , + }, + ]} + /> + + + ); +}; + +export const ListTasksPage = (props: MyTaskPageProps) => { + return ( + +
+ List template tasks + + } + subtitle="All tasks that have been started" + /> + + + + + ); +}; diff --git a/plugins/scaffolder/src/components/ListTasksPage/OwnerListPicker.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/OwnerListPicker.test.tsx new file mode 100644 index 0000000000..bf0ae6d946 --- /dev/null +++ b/plugins/scaffolder/src/components/ListTasksPage/OwnerListPicker.test.tsx @@ -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 { renderInTestApp } from '@backstage/test-utils'; + +import React from 'react'; +import { OwnerListPicker } from './OwnerListPicker'; +import { fireEvent } from '@testing-library/react'; + +describe('', () => { + it('should render the tasks owner filter', async () => { + const props = { + filter: 'owned', + onSelectOwner: jest.fn(), + }; + + const { getByText } = await renderInTestApp(); + + expect(await getByText('Owned')).toBeDefined(); + expect(await getByText('All')).toBeDefined(); + }); + + it('should call the function on select other item', async () => { + const props = { + filter: 'owned', + onSelectOwner: jest.fn(), + }; + + const { getByText } = await renderInTestApp(); + + fireEvent.click(await getByText('All')); + expect(props.onSelectOwner).toBeCalledWith('all'); + }); +}); diff --git a/plugins/scaffolder/src/components/ListTasksPage/OwnerListPicker.tsx b/plugins/scaffolder/src/components/ListTasksPage/OwnerListPicker.tsx new file mode 100644 index 0000000000..e239938cb9 --- /dev/null +++ b/plugins/scaffolder/src/components/ListTasksPage/OwnerListPicker.tsx @@ -0,0 +1,135 @@ +/* + * 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 { IconComponent } from '@backstage/core-plugin-api'; +import { + Card, + List, + ListItemIcon, + ListItemText, + makeStyles, + MenuItem, + Theme, + Typography, +} from '@material-ui/core'; +import SettingsIcon from '@material-ui/icons/Settings'; +import React, { Fragment } from 'react'; + +import AllIcon from '@material-ui/icons/FontDownload'; + +import { TasksOwnerFilterKind } from '../../types'; + +const useStyles = makeStyles( + theme => ({ + root: { + backgroundColor: 'rgba(0, 0, 0, .11)', + boxShadow: 'none', + margin: theme.spacing(1, 0, 1, 0), + }, + title: { + margin: theme.spacing(1, 0, 0, 1), + textTransform: 'uppercase', + fontSize: 12, + fontWeight: 'bold', + }, + listIcon: { + minWidth: 30, + color: theme.palette.text.primary, + }, + menuItem: { + minHeight: theme.spacing(6), + }, + groupWrapper: { + margin: theme.spacing(1, 1, 2, 1), + }, + }), + { + name: 'ScaffolderReactOwnerListPicker', + }, +); + +export type ButtonGroup = { + name: string; + items: { + id: 'owned' | 'starred' | 'all'; + label: string; + icon?: IconComponent; + }[]; +}; + +function getFilterGroups(): ButtonGroup[] { + return [ + { + name: 'Task Owner', + items: [ + { + id: 'owned', + label: 'Owned', + icon: SettingsIcon, + }, + { + id: 'all', + label: 'All', + icon: AllIcon, + }, + ], + }, + ]; +} + +export const OwnerListPicker = (props: { + filter: string; + onSelectOwner: (id: TasksOwnerFilterKind) => void; +}) => { + const { filter, onSelectOwner } = props; + const classes = useStyles(); + + const filterGroups = getFilterGroups(); + return ( + + {filterGroups.map(group => ( + + + {group.name} + + + + {group.items.map(item => ( + onSelectOwner(item.id as TasksOwnerFilterKind)} + selected={item.id === filter} + className={classes.menuItem} + data-testid={`owner-picker-${item.id}`} + > + {item.icon && ( + + + + )} + + {item.label} + + + ))} + + + + ))} + + ); +}; diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/CreatedAtColumn.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/CreatedAtColumn.test.tsx new file mode 100644 index 0000000000..a30c1d370b --- /dev/null +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/CreatedAtColumn.test.tsx @@ -0,0 +1,34 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; + +import React from 'react'; +import { CreatedAtColumn } from './CreatedAtColumn'; +import { DateTime } from 'luxon'; + +describe('', () => { + it('should render the column with the time', async () => { + const props = { + createdAt: DateTime.now().toISO(), + }; + + const { getByText } = await renderInTestApp(); + + const text = getByText('0 seconds ago'); + expect(text).toBeDefined(); + }); +}); diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/CreatedAtColumn.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/CreatedAtColumn.tsx new file mode 100644 index 0000000000..8e48d4a561 --- /dev/null +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/CreatedAtColumn.tsx @@ -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 { DateTime, Interval } from 'luxon'; +import humanizeDuration from 'humanize-duration'; +import React from 'react'; + +export const CreatedAtColumn = ({ createdAt }: { createdAt: string }) => { + const createdAtTime = DateTime.fromISO(createdAt); + const formatted = Interval.fromDateTimes(createdAtTime, DateTime.local()) + .toDuration() + .valueOf(); + + return

{humanizeDuration(formatted, { round: true })} ago

; +}; diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx new file mode 100644 index 0000000000..c2672e22bd --- /dev/null +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx @@ -0,0 +1,82 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + CatalogApi, + catalogApiRef, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { OwnerEntityColumn } from './OwnerEntityColumn'; +import { identityApiRef } from '@backstage/core-plugin-api'; + +describe('', () => { + const catalogApi: jest.Mocked = { + getEntityByRef: jest.fn(), + } as any; + + const identityApi = { + getBackstageIdentity: jest.fn(), + getProfileInfo: jest.fn(), + getCredentials: jest.fn(), + signOut: jest.fn(), + }; + + it('should render the column with the time', async () => { + const props = { + entityRef: 'user:default/foo', + }; + + const entity: Entity = { + apiVersion: 'v1', + kind: 'User', + metadata: { + name: 'test', + }, + spec: { + profile: { + displayName: 'BackUser', + }, + }, + }; + catalogApi.getEntityByRef.mockResolvedValue(entity); + + const { getByText, getByRole } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(getByRole('link')).toHaveAttribute( + 'href', + '/catalog/default/user/foo', + ); + const text = getByText('BackUser'); + expect(text).toBeDefined(); + }); +}); diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx new file mode 100644 index 0000000000..d2ca8f2620 --- /dev/null +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx @@ -0,0 +1,50 @@ +/* + * 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 { Link } from '@backstage/core-components'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import React from 'react'; + +import useAsync from 'react-use/lib/useAsync'; +import { ListItemText } from '@material-ui/core'; + +import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; +import { parseEntityRef, UserEntity } from '@backstage/catalog-model'; + +export const OwnerEntityColumn = ({ + entityRef, +}: { + entityRef?: string | null; +}) => { + const catalogApi = useApi(catalogApiRef); + const catalogEntityRoute = useRouteRef(entityRouteRef); + + const { value, loading, error } = useAsync( + () => catalogApi.getEntityByRef(entityRef || ''), + [catalogApi, entityRef], + ); + + if (loading || error) { + return null; + } + + return ( + + + + ); +}; diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/TaskStatusColumn.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/TaskStatusColumn.test.tsx new file mode 100644 index 0000000000..5eabcf72ff --- /dev/null +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/TaskStatusColumn.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; + +import React from 'react'; +import { TaskStatusColumn } from './TaskStatusColumn'; + +describe('', () => { + it.each(['processing', 'error', 'completed'])( + 'should render the column with the status %s', + async status => { + const props = { + status: status, + }; + + const { getByText } = await renderInTestApp( + , + ); + + const text = getByText(status); + expect(text).toBeDefined(); + }, + ); +}); diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/TaskStatusColumn.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/TaskStatusColumn.tsx new file mode 100644 index 0000000000..995e505c36 --- /dev/null +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/TaskStatusColumn.tsx @@ -0,0 +1,33 @@ +/* + * 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 { + StatusError, + StatusOK, + StatusPending, +} from '@backstage/core-components'; +import React from 'react'; + +export const TaskStatusColumn = ({ status }: { status: string }) => { + switch (status) { + case 'processing': + return {status}; + case 'completed': + return {status}; + case 'error': + default: + return {status}; + } +}; diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx new file mode 100644 index 0000000000..5ce11da28a --- /dev/null +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx @@ -0,0 +1,48 @@ +/* + * 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 { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; + +import React from 'react'; +import { TemplateTitleColumn } from './TemplateTitleColumn'; +import { scaffolderApiRef } from '../../../api'; +import { ScaffolderApi } from '../../../types'; + +describe('', () => { + const scaffolderApiMock: jest.Mocked = { + scaffold: jest.fn(), + getTemplateParameterSchema: jest.fn(), + } as any; + + it('should render the column with the time', async () => { + const props = { + entityRef: 'template:default/one-template', + }; + scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ + title: 'One Template', + steps: [], + }); + + const { getByText } = await renderInTestApp( + + + , + ); + + const text = getByText('One Template'); + expect(text).toBeDefined(); + }); +}); diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.tsx new file mode 100644 index 0000000000..8f86734809 --- /dev/null +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.tsx @@ -0,0 +1,33 @@ +/* + * 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 { useApi } from '@backstage/core-plugin-api'; +import React from 'react'; +import { scaffolderApiRef } from '../../../api'; +import useAsync from 'react-use/lib/useAsync'; + +export const TemplateTitleColumn = ({ entityRef }: { entityRef?: string }) => { + const scaffolder = useApi(scaffolderApiRef); + const { value, loading, error } = useAsync( + () => scaffolder.getTemplateParameterSchema(entityRef || ''), + [scaffolder, entityRef], + ); + + if (loading || error) { + return null; + } + + return

{value?.title}

; +}; diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/index.ts b/plugins/scaffolder/src/components/ListTasksPage/columns/index.ts new file mode 100644 index 0000000000..f6f356a3da --- /dev/null +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { CreatedAtColumn } from './CreatedAtColumn'; +export { OwnerEntityColumn } from './OwnerEntityColumn'; +export { TaskStatusColumn } from './TaskStatusColumn'; +export { TemplateTitleColumn } from './TemplateTitleColumn'; diff --git a/plugins/scaffolder/src/components/MyTasksPage/index.tsx b/plugins/scaffolder/src/components/ListTasksPage/index.tsx similarity index 92% rename from plugins/scaffolder/src/components/MyTasksPage/index.tsx rename to plugins/scaffolder/src/components/ListTasksPage/index.tsx index 2535904349..56fc7d3d28 100644 --- a/plugins/scaffolder/src/components/MyTasksPage/index.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/index.tsx @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { MyTaskPage } from './MyTaskPage'; +export { ListTasksPage } from './ListTasksPage'; diff --git a/plugins/scaffolder/src/components/MyTasksPage/MyTaskPage.tsx b/plugins/scaffolder/src/components/MyTasksPage/MyTaskPage.tsx deleted file mode 100644 index 36f7a68616..0000000000 --- a/plugins/scaffolder/src/components/MyTasksPage/MyTaskPage.tsx +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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 { - Content, - Header, - Lifecycle, - Link, - Page, - Progress, -} from '@backstage/core-components'; -import { useApi, useRouteRef } from '@backstage/core-plugin-api'; -import { Grid } from '@material-ui/core'; -import React from 'react'; -import { scaffolderApiRef } from '../../api'; -import useAsync from 'react-use/lib/useAsync'; -import MaterialTable, { Column } from '@material-table/core'; -import { rootRouteRef } from '../../routes'; -import { Duration, Interval, DateTime } from 'luxon'; -import humanizeDuration from 'humanize-duration'; -import { - StatusError, - StatusPending, - StatusOK, -} from '@backstage/core-components'; - -const CreatedAtColumn = ({ createdAt }: { createdAt: string }) => { - const createdAtTime = DateTime.fromISO(createdAt); - const formatted = Interval.fromDateTimes(createdAtTime, DateTime.local()) - .toDuration() - .valueOf(); - - return

{humanizeDuration(formatted, { round: true })} ago

; -}; - -const TemplateTitle = ({ templateName }: { templateName?: string }) => { - const scaffolder = useApi(scaffolderApiRef); - const { value, loading, error } = useAsync( - () => - scaffolder.getTemplateParameterSchema({ - kind: 'Template', - namespace: 'default', - name: templateName, - }), - [scaffolder, templateName], - ); - - if (loading) { - return null; - } - - return

{value?.title}

; -}; - -const Status = ({ status }) => { - switch (status) { - case 'processing': - return {status}; - case 'completed': - return {status}; - case 'error': - default: - return {status}; - } -}; -export const MyTaskPage = () => { - const scaffolderApi = useApi(scaffolderApiRef); - const { value, loading, error } = useAsync( - () => scaffolderApi.listTasks(), - [scaffolderApi], - ); - - const rootLink = useRouteRef(rootRouteRef); - - return ( - -
- All my tasks - - } - subtitle="All tasks that have been started by me" - /> - - {loading ? ( - - ) : ( - ( - {row.id} - ), - }, - { - title: 'Name', - render: row => ( - - ), - }, - { - title: 'Created', - field: 'createdAt', - render: row => , - }, - { - title: 'Status', - field: 'status', - render: row => , - }, - ]} - /> - )} - - - ); -}; diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index d7158f81cb..6beaa3419a 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -35,10 +35,11 @@ import { useElementFilter } from '@backstage/core-plugin-api'; import { actionsRouteRef, editRouteRef, + scaffolderListTaskRouteRef, scaffolderTaskRouteRef, selectedTemplateRouteRef, } from '../routes'; -import { MyTaskPage } from './MyTasksPage'; +import { ListTasksPage } from './ListTasksPage'; /** * The props for the entrypoint `ScaffolderPage` component the plugin. @@ -119,7 +120,10 @@ export const Router = (props: RouterProps) => { } /> - } /> + } + /> } /> } /> = { getTask: jest.fn(), streamLogs: jest.fn(), listActions: jest.fn(), + listTasks: jest.fn(), }; const featureFlagsApiMock: jest.Mocked = { diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index 9bbb52a71f..5ea4ae49a4 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -30,6 +30,7 @@ import { createRoutableExtension, discoveryApiRef, fetchApiRef, + identityApiRef, } from '@backstage/core-plugin-api'; import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker/OwnedEntityPicker'; import { EntityTagsPicker } from './components/fields/EntityTagsPicker/EntityTagsPicker'; @@ -47,12 +48,14 @@ export const scaffolderPlugin = createPlugin({ discoveryApi: discoveryApiRef, scmIntegrationsApi: scmIntegrationsApiRef, fetchApi: fetchApiRef, + identityApi: identityApiRef, }, - factory: ({ discoveryApi, scmIntegrationsApi, fetchApi }) => + factory: ({ discoveryApi, scmIntegrationsApi, fetchApi, identityApi }) => new ScaffolderClient({ discoveryApi, scmIntegrationsApi, fetchApi, + identityApi, }), }), ], diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts index 8ec9f9b68a..be0d69135a 100644 --- a/plugins/scaffolder/src/routes.ts +++ b/plugins/scaffolder/src/routes.ts @@ -40,6 +40,12 @@ export const scaffolderTaskRouteRef = createSubRouteRef({ path: '/tasks/:taskId', }); +export const scaffolderListTaskRouteRef = createSubRouteRef({ + id: 'scaffolder/list-tasks', + parent: rootRouteRef, + path: '/tasks', +}); + export const actionsRouteRef = createSubRouteRef({ id: 'scaffolder/actions', parent: rootRouteRef, diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index bddd6a7b70..acd686019c 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -102,6 +102,13 @@ export type LogEvent = { taskId: string; }; +/** + * The status of each task you can filter from `ScaffolderClient` + * + * @public + */ +export type TasksOwnerFilterKind = 'owned' | 'all'; + /** * The input options to the `scaffold` method of the `ScaffolderClient`. * @@ -171,6 +178,8 @@ export interface ScaffolderApi { getTask(taskId: string): Promise; + listTasks(createdBy: TasksOwnerFilterKind): Promise; + getIntegrationsList( options: ScaffolderGetIntegrationsListOptions, ): Promise; From 77f4e147c2eba1aeca787dceabc81837af4d0d54 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 28 Apr 2022 09:16:23 -0300 Subject: [PATCH 07/30] faat: change tasks page exibition Signed-off-by: Rogerio Angeliski --- packages/app/src/components/Root/Root.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index ff8a260540..e724adc345 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -23,7 +23,6 @@ import MapIcon from '@material-ui/icons/MyLocation'; import LayersIcon from '@material-ui/icons/Layers'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; -import Add from '@material-ui/icons/Add'; import List from '@material-ui/icons/List'; import SearchIcon from '@material-ui/icons/Search'; import MenuIcon from '@material-ui/icons/Menu'; @@ -48,8 +47,6 @@ import { SidebarScrollWrapper, SidebarSpace, useSidebarOpenState, - SidebarSubmenu, - SidebarSubmenuItem, } from '@backstage/core-components'; import { MyGroupsSidebarItem } from '@backstage/plugin-org'; import GroupIcon from '@material-ui/icons/People'; @@ -110,16 +107,8 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( - - - - - - + + {/* End global nav */} From 0d6167f1aa28ae50ff04d94368ad77114e7ddbd5 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 28 Apr 2022 09:51:20 -0300 Subject: [PATCH 08/30] feat: update api-report Signed-off-by: Rogerio Angeliski --- plugins/scaffolder-backend/api-report.md | 7 +++++++ plugins/scaffolder/api-report.md | 9 +++++++++ .../ListTasksPage/columns/OwnerEntityColumn.tsx | 6 +----- plugins/scaffolder/src/index.ts | 1 + 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index b059a30d9b..fe3b14762b 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -395,6 +395,8 @@ export class DatabaseTaskStore implements TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) + list(options: Partial): Promise; + // (undocumented) listEvents({ taskId, after }: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent[]; }>; @@ -493,6 +495,7 @@ export type SerializedTask = { lastHeartbeatAt?: string; createdBy?: string; secrets?: TaskSecrets; + createdBy: string | null; }; // @public @@ -519,6 +522,8 @@ export interface TaskBroker { // (undocumented) get(taskId: string): Promise; // (undocumented) + list(options?: Partial): Promise; + // (undocumented) vacuumTasks(options: { timeoutS: number }): Promise; } @@ -616,6 +621,8 @@ export interface TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) + list(options: Partial): Promise; + // (undocumented) listEvents({ taskId, after }: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent[]; }>; diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 229737eaf6..159fd10871 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -16,6 +16,7 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; import { FieldProps } from '@rjsf/core'; import { FieldValidation } from '@rjsf/core'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; import { JsonValue } from '@backstage/types'; @@ -237,6 +238,8 @@ export interface ScaffolderApi { templateRef: string, ): Promise; listActions(): Promise; + // (undocumented) + listTasks(createdBy: TasksOwnerFilterKind): Promise; scaffold( options: ScaffolderScaffoldOptions, ): Promise; @@ -252,6 +255,7 @@ export class ScaffolderClient implements ScaffolderApi { constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi; + identityApi: IdentityApi; scmIntegrationsApi: ScmIntegrationRegistry; useLongPollingLogs?: boolean; }); @@ -267,6 +271,8 @@ export class ScaffolderClient implements ScaffolderApi { ): Promise; // (undocumented) listActions(): Promise; + // (undocumented) + listTasks(createdBy: TasksOwnerFilterKind): Promise; scaffold( options: ScaffolderScaffoldOptions, ): Promise; @@ -376,6 +382,9 @@ export type TaskPageProps = { loadingText?: string; }; +// @public +export type TasksOwnerFilterKind = 'owned' | 'all'; + // @alpha (undocumented) export type TemplateGroupFilter = { title?: React_2.ReactNode; diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx index d2ca8f2620..e595c11199 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx @@ -23,11 +23,7 @@ import { ListItemText } from '@material-ui/core'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { parseEntityRef, UserEntity } from '@backstage/catalog-model'; -export const OwnerEntityColumn = ({ - entityRef, -}: { - entityRef?: string | null; -}) => { +export const OwnerEntityColumn = ({ entityRef }: { entityRef?: string }) => { const catalogApi = useApi(catalogApiRef); const catalogEntityRoute = useRouteRef(entityRouteRef); diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index fc132f644c..4f4c650f1c 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -35,6 +35,7 @@ export type { ScaffolderTaskOutput, ScaffolderTaskStatus, TemplateParameterSchema, + TasksOwnerFilterKind, } from './types'; export { createScaffolderFieldExtension, From dc39366bdb50e15bf366c63cc4bd419a7d6fc764 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 28 Apr 2022 09:56:33 -0300 Subject: [PATCH 09/30] chore: add changeset Signed-off-by: Rogerio Angeliski --- .changeset/rich-zebras-design.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/rich-zebras-design.md diff --git a/.changeset/rich-zebras-design.md b/.changeset/rich-zebras-design.md new file mode 100644 index 0000000000..2c8a8248b5 --- /dev/null +++ b/.changeset/rich-zebras-design.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder-backend': minor +'@backstage/plugin-scaffolder-common': minor +--- + +- add createdBy information for TaskSpec +- add listTaksks option for show current status from scaffolder tasks + +This allow the scaffolder-backend to show who creates each task and we included +a new page in `/create/tasks` to show those tasks From d750e3a97f530a3aca5ab19c18106a66b89399e7 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 28 Apr 2022 11:19:55 -0300 Subject: [PATCH 10/30] chore: update test name Signed-off-by: Rogerio Angeliski --- .../components/ListTasksPage/columns/OwnerEntityColumn.test.tsx | 2 +- .../ListTasksPage/columns/TemplateTitleColumn.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx index c2672e22bd..84b86599d5 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx @@ -37,7 +37,7 @@ describe('', () => { signOut: jest.fn(), }; - it('should render the column with the time', async () => { + it('should render the column with the user', async () => { const props = { entityRef: 'user:default/foo', }; diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx index 5ce11da28a..d7d194975e 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx @@ -27,7 +27,7 @@ describe('', () => { getTemplateParameterSchema: jest.fn(), } as any; - it('should render the column with the time', async () => { + it('should render the column with the template name', async () => { const props = { entityRef: 'template:default/one-template', }; From eb0f2f1a5eb6ca410d31738aa5f8b52373c21724 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 28 Apr 2022 11:28:52 -0300 Subject: [PATCH 11/30] feat: update api-report Signed-off-by: Rogerio Angeliski --- plugins/scaffolder-common/api-report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md index a1c5c2bf23..80b6833d4c 100644 --- a/plugins/scaffolder-common/api-report.md +++ b/plugins/scaffolder-common/api-report.md @@ -15,6 +15,8 @@ export type TaskSpec = TaskSpecV1beta3; // @public export interface TaskSpecV1beta3 { apiVersion: 'scaffolder.backstage.io/v1beta3'; + // (undocumented) + createdBy?: string; output: { [name: string]: JsonValue; }; From eb7c53a005bf05f00b06eb345d51064f13c5f676 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 11 May 2022 11:40:20 +0200 Subject: [PATCH 12/30] chore: fix up to work with the other code Signed-off-by: blam --- .../migrations/20220211013100_created_by.js | 38 ------------------- .../src/scaffolder/tasks/DatabaseTaskStore.ts | 4 +- .../tasks/StorageTaskBroker.test.ts | 5 ++- .../src/scaffolder/tasks/types.ts | 1 - .../scaffolder-backend/src/service/router.ts | 16 -------- .../ListTasksPage/ListTasksPage.tsx | 2 +- .../columns/OwnerEntityColumn.tsx | 5 ++- 7 files changed, 10 insertions(+), 61 deletions(-) delete mode 100644 plugins/scaffolder-backend/migrations/20220211013100_created_by.js diff --git a/plugins/scaffolder-backend/migrations/20220211013100_created_by.js b/plugins/scaffolder-backend/migrations/20220211013100_created_by.js deleted file mode 100644 index 257cda21d2..0000000000 --- a/plugins/scaffolder-backend/migrations/20220211013100_created_by.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - await knex.schema.alterTable('tasks', table => { - table - .text('created_by') - .nullable() - .comment('an entity ref of the user that created the task'); - }); -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - await knex.schema.alterTable('tasks', table => { - table.dropColumn('created_by'); - }); -}; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 1c4b9b5210..2ebe1e17a7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -100,7 +100,7 @@ export class DatabaseTaskStore implements TaskStore { id: result.id, spec: JSON.parse(result.spec), status: result.status, - createdBy: result.created_by, + createdBy: result.created_by ?? undefined, lastHeartbeatAt: typeof result.last_heartbeat_at === 'string' ? DateTime.fromSQL(result.last_heartbeat_at, { @@ -156,8 +156,6 @@ export class DatabaseTaskStore implements TaskStore { secrets: options.secrets ? JSON.stringify(options.secrets) : undefined, created_by: options.createdBy ?? null, status: 'open', - created_by: - ('createdBy' in options.spec && options.spec.createdBy) || null, }); return { taskId }; } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 7ed2eb620a..8558a371d5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -15,6 +15,7 @@ */ import { getVoidLogger, DatabaseManager } from '@backstage/backend-common'; +import { UserEntity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { DatabaseTaskStore } from './DatabaseTaskStore'; @@ -220,7 +221,9 @@ describe('StorageTaskBroker', () => { it('should list only tasks createdBy a specific user', async () => { const broker = new StorageTaskBroker(storage, logger); const { taskId } = await broker.dispatch({ - spec: { createdBy: 'user:default/foo' } as TaskSpec, + spec: { + user: { ref: 'user:default/foo', entity: {} as UserEntity }, + } as TaskSpec, }); const task = await storage.getTask(taskId); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 1fc7670e79..f3939a8668 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -49,7 +49,6 @@ export type SerializedTask = { lastHeartbeatAt?: string; createdBy?: string; secrets?: TaskSecrets; - createdBy: string | null; }; /** diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 61fbe74b8b..c1a9b50e0d 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -129,19 +129,6 @@ export async function createRouter( actionsToRegister.forEach(action => actionRegistry.register(action)); workers.forEach(worker => worker.start()); - const getUserEntityRefFromToken = (backstageToken: string) => { - try { - const [_header, payload, _signature] = backstageToken.split('.'); - const parsedToken = JSON.parse(Buffer.from(payload, 'base64').toString()); - - return parsedToken.sub; - } catch (e) { - logger.warn('Could not parse token from request to create template'); - logger.debug(e); - return null; - } - }; - router .get( '/v2/templates/:namespace/:kind/:name/parameter-schema', @@ -220,8 +207,6 @@ export async function createRouter( const baseUrl = getEntityBaseUrl(template); - const createdBy = token && getUserEntityRefFromToken(token); - const taskSpec: TaskSpec = { apiVersion: template.apiVersion, steps: template.spec.steps.map((step, index) => ({ @@ -243,7 +228,6 @@ export async function createRouter( }), baseUrl, }, - createdBy, }; const result = await taskBroker.dispatch({ diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx index d66355b0a5..7866375fb1 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx @@ -109,7 +109,7 @@ const ListTaskPageContent = (props: MyTaskPageProps) => { title: 'Owner', field: 'createdBy', render: row => ( - + ), }, { diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx index e595c11199..49c00316c7 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx @@ -39,7 +39,10 @@ export const OwnerEntityColumn = ({ entityRef }: { entityRef?: string }) => { return ( ); From 935d47d8588c79c2083e5026721a4b01459d4a61 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 May 2022 12:59:25 +0200 Subject: [PATCH 13/30] chore: remove createdBy from the API report Signed-off-by: blam --- plugins/scaffolder-backend/api-report.md | 1 - plugins/scaffolder-common/api-report.md | 2 -- 2 files changed, 3 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index fe3b14762b..b09939e68a 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -495,7 +495,6 @@ export type SerializedTask = { lastHeartbeatAt?: string; createdBy?: string; secrets?: TaskSecrets; - createdBy: string | null; }; // @public diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md index 80b6833d4c..a1c5c2bf23 100644 --- a/plugins/scaffolder-common/api-report.md +++ b/plugins/scaffolder-common/api-report.md @@ -15,8 +15,6 @@ export type TaskSpec = TaskSpecV1beta3; // @public export interface TaskSpecV1beta3 { apiVersion: 'scaffolder.backstage.io/v1beta3'; - // (undocumented) - createdBy?: string; output: { [name: string]: JsonValue; }; From 8db0983eb75dd13d07a7c3aca5a392147aa9ce88 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 May 2022 13:16:26 +0200 Subject: [PATCH 14/30] chore: reworking some of the table to provider better links Signed-off-by: blam --- .../ListTasksPage/ListTasksPage.tsx | 6 ++++- .../columns/OwnerEntityColumn.tsx | 26 +++++++++---------- .../columns/TemplateTitleColumn.tsx | 8 ++++-- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx index 7866375fb1..8d51c9c403 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx @@ -24,7 +24,10 @@ import { Progress, } from '@backstage/core-components'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; -import { CatalogFilterLayout } from '@backstage/plugin-catalog-react'; +import { + CatalogFilterLayout, + EntityRefLink, +} from '@backstage/plugin-catalog-react'; import useAsync from 'react-use/lib/useAsync'; import MaterialTable from '@material-table/core'; import React, { useState } from 'react'; @@ -38,6 +41,7 @@ import { TaskStatusColumn, TemplateTitleColumn, } from './columns'; +import { parseEntityRef } from '@backstage/catalog-model'; export interface MyTaskPageProps { initiallySelectedFilter?: TasksOwnerFilterKind; diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx index 49c00316c7..0c83c7cbc2 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.tsx @@ -13,37 +13,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Link } from '@backstage/core-components'; -import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; import React from 'react'; import useAsync from 'react-use/lib/useAsync'; -import { ListItemText } from '@material-ui/core'; -import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; +import { catalogApiRef, EntityRefLink } from '@backstage/plugin-catalog-react'; import { parseEntityRef, UserEntity } from '@backstage/catalog-model'; export const OwnerEntityColumn = ({ entityRef }: { entityRef?: string }) => { const catalogApi = useApi(catalogApiRef); - const catalogEntityRoute = useRouteRef(entityRouteRef); const { value, loading, error } = useAsync( () => catalogApi.getEntityByRef(entityRef || ''), [catalogApi, entityRef], ); + if (!entityRef) { + return

Unknown

; + } + if (loading || error) { return null; } return ( - - - + ); }; diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.tsx index 8f86734809..0561b40392 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.tsx @@ -17,6 +17,8 @@ import { useApi } from '@backstage/core-plugin-api'; import React from 'react'; import { scaffolderApiRef } from '../../../api'; import useAsync from 'react-use/lib/useAsync'; +import { parseEntityRef } from '@backstage/catalog-model'; +import { EntityRefLink } from '@backstage/plugin-catalog-react'; export const TemplateTitleColumn = ({ entityRef }: { entityRef?: string }) => { const scaffolder = useApi(scaffolderApiRef); @@ -25,9 +27,11 @@ export const TemplateTitleColumn = ({ entityRef }: { entityRef?: string }) => { [scaffolder, entityRef], ); - if (loading || error) { + if (loading || error || !entityRef) { return null; } - return

{value?.title}

; + return ( + + ); }; From 023b5aa27246c76b61472677c2803bfe935b723b Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 May 2022 13:35:10 +0200 Subject: [PATCH 15/30] chore: refactor the query string building a little Signed-off-by: blam --- plugins/scaffolder/src/api.ts | 10 +++++----- .../src/components/ListTasksPage/ListTasksPage.tsx | 6 +----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 8f2319cb9d..eaf92f949e 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -39,6 +39,7 @@ import { ScaffolderTask, TasksOwnerFilterKind, } from './types'; +import queryString from 'qs'; /** * Utility API reference for the {@link ScaffolderApi}. @@ -77,12 +78,11 @@ export class ScaffolderClient implements ScaffolderApi { async listTasks(createdBy: TasksOwnerFilterKind): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); + const { userEntityRef } = await this.identityApi.getBackstageIdentity(); - let query = ''; - if (createdBy === 'owned') { - const { userEntityRef } = await this.identityApi.getBackstageIdentity(); - query = `createdBy=${encodeURIComponent(userEntityRef)}`; - } + const query = queryString.stringify( + createdBy === 'owned' ? { createdBy: userEntityRef } : {}, + ); const url = `${baseUrl}/v2/tasks?${query}`; diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx index 8d51c9c403..7866375fb1 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx @@ -24,10 +24,7 @@ import { Progress, } from '@backstage/core-components'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; -import { - CatalogFilterLayout, - EntityRefLink, -} from '@backstage/plugin-catalog-react'; +import { CatalogFilterLayout } from '@backstage/plugin-catalog-react'; import useAsync from 'react-use/lib/useAsync'; import MaterialTable from '@material-table/core'; import React, { useState } from 'react'; @@ -41,7 +38,6 @@ import { TaskStatusColumn, TemplateTitleColumn, } from './columns'; -import { parseEntityRef } from '@backstage/catalog-model'; export interface MyTaskPageProps { initiallySelectedFilter?: TasksOwnerFilterKind; From 1c3f99c65456108c460081d203b052c14f9ae42d Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 May 2022 16:47:23 +0200 Subject: [PATCH 16/30] chore: reworking the api Signed-off-by: blam Signed-off-by: blam --- .../src/scaffolder/tasks/DatabaseTaskStore.ts | 39 +++++++------------ .../src/scaffolder/tasks/types.ts | 4 +- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 2ebe1e17a7..145747124a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -64,6 +64,14 @@ export type DatabaseTaskStoreOptions = { database: Knex; }; +const parseSqlDateToIsoString = (input: T): T | string => { + if (typeof input === 'string') { + return DateTime.fromSQL(input, { zone: 'UTC' }).toISO(); + } + + return input; +}; + /** * DatabaseTaskStore * @@ -85,7 +93,7 @@ export class DatabaseTaskStore implements TaskStore { this.db = options.database; } - async list(options: Partial): Promise { + async list(options: { createdBy?: string }): Promise { const queryBuilder = this.db('tasks'); if (options.createdBy) { @@ -101,16 +109,8 @@ export class DatabaseTaskStore implements TaskStore { spec: JSON.parse(result.spec), status: result.status, createdBy: result.created_by ?? undefined, - lastHeartbeatAt: - typeof result.last_heartbeat_at === 'string' - ? DateTime.fromSQL(result.last_heartbeat_at, { - zone: 'UTC', - }).toISO() - : result.last_heartbeat_at, - createdAt: - typeof result.created_at === 'string' - ? DateTime.fromSQL(result.created_at, { zone: 'UTC' }).toISO() - : result.created_at, + lastHeartbeatAt: parseSqlDateToIsoString(result.last_heartbeat_at), + createdAt: parseSqlDateToIsoString(result.created_at), })); } @@ -128,16 +128,8 @@ export class DatabaseTaskStore implements TaskStore { id: result.id, spec, status: result.status, - lastHeartbeatAt: - typeof result.last_heartbeat_at === 'string' - ? DateTime.fromSQL(result.last_heartbeat_at, { - zone: 'UTC', - }).toISO() - : result.last_heartbeat_at, - createdAt: - typeof result.created_at === 'string' - ? DateTime.fromSQL(result.created_at, { zone: 'UTC' }).toISO() - : result.created_at, + lastHeartbeatAt: parseSqlDateToIsoString(result.last_heartbeat_at), + createdAt: parseSqlDateToIsoString(result.created_at), createdBy: result.created_by ?? undefined, secrets, }; @@ -329,10 +321,7 @@ export class DatabaseTaskStore implements TaskStore { taskId, body, type: event.event_type, - createdAt: - typeof event.created_at === 'string' - ? DateTime.fromSQL(event.created_at, { zone: 'UTC' }).toISO() - : event.created_at, + createdAt: parseSqlDateToIsoString(event.created_at), }; } catch (error) { throw new Error( diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index f3939a8668..e56d1b682a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -132,7 +132,7 @@ export interface TaskBroker { after: number | undefined; }): Observable<{ events: SerializedTaskEvent[] }>; get(taskId: string): Promise; - list(options?: Partial): Promise; + list(options?: { createdBy?: string }): Promise; } /** @@ -193,7 +193,7 @@ export interface TaskStore { listStaleTasks(options: { timeoutS: number }): Promise<{ tasks: { taskId: string }[]; }>; - list(options: Partial): Promise; + list(options: { createdBy?: string }): Promise; emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; listEvents({ From bcb040de3e271376381d1b1346b3858e34ea6bfb Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 May 2022 16:51:08 +0200 Subject: [PATCH 17/30] chore: change api report to make it more narrow Signed-off-by: blam Signed-off-by: blam --- plugins/scaffolder-backend/api-report.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index b09939e68a..1c333eb85f 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -395,7 +395,7 @@ export class DatabaseTaskStore implements TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) - list(options: Partial): Promise; + list(options: { createdBy?: string }): Promise; // (undocumented) listEvents({ taskId, after }: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent[]; @@ -521,7 +521,7 @@ export interface TaskBroker { // (undocumented) get(taskId: string): Promise; // (undocumented) - list(options?: Partial): Promise; + list(options?: { createdBy?: string }): Promise; // (undocumented) vacuumTasks(options: { timeoutS: number }): Promise; } @@ -620,7 +620,7 @@ export interface TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) - list(options: Partial): Promise; + list(options: { createdBy?: string }): Promise; // (undocumented) listEvents({ taskId, after }: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent[]; From 2de8c51462b27fe18e4e95ff8e7c9e77121d87f6 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 13 May 2022 11:11:08 +0200 Subject: [PATCH 18/30] chore: fixing up the tests for the scaffolder tasks now the API has changed Signed-off-by: blam --- plugins/scaffolder/src/api.test.ts | 5 +++- .../ListTasksPage/ListTaskPage.test.tsx | 30 +++++++++++++------ .../columns/TemplateTitleColumn.test.tsx | 2 ++ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index a5a3a3c51e..0b21f938ce 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -17,6 +17,7 @@ import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { identity } from 'lodash'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ScaffolderClient } from './api'; @@ -60,6 +61,9 @@ describe('api', () => { fetchApi, identityApi, }); + + jest.restoreAllMocks(); + identityApi.getBackstageIdentity.mockReturnValue({}); }); it('should return default and custom integrations', async () => { @@ -345,7 +349,6 @@ describe('api', () => { ); const result = await apiClient.listTasks('all'); - expect(identityApi.getBackstageIdentity).not.toBeCalled(); expect(result).toHaveLength(2); }); it('should list task using the current user as owner', async () => { diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx index fced397ad3..cf7204a3c6 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx @@ -105,19 +105,23 @@ describe('', () => { { id: 'a-random-id', spec: { - createdBy: 'user:default/foo', + user: { ref: 'user:default/foo' }, + templateInfo: { + entityRef: 'template:default/test', + }, } as any, status: 'completed', createdAt: '', lastHeartbeatAt: '', }, ]); + scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ title: 'One Template', steps: [], }); - const { getByText } = await renderInTestApp( + const { getByText, findByText } = await renderInTestApp( ', () => { expect(getByText('List template tasks')).toBeInTheDocument(); expect(getByText('All tasks that have been started')).toBeInTheDocument(); expect(getByText('Tasks')).toBeInTheDocument(); - expect(getByText('One Template')).toBeInTheDocument(); - expect(getByText('BackUser')).toBeInTheDocument(); + expect(await findByText('One Template')).toBeInTheDocument(); + expect(await findByText('BackUser')).toBeInTheDocument(); }); it('should render all tasks', async () => { @@ -172,7 +176,10 @@ describe('', () => { { id: 'a-random-id', spec: { - createdBy: 'user:default/foo', + user: { ref: 'user:default/foo' }, + templateInfo: { + entityRef: 'template:default/mock', + }, } as any, status: 'completed', createdAt: '', @@ -183,7 +190,12 @@ describe('', () => { { id: 'b-random-id', spec: { - createdBy: 'user:default/boo', + templateInfo: { + entityRef: 'template:default/mock', + }, + user: { + ref: 'user:default/boo', + }, } as any, status: 'completed', createdAt: '', @@ -196,7 +208,7 @@ describe('', () => { steps: [], }); - const { getByText } = await renderInTestApp( + const { getByText, findByText } = await renderInTestApp( ', () => { }); expect(scaffolderApiMock.listTasks).toBeCalledWith('all'); - expect(getByText('One Template')).toBeInTheDocument(); - expect(getByText('OtherUser')).toBeInTheDocument(); + expect(await findByText('One Template')).toBeInTheDocument(); + expect(await findByText('OtherUser')).toBeInTheDocument(); }); }); diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx index d7d194975e..3dc20312aa 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx @@ -20,6 +20,7 @@ import React from 'react'; import { TemplateTitleColumn } from './TemplateTitleColumn'; import { scaffolderApiRef } from '../../../api'; import { ScaffolderApi } from '../../../types'; +import { entityRouteRef } from '@backstage/plugin-catalog-react'; describe('', () => { const scaffolderApiMock: jest.Mocked = { @@ -40,6 +41,7 @@ describe('', () => { , + { mountedRoutes: { '/test': entityRouteRef } }, ); const text = getByText('One Template'); From 4ee9ef6d366c3188bfac85be0ab3bfbb93664a51 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 13 May 2022 11:14:03 +0200 Subject: [PATCH 19/30] chore: fixing these tests too Signed-off-by: blam --- .../tasks/StorageTaskBroker.test.ts | 5 ++-- .../src/service/router.test.ts | 29 ++++++++----------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 8558a371d5..710c9af2f0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -221,9 +221,8 @@ describe('StorageTaskBroker', () => { it('should list only tasks createdBy a specific user', async () => { const broker = new StorageTaskBroker(storage, logger); const { taskId } = await broker.dispatch({ - spec: { - user: { ref: 'user:default/foo', entity: {} as UserEntity }, - } as TaskSpec, + spec: {} as TaskSpec, + createdBy: 'user:default/foo', }); const task = await storage.getTask(taskId); diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 52f11357a6..b65c26949c 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -217,23 +217,18 @@ describe('createRouter', () => { const mockToken = 'blob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvZ3Vlc3QiLCJuYW1lIjoiSm9obiBEb2UifQ.blob'; - console.log( - JSON.stringify( - await request(app) - .post('/v2/tasks') - .set('Authorization', `Bearer ${mockToken}`) - .send({ - templateRef: stringifyEntityRef({ - kind: 'template', - name: 'create-react-app-template', - }), - values: { - required: 'required-value', - }, - }), - ), - ); - + await request(app) + .post('/v2/tasks') + .set('Authorization', `Bearer ${mockToken}`) + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + required: 'required-value', + }, + }); expect(broker).toHaveBeenCalledWith( expect.objectContaining({ createdBy: 'user:default/guest', From eb2cc98152871fc745259291d7eb6718ffbb3ee5 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 13 May 2022 11:16:36 +0200 Subject: [PATCH 20/30] chore: updating changeset Signed-off-by: blam --- .changeset/rich-zebras-design.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.changeset/rich-zebras-design.md b/.changeset/rich-zebras-design.md index 2c8a8248b5..d8ce399bce 100644 --- a/.changeset/rich-zebras-design.md +++ b/.changeset/rich-zebras-design.md @@ -1,11 +1,8 @@ --- '@backstage/plugin-scaffolder': minor '@backstage/plugin-scaffolder-backend': minor -'@backstage/plugin-scaffolder-common': minor --- -- add createdBy information for TaskSpec -- add listTaksks option for show current status from scaffolder tasks +- Add `listTasks` option to get tasks optionally filtered by a `userRef` -This allow the scaffolder-backend to show who creates each task and we included -a new page in `/create/tasks` to show those tasks +- Added a new page under `/create/tasks` to show those tasks, optionally grouped by the current signed in user. From 3d1d561a4de4094ee1b71b14718b09dd00af5d78 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 13 May 2022 11:18:06 +0200 Subject: [PATCH 21/30] chore: fixing type for storage task broker Signed-off-by: blam Signed-off-by: blam --- .../src/scaffolder/tasks/StorageTaskBroker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 8e3dadd844..fb0d673083 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -151,7 +151,7 @@ export class StorageTaskBroker implements TaskBroker { private readonly logger: Logger, ) {} - async list(options?: Partial): Promise { + async list(options?: { createdBy?: string }): Promise { return await this.storage.list({ createdBy: options?.createdBy }); } From ee63f03e9196207c659955b8d0059e9d207053c3 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 13 May 2022 11:22:44 +0200 Subject: [PATCH 22/30] chore: fixing typescript Signed-off-by: blam --- packages/catalog-model/examples/acme/team-a-group.yaml | 10 ++++++++++ .../src/scaffolder/tasks/StorageTaskBroker.test.ts | 1 - plugins/scaffolder/src/api.test.ts | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/catalog-model/examples/acme/team-a-group.yaml b/packages/catalog-model/examples/acme/team-a-group.yaml index e343209d5f..2b3e2999d2 100644 --- a/packages/catalog-model/examples/acme/team-a-group.yaml +++ b/packages/catalog-model/examples/acme/team-a-group.yaml @@ -58,3 +58,13 @@ spec: email: guest@example.com picture: https://avatars.dicebear.com/api/avataaars/guest@example.com.svg?background=%23fff memberOf: [team-a] + +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: benjdlambert +spec: + profile: + picture: https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg?background=%23fff + memberOf: [] diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 710c9af2f0..caca1580ad 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -15,7 +15,6 @@ */ import { getVoidLogger, DatabaseManager } from '@backstage/backend-common'; -import { UserEntity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { DatabaseTaskStore } from './DatabaseTaskStore'; diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index 0b21f938ce..d5b91ebdb8 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -17,7 +17,6 @@ import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; -import { identity } from 'lodash'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ScaffolderClient } from './api'; From 61d31317a106b1b0ea0228834478894d4dc227a7 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 30 May 2022 14:43:14 +0200 Subject: [PATCH 23/30] chore: fixing context menu Signed-off-by: blam Signed-off-by: blam --- .changeset/rich-zebras-design.md | 2 +- packages/app/src/components/Root/Root.tsx | 2 -- .../ScaffolderPage/ScaffolderPage.tsx | 1 + .../ScaffolderPageContextMenu.tsx | 26 ++++++++++++++++--- plugins/scaffolder/src/types.ts | 2 +- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.changeset/rich-zebras-design.md b/.changeset/rich-zebras-design.md index d8ce399bce..fc121349aa 100644 --- a/.changeset/rich-zebras-design.md +++ b/.changeset/rich-zebras-design.md @@ -3,6 +3,6 @@ '@backstage/plugin-scaffolder-backend': minor --- -- Add `listTasks` option to get tasks optionally filtered by a `userRef` +- Add `listTasks` option to get tasks optionally filtered by `createdBy` - Added a new page under `/create/tasks` to show those tasks, optionally grouped by the current signed in user. diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index e724adc345..30da01841c 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -23,7 +23,6 @@ import MapIcon from '@material-ui/icons/MyLocation'; import LayersIcon from '@material-ui/icons/Layers'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; -import List from '@material-ui/icons/List'; import SearchIcon from '@material-ui/icons/Search'; import MenuIcon from '@material-ui/icons/Menu'; import MoneyIcon from '@material-ui/icons/MonetizationOn'; @@ -108,7 +107,6 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( - {/* End global nav */} diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 79d176cd11..2bc36ff0d2 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -52,6 +52,7 @@ export type ScaffolderPageProps = { contextMenu?: { editor?: boolean; actions?: boolean; + tasks?: boolean; }; }; diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.tsx index 6d9a89cfab..74366753f7 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.tsx @@ -24,10 +24,15 @@ import Popover from '@material-ui/core/Popover'; import { makeStyles } from '@material-ui/core/styles'; import Description from '@material-ui/icons/Description'; import Edit from '@material-ui/icons/Edit'; +import List from '@material-ui/icons/List'; import MoreVert from '@material-ui/icons/MoreVert'; import React, { useState } from 'react'; import { useNavigate } from 'react-router'; -import { rootRouteRef } from '../../routes'; +import { + actionsRouteRef, + editRouteRef, + scaffolderListTaskRouteRef, +} from '../../routes'; const useStyles = makeStyles({ button: { @@ -38,6 +43,7 @@ const useStyles = makeStyles({ export type ScaffolderPageContextMenuProps = { editor?: boolean; actions?: boolean; + tasks?: boolean; }; export function ScaffolderPageContextMenu( @@ -45,11 +51,15 @@ export function ScaffolderPageContextMenu( ) { const classes = useStyles(); const [anchorEl, setAnchorEl] = useState(); - const pageLink = useRouteRef(rootRouteRef); + const editLink = useRouteRef(editRouteRef); + const actionsLink = useRouteRef(actionsRouteRef); + const tasksLink = useRouteRef(scaffolderListTaskRouteRef); + const navigate = useNavigate(); const showEditor = props.editor !== false; const showActions = props.actions !== false; + const showTasks = props.tasks !== false; if (!showEditor && !showActions) { return null; @@ -85,7 +95,7 @@ export function ScaffolderPageContextMenu( > {showEditor && ( - navigate(`${pageLink()}/edit`)}> + navigate(editLink())}> @@ -93,13 +103,21 @@ export function ScaffolderPageContextMenu( )} {showActions && ( - navigate(`${pageLink()}/actions`)}> + navigate(actionsLink())}> )} + {showTasks && ( + navigate(tasksLink())}> + + + + + + )} diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index acd686019c..6792f069cc 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -178,7 +178,7 @@ export interface ScaffolderApi { getTask(taskId: string): Promise; - listTasks(createdBy: TasksOwnerFilterKind): Promise; + listTasks?(createdBy: TasksOwnerFilterKind): Promise; getIntegrationsList( options: ScaffolderGetIntegrationsListOptions, From 1cd1c184eeeb47d97baa843d833f0a416398a655 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 30 May 2022 14:53:54 +0200 Subject: [PATCH 24/30] chore: reworking the api to wrap up in an object Signed-off-by: blam --- plugins/scaffolder/src/api.test.ts | 4 ++-- plugins/scaffolder/src/api.ts | 6 ++++-- .../ListTasksPage/ListTaskPage.test.tsx | 7 ++++--- .../components/ListTasksPage/ListTasksPage.tsx | 15 +++++++++++---- plugins/scaffolder/src/types.ts | 6 +++++- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index d5b91ebdb8..7dc840ee96 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -347,7 +347,7 @@ describe('api', () => { }), ); - const result = await apiClient.listTasks('all'); + const result = await apiClient.listTasks({ createdBy: 'all' }); expect(result).toHaveLength(2); }); it('should list task using the current user as owner', async () => { @@ -382,7 +382,7 @@ describe('api', () => { userEntityRef: 'user:default/foo', }); - const result = await apiClient.listTasks('owned'); + const result = await apiClient.listTasks({ createdBy: 'owned' }); expect(identityApi.getBackstageIdentity).toBeCalled(); expect(result).toHaveLength(1); }); diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index eaf92f949e..c19853b5f1 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -76,12 +76,14 @@ export class ScaffolderClient implements ScaffolderApi { this.identityApi = options.identityApi; } - async listTasks(createdBy: TasksOwnerFilterKind): Promise { + async listTasks(options: { + createdBy: TasksOwnerFilterKind; + }): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const { userEntityRef } = await this.identityApi.getBackstageIdentity(); const query = queryString.stringify( - createdBy === 'owned' ? { createdBy: userEntityRef } : {}, + options.createdBy === 'owned' ? { createdBy: userEntityRef } : {}, ); const url = `${baseUrl}/v2/tasks?${query}`; diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx index cf7204a3c6..3d20fd1fd8 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx @@ -41,7 +41,7 @@ describe('', () => { signOut: jest.fn(), }; - const scaffolderApiMock: jest.Mocked = { + const scaffolderApiMock: jest.Mocked> = { scaffold: jest.fn(), getTemplateParameterSchema: jest.fn(), listTasks: jest.fn(), @@ -139,7 +139,7 @@ describe('', () => { }, ); - expect(scaffolderApiMock.listTasks).toBeCalledWith('owned'); + expect(scaffolderApiMock.listTasks).toBeCalledWith({ createdBy: 'owned' }); expect(getByText('List template tasks')).toBeInTheDocument(); expect(getByText('All tasks that have been started')).toBeInTheDocument(); expect(getByText('Tasks')).toBeInTheDocument(); @@ -171,6 +171,7 @@ describe('', () => { }, }, }); + scaffolderApiMock.listTasks .mockResolvedValue([ { @@ -231,7 +232,7 @@ describe('', () => { fireEvent.click(allButton); }); - expect(scaffolderApiMock.listTasks).toBeCalledWith('all'); + expect(scaffolderApiMock.listTasks).toBeCalledWith({ createdBy: 'all' }); expect(await findByText('One Template')).toBeInTheDocument(); expect(await findByText('OtherUser')).toBeInTheDocument(); }); diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx index 7866375fb1..5f099ed331 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx @@ -50,10 +50,17 @@ const ListTaskPageContent = (props: MyTaskPageProps) => { const rootLink = useRouteRef(rootRouteRef); const [ownerFilter, setOwnerFilter] = useState(initiallySelectedFilter); - const { value, loading, error } = useAsync( - () => scaffolderApi.listTasks(ownerFilter), - [scaffolderApi, ownerFilter], - ); + const { value, loading, error } = useAsync(() => { + if (scaffolderApi.listTasks) { + return scaffolderApi.listTasks?.({ createdBy: ownerFilter }); + } + + // eslint-disable-next-line no-console + console.warn( + 'listTasks is not implemented in the scaffolderApi, please make sure to implement this method.', + ); + return Promise.resolve([]); + }, [scaffolderApi, ownerFilter]); if (loading) { return ; diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index 6792f069cc..608d586b09 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -178,7 +178,11 @@ export interface ScaffolderApi { getTask(taskId: string): Promise; - listTasks?(createdBy: TasksOwnerFilterKind): Promise; + listTasks?({ + createdBy, + }: { + createdBy: TasksOwnerFilterKind; + }): Promise; getIntegrationsList( options: ScaffolderGetIntegrationsListOptions, From f7c604a25e4f55ba3e959d6e6d01be7e4845e093 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 30 May 2022 14:56:55 +0200 Subject: [PATCH 25/30] chore: fixing some things Signed-off-by: blam --- plugins/scaffolder/api-report.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 159fd10871..2469450f93 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -239,7 +239,11 @@ export interface ScaffolderApi { ): Promise; listActions(): Promise; // (undocumented) - listTasks(createdBy: TasksOwnerFilterKind): Promise; + listTasks?({ + createdBy, + }: { + createdBy: TasksOwnerFilterKind; + }): Promise; scaffold( options: ScaffolderScaffoldOptions, ): Promise; @@ -272,7 +276,9 @@ export class ScaffolderClient implements ScaffolderApi { // (undocumented) listActions(): Promise; // (undocumented) - listTasks(createdBy: TasksOwnerFilterKind): Promise; + listTasks(options: { + createdBy: TasksOwnerFilterKind; + }): Promise; scaffold( options: ScaffolderScaffoldOptions, ): Promise; From f5732feb25089bb8c4c7cf63678e47faac3d9c99 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 31 May 2022 10:47:52 +0200 Subject: [PATCH 26/30] chore: use the existing material-table version Signed-off-by: blam --- plugins/scaffolder/package.json | 2 +- yarn.lock | 19 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index bb69adfe16..8bb88b28bb 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -56,7 +56,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@rjsf/core": "^3.2.1", - "@material-table/core": "^4.3.29", + "@material-table/core": "^3.1.0", "@rjsf/material-ui": "^3.2.1", "@types/json-schema": "^7.0.9", "@uiw/react-codemirror": "^4.7.0", diff --git a/yarn.lock b/yarn.lock index 71c81d03e7..4ed8ee03bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3988,25 +3988,6 @@ react-beautiful-dnd "^13.0.0" react-double-scrollbar "0.0.15" -"@material-table/core@^4.3.29": - version "4.3.29" - resolved "https://registry.npmjs.org/@material-table/core/-/core-4.3.29.tgz#7f60fe4fc191e016d9be663fa437e00b012eae05" - integrity sha512-7vz1oc3bo1eSzk450NgpA61xQjlgCBg7q1TDGrB8ZDMyx2NvLRxTyNBtW8JEPLaQLoCWoDFK7F/VHOYRN1fKuQ== - dependencies: - "@babel/runtime" "^7.12.5" - "@date-io/date-fns" "^1.3.13" - "@material-ui/icons" "^4.11.2" - "@material-ui/pickers" "^3.2.10" - "@material-ui/styles" "^4.11.4" - classnames "^2.2.6" - date-fns "^2.16.1" - debounce "^1.2.0" - fast-deep-equal "^3.1.3" - prop-types "^15.7.2" - react-beautiful-dnd "^13.0.0" - react-double-scrollbar "0.0.15" - uuid "^3.4.0" - "@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2", "@material-ui/core@^4.9.10", "@material-ui/core@^4.9.13": version "4.12.4" resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.4.tgz#4ac17488e8fcaf55eb6a7f5efb2a131e10138a73" From 698c55f1820cad93ad157959ac0c720c5886e764 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 31 May 2022 16:42:51 +0200 Subject: [PATCH 27/30] chore: code review changes Signed-off-by: blam --- .../examples/acme/team-a-group.yaml | 70 ------------ .../src/scaffolder/tasks/DatabaseTaskStore.ts | 8 +- .../tasks/StorageTaskBroker.test.ts | 8 +- .../src/scaffolder/tasks/StorageTaskBroker.ts | 9 +- .../src/scaffolder/tasks/types.ts | 4 +- .../src/service/router.test.ts | 84 ++++++++------ .../scaffolder-backend/src/service/router.ts | 15 ++- plugins/scaffolder/src/api.test.ts | 36 +++--- plugins/scaffolder/src/api.ts | 20 ++-- .../ListTasksPage/ListTaskPage.test.tsx | 104 ++++++++++-------- .../ListTasksPage/ListTasksPage.tsx | 10 +- .../ListTasksPage/OwnerListPicker.tsx | 6 +- plugins/scaffolder/src/index.ts | 1 - plugins/scaffolder/src/types.ts | 13 +-- yarn.lock | 83 ++++++++++++-- 15 files changed, 256 insertions(+), 215 deletions(-) delete mode 100644 packages/catalog-model/examples/acme/team-a-group.yaml diff --git a/packages/catalog-model/examples/acme/team-a-group.yaml b/packages/catalog-model/examples/acme/team-a-group.yaml deleted file mode 100644 index 2b3e2999d2..0000000000 --- a/packages/catalog-model/examples/acme/team-a-group.yaml +++ /dev/null @@ -1,70 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Group -metadata: - name: team-a - description: Team A -spec: - type: team - profile: - # Intentional no displayName for testing - email: team-a@example.com - picture: https://avatars.dicebear.com/api/identicon/team-a@example.com.svg?background=%23fff&margin=25 - parent: backstage - children: [] ---- -apiVersion: backstage.io/v1alpha1 -kind: User -metadata: - name: breanna.davison -spec: - profile: - # Intentional no displayName for testing - email: breanna-davison@example.com - picture: https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg?background=%23fff - memberOf: [team-a] ---- -apiVersion: backstage.io/v1alpha1 -kind: User -metadata: - name: janelle.dawe -spec: - profile: - displayName: Janelle Dawe - email: janelle-dawe@example.com - picture: https://avatars.dicebear.com/api/avataaars/janelle-dawe@example.com.svg?background=%23fff - memberOf: [team-a] ---- -apiVersion: backstage.io/v1alpha1 -kind: User -metadata: - name: nigel.manning -spec: - profile: - displayName: Nigel Manning - email: nigel-manning@example.com - picture: https://avatars.dicebear.com/api/avataaars/nigel-manning@example.com.svg?background=%23fff - memberOf: [team-a] ---- -# This user is added as an example, to make it more easy for the "Guest" -# sign-in option to demonstrate some entities being owned. In a regular org, -# a guest user would probably not be registered like this. -apiVersion: backstage.io/v1alpha1 -kind: User -metadata: - name: guest -spec: - profile: - displayName: Guest User - email: guest@example.com - picture: https://avatars.dicebear.com/api/avataaars/guest@example.com.svg?background=%23fff - memberOf: [team-a] - ---- -apiVersion: backstage.io/v1alpha1 -kind: User -metadata: - name: benjdlambert -spec: - profile: - picture: https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg?background=%23fff - memberOf: [] diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 145747124a..d62e59744f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -93,7 +93,9 @@ export class DatabaseTaskStore implements TaskStore { this.db = options.database; } - async list(options: { createdBy?: string }): Promise { + async list(options: { + createdBy?: string; + }): Promise<{ tasks: SerializedTask[] }> { const queryBuilder = this.db('tasks'); if (options.createdBy) { @@ -104,7 +106,7 @@ export class DatabaseTaskStore implements TaskStore { const results = await queryBuilder.orderBy('created_at', 'desc').select(); - return results.map(result => ({ + const tasks = results.map(result => ({ id: result.id, spec: JSON.parse(result.spec), status: result.status, @@ -112,6 +114,8 @@ export class DatabaseTaskStore implements TaskStore { lastHeartbeatAt: parseSqlDateToIsoString(result.last_heartbeat_at), createdAt: parseSqlDateToIsoString(result.created_at), })); + + return { tasks }; } async getTask(taskId: string): Promise { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index caca1580ad..2350072806 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -208,13 +208,13 @@ describe('StorageTaskBroker', () => { const { taskId } = await broker.dispatch({ spec: {} as TaskSpec }); const promise = broker.list(); - await expect(promise).resolves.toEqual( - expect.arrayContaining([ + await expect(promise).resolves.toEqual({ + tasks: expect.arrayContaining([ expect.objectContaining({ id: taskId, }), ]), - ); + }); }); it('should list only tasks createdBy a specific user', async () => { @@ -227,6 +227,6 @@ describe('StorageTaskBroker', () => { const task = await storage.getTask(taskId); const promise = broker.list({ createdBy: 'user:default/foo' }); - await expect(promise).resolves.toEqual([task]); + await expect(promise).resolves.toEqual({ tasks: [task] }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index fb0d673083..9e60bc7727 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -151,7 +151,14 @@ export class StorageTaskBroker implements TaskBroker { private readonly logger: Logger, ) {} - async list(options?: { createdBy?: string }): Promise { + async list(options?: { + createdBy?: string; + }): Promise<{ tasks: SerializedTask[] }> { + if (!this.storage.list) { + throw new Error( + 'TaskStore does not implement the list method. Please implement the list method to be able to list tasks', + ); + } return await this.storage.list({ createdBy: options?.createdBy }); } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index a9d3e3ae5b..4b63a0cdd3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -133,7 +133,7 @@ export interface TaskBroker { after: number | undefined; }): Observable<{ events: SerializedTaskEvent[] }>; get(taskId: string): Promise; - list(options?: { createdBy?: string }): Promise; + list?(options?: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>; } /** @@ -194,7 +194,7 @@ export interface TaskStore { listStaleTasks(options: { timeoutS: number }): Promise<{ tasks: { taskId: string }[]; }>; - list(options: { createdBy?: string }): Promise; + list?(options: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>; emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; listEvents({ diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index b65c26949c..aea33f1158 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -292,42 +292,52 @@ describe('createRouter', () => { describe('GET /v2/tasks', () => { it('return all tasks', async () => { - (taskBroker.list as jest.Mocked['list']).mockResolvedValue([ - { - id: 'a-random-id', - spec: {} as any, - status: 'completed', - createdAt: '', - createdBy: '', - }, - ]); + ( + taskBroker.list as jest.Mocked>['list'] + ).mockResolvedValue({ + tasks: [ + { + id: 'a-random-id', + spec: {} as any, + status: 'completed', + createdAt: '', + createdBy: '', + }, + ], + }); const response = await request(app).get(`/v2/tasks`); expect(taskBroker.list).toBeCalledWith({ createdBy: undefined, }); expect(response.status).toEqual(200); - expect(response.body).toStrictEqual([ - { - id: 'a-random-id', - spec: {} as any, - status: 'completed', - createdAt: '', - createdBy: '', - }, - ]); + expect(response.body).toStrictEqual({ + tasks: [ + { + id: 'a-random-id', + spec: {} as any, + status: 'completed', + createdAt: '', + createdBy: '', + }, + ], + }); }); it('return filtered tasks', async () => { - (taskBroker.list as jest.Mocked['list']).mockResolvedValue([ - { - id: 'a-random-id', - spec: {} as any, - status: 'completed', - createdAt: '', - createdBy: 'user:default/foo', - }, - ]); + ( + taskBroker.list as jest.Mocked>['list'] + ).mockResolvedValue({ + tasks: [ + { + id: 'a-random-id', + spec: {} as any, + status: 'completed', + createdAt: '', + createdBy: 'user:default/foo', + }, + ], + }); const response = await request(app).get( `/v2/tasks?createdBy=user:default/foo`, @@ -337,15 +347,17 @@ describe('createRouter', () => { }); expect(response.status).toEqual(200); - expect(response.body).toStrictEqual([ - { - id: 'a-random-id', - spec: {} as any, - status: 'completed', - createdAt: '', - createdBy: 'user:default/foo', - }, - ]); + expect(response.body).toStrictEqual({ + tasks: [ + { + id: 'a-random-id', + spec: {} as any, + status: 'completed', + createdAt: '', + createdBy: 'user:default/foo', + }, + ], + }); }); }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 553ff2b8bc..4919d2e5f9 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -253,7 +253,20 @@ export async function createRouter( res.status(201).json({ id: result.taskId }); }) .get('/v2/tasks', async (req, res) => { - const userEntityRef = req.query.createdBy?.toString(); + const [userEntityRef] = [req.query.createdBy].flat(); + + if ( + typeof userEntityRef !== 'string' && + typeof userEntityRef !== 'undefined' + ) { + throw new InputError('createdBy query parameter must be a string'); + } + + if (!taskBroker.list) { + throw new Error( + 'TaskBroker does not support listing tasks, please implement the list method on the TaskBroker.', + ); + } const tasks = await taskBroker.list({ createdBy: userEntityRef, diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index 7dc840ee96..583258c069 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -347,7 +347,7 @@ describe('api', () => { }), ); - const result = await apiClient.listTasks({ createdBy: 'all' }); + const result = await apiClient.listTasks({ filterByOwnership: 'all' }); expect(result).toHaveLength(2); }); it('should list task using the current user as owner', async () => { @@ -357,23 +357,27 @@ describe('api', () => { if (createdBy) { return res( - ctx.json([ - { - createdBy, - }, - ]), + ctx.json({ + tasks: [ + { + createdBy, + }, + ], + }), ); } return res( - ctx.json([ - { - createdBy: null, - }, - { - createdBy: null, - }, - ]), + ctx.json({ + tasks: [ + { + createdBy: null, + }, + { + createdBy: null, + }, + ], + }), ); }), ); @@ -382,9 +386,9 @@ describe('api', () => { userEntityRef: 'user:default/foo', }); - const result = await apiClient.listTasks({ createdBy: 'owned' }); + const result = await apiClient.listTasks({ filterByOwnership: 'owned' }); expect(identityApi.getBackstageIdentity).toBeCalled(); - expect(result).toHaveLength(1); + expect(result.tasks).toHaveLength(1); }); }); }); diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 6d236bb8fd..64141dd83d 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -37,7 +37,6 @@ import { ScaffolderGetIntegrationsListOptions, ScaffolderGetIntegrationsListResponse, ScaffolderTask, - TasksOwnerFilterKind, ScaffolderDryRunOptions, ScaffolderDryRunResponse, } from './types'; @@ -61,13 +60,13 @@ export class ScaffolderClient implements ScaffolderApi { private readonly discoveryApi: DiscoveryApi; private readonly scmIntegrationsApi: ScmIntegrationRegistry; private readonly fetchApi: FetchApi; - private readonly identityApi: IdentityApi; + private readonly identityApi?: IdentityApi; private readonly useLongPollingLogs: boolean; constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi; - identityApi: IdentityApi; + identityApi?: IdentityApi; scmIntegrationsApi: ScmIntegrationRegistry; useLongPollingLogs?: boolean; }) { @@ -79,18 +78,21 @@ export class ScaffolderClient implements ScaffolderApi { } async listTasks(options: { - createdBy: TasksOwnerFilterKind; - }): Promise { + filterByOwnership: 'owned' | 'all'; + }): Promise<{ tasks: ScaffolderTask[] }> { + if (!this.identityApi) { + throw new Error( + 'IdentityApi is not available in the ScaffolderClient, please pass through the IdentityApi to the ScaffolderClient constructor in order to use the listTasks method', + ); + } const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const { userEntityRef } = await this.identityApi.getBackstageIdentity(); const query = queryString.stringify( - options.createdBy === 'owned' ? { createdBy: userEntityRef } : {}, + options.filterByOwnership === 'owned' ? { createdBy: userEntityRef } : {}, ); - const url = `${baseUrl}/v2/tasks?${query}`; - - const response = await this.fetchApi.fetch(url); + const response = await this.fetchApi.fetch(`${baseUrl}/v2/tasks?${query}`); if (!response.ok) { throw await ResponseError.fromResponse(response); } diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx index 3d20fd1fd8..6dc26abd68 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx @@ -62,7 +62,7 @@ describe('', () => { }; catalogApi.getEntityByRef.mockResolvedValue(entity); - scaffolderApiMock.listTasks.mockResolvedValue([]); + scaffolderApiMock.listTasks.mockResolvedValue({ tasks: [] }); const { getByText } = await renderInTestApp( ', () => { }, }; catalogApi.getEntityByRef.mockResolvedValue(entity); - scaffolderApiMock.listTasks.mockResolvedValue([ - { - id: 'a-random-id', - spec: { - user: { ref: 'user:default/foo' }, - templateInfo: { - entityRef: 'template:default/test', - }, - } as any, - status: 'completed', - createdAt: '', - lastHeartbeatAt: '', - }, - ]); + scaffolderApiMock.listTasks.mockResolvedValue({ + tasks: [ + { + id: 'a-random-id', + spec: { + user: { ref: 'user:default/foo' }, + templateInfo: { + entityRef: 'template:default/test', + }, + } as any, + status: 'completed', + createdAt: '', + lastHeartbeatAt: '', + }, + ], + }); scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ title: 'One Template', @@ -139,7 +141,9 @@ describe('', () => { }, ); - expect(scaffolderApiMock.listTasks).toBeCalledWith({ createdBy: 'owned' }); + expect(scaffolderApiMock.listTasks).toBeCalledWith({ + filterByOwnership: 'owned', + }); expect(getByText('List template tasks')).toBeInTheDocument(); expect(getByText('All tasks that have been started')).toBeInTheDocument(); expect(getByText('Tasks')).toBeInTheDocument(); @@ -173,36 +177,40 @@ describe('', () => { }); scaffolderApiMock.listTasks - .mockResolvedValue([ - { - id: 'a-random-id', - spec: { - user: { ref: 'user:default/foo' }, - templateInfo: { - entityRef: 'template:default/mock', - }, - } as any, - status: 'completed', - createdAt: '', - lastHeartbeatAt: '', - }, - ]) - .mockResolvedValue([ - { - id: 'b-random-id', - spec: { - templateInfo: { - entityRef: 'template:default/mock', - }, - user: { - ref: 'user:default/boo', - }, - } as any, - status: 'completed', - createdAt: '', - lastHeartbeatAt: '', - }, - ]); + .mockResolvedValue({ + tasks: [ + { + id: 'a-random-id', + spec: { + user: { ref: 'user:default/foo' }, + templateInfo: { + entityRef: 'template:default/mock', + }, + } as any, + status: 'completed', + createdAt: '', + lastHeartbeatAt: '', + }, + ], + }) + .mockResolvedValue({ + tasks: [ + { + id: 'b-random-id', + spec: { + templateInfo: { + entityRef: 'template:default/mock', + }, + user: { + ref: 'user:default/boo', + }, + } as any, + status: 'completed', + createdAt: '', + lastHeartbeatAt: '', + }, + ], + }); scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ title: 'One Template', @@ -232,7 +240,9 @@ describe('', () => { fireEvent.click(allButton); }); - expect(scaffolderApiMock.listTasks).toBeCalledWith({ createdBy: 'all' }); + expect(scaffolderApiMock.listTasks).toBeCalledWith({ + filterByOwnership: 'all', + }); expect(await findByText('One Template')).toBeInTheDocument(); expect(await findByText('OtherUser')).toBeInTheDocument(); }); diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx index 5f099ed331..d45d6f1ae7 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx @@ -31,7 +31,6 @@ import React, { useState } from 'react'; import { scaffolderApiRef } from '../../api'; import { rootRouteRef } from '../../routes'; import { OwnerListPicker } from './OwnerListPicker'; -import { TasksOwnerFilterKind } from '../../types'; import { CreatedAtColumn, OwnerEntityColumn, @@ -40,7 +39,7 @@ import { } from './columns'; export interface MyTaskPageProps { - initiallySelectedFilter?: TasksOwnerFilterKind; + initiallySelectedFilter?: 'owned' | 'all'; } const ListTaskPageContent = (props: MyTaskPageProps) => { @@ -52,14 +51,15 @@ const ListTaskPageContent = (props: MyTaskPageProps) => { const [ownerFilter, setOwnerFilter] = useState(initiallySelectedFilter); const { value, loading, error } = useAsync(() => { if (scaffolderApi.listTasks) { - return scaffolderApi.listTasks?.({ createdBy: ownerFilter }); + return scaffolderApi.listTasks?.({ filterByOwnership: ownerFilter }); } // eslint-disable-next-line no-console console.warn( 'listTasks is not implemented in the scaffolderApi, please make sure to implement this method.', ); - return Promise.resolve([]); + + return Promise.resolve({ tasks: [] }); }, [scaffolderApi, ownerFilter]); if (loading) { @@ -89,7 +89,7 @@ const ListTaskPageContent = (props: MyTaskPageProps) => { ( theme => ({ root: { @@ -91,7 +89,7 @@ function getFilterGroups(): ButtonGroup[] { export const OwnerListPicker = (props: { filter: string; - onSelectOwner: (id: TasksOwnerFilterKind) => void; + onSelectOwner: (id: 'owned' | 'all') => void; }) => { const { filter, onSelectOwner } = props; const classes = useStyles(); @@ -111,7 +109,7 @@ export const OwnerListPicker = (props: { key={item.id} button divider - onClick={() => onSelectOwner(item.id as TasksOwnerFilterKind)} + onClick={() => onSelectOwner(item.id as 'owned' | 'all')} selected={item.id === filter} className={classes.menuItem} data-testid={`owner-picker-${item.id}`} diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index d98fd96e55..c42dc95f9e 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -37,7 +37,6 @@ export type { ScaffolderTaskOutput, ScaffolderTaskStatus, TemplateParameterSchema, - TasksOwnerFilterKind, } from './types'; export { createScaffolderFieldExtension, diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index df7afe065f..710af6ad23 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -102,13 +102,6 @@ export type LogEvent = { taskId: string; }; -/** - * The status of each task you can filter from `ScaffolderClient` - * - * @public - */ -export type TasksOwnerFilterKind = 'owned' | 'all'; - /** * The input options to the `scaffold` method of the `ScaffolderClient`. * @@ -200,10 +193,10 @@ export interface ScaffolderApi { getTask(taskId: string): Promise; listTasks?({ - createdBy, + filterByOwnership, }: { - createdBy: TasksOwnerFilterKind; - }): Promise; + filterByOwnership: 'owned' | 'all'; + }): Promise<{ tasks: ScaffolderTask[] }>; getIntegrationsList( options: ScaffolderGetIntegrationsListOptions, diff --git a/yarn.lock b/yarn.lock index aeac084923..e7d10f183d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4176,6 +4176,14 @@ resolved "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz#155ef21065427901994e765da8a0ba0eaae8b8bd" integrity sha512-6Wci+Tp3CgPt/B9B0a3J4s3yMgLNSku6w5TV6mN+61C71UqsRBv2FUibBf3tPGlNxebgPHMEUzKpb1ggE8KCKw== +"@mswjs/cookies@^0.1.6": + version "0.1.7" + resolved "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.1.7.tgz#d334081b2c51057a61c1dd7b76ca3cac02251651" + integrity sha512-bDg1ReMBx+PYDB4Pk7y1Q07Zz1iKIEUWQpkEXiA2lEWg9gvOZ8UBmGXilCEUvyYoRFlmr/9iXTRR69TrgSwX/Q== + dependencies: + "@types/set-cookie-parser" "^2.4.0" + set-cookie-parser "^2.4.6" + "@mswjs/cookies@^0.2.0": version "0.2.0" resolved "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.2.0.tgz#7ef2b5d7e444498bb27cf57720e61f76a4ce9f23" @@ -4184,6 +4192,18 @@ "@types/set-cookie-parser" "^2.4.0" set-cookie-parser "^2.4.6" +"@mswjs/interceptors@^0.12.6": + version "0.12.7" + resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.12.7.tgz#0d1cd4cd31a0f663e0455993951201faa09d0909" + integrity sha512-eGjZ3JRAt0Fzi5FgXiV/P3bJGj0NqsN7vBS0J0FO2AQRQ0jCKQS4lEFm4wvlSgKQNfeuc/Vz6d81VtU3Gkx/zg== + dependencies: + "@open-draft/until" "^1.0.3" + "@xmldom/xmldom" "^0.7.2" + debug "^4.3.2" + headers-utils "^3.0.2" + outvariant "^1.2.0" + strict-event-emitter "^0.2.0" + "@mswjs/interceptors@^0.15.1": version "0.15.3" resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.15.3.tgz#bcd17b5d7558d4f598007a4bb383b42dc9264f8d" @@ -5967,6 +5987,14 @@ resolved "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.27.1.tgz#f14740d1f585a0a8e3f46359b62fda8b0eaa31e7" integrity sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w== +"@types/inquirer@^7.3.3": + version "7.3.3" + resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.3.tgz#92e6676efb67fa6925c69a2ee638f67a822952ac" + integrity sha512-HhxyLejTHMfohAuhRun4csWigAMjXTmRyiJTU1Y/I1xmggikFMkOUoMQRlFm+zQcPEGHSs3io/0FAmNZf8EymQ== + dependencies: + "@types/through" "*" + rxjs "^6.4.0" + "@types/inquirer@^8.1.3": version "8.2.1" resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.1.tgz#28a139be3105a1175e205537e8ac10830e38dbf4" @@ -6043,7 +6071,7 @@ resolved "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f" integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== -"@types/js-levenshtein@^1.1.1": +"@types/js-levenshtein@^1.1.0", "@types/js-levenshtein@^1.1.1": version "1.1.1" resolved "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5" integrity sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g== @@ -7163,7 +7191,7 @@ "@webassemblyjs/ast" "1.11.1" "@xtuc/long" "4.2.2" -"@xmldom/xmldom@^0.7.0", "@xmldom/xmldom@^0.7.5": +"@xmldom/xmldom@^0.7.0", "@xmldom/xmldom@^0.7.2", "@xmldom/xmldom@^0.7.5": version "0.7.5" resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d" integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A== @@ -9827,7 +9855,7 @@ cookie@0.4.1: resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== -cookie@0.4.2, cookie@^0.4.2, cookie@~0.4.1: +cookie@0.4.2, cookie@^0.4.1, cookie@^0.4.2, cookie@~0.4.1: version "0.4.2" resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== @@ -13556,6 +13584,11 @@ graphql-ws@^5.4.1: resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.5.5.tgz#f375486d3f196e2a2527b503644693ae3a8670a9" integrity sha512-hvyIS71vs4Tu/yUYHPvGXsTgo0t3arU820+lT5VjZS2go0ewp2LqyCgxEN56CzOG7Iys52eRhHBiD1gGRdiQtw== +graphql@^15.5.1: + version "15.8.0" + resolved "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" + integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== + graphql@^16.0.0, graphql@^16.3.0: version "16.5.0" resolved "https://registry.npmjs.org/graphql/-/graphql-16.5.0.tgz#41b5c1182eaac7f3d47164fb247f61e4dfb69c85" @@ -13782,6 +13815,11 @@ headers-polyfill@^3.0.4: resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.0.7.tgz#725c4f591e6748f46b036197eae102c92b959ff4" integrity sha512-JoLCAdCEab58+2/yEmSnOlficyHFpIl0XJqwu3l+Unkm1gXpFUYsThz6Yha3D6tNhocWkCPfyW0YVIGWFqTi7w== +headers-utils@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/headers-utils/-/headers-utils-3.0.2.tgz#dfc65feae4b0e34357308aefbcafa99c895e59ef" + integrity sha512-xAxZkM1dRyGV2Ou5bzMxBPNLoRCjcX+ya7KSWybQD2KwLphxsapUVK6x/02o7f4VU6GPSXch9vNY2+gkU8tYWQ== + helmet@^5.0.2: version "5.0.2" resolved "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz#3264ec6bab96c82deaf65e3403c369424cb2366c" @@ -14361,7 +14399,7 @@ inquirer@^7.3.3: strip-ansi "^6.0.0" through "^2.3.6" -inquirer@^8.0.0, inquirer@^8.2.0: +inquirer@^8.0.0, inquirer@^8.1.1, inquirer@^8.2.0: version "8.2.4" resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== @@ -18146,6 +18184,32 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msw@^0.35.0: + version "0.35.0" + resolved "https://registry.npmjs.org/msw/-/msw-0.35.0.tgz#18a4ceb6c822ef226a30421d434413bc45030d38" + integrity sha512-V7A6PqaS31F1k//fPS0OnO7vllfaqBUFsMEu3IpYixyWpiUInfyglodnbXhhtDyytkQikpkPZv8TZi/CvZzv/w== + dependencies: + "@mswjs/cookies" "^0.1.6" + "@mswjs/interceptors" "^0.12.6" + "@open-draft/until" "^1.0.3" + "@types/cookie" "^0.4.1" + "@types/inquirer" "^7.3.3" + "@types/js-levenshtein" "^1.1.0" + chalk "^4.1.1" + chokidar "^3.4.2" + cookie "^0.4.1" + graphql "^15.5.1" + headers-utils "^3.0.2" + inquirer "^8.1.1" + is-node-process "^1.0.1" + js-levenshtein "^1.1.6" + node-fetch "^2.6.1" + node-match-path "^0.6.3" + statuses "^2.0.0" + strict-event-emitter "^0.2.0" + type-fest "^1.2.2" + yargs "^17.0.1" + msw@^0.39.2: version "0.39.2" resolved "https://registry.npmjs.org/msw/-/msw-0.39.2.tgz#832e9274db62c43cb79854d5a69dce031c700de8" @@ -18490,6 +18554,11 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" +node-match-path@^0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/node-match-path/-/node-match-path-0.6.3.tgz#55dd8443d547f066937a0752dce462ea7dc27551" + integrity sha512-fB1reOHKLRZCJMAka28hIxCwQLxGmd7WewOCBDYKpyA1KXi68A7vaGgdZAPhY2E6SXoYt3KqYCCvXLJ+O0Fu/Q== + node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" @@ -19099,7 +19168,7 @@ outdent@^0.5.0: resolved "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz#9e10982fdc41492bb473ad13840d22f9655be2ff" integrity sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q== -outvariant@^1.2.1, outvariant@^1.3.0: +outvariant@^1.2.0, outvariant@^1.2.1, outvariant@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/outvariant/-/outvariant-1.3.0.tgz#c39723b1d2cba729c930b74bf962317a81b9b1c9" integrity sha512-yeWM9k6UPfG/nzxdaPlJkB2p08hCg4xP6Lx99F+vP8YF7xyZVfTmJjrrNalkmzudD4WFvNLVudQikqUmF8zhVQ== @@ -22133,7 +22202,7 @@ rxjs@7.5.5, rxjs@^7.1.0, rxjs@^7.2.0, rxjs@^7.5.1, rxjs@^7.5.5: dependencies: tslib "^2.1.0" -rxjs@^6.3.3, rxjs@^6.6.0, rxjs@^6.6.3: +rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.6.0, rxjs@^6.6.3: version "6.6.7" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== @@ -25768,7 +25837,7 @@ yargs@^17.0.0, yargs@^17.1.1, yargs@^17.2.1: y18n "^5.0.5" yargs-parser "^21.0.0" -yargs@^17.3.1: +yargs@^17.0.1, yargs@^17.3.1: version "17.5.1" resolved "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== From c67af10222f2d05b348accf583f689c6c9c716b6 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 31 May 2022 16:43:57 +0200 Subject: [PATCH 28/30] chore: fixing api-report Signed-off-by: blam --- plugins/scaffolder-backend/api-report.md | 12 +++++++++--- plugins/scaffolder/api-report.md | 19 +++++++++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index f26c242f97..b6a67b7c55 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -396,7 +396,9 @@ export class DatabaseTaskStore implements TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) - list(options: { createdBy?: string }): Promise; + list(options: { createdBy?: string }): Promise<{ + tasks: SerializedTask[]; + }>; // (undocumented) listEvents({ taskId, after }: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent[]; @@ -522,7 +524,9 @@ export interface TaskBroker { // (undocumented) get(taskId: string): Promise; // (undocumented) - list(options?: { createdBy?: string }): Promise; + list?(options?: { createdBy?: string }): Promise<{ + tasks: SerializedTask[]; + }>; // (undocumented) vacuumTasks(options: { timeoutS: number }): Promise; } @@ -623,7 +627,9 @@ export interface TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) - list(options: { createdBy?: string }): Promise; + list?(options: { createdBy?: string }): Promise<{ + tasks: SerializedTask[]; + }>; // (undocumented) listEvents({ taskId, after }: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent[]; diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 6fbcd78df1..9ffca426ac 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -243,10 +243,12 @@ export interface ScaffolderApi { listActions(): Promise; // (undocumented) listTasks?({ - createdBy, + filterByOwnership, }: { - createdBy: TasksOwnerFilterKind; - }): Promise; + filterByOwnership: 'owned' | 'all'; + }): Promise<{ + tasks: ScaffolderTask[]; + }>; scaffold( options: ScaffolderScaffoldOptions, ): Promise; @@ -262,7 +264,7 @@ export class ScaffolderClient implements ScaffolderApi { constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi; - identityApi: IdentityApi; + identityApi?: IdentityApi; scmIntegrationsApi: ScmIntegrationRegistry; useLongPollingLogs?: boolean; }); @@ -281,9 +283,9 @@ export class ScaffolderClient implements ScaffolderApi { // (undocumented) listActions(): Promise; // (undocumented) - listTasks(options: { - createdBy: TasksOwnerFilterKind; - }): Promise; + listTasks(options: { filterByOwnership: 'owned' | 'all' }): Promise<{ + tasks: ScaffolderTask[]; + }>; scaffold( options: ScaffolderScaffoldOptions, ): Promise; @@ -424,9 +426,6 @@ export type TaskPageProps = { loadingText?: string; }; -// @public -export type TasksOwnerFilterKind = 'owned' | 'all'; - // @alpha (undocumented) export type TemplateGroupFilter = { title?: React_2.ReactNode; From 14f38803d00247f7f3428374290916c6368281df Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 31 May 2022 16:54:39 +0200 Subject: [PATCH 29/30] chore: reset one file Signed-off-by: blam --- .../examples/acme/team-a-group.yaml | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 packages/catalog-model/examples/acme/team-a-group.yaml diff --git a/packages/catalog-model/examples/acme/team-a-group.yaml b/packages/catalog-model/examples/acme/team-a-group.yaml new file mode 100644 index 0000000000..e343209d5f --- /dev/null +++ b/packages/catalog-model/examples/acme/team-a-group.yaml @@ -0,0 +1,60 @@ +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: team-a + description: Team A +spec: + type: team + profile: + # Intentional no displayName for testing + email: team-a@example.com + picture: https://avatars.dicebear.com/api/identicon/team-a@example.com.svg?background=%23fff&margin=25 + parent: backstage + children: [] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: breanna.davison +spec: + profile: + # Intentional no displayName for testing + email: breanna-davison@example.com + picture: https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg?background=%23fff + memberOf: [team-a] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: janelle.dawe +spec: + profile: + displayName: Janelle Dawe + email: janelle-dawe@example.com + picture: https://avatars.dicebear.com/api/avataaars/janelle-dawe@example.com.svg?background=%23fff + memberOf: [team-a] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: nigel.manning +spec: + profile: + displayName: Nigel Manning + email: nigel-manning@example.com + picture: https://avatars.dicebear.com/api/avataaars/nigel-manning@example.com.svg?background=%23fff + memberOf: [team-a] +--- +# This user is added as an example, to make it more easy for the "Guest" +# sign-in option to demonstrate some entities being owned. In a regular org, +# a guest user would probably not be registered like this. +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: guest +spec: + profile: + displayName: Guest User + email: guest@example.com + picture: https://avatars.dicebear.com/api/avataaars/guest@example.com.svg?background=%23fff + memberOf: [team-a] From 582003a0596a16d4d37e119afe2b0420504af699 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Jun 2022 10:42:47 +0200 Subject: [PATCH 30/30] chore: split out the changeset into two seperate ones Signed-off-by: blam --- .changeset/poor-zebras-design.md | 7 +++++++ .changeset/rich-zebras-design.md | 7 +++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 .changeset/poor-zebras-design.md diff --git a/.changeset/poor-zebras-design.md b/.changeset/poor-zebras-design.md new file mode 100644 index 0000000000..8b618610d1 --- /dev/null +++ b/.changeset/poor-zebras-design.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +- Added an optional `list` method on the `TaskBroker` and `TaskStore` interface to list tasks by an optional `userEntityRef` +- Implemented a `list` method on the `DatabaseTaskStore` class to list tasks by an optional `userEntityRef` +- Added a route under `/v2/tasks` to list tasks by a `userEntityRef` using the `createdBy` query parameter diff --git a/.changeset/rich-zebras-design.md b/.changeset/rich-zebras-design.md index fc121349aa..916226787a 100644 --- a/.changeset/rich-zebras-design.md +++ b/.changeset/rich-zebras-design.md @@ -1,8 +1,7 @@ --- '@backstage/plugin-scaffolder': minor -'@backstage/plugin-scaffolder-backend': minor --- -- Add `listTasks` option to get tasks optionally filtered by `createdBy` - -- Added a new page under `/create/tasks` to show those tasks, optionally grouped by the current signed in user. +- Added a new page under `/create/tasks` to show tasks that have been run by the Scaffolder. +- Ability to filter these tasks by the signed in user, and all tasks. +- Added optional method to the `ScaffolderApi` interface called `listTasks` to get tasks with an required `filterByOwnership` parameter.