diff --git a/.changeset/good-fans-dance.md b/.changeset/good-fans-dance.md new file mode 100644 index 0000000000..a24b467c01 --- /dev/null +++ b/.changeset/good-fans-dance.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-bazaar': patch +'@backstage/plugin-bazaar-backend': patch +--- + +made the linkage between a Bazaar project to a catalog Entity optional diff --git a/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js b/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js new file mode 100644 index 0000000000..03ed0a8d05 --- /dev/null +++ b/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js @@ -0,0 +1,171 @@ +/* + * Copyright 2021 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. + */ + +exports.up = async function up(knex) { + if (knex.client.config.client === 'sqlite3') { + await knex.schema.dropTable('metadata'); + await knex.schema.createTable('metadata', table => { + table.increments('id').comment('Automatically generated unique ID'); + table.text('entity_ref').unique().comment('The ref of the entity'); + table.text('name').notNullable().comment('The name of the entity'); + table + .text('community') + .comment('Link to where the community can discuss ideas'); + table + .text('description') + .notNullable() + .comment('The description of the Bazaar project'); + table + .text('status') + .defaultTo('proposed') + .notNullable() + .comment('The status of the Bazaar project'); + table + .text('updated_at') + .notNullable() + .comment('Timestamp on ISO 8601 format when entity was last updated'); + table + .text('size') + .defaultTo('medium') + .notNullable() + .comment('The estimated magnitude of the project'); + table + .text('start_date') + .comment('Optional start date of the project (ISO 8601 format)'); + table + .text('end_date') + .comment('Optional end date of the project (ISO 8601 format)'); + table + .text('responsible') + .notNullable() + .comment('Contact person of the project'); + }); + + await knex.schema.dropTable('members'); + await knex.schema.createTable('members', table => { + table + .integer('item_id') + .references('metadata.id') + .onDelete('CASCADE') + .comment('Id of the associated item'); + table + .text('entity_ref') + .references('metadata.entity_ref') + .onDelete('CASCADE') + .comment('The ref of the entity'); + table.text('user_id').notNullable().comment('The user id of the member'); + table + .dateTime('join_date') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('The timestamp when this member joined'); + table.text('picture').comment('Link to profile picture'); + }); + } else { + await knex.schema.alterTable('metadata', table => { + table.renameColumn('announcement', 'description'); + table.increments('id').comment('Automatically generated unique ID'); + table.string('entity_ref').nullable().alter(); + }); + + await knex.schema.alterTable('members', table => { + table + .integer('item_id') + .references('metadata.id') + .onDelete('CASCADE') + .comment('Id of the associated item'); + table.dropColumn('entity_ref'); + }); + } +}; + +exports.down = async function down(knex) { + if (knex.client.config.client === 'sqlite3') { + await knex.schema.dropTable('metadata'); + await knex.schema.createTable('metadata', table => { + table + .text('entity_ref') + .notNullable() + .unique() + .comment('The ref of the entity'); + table.text('name').notNullable().comment('The name of the entity'); + table + .text('community') + .comment('Link to where the community can discuss ideas'); + table + .text('announcement') + .notNullable() + .comment('The announcement of the Bazaar project'); + table + .text('status') + .defaultTo('proposed') + .notNullable() + .comment('The status of the Bazaar project'); + table + .text('updated_at') + .notNullable() + .comment('Timestamp on ISO 8601 format when entity was last updated'); + table + .text('size') + .defaultTo('medium') + .notNullable() + .comment('The estimated magnitude of the project'); + table + .text('start_date') + .comment('Optional start date of the project (ISO 8601 format)'); + table + .text('end_date') + .comment('Optional end date of the project (ISO 8601 format)'); + table + .text('responsible') + .notNullable() + .comment('Contact person of the project'); + }); + + await knex.schema.dropTable('members'); + await knex.schema.createTable('members', table => { + table + .text('entity_ref') + .notNullable() + .references('metadata.entity_ref') + .onDelete('CASCADE') + .comment('The ref of the entity'); + table.text('user_id').notNullable().comment('The user id of the member'); + table + .dateTime('join_date') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('The timestamp when this member joined'); + table.text('picture').comment('Link to profile picture'); + }); + } else { + await knex.schema.alterTable('metadata', table => { + table.renameColumn('description', 'announcement'); + table.string('entity_ref').notNullable().alter(); + table.dropColumn('id'); + }); + + await knex.schema.alterTable('members', table => { + table.dropColumn('item_id'); + table + .text('entity_ref') + .notNullable() + .references('metadata.entity_ref') + .onDelete('CASCADE') + .comment('The ref of the entity'); + }); + } +}; diff --git a/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts b/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts index 3afde44d5e..3099e5bbb2 100644 --- a/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts +++ b/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts @@ -22,13 +22,14 @@ const bazaarProject: any = { entityRef: 'ref1', community: '', status: 'proposed', - announcement: 'a', + description: 'a', membersCount: 0, startDate: '2021-11-07T13:27:00.000Z', endDate: null, size: 'small', responsible: 'r', }; + describe('DatabaseHandler', () => { const databases = TestDatabases.create({ ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], @@ -50,7 +51,7 @@ describe('DatabaseHandler', () => { await knex('metadata').insert({ entity_ref: bazaarProject.entityRef, name: bazaarProject.name, - announcement: bazaarProject.announcement, + description: bazaarProject.description, community: bazaarProject.community, status: bazaarProject.status, updated_at: new Date().toISOString(), @@ -60,10 +61,10 @@ describe('DatabaseHandler', () => { responsible: bazaarProject.responsible, }); - const res = await dbHandler.getMetadata('ref1'); + const res = await dbHandler.getMetadataByRef('ref1'); expect(res).toHaveLength(1); - expect(res[0].announcement).toEqual('a'); + expect(res[0].description).toEqual('a'); expect(res[0].community).toEqual(''); expect(res[0].status).toEqual('proposed'); expect(res[0].start_date).toEqual('2021-11-07T13:27:00.000Z'); diff --git a/plugins/bazaar-backend/src/service/DatabaseHandler.ts b/plugins/bazaar-backend/src/service/DatabaseHandler.ts index 60f579af2d..7da06a54b8 100644 --- a/plugins/bazaar-backend/src/service/DatabaseHandler.ts +++ b/plugins/bazaar-backend/src/service/DatabaseHandler.ts @@ -44,10 +44,10 @@ export class DatabaseHandler { } private columns = [ - 'members.entity_ref', + 'metadata.id', 'metadata.entity_ref', 'metadata.name', - 'metadata.announcement', + 'metadata.description', 'metadata.status', 'metadata.updated_at', 'metadata.community', @@ -57,40 +57,52 @@ export class DatabaseHandler { 'metadata.responsible', ]; - async getMembers(entityRef: string) { + async getMembers(id: string) { return await this.database .select('*') .from('members') - .where({ entity_ref: entityRef }); + .where({ item_id: id }); } - async addMember(userId: string, entityRef: string, picture?: string) { + async addMember(id: number, userId: string, picture?: string) { await this.database .insert({ - entity_ref: entityRef, + item_id: id, user_id: userId, picture: picture, }) .into('members'); } - async deleteMember(userId: string, entityRef: string) { + async deleteMember(id: number, userId: string) { return await this.database('members') - .where({ entity_ref: decodeURIComponent(entityRef) }) + .where({ item_id: id }) .andWhere('user_id', userId) .del(); } - async getMetadata(entityRef: string) { + async getMetadataById(id: number) { const coalesce = this.database.raw( - 'coalesce(count(members.entity_ref), 0) as members_count', + 'coalesce(count(members.item_id), 0) as members_count', + ); + + return await this.database('metadata') + .select([...this.columns, coalesce]) + .where({ 'metadata.id': id }) + .groupBy(this.columns) + .leftJoin('members', 'metadata.id', '=', 'members.item_id'); + } + + async getMetadataByRef(entityRef: string) { + const coalesce = this.database.raw( + 'coalesce(count(members.item_id), 0) as members_count', ); return await this.database('metadata') .select([...this.columns, coalesce]) .where({ 'metadata.entity_ref': entityRef }) .groupBy(this.columns) - .leftJoin('members', 'metadata.entity_ref', '=', 'members.entity_ref'); + .leftJoin('members', 'metadata.id', '=', 'members.item_id'); } async insertMetadata(bazaarProject: any) { @@ -98,7 +110,7 @@ export class DatabaseHandler { name, entityRef, community, - announcement, + description, status, size, startDate, @@ -108,11 +120,11 @@ export class DatabaseHandler { await this.database .insert({ - name: name, + name, entity_ref: entityRef, - community: community, - announcement: announcement, - status: status, + community, + description, + status, updated_at: new Date().toISOString(), size, start_date: startDate, @@ -124,9 +136,11 @@ export class DatabaseHandler { async updateMetadata(bazaarProject: any) { const { + name, + id, entityRef, community, - announcement, + description, status, size, startDate, @@ -134,34 +148,32 @@ export class DatabaseHandler { responsible, } = bazaarProject; - return await this.database('metadata') - .where({ entity_ref: entityRef }) - .update({ - announcement: announcement, - community: community, - status: status, - updated_at: new Date().toISOString(), - size, - start_date: startDate, - end_date: endDate, - responsible, - }); + return await this.database('metadata').where({ id: id }).update({ + name, + entity_ref: entityRef, + description, + community, + status, + updated_at: new Date().toISOString(), + size, + start_date: startDate, + end_date: endDate, + responsible, + }); } - async deleteMetadata(entityRef: string) { - return await this.database('metadata') - .where({ entity_ref: entityRef }) - .del(); + async deleteMetadata(id: number) { + return await this.database('metadata').where({ id: id }).del(); } - async getEntities() { + async getProjects() { const coalesce = this.database.raw( - 'coalesce(count(members.entity_ref), 0) as members_count', + 'coalesce(count(members.item_id), 0) as members_count', ); return await this.database('metadata') .select([...this.columns, coalesce]) .groupBy(this.columns) - .leftJoin('members', 'metadata.entity_ref', '=', 'members.entity_ref'); + .leftJoin('members', 'metadata.id', '=', 'members.item_id'); } } diff --git a/plugins/bazaar-backend/src/service/router.ts b/plugins/bazaar-backend/src/service/router.ts index f0bc02c65b..730d958fcd 100644 --- a/plugins/bazaar-backend/src/service/router.ts +++ b/plugins/bazaar-backend/src/service/router.ts @@ -40,28 +40,27 @@ export async function createRouter( const router = Router(); router.use(express.json()); - router.get('/members/:ref', async (request, response) => { - const entity_ref = decodeURIComponent(request.params.ref); - const data = await dbHandler.getMembers(entity_ref); + router.get('/projects/:id/members', async (request, response) => { + const members = await dbHandler.getMembers(request.params.id); - if (data?.length) { - response.json({ status: 'ok', data: data }); + if (members?.length) { + response.json({ status: 'ok', data: members }); } else { response.json({ status: 'ok', data: [] }); } }); - router.put('/member', async (request, response) => { - const { user_id, entity_ref, picture } = request.body; - await dbHandler.addMember(user_id, entity_ref, picture); + router.put('/projects/:id/member/:userId', async (request, response) => { + const { id, userId } = request.params; + await dbHandler.addMember(parseInt(id, 10), userId, request.body?.picture); response.json({ status: 'ok' }); }); - router.delete('/member/:ref/:id', async (request, response) => { - const { ref, id } = request.params; + router.delete('/projects/:id/member/:userId', async (request, response) => { + const { id, userId } = request.params; - const count = await dbHandler.deleteMember(id, ref); + const count = await dbHandler.deleteMember(parseInt(id, 10), userId); if (count) { response.json({ status: 'ok' }); @@ -70,36 +69,48 @@ export async function createRouter( } }); - router.get('/metadata/:ref', async (request, response) => { + router.get('/projects/id/:id', async (request, response) => { + const id = decodeURIComponent(request.params.id); + + const data = await dbHandler.getMetadataById(parseInt(id, 10)); + + response.json({ status: 'ok', data: data }); + }); + + router.get('/projects/ref/:ref', async (request, response) => { const ref = decodeURIComponent(request.params.ref); - const data = await dbHandler.getMetadata(ref); + const data = await dbHandler.getMetadataByRef(ref); response.json({ status: 'ok', data: data }); }); - router.get('/entities', async (_, response) => { - const data = await dbHandler.getEntities(); + router.get('/projects', async (_, response) => { + const data = await dbHandler.getProjects(); response.json({ status: 'ok', data: data }); }); - router.put('/metadata', async (request, response) => { + router.put('/projects', async (request, response) => { const bazaarProject = request.body; const count = await dbHandler.updateMetadata(bazaarProject); if (count) { response.json({ status: 'ok' }); - } else { - await dbHandler.insertMetadata(bazaarProject); - response.json({ status: 'ok' }); } }); - router.delete('/metadata/:ref', async (request, response) => { - const ref = decodeURIComponent(request.params.ref); + router.post('/projects', async (request, response) => { + const bazaarProject = request.body; - const count = await dbHandler.deleteMetadata(ref); + await dbHandler.insertMetadata(bazaarProject); + response.json({ status: 'ok' }); + }); + + router.delete('/projects/:id', async (request, response) => { + const id = decodeURIComponent(request.params.id); + + const count = await dbHandler.deleteMetadata(parseInt(id, 10)); if (count) { response.json({ status: 'ok' }); diff --git a/plugins/bazaar/README.md b/plugins/bazaar/README.md index f07e751984..dea25727b0 100644 --- a/plugins/bazaar/README.md +++ b/plugins/bazaar/README.md @@ -69,61 +69,42 @@ const overviewContent = ( ### Layout -The latest modified Bazaar projects are displayed in the Bazaar landing page, located at the Bazaar icon in the sidebar. Each project is represented as a card containing its most relevant data to give an overview of the project. The list of project is paginated. +The latest modified Bazaar projects are displayed in the Bazaar landing page, located at the Bazaar icon in the sidebar. Each project is represented as a card containing its most relevant data to give an overview of the project. It is also possible to sort in alphabetical order or on the number of members. Here you can also search or add project to the Bazaar. -![home](media/bazaar_pr_fullscreen.png) +![home](media/layout.png) ### Workflow -To add a project to the Bazaar, you need to create a project with one of the templates in Backstage. Click the add project-button, choose the project and fill in the form. +To add a project to the bazaar, simply click on the `add-project` button and fill in the form. The following fields are mandatory: -- announcement - present your idea and what skills you are looking for +- name - name of the project on URL safe format +- description - present your idea and what skills you are looking for - status - whether or not the project has started - size - small, medium or large - responsible - main contact person of the project The other fields are: +- project - link Bazaar project to existing entity in the catalog +- community link - link to where the project members can communicate, e.g. Teams or Discord link - start date - end date -- community link - link where the project members can chat, e.g. Teams or Discord link -When the project is added, you will see the Bazaar information in the Bazaar card on the entity page. There you can join a project, edit or delete it. +When clicking on a Bazaar project a card containing the Bazaar information will show up. If the Bazaar project is linked to an entity, the card is also visible on that entity's EntityPage. From that card it is possible to either link or unlink an entity to a project, edit or delete the project and join the project if it seems interesting to you. Once you have joined a project, you will get access to the community link if it exists. -### Database - -The metadata related to the Bazaar is stored in a database. Right now there are two tables, one for storing the metadata and one for storing the members of a Bazaar project. - -**metadata**: - -- name - name of the entity -- entity_ref - namespace/kind/name of the entity -- announcement - announcement of the project and its current need of skills/team member -- status - status of the project, 'proposed' or 'ongoing' -- updated_at - date when the Bazaar information was last modified (ISO 8601 format) -- size - small, medium or large -- start_date - date when the project is estimated to start (ISO 8601 format) -- end_date - date when the project is estimated to end (ISO 8601 format) -- responsible - main contact person of the project - -**members**: - -- entity_ref - namespace/kind/name of the entity -- user_name -- join_date - date when the user joined the project (ISO 8601 format) +![home](media/demo.gif) ## Future work and ideas - Workflow - - Make it possible to add a Bazaar project without linking it to a Backstage entity, this would make it easier to just add an idea to the Bazaar. + - Make it possible for multiple Bazaar project to link to the same catalog entity - Bazaar landing page - Add a tab 'My page', where your personal data is displayed. For example: your projects and its latest activities etc. - - Make it possible to sort the project based on e.g. the number of members - Bazaar tab on the EntityPage @@ -133,6 +114,3 @@ The metadata related to the Bazaar is stored in a database. Right now there are - Dialogues - Extend the dialogue for adding a project with more fields, e.g. the possibility to add images - -- Testing - - Add tests to all the components diff --git a/plugins/bazaar/media/bazaar_pr_fullscreen.png b/plugins/bazaar/media/bazaar_pr_fullscreen.png deleted file mode 100644 index 6c5cf1277b..0000000000 Binary files a/plugins/bazaar/media/bazaar_pr_fullscreen.png and /dev/null differ diff --git a/plugins/bazaar/media/demo.gif b/plugins/bazaar/media/demo.gif new file mode 100644 index 0000000000..6369ce7551 Binary files /dev/null and b/plugins/bazaar/media/demo.gif differ diff --git a/plugins/bazaar/media/layout.png b/plugins/bazaar/media/layout.png new file mode 100644 index 0000000000..cc02a374a3 Binary files /dev/null and b/plugins/bazaar/media/layout.png differ diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 88da003778..6ccda73f93 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -34,6 +34,7 @@ "@material-ui/pickers": "^3.3.10", "@testing-library/jest-dom": "^5.10.1", "luxon": "^2.0.2", + "material-ui-search-bar": "^1.0.0", "react-hook-form": "^7.13.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" diff --git a/plugins/bazaar/plugin-bazaar.api.md b/plugins/bazaar/plugin-bazaar.api.md new file mode 100644 index 0000000000..33a674ffc4 --- /dev/null +++ b/plugins/bazaar/plugin-bazaar.api.md @@ -0,0 +1,25 @@ +## API Report File for "@backstage/plugin-bazaar" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import * as _backstage_core_plugin_api from '@backstage/core-plugin-api'; + +// @public (undocumented) +export const BazaarPage: () => JSX.Element; + +// @public (undocumented) +export const bazaarPlugin: _backstage_core_plugin_api.BackstagePlugin< + { + root: _backstage_core_plugin_api.RouteRef; + }, + {} +>; + +// @public (undocumented) +export const EntityBazaarInfoCard: () => JSX.Element; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/bazaar/src/api.ts b/plugins/bazaar/src/api.ts index db20e5e0cb..2faa233077 100644 --- a/plugins/bazaar/src/api.ts +++ b/plugins/bazaar/src/api.ts @@ -14,13 +14,11 @@ * limitations under the License. */ -import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { createApiRef, DiscoveryApi, IdentityApi, } from '@backstage/core-plugin-api'; -import { BazaarProject } from './types'; export const bazaarApiRef = createApiRef({ id: 'bazaar', @@ -28,19 +26,23 @@ export const bazaarApiRef = createApiRef({ }); export interface BazaarApi { - updateMetadata(bazaarProject: BazaarProject): Promise; + updateProject(bazaarProject: any): Promise; - getMetadata(entity: Entity): Promise; + addProject(bazaarProject: any): Promise; - getMembers(entity: Entity): Promise; + getProjectById(id: number): Promise; - deleteMember(entity: Entity): Promise; + getProjectByRef(entityRef: string): Promise; - addMember(entity: Entity): Promise; + getMembers(id: number): Promise; - getEntities(): Promise; + deleteMember(id: number, userId: string): Promise; - deleteEntity(bazaarProject: BazaarProject): Promise; + addMember(id: number, userId: string): Promise; + + getProjects(): Promise; + + deleteProject(id: number): Promise; } export class BazaarClient implements BazaarApi { @@ -55,10 +57,10 @@ export class BazaarClient implements BazaarApi { this.discoveryApi = options.discoveryApi; } - async updateMetadata(bazaarProject: BazaarProject): Promise { + async updateProject(bazaarProject: any): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - return await fetch(`${baseUrl}/metadata`, { + return await fetch(`${baseUrl}/projects`, { method: 'PUT', headers: { Accept: 'application/json', @@ -68,11 +70,34 @@ export class BazaarClient implements BazaarApi { }).then(resp => resp.json()); } - async getMetadata(entity: Entity): Promise { + async addProject(bazaarProject: any): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); + + return await fetch(`${baseUrl}/projects`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(bazaarProject), + }).then(resp => resp.json()); + } + + async getProjectById(id: number): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); + + const response = await fetch(`${baseUrl}/projects/id/${id}`, { + method: 'GET', + }); + + return response.ok ? response : null; + } + + async getProjectByRef(entityRef: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); const response = await fetch( - `${baseUrl}/metadata/${encodeURIComponent(stringifyEntityRef(entity))}`, + `${baseUrl}/projects/ref/${encodeURIComponent(entityRef)}`, { method: 'GET', }, @@ -81,60 +106,49 @@ export class BazaarClient implements BazaarApi { return response.ok ? response : null; } - async getMembers(entity: Entity): Promise { + async getMembers(id: number): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - return await fetch( - `${baseUrl}/members/${encodeURIComponent(stringifyEntityRef(entity))}`, - { - method: 'GET', - }, - ).then(resp => resp.json()); + return await fetch(`${baseUrl}/projects/${id}/members`, { + method: 'GET', + }).then(resp => resp.json()); } - async addMember(entity: Entity): Promise { + async addMember(id: number, userId: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - await fetch(`${baseUrl}/member`, { + await fetch(`${baseUrl}/projects/${id}/member/${userId}`, { method: 'PUT', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ - entity_ref: stringifyEntityRef(entity), - user_id: this.identityApi.getUserId(), picture: this.identityApi.getProfile()?.picture, }), }); } - async deleteMember(entity: Entity): Promise { + async deleteMember(id: number, userId: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - await fetch( - `${baseUrl}/member/${encodeURIComponent( - stringifyEntityRef(entity), - )}/${this.identityApi.getUserId()}`, - { - method: 'DELETE', - }, - ); + await fetch(`${baseUrl}/projects/${id}/member/${userId}`, { + method: 'DELETE', + }); } - async getEntities(): Promise { + async getProjects(): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - return await fetch(`${baseUrl}/entities`, { + return await fetch(`${baseUrl}/projects`, { method: 'GET', }).then(resp => resp.json()); } - async deleteEntity(bazaarProject: BazaarProject): Promise { + async deleteProject(id: number): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - const entityRef = bazaarProject.entityRef as string; - await fetch(`${baseUrl}/metadata/${encodeURIComponent(entityRef)}`, { + await fetch(`${baseUrl}/projects/${id}`, { method: 'DELETE', }); } diff --git a/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx b/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx index 310ffe051d..4333ff11fd 100644 --- a/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx +++ b/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { UseFormReset, UseFormGetValues } from 'react-hook-form'; import { useApi } from '@backstage/core-plugin-api'; @@ -39,18 +39,13 @@ export const AddProjectDialog = ({ fetchCatalogEntities, }: Props) => { const bazaarApi = useApi(bazaarApiRef); - const [selectedEntity, setSelectedEntity] = useState( - catalogEntities ? catalogEntities[0] : null, - ); - - useEffect(() => { - setSelectedEntity(catalogEntities ? catalogEntities[0] : null); - }, [catalogEntities]); + const [selectedEntity, setSelectedEntity] = useState(null); const defaultValues = { + name: '', title: 'Add project', community: '', - announcement: '', + description: '', status: 'proposed' as Status, size: 'medium' as Size, responsible: '', @@ -58,59 +53,48 @@ export const AddProjectDialog = ({ endDate: null, }; - const handleListItemClick = (entity: Entity) => { + const handleEntityClick = (entity: Entity) => { setSelectedEntity(entity); }; - const handleCloseDialog = () => { - setSelectedEntity(catalogEntities ? catalogEntities[0] : null); - handleClose(); - }; - - const handleSave: any = async ( + const handleSubmit: any = async ( getValues: UseFormGetValues, reset: UseFormReset, ) => { const formValues = getValues(); + const response = await bazaarApi.addProject({ + ...formValues, + entityRef: selectedEntity ? stringifyEntityRef(selectedEntity) : null, + startDate: formValues.startDate ?? null, + endDate: formValues.endDate ?? null, + } as BazaarProject); - if (selectedEntity) { - await bazaarApi.updateMetadata({ - name: selectedEntity.metadata.name, - entityRef: stringifyEntityRef(selectedEntity), - announcement: formValues.announcement, - status: formValues.status, - community: formValues.community, - membersCount: 0, - size: formValues.size, - startDate: formValues.startDate ?? null, - endDate: formValues.endDate ?? null, - responsible: formValues.responsible, - } as BazaarProject); - + if (response.status === 'ok') { fetchBazaarProjects(); fetchCatalogEntities(); - - handleClose(); - reset(defaultValues); } + + handleClose(); + reset(defaultValues); }; return ( } - handleClose={handleCloseDialog} + handleClose={handleClose} /> ); }; diff --git a/plugins/bazaar/src/components/AlertBanner/AlertBanner.tsx b/plugins/bazaar/src/components/AlertBanner/AlertBanner.tsx deleted file mode 100644 index 1b6c1331c3..0000000000 --- a/plugins/bazaar/src/components/AlertBanner/AlertBanner.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { Snackbar, IconButton } from '@material-ui/core'; -import CloseIcon from '@material-ui/icons/Close'; -import { Alert } from '@material-ui/lab'; - -type Props = { - open: boolean; - message: JSX.Element; - handleClose: () => void; -}; - -export const AlertBanner = ({ open, message, handleClose }: Props) => { - return ( - - - - - } - > - {message} - - - ); -}; diff --git a/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx b/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx new file mode 100644 index 0000000000..5145dc54b8 --- /dev/null +++ b/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx @@ -0,0 +1,168 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + Grid, + makeStyles, + Card, + CardContent, + Typography, + Link, + GridSize, +} from '@material-ui/core'; +import { Avatar } from '@backstage/core-components'; +import { AboutField } from '@backstage/plugin-catalog'; +import { StatusTag } from '../StatusTag'; +import { Member, BazaarProject } from '../../types'; + +const useStyles = makeStyles({ + break: { + wordBreak: 'break-word', + }, +}); + +type Props = { + bazaarProject: BazaarProject; + members: Member[]; + descriptionSize: GridSize; + membersSize: GridSize; +}; + +export const CardContentFields = ({ + bazaarProject, + members, + descriptionSize, + membersSize, +}: Props) => { + const classes = useStyles(); + + return ( +
+ + + + + + {bazaarProject.description + .split('\n') + .map((str: string, i: number) => ( + + {str} + + ))} + + + + + + {members.length ? ( + members.slice(0, 7).map((member: Member) => { + return ( +
+ + + {member?.userId} + +
+ ); + }) + ) : ( +
+ )} + + + + + + + + + + + + {bazaarProject.size} + + + + + + + {bazaarProject.startDate?.substring(0, 10) || ''} + + + + + + + + {bazaarProject.endDate?.substring(0, 10) || ''} + + + + + + + + {bazaarProject.responsible || ''} + + + + + + +
+ ); +}; diff --git a/plugins/bazaar/src/components/AlertBanner/index.ts b/plugins/bazaar/src/components/CardContentFields/index.ts similarity index 91% rename from plugins/bazaar/src/components/AlertBanner/index.ts rename to plugins/bazaar/src/components/CardContentFields/index.ts index 1728cab9fc..e6f990322b 100644 --- a/plugins/bazaar/src/components/AlertBanner/index.ts +++ b/plugins/bazaar/src/components/CardContentFields/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { AlertBanner } from './AlertBanner'; +export { CardContentFields } from './CardContentFields'; diff --git a/plugins/bazaar/src/components/ConfirmationDialog/ConfirmationDialog.tsx b/plugins/bazaar/src/components/ConfirmationDialog/ConfirmationDialog.tsx new file mode 100644 index 0000000000..1e00ac5e30 --- /dev/null +++ b/plugins/bazaar/src/components/ConfirmationDialog/ConfirmationDialog.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import { + CustomDialogTitle, + DialogActions, + DialogContent, +} from '../CustomDialogTitle'; + +type Props = { + open: boolean; + handleClose: () => void; + message: (string | JSX.Element)[]; + type: 'delete' | 'unlink'; + handleSubmit: () => void; +}; + +export const ConfirmationDialog = ({ + open, + handleClose, + message, + type, + handleSubmit, +}: Props) => { + return ( + + + {type.charAt(0).toLocaleUpperCase('en-US') + type.slice(1)} project + + + {message} + + + + + + ); +}; diff --git a/plugins/bazaar/src/components/DeleteProjectDialog/index.ts b/plugins/bazaar/src/components/ConfirmationDialog/index.ts similarity index 90% rename from plugins/bazaar/src/components/DeleteProjectDialog/index.ts rename to plugins/bazaar/src/components/ConfirmationDialog/index.ts index fdd488aeb9..a3252eb6a9 100644 --- a/plugins/bazaar/src/components/DeleteProjectDialog/index.ts +++ b/plugins/bazaar/src/components/ConfirmationDialog/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { DeleteProjectDialog } from './DeleteProjectDialog'; +export { ConfirmationDialog } from './ConfirmationDialog'; diff --git a/plugins/bazaar/src/components/CustomDialogTitle/CustomDialogTitle.tsx b/plugins/bazaar/src/components/CustomDialogTitle/CustomDialogTitle.tsx new file mode 100644 index 0000000000..2cf8b13874 --- /dev/null +++ b/plugins/bazaar/src/components/CustomDialogTitle/CustomDialogTitle.tsx @@ -0,0 +1,87 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import IconButton from '@material-ui/core/IconButton'; +import CloseIcon from '@material-ui/icons/Close'; +import Typography from '@material-ui/core/Typography'; +import MuiDialogTitle from '@material-ui/core/DialogTitle'; +import { + Theme, + WithStyles, + withStyles, + createStyles, +} from '@material-ui/core/styles'; +import MuiDialogContent from '@material-ui/core/DialogContent'; +import MuiDialogActions from '@material-ui/core/DialogActions'; + +/* + DialogTitleProps, DialogTitle, DialogContent and DialogActions + are copied from the git-release plugin +*/ + +export interface DialogTitleProps extends WithStyles { + id: string; + children: React.ReactNode; + onClose: () => void; +} + +const styles = (theme: Theme) => + createStyles({ + root: { + margin: 0, + padding: theme.spacing(2), + }, + closeButton: { + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, + }); + +export const DialogContent = withStyles((theme: Theme) => ({ + root: { + padding: theme.spacing(2), + }, +}))(MuiDialogContent); + +export const DialogActions = withStyles((theme: Theme) => ({ + root: { + margin: 0, + padding: theme.spacing(1), + }, +}))(MuiDialogActions); + +export const CustomDialogTitle = withStyles(styles)( + (props: DialogTitleProps) => { + const { children, classes, onClose, ...other } = props; + return ( + + {children} + {onClose ? ( + + + + ) : null} + + ); + }, +); diff --git a/plugins/bazaar/src/components/CustomDialogTitle/index.ts b/plugins/bazaar/src/components/CustomDialogTitle/index.ts new file mode 100644 index 0000000000..8b615ac211 --- /dev/null +++ b/plugins/bazaar/src/components/CustomDialogTitle/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2021 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 { + CustomDialogTitle, + DialogContent, + DialogActions, +} from './CustomDialogTitle'; diff --git a/plugins/bazaar/src/components/DateSelector/DateSelector.tsx b/plugins/bazaar/src/components/DateSelector/DateSelector.tsx index db564a7b68..600247a53f 100644 --- a/plugins/bazaar/src/components/DateSelector/DateSelector.tsx +++ b/plugins/bazaar/src/components/DateSelector/DateSelector.tsx @@ -45,7 +45,6 @@ export const DateSelector = ({ name, control, setValue }: Props) => { - createStyles({ - root: { - margin: 0, - padding: theme.spacing(2), - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], - }, - }); - -/* - DialogTitleProps, DialogTitle, DialogContent and DialogActions - are copied from the git-release plugin -*/ -export interface DialogTitleProps extends WithStyles { - id: string; - children: React.ReactNode; - onClose: () => void; -} - -const DialogTitle = withStyles(styles)((props: DialogTitleProps) => { - const { children, classes, onClose, ...other } = props; - return ( - - {children} - {onClose ? ( - - - - ) : null} - - ); -}); - -const DialogContent = withStyles((theme: Theme) => ({ - root: { - padding: theme.spacing(2), - }, -}))(MuiDialogContent); - -const DialogActions = withStyles((theme: Theme) => ({ - root: { - margin: 0, - padding: theme.spacing(1), - }, -}))(MuiDialogActions); - -type Props = { - bazaarProject: BazaarProject; - openDelete: boolean; - handleClose: () => void; - setIsBazaar: Dispatch>; -}; - -export const DeleteProjectDialog = ({ - bazaarProject, - openDelete, - handleClose, - setIsBazaar, -}: Props) => { - const handleCloseAndClear = () => { - handleClose(); - }; - - const bazaarApi = useApi(bazaarApiRef); - - const handleSubmit = async () => { - await bazaarApi.deleteEntity(bazaarProject); - setIsBazaar(false); - handleCloseAndClear(); - }; - - return ( - - - Delete project - - - - Are you sure you want to delete this project from the Bazaar? - - - - - - - ); -}; diff --git a/plugins/bazaar/src/components/DoubleDateSelector/DoubleDateSelector.tsx b/plugins/bazaar/src/components/DoubleDateSelector/DoubleDateSelector.tsx index b37e734453..eb72ee67dc 100644 --- a/plugins/bazaar/src/components/DoubleDateSelector/DoubleDateSelector.tsx +++ b/plugins/bazaar/src/components/DoubleDateSelector/DoubleDateSelector.tsx @@ -17,49 +17,48 @@ import React from 'react'; import { Control, UseFormSetValue } from 'react-hook-form'; import { FormValues } from '../../types'; -import { Typography } from '@material-ui/core'; import { DateSelector } from '../DateSelector/DateSelector'; +import { Typography, makeStyles } from '@material-ui/core'; type Props = { control: Control; setValue: UseFormSetValue; }; +const useStyles = makeStyles({ + container: { + marginTop: '0.25rem', + textAlign: 'center', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + }, + startDate: { + float: 'left', + }, + endDate: { + float: 'right', + }, + dash: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + fontSize: '1.5rem', + margin: '0 1rem', + }, +}); + export const DoubleDateSelector = ({ control, setValue }: Props) => { + const classes = useStyles(); + return ( -
-
+
+
- - - - -
+ - +
diff --git a/plugins/bazaar/src/components/EditProjectDialog/EditProjectDialog.tsx b/plugins/bazaar/src/components/EditProjectDialog/EditProjectDialog.tsx index 41c9e474d4..0b2a8ef391 100644 --- a/plugins/bazaar/src/components/EditProjectDialog/EditProjectDialog.tsx +++ b/plugins/bazaar/src/components/EditProjectDialog/EditProjectDialog.tsx @@ -15,81 +15,119 @@ */ import React, { useState, useEffect } from 'react'; -import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { ProjectDialog } from '../ProjectDialog'; -import { BazaarProject, FormValues, Size, Status } from '../../types'; +import { BazaarProject, FormValues } from '../../types'; import { bazaarApiRef } from '../../api'; import { UseFormGetValues } from 'react-hook-form'; +import { ConfirmationDialog } from '../ConfirmationDialog'; +import { Button, makeStyles } from '@material-ui/core'; type Props = { - entity: Entity; bazaarProject: BazaarProject; + openEdit: boolean; + handleEditClose: () => void; + handleCardClose?: () => void; fetchBazaarProject: () => Promise; - open: boolean; - handleClose: () => void; - isAddForm: boolean; }; +const useStyles = makeStyles({ + button: { + marginLeft: '0', + marginRight: 'auto', + }, +}); + export const EditProjectDialog = ({ - entity, bazaarProject, + openEdit, + handleEditClose, + handleCardClose, fetchBazaarProject, - open, - handleClose, }: Props) => { + const classes = useStyles(); + const bazaarApi = useApi(bazaarApiRef); + const [openDelete, setOpenDelete] = useState(false); const [defaultValues, setDefaultValues] = useState({ - announcement: bazaarProject.announcement, - community: bazaarProject.community, - status: bazaarProject.status, - size: bazaarProject.size, - startDate: bazaarProject?.startDate ?? null, - endDate: bazaarProject?.endDate ?? null, - responsible: bazaarProject.responsible, + ...bazaarProject, + startDate: bazaarProject.startDate ?? null, + endDate: bazaarProject.endDate ?? null, }); - const bazaarApi = useApi(bazaarApiRef); + const handleDeleteClose = () => { + setOpenDelete(false); + handleEditClose(); + + if (handleCardClose) handleCardClose(); + }; + + const handleDeleteSubmit = async () => { + await bazaarApi.deleteProject(bazaarProject.id); + + handleDeleteClose(); + fetchBazaarProject(); + }; useEffect(() => { setDefaultValues({ - announcement: bazaarProject.announcement, - community: bazaarProject.community, - status: bazaarProject.status, - size: bazaarProject.size, - startDate: bazaarProject?.startDate ?? null, - endDate: bazaarProject?.endDate ?? null, - responsible: bazaarProject.responsible, + ...bazaarProject, + startDate: bazaarProject.startDate ?? null, + endDate: bazaarProject.endDate ?? null, }); }, [bazaarProject]); - const handleSave: any = async (getValues: UseFormGetValues) => { + const handleEditSubmit: any = async ( + getValues: UseFormGetValues, + ) => { const formValues = getValues(); - const updateResponse = await bazaarApi.updateMetadata({ - name: entity.metadata.name, - entityRef: stringifyEntityRef(entity), - announcement: formValues.announcement, - status: formValues.status as Status, - community: formValues.community, + const updateResponse = await bazaarApi.updateProject({ + ...formValues, + id: bazaarProject.id, + entityRef: bazaarProject.entityRef, membersCount: bazaarProject.membersCount, - size: formValues.size as Size, startDate: formValues?.startDate ?? null, endDate: formValues?.endDate ?? null, - responsible: formValues.responsible, }); if (updateResponse.status === 'ok') fetchBazaarProject(); - handleClose(); + handleEditClose(); }; return ( - +
+ {bazaarProject.name}, + ' from the Bazaar?', + ]} + type="delete" + handleSubmit={handleDeleteSubmit} + /> + + { + setOpenDelete(true); + }} + > + Delete project + + } + /> +
); }; diff --git a/plugins/bazaar/src/components/EntityBazaarInfoCard/EntityBazaarInfoCard.tsx b/plugins/bazaar/src/components/EntityBazaarInfoCard/EntityBazaarInfoCard.tsx index acae8f5342..685d400d3e 100644 --- a/plugins/bazaar/src/components/EntityBazaarInfoCard/EntityBazaarInfoCard.tsx +++ b/plugins/bazaar/src/components/EntityBazaarInfoCard/EntityBazaarInfoCard.tsx @@ -15,347 +15,48 @@ */ import React, { useState, useEffect } from 'react'; -import { - Grid, - makeStyles, - Card, - CardContent, - CardHeader, - Typography, - Divider, - IconButton, - Popover, - MenuList, - MenuItem, - ListItemText, - Link, -} from '@material-ui/core'; -import { - Progress, - HeaderIconLinkRow, - IconLinkVerticalProps, - Avatar, -} from '@backstage/core-components'; -import { useEntity } from '@backstage/plugin-catalog-react'; -import { AboutField } from '@backstage/plugin-catalog'; -import { StatusTag } from '../StatusTag'; -import EditIcon from '@material-ui/icons/Edit'; -import ChatIcon from '@material-ui/icons/Chat'; -import PersonAddIcon from '@material-ui/icons/PersonAdd'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; -import DeleteIcon from '@material-ui/icons/Delete'; -import { EditProjectDialog } from '../EditProjectDialog'; -import { DeleteProjectDialog } from '../DeleteProjectDialog'; -import ExitToAppIcon from '@material-ui/icons/ExitToApp'; -import { useApi, identityApiRef } from '@backstage/core-plugin-api'; -import { Member, BazaarProject } from '../../types'; -import { bazaarApiRef } from '../../api'; -import { Alert } from '@material-ui/lab'; import { useAsyncFn } from 'react-use'; - -const useStyles = makeStyles({ - description: { - wordBreak: 'break-word', - }, - icon: { - marginRight: '1.75rem', - }, - link: { - color: '#9cc9ff', - '&:hover': { - textDecoration: 'underline', - }, - }, - memberLink: { - display: 'block', - marginBottom: '0.3rem', - }, -}); - -const sortMembers = (m1: Member, m2: Member) => { - return new Date(m2.joinDate!).getTime() - new Date(m1.joinDate!).getTime(); -}; +import { useApi } from '@backstage/core-plugin-api'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { bazaarApiRef } from '../../api'; +import { EntityBazaarInfoContent } from '../EntityBazaarInfoContent'; +import { Card } from '@material-ui/core'; +import { parseBazaarResponse } from '../../util/parseMethods'; export const EntityBazaarInfoCard = () => { const { entity } = useEntity(); - const classes = useStyles(); const bazaarApi = useApi(bazaarApiRef); - const identity = useApi(identityApiRef); - const [anchorEl, setAnchorEl] = useState(); - const [open, setOpen] = useState(false); - const [popoverOpen, setPopoverOpen] = useState(false); - const [openDelete, setOpenDelete] = useState(false); - const [isMember, setIsMember] = useState(false); - const [isBazaar, setIsBazaar] = useState(false); - const [members, fetchMembers] = useAsyncFn(async () => { - const response = await bazaarApi.getMembers(entity); - - const dbMembers = response.data.map((obj: any) => { - const member: Member = { - userId: obj.user_id, - entityRef: obj.entity_ref, - joinDate: obj.join_date, - picture: obj.picture, - }; - - return member; - }); - - dbMembers.sort(sortMembers); - - return dbMembers; - }); const [bazaarProject, fetchBazaarProject] = useAsyncFn(async () => { - const response = await bazaarApi.getMetadata(entity); + const response = await bazaarApi.getProjectByRef( + stringifyEntityRef(entity), + ); - if (response) { - const metadata = await response.json().then((resp: any) => resp.data[0]); - - if (metadata) { - return { - entityRef: metadata.entity_ref, - name: metadata.name, - community: metadata.community, - announcement: metadata.announcement, - status: metadata.status, - updatedAt: metadata.updated_at, - membersCount: metadata.members_count, - size: metadata.size, - startDate: metadata.start_date, - endDate: metadata.end_date, - responsible: metadata.responsible, - } as BazaarProject; - } - } - return null; + return await parseBazaarResponse(response); }); + const [isBazaar, setIsBazaar] = useState(bazaarProject.value ?? false); + useEffect(() => { - fetchMembers(); fetchBazaarProject(); - }, [fetchMembers, fetchBazaarProject]); + }, [fetchBazaarProject]); useEffect(() => { - const isBazaarMember = - members?.value - ?.map((member: Member) => member.userId) - .indexOf(identity.getUserId()) >= 0; - const isBazaarProject = bazaarProject.value !== null; + const isBazaarProject = bazaarProject.value !== undefined; - setIsMember(isBazaarMember); setIsBazaar(isBazaarProject); - }, [bazaarProject, members, identity]); + }, [bazaarProject.value]); - const onOpen = (event: React.SyntheticEvent) => { - setAnchorEl(event.currentTarget); - setPopoverOpen(true); - }; - - const closeEdit = () => { - setOpen(false); - }; - - const closeDelete = () => { - setOpenDelete(false); - }; - - const popoverCloseHandler = () => { - setPopoverOpen(false); - }; - - const handleMembersClick = async () => { - if (!isMember) { - await bazaarApi.addMember(entity); - } else { - await bazaarApi.deleteMember(entity); - } - - fetchMembers(); - fetchBazaarProject(); - }; - - const links: IconLinkVerticalProps[] = [ - { - label: isMember ? 'Leave' : 'Join', - icon: isMember ? : , - href: '', - onClick: async () => { - handleMembersClick(); - }, - }, - { - label: 'Community', - icon: , - href: bazaarProject?.value?.community, - disabled: !bazaarProject?.value?.community || !isMember, - }, - ]; - - if (!isBazaar) { - return null; - } else if (bazaarProject.loading || members.loading) { - return ; - } else if (bazaarProject.error) { - return {bazaarProject?.error?.message}; - } else if (members.error) { - return {members?.error?.message}; - } - return ( - - {bazaarProject?.value && ( - + - )} - - {bazaarProject?.value && ( - - )} - - - - } - subheader={} - /> - - - - - { - setOpen(true); - setPopoverOpen(false); - }} - > - - - - - { - setOpenDelete(true); - setPopoverOpen(false); - }} - > - - - - - - - - - {bazaarProject?.value?.announcement - ? bazaarProject?.value?.announcement - .split('\n') - .map((str: string, i: number) => ( - - {str} - - )) - : 'No announcement'} - - - - - {' '} - - {members?.value?.length ? ( - members.value.slice(0, 7).map((member: Member) => { - return ( -
- - - {member?.userId} - -
- ); - }) - ) : ( -
- )} - - - - - - - - - - - - - {bazaarProject?.value?.size} - - - - - - - - {bazaarProject?.value?.startDate?.substring(0, 10) || ''} - - - - - - - - {bazaarProject?.value?.endDate?.substring(0, 10) || ''} - - - - - - - - {bazaarProject?.value?.responsible || ''} - - - - - - - ); + + ); + } + return null; }; diff --git a/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx b/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx new file mode 100644 index 0000000000..f2c005abd1 --- /dev/null +++ b/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx @@ -0,0 +1,200 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState, useEffect } from 'react'; +import { CardHeader, Divider, IconButton } from '@material-ui/core'; +import { + HeaderIconLinkRow, + IconLinkVerticalProps, +} from '@backstage/core-components'; +import EditIcon from '@material-ui/icons/Edit'; +import ChatIcon from '@material-ui/icons/Chat'; +import PersonAddIcon from '@material-ui/icons/PersonAdd'; +import DashboardIcon from '@material-ui/icons/Dashboard'; +import LinkOffIcon from '@material-ui/icons/LinkOff'; +import { EditProjectDialog } from '../EditProjectDialog'; +import { useApi, identityApiRef } from '@backstage/core-plugin-api'; +import { BazaarProject, Member } from '../../types'; +import { bazaarApiRef } from '../../api'; +import { Alert } from '@material-ui/lab'; +import { useAsyncFn } from 'react-use'; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; +import { parseEntityRef } from '@backstage/catalog-model'; +import { ConfirmationDialog } from '../ConfirmationDialog'; +import { CardContentFields } from '../CardContentFields'; +import { fetchProjectMembers } from '../../util/fetchMethods'; + +type Props = { + bazaarProject: BazaarProject | null | undefined; + fetchBazaarProject: () => Promise; +}; + +export const EntityBazaarInfoContent = ({ + bazaarProject, + fetchBazaarProject, +}: Props) => { + const bazaarApi = useApi(bazaarApiRef); + const identity = useApi(identityApiRef); + const [openEdit, setOpenEdit] = useState(false); + const [isMember, setIsMember] = useState(false); + const [openUnlink, setOpenUnlink] = useState(false); + const [members, fetchMembers] = useAsyncFn(async () => { + return bazaarProject + ? await fetchProjectMembers(bazaarApi, bazaarProject) + : []; + }); + + const [userId, fetchUserId] = useAsyncFn(async () => { + return await ( + await identity.getProfileInfo() + ).displayName; + }); + + useEffect(() => { + fetchMembers(); + fetchUserId(); + }, [fetchMembers, fetchUserId]); + + useEffect(() => { + if (members.value && userId.value) { + setIsMember( + members.value + ?.map((member: Member) => member.userId) + .indexOf(userId.value) >= 0, + ); + } + }, [bazaarProject, members, identity, userId.value]); + + const handleMembersClick = async () => { + if (userId.value) { + if (!isMember) { + await bazaarApi.addMember(bazaarProject?.id!, userId.value); + } else { + await bazaarApi.deleteMember(bazaarProject!.id, userId.value); + } + setIsMember(!isMember); + fetchMembers(); + } + }; + + const links: IconLinkVerticalProps[] = [ + { + label: 'Entity page', + icon: , + disabled: true, + }, + { + label: 'Unlink project', + icon: , + disabled: false, + onClick: () => { + setOpenUnlink(true); + }, + }, + { + label: isMember ? 'Leave' : 'Join', + icon: isMember ? : , + href: '', + onClick: async () => { + handleMembersClick(); + }, + }, + { + label: 'Community', + icon: , + href: bazaarProject?.community, + disabled: bazaarProject?.community === '' || !isMember, + }, + ]; + + const handleEditClose = () => { + setOpenEdit(false); + }; + + const handleUnlinkClose = () => { + setOpenUnlink(false); + }; + + const handleUnlinkSubmit = async () => { + const updateResponse = await bazaarApi.updateProject({ + ...bazaarProject, + entityRef: null, + }); + + if (updateResponse.status === 'ok') { + handleUnlinkClose(); + fetchBazaarProject(); + } + }; + + if (members.error) { + return {members?.error?.message}; + } + + if (bazaarProject) { + return ( +
+ + + {openUnlink && ( + {parseEntityRef(bazaarProject.entityRef!).name}, + ' from ', + {bazaarProject.name}, + ' ?', + ]} + type="unlink" + handleSubmit={handleUnlinkSubmit} + /> + )} + + + { + setOpenEdit(true); + }} + > + + +
+ } + subheader={} + /> + + + +
+ ); + } + return null; +}; diff --git a/plugins/bazaar/src/components/EntityBazaarInfoContent/index.ts b/plugins/bazaar/src/components/EntityBazaarInfoContent/index.ts new file mode 100644 index 0000000000..6be5ad7dbf --- /dev/null +++ b/plugins/bazaar/src/components/EntityBazaarInfoContent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { EntityBazaarInfoContent } from './EntityBazaarInfoContent'; diff --git a/plugins/bazaar/src/components/HomePage/HomePage.tsx b/plugins/bazaar/src/components/HomePage/HomePage.tsx index 32a16291fc..b34b7ac949 100644 --- a/plugins/bazaar/src/components/HomePage/HomePage.tsx +++ b/plugins/bazaar/src/components/HomePage/HomePage.tsx @@ -36,11 +36,7 @@ export const HomePage = () => { return (
-
+
); diff --git a/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx b/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx new file mode 100644 index 0000000000..daca0aec1d --- /dev/null +++ b/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx @@ -0,0 +1,269 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState, useEffect } from 'react'; +import { Card, CardHeader, Divider, IconButton } from '@material-ui/core'; +import { + HeaderIconLinkRow, + IconLinkVerticalProps, +} from '@backstage/core-components'; +import EditIcon from '@material-ui/icons/Edit'; +import ChatIcon from '@material-ui/icons/Chat'; +import PersonAddIcon from '@material-ui/icons/PersonAdd'; +import InsertLinkIcon from '@material-ui/icons/InsertLink'; +import DashboardIcon from '@material-ui/icons/Dashboard'; +import CloseIcon from '@material-ui/icons/Close'; +import LinkOffIcon from '@material-ui/icons/LinkOff'; +import { EditProjectDialog } from '../EditProjectDialog'; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; +import { + useApi, + identityApiRef, + useRouteRef, +} from '@backstage/core-plugin-api'; +import { Member, BazaarProject } from '../../types'; +import { bazaarApiRef } from '../../api'; +import { Alert } from '@material-ui/lab'; +import { useAsyncFn } from 'react-use'; +import { + catalogApiRef, + catalogRouteRef, +} from '@backstage/plugin-catalog-react'; + +import { + parseEntityName, + stringifyEntityRef, + Entity, + parseEntityRef, +} from '@backstage/catalog-model'; + +import { ConfirmationDialog } from '../ConfirmationDialog/ConfirmationDialog'; +import { CardContentFields } from '../CardContentFields/CardContentFields'; +import { LinkProjectDialog } from '../LinkProjectDialog'; +import { + fetchCatalogItems, + fetchProjectMembers, +} from '../../util/fetchMethods'; +import { parseBazaarResponse } from '../../util/parseMethods'; + +type Props = { + initProject: BazaarProject; + handleClose: () => void; + initEntity: Entity; +}; + +export const HomePageBazaarInfoCard = ({ + initProject, + handleClose, + initEntity, +}: Props) => { + const catalogLink = useRouteRef(catalogRouteRef); + const bazaarApi = useApi(bazaarApiRef); + const identity = useApi(identityApiRef); + const catalogApi = useApi(catalogApiRef); + const [openEdit, setOpenEdit] = useState(false); + const [openProjectSelector, setOpenProjectSelector] = useState(false); + const [openUnlink, setOpenUnlink] = useState(false); + const [isMember, setIsMember] = useState(false); + + const [catalogEntities, fetchCatalogEntities] = useAsyncFn(async () => { + const entities = await fetchCatalogItems(catalogApi); + const bazaarProjects = await bazaarApi.getProjects(); + const bazaarLinkedRefs: string[] = bazaarProjects.data + .filter((entity: any) => entity.entity_ref !== null) + .map((entity: any) => entity.entity_ref); + + return entities.filter( + (entity: Entity) => + !bazaarLinkedRefs.includes(stringifyEntityRef(entity)), + ); + }); + + const [bazaarProject, fetchBazaarProject] = useAsyncFn(async () => { + const response = await bazaarApi.getProjectById(initProject.id); + return await parseBazaarResponse(response); + }); + + const [members, fetchMembers] = useAsyncFn(async () => { + return fetchProjectMembers(bazaarApi, bazaarProject.value ?? initProject); + }); + + const [userId, fetchUserId] = useAsyncFn(async () => { + return await ( + await identity.getProfileInfo() + ).displayName; + }); + + useEffect(() => { + fetchMembers(); + fetchBazaarProject(); + fetchCatalogEntities(); + fetchUserId(); + }, [fetchMembers, fetchBazaarProject, fetchCatalogEntities, fetchUserId]); + + useEffect(() => { + if (members.value && userId.value) { + setIsMember( + members.value + ?.map((member: Member) => member.userId) + .indexOf(userId.value) >= 0, + ); + } + }, [bazaarProject.value, members, identity, userId.value]); + + const handleMembersClick = async () => { + if (userId.value) { + if (!isMember) { + await bazaarApi.addMember(bazaarProject.value!.id, userId.value); + } else { + await bazaarApi.deleteMember(bazaarProject.value!.id, userId.value); + } + setIsMember(!isMember); + fetchMembers(); + } + }; + + const getEntityPageLink = () => { + if (bazaarProject?.value?.entityRef) { + const { name, kind, namespace } = parseEntityName( + bazaarProject.value.entityRef, + ); + return `${catalogLink()}/${namespace}/${kind}/${name}`; + } + return ''; + }; + + const handleLink = () => { + if (bazaarProject.value?.entityRef) { + setOpenUnlink(true); + } else { + fetchCatalogEntities(); + setOpenProjectSelector(true); + } + }; + + const links: IconLinkVerticalProps[] = [ + { + label: 'Entity page', + icon: , + href: bazaarProject.value?.entityRef ? getEntityPageLink() : '', + disabled: bazaarProject.value?.entityRef === null, + }, + { + label: bazaarProject.value?.entityRef ? 'Unlink project' : 'Link project', + icon: bazaarProject.value?.entityRef ? ( + + ) : ( + + ), + onClick: handleLink, + }, + { + label: isMember ? 'Leave' : 'Join', + icon: isMember ? : , + href: '', + onClick: async () => { + handleMembersClick(); + }, + }, + { + label: 'Community', + icon: , + href: bazaarProject.value?.community, + disabled: !bazaarProject.value?.community || !isMember, + }, + ]; + + const handleUnlinkSubmit = async () => { + const updateResponse = await bazaarApi.updateProject({ + ...bazaarProject.value, + entityRef: null, + }); + + if (updateResponse.status === 'ok') { + setOpenUnlink(false); + fetchBazaarProject(); + } + }; + + if (bazaarProject.error) { + return {bazaarProject?.error?.message}; + } else if (members.error) { + return {members?.error?.message}; + } + + return ( +
+ setOpenProjectSelector(false)} + catalogEntities={catalogEntities.value || []} + bazaarProject={bazaarProject.value || initProject} + fetchBazaarProject={fetchBazaarProject} + initEntity={initEntity} + /> + + {openUnlink && ( + setOpenUnlink(false)} + message={[ + 'Are you sure you want to unlink ', + {parseEntityRef(bazaarProject.value?.entityRef!).name}, + ' from ', + {bazaarProject.value?.name}, + ' ?', + ]} + type="unlink" + handleSubmit={handleUnlinkSubmit} + /> + )} + + + setOpenEdit(false)} + handleCardClose={handleClose} + fetchBazaarProject={fetchBazaarProject} + /> + + + setOpenEdit(true)}> + + + + + +
+ } + subheader={} + /> + + + +
+
+ ); +}; diff --git a/plugins/bazaar/src/components/HomePageBazaarInfoCard/index.ts b/plugins/bazaar/src/components/HomePageBazaarInfoCard/index.ts new file mode 100644 index 0000000000..0edfba0652 --- /dev/null +++ b/plugins/bazaar/src/components/HomePageBazaarInfoCard/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { HomePageBazaarInfoCard } from './HomePageBazaarInfoCard'; diff --git a/plugins/bazaar/src/components/InputField/InputField.tsx b/plugins/bazaar/src/components/InputField/InputField.tsx index 9932e5b1fc..76eb28da6c 100644 --- a/plugins/bazaar/src/components/InputField/InputField.tsx +++ b/plugins/bazaar/src/components/InputField/InputField.tsx @@ -25,7 +25,7 @@ type Rules = { }; type Props = { - inputType: 'announcement' | 'community' | 'responsible'; + inputType: 'description' | 'community' | 'responsible' | 'name'; error?: FieldError | undefined; control: Control; helperText?: string; diff --git a/plugins/bazaar/src/components/InputSelector/InputSelector.tsx b/plugins/bazaar/src/components/InputSelector/InputSelector.tsx index 25ed459963..b7e9ecd96a 100644 --- a/plugins/bazaar/src/components/InputSelector/InputSelector.tsx +++ b/plugins/bazaar/src/components/InputSelector/InputSelector.tsx @@ -60,12 +60,7 @@ export const InputSelector = ({ name, options, control, error }: Props) => { > {options.map(option => { return ( - + {option} ); diff --git a/plugins/bazaar/src/components/LinkProjectDialog/LinkProjectDialog.tsx b/plugins/bazaar/src/components/LinkProjectDialog/LinkProjectDialog.tsx new file mode 100644 index 0000000000..92a7344dac --- /dev/null +++ b/plugins/bazaar/src/components/LinkProjectDialog/LinkProjectDialog.tsx @@ -0,0 +1,97 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState } from 'react'; +import { + Dialog, + DialogActions, + Button, + DialogContent, + makeStyles, +} from '@material-ui/core'; +import { ProjectSelector } from '../ProjectSelector'; +import { CustomDialogTitle } from '../CustomDialogTitle'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; + +import { bazaarApiRef } from '../../api'; +import { useApi } from '@backstage/core-plugin-api'; + +import { BazaarProject } from '../../types'; + +type Props = { + openProjectSelector: boolean; + handleProjectSelectorClose: () => void; + catalogEntities: Entity[]; + bazaarProject: BazaarProject; + fetchBazaarProject: () => Promise; + initEntity: Entity; +}; + +const useStyles = makeStyles({ + content: { padding: '0 1rem' }, +}); + +export const LinkProjectDialog = ({ + openProjectSelector, + handleProjectSelectorClose, + catalogEntities, + bazaarProject, + fetchBazaarProject, + initEntity, +}: Props) => { + const classes = useStyles(); + const bazaarApi = useApi(bazaarApiRef); + const [selectedEntity, setSelectedEntity] = useState(initEntity); + const handleEntityClick = (entity: Entity) => { + setSelectedEntity(entity); + }; + + const handleSubmit = async () => { + handleProjectSelectorClose(); + + const updateResponse = await bazaarApi.updateProject({ + ...bazaarProject, + entityRef: stringifyEntityRef(selectedEntity!), + }); + if (updateResponse.status === 'ok') fetchBazaarProject(); + }; + + return ( + + + Select entity + + + + + + + + + + ); +}; diff --git a/plugins/bazaar/src/components/LinkProjectDialog/index.ts b/plugins/bazaar/src/components/LinkProjectDialog/index.ts new file mode 100644 index 0000000000..68d8e7c89e --- /dev/null +++ b/plugins/bazaar/src/components/LinkProjectDialog/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { LinkProjectDialog } from './LinkProjectDialog'; diff --git a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx index b3c8b3e2e7..a558d347a5 100644 --- a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx +++ b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx @@ -14,85 +14,94 @@ * limitations under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { ItemCardHeader } from '@backstage/core-components'; import { Card, CardActionArea, CardContent, + Dialog, makeStyles, Typography, } from '@material-ui/core'; import { StatusTag } from '../StatusTag/StatusTag'; -import { Link as RouterLink } from 'react-router-dom'; -import { catalogRouteRef } from '@backstage/plugin-catalog-react'; -import { useRouteRef } from '@backstage/core-plugin-api'; import { BazaarProject } from '../../types'; -import { parseEntityName } from '@backstage/catalog-model'; import { DateTime } from 'luxon'; +import { HomePageBazaarInfoCard } from '../HomePageBazaarInfoCard'; +import { Entity } from '@backstage/catalog-model'; const useStyles = makeStyles({ statusTag: { display: 'inline-block', whiteSpace: 'nowrap', - marginBottom: '0.5rem', - }, - announcement: { - display: '-webkit-box', - WebkitLineClamp: 5, - WebkitBoxOrient: 'vertical', marginBottom: '0.8rem', + }, + description: { + display: '-webkit-box', + WebkitLineClamp: 7, + WebkitBoxOrient: 'vertical', overflow: 'hidden', + backgroundColor: '', }, memberCount: { float: 'right', }, + content: { height: '13rem', marginBottom: '-0.5rem' }, }); type Props = { - bazaarProject: BazaarProject; + project: BazaarProject; + fetchBazaarProjects: () => Promise; + catalogEntities: Entity[]; }; -export const ProjectCard = ({ bazaarProject }: Props) => { +export const ProjectCard = ({ + project, + fetchBazaarProjects, + catalogEntities, +}: Props) => { const classes = useStyles(); - const { entityRef, name, status, updatedAt, announcement, membersCount } = - bazaarProject; - const catalogLink = useRouteRef(catalogRouteRef); - const { namespace, kind } = parseEntityName(entityRef); + const [openCard, setOpenCard] = useState(false); + const { id, name, status, updatedAt, description, membersCount } = project; + + const handleClose = () => { + setOpenCard(false); + fetchBazaarProjects(); + }; return ( - - - + + - - - - {membersCount === 1 - ? `${membersCount} member` - : `${membersCount} members`} - -
- - {announcement} +
+ + + setOpenCard(true)}> + + + + + {Number(membersCount) === Number(1) + ? `${membersCount} member` + : `${membersCount} members`} -
- - - + + {description} + + + + +
); }; diff --git a/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx b/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx index a65122b26e..08c43d1c2e 100644 --- a/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx +++ b/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx @@ -15,17 +15,7 @@ */ import React from 'react'; -import { - createStyles, - Theme, - withStyles, - WithStyles, -} from '@material-ui/core/styles'; -import MuiDialogTitle from '@material-ui/core/DialogTitle'; -import MuiDialogContent from '@material-ui/core/DialogContent'; -import MuiDialogActions from '@material-ui/core/DialogActions'; -import CloseIcon from '@material-ui/icons/Close'; -import { Button, Dialog, Typography, IconButton } from '@material-ui/core'; +import { Button, Dialog } from '@material-ui/core'; import { useForm, SubmitHandler, @@ -36,61 +26,11 @@ import { InputField } from '../InputField/InputField'; import { InputSelector } from '../InputSelector/InputSelector'; import { FormValues } from '../../types'; import { DoubleDateSelector } from '../DoubleDateSelector/DoubleDateSelector'; - -const styles = (theme: Theme) => - createStyles({ - root: { - margin: 0, - padding: theme.spacing(2), - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], - }, - }); - -/* - DialogTitleProps, DialogTitle, DialogContent and DialogActions - are copied from the git-release plugin -*/ -export interface DialogTitleProps extends WithStyles { - id: string; - children: React.ReactNode; - onClose: () => void; -} - -const DialogTitle = withStyles(styles)((props: DialogTitleProps) => { - const { children, classes, onClose, ...other } = props; - return ( - - {children} - {onClose ? ( - - - - ) : null} - - ); -}); - -const DialogContent = withStyles((theme: Theme) => ({ - root: { - padding: theme.spacing(2), - }, -}))(MuiDialogContent); - -const DialogActions = withStyles((theme: Theme) => ({ - root: { - margin: 0, - padding: theme.spacing(1), - }, -}))(MuiDialogActions); +import { + CustomDialogTitle, + DialogActions, + DialogContent, +} from '../CustomDialogTitle'; type Props = { handleSave: ( @@ -102,6 +42,7 @@ type Props = { defaultValues: FormValues; open: boolean; projectSelector?: JSX.Element; + deleteButton?: JSX.Element; handleClose: () => void; }; @@ -112,6 +53,7 @@ export const ProjectDialog = ({ defaultValues, open, projectSelector, + deleteButton, handleClose, }: Props) => { const { @@ -123,16 +65,16 @@ export const ProjectDialog = ({ setValue, } = useForm({ mode: 'onChange', - defaultValues: defaultValues, + defaultValues, }); - const handleCloseAndClear = () => { - handleClose(); - reset(defaultValues); + const handleSaveForm = () => { + handleSave(getValues, reset); }; - const handleSaveProject = () => { - handleSave(getValues, reset); + const handleCloseDialog = () => { + handleClose(); + reset(defaultValues); }; return ( @@ -140,25 +82,36 @@ export const ProjectDialog = ({ - + {title} - - - {isAddForm && projectSelector} + + + - + {isAddForm && projectSelector} + + + {!isAddForm && deleteButton}
- } - handleClose={handleCloseNoProjects} - /> - +
+ + + { + setSearchValue(newSortMethod); + }} + onCancelSearch={() => { + setSearchValue(''); + }} + /> { setOpenAdd(false); }} @@ -176,10 +191,11 @@ export const SortView = () => { fetchCatalogEntities={fetchCatalogEntities} /> - +
diff --git a/plugins/bazaar/src/types.ts b/plugins/bazaar/src/types.ts index 7d810c4eab..30c07286ac 100644 --- a/plugins/bazaar/src/types.ts +++ b/plugins/bazaar/src/types.ts @@ -17,7 +17,7 @@ import { EntityRef } from '@backstage/catalog-model'; export type Member = { - entityRef: EntityRef; + itemId: number; userId: string; joinDate?: string; picture?: string; @@ -29,10 +29,11 @@ export type Size = 'small' | 'medium' | 'large'; export type BazaarProject = { name: string; - entityRef: EntityRef; + id: number; + entityRef?: EntityRef; community: string; status: Status; - announcement: string; + description: string; updatedAt?: string; membersCount: number; size: Size; @@ -42,7 +43,8 @@ export type BazaarProject = { }; export type FormValues = { - announcement: string; + name: string; + description: string; community: string; status: string; size: Size; diff --git a/plugins/bazaar/src/util/fetchMethods.ts b/plugins/bazaar/src/util/fetchMethods.ts new file mode 100644 index 0000000000..015abae210 --- /dev/null +++ b/plugins/bazaar/src/util/fetchMethods.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2021 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 { sortMembers } from './sortMethods'; +import { parseMember } from './parseMethods'; +import { BazaarProject, Member } from '../types'; +import { BazaarApi } from '../api'; +import { Entity } from '@backstage/catalog-model'; + +export const fetchProjectMembers = async ( + bazaarApi: BazaarApi, + project: BazaarProject, +): Promise => { + const response = await bazaarApi.getMembers(project.id); + + if (response.data.length > 0) { + const dbMembers = response.data.map((member: any) => { + return parseMember(member); + }); + + dbMembers.sort(sortMembers); + return dbMembers; + } + return []; +}; + +export const fetchCatalogItems = async (catalogApi: any): Promise => { + const entities = await catalogApi.getEntities({ + filter: { + kind: ['Component', 'Resource'], + }, + fields: ['kind', 'metadata.name', 'metadata.namespace'], + }); + + return entities.items; +}; diff --git a/plugins/bazaar/src/util/parseMethods.ts b/plugins/bazaar/src/util/parseMethods.ts new file mode 100644 index 0000000000..1b4ebfd308 --- /dev/null +++ b/plugins/bazaar/src/util/parseMethods.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2021 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 { BazaarProject, Member } from '../types'; + +export const parseBazaarProject = (metadata: any): BazaarProject => { + return { + id: metadata.id, + entityRef: metadata.entity_ref, + name: metadata.name, + community: metadata.community, + description: metadata.description, + status: metadata.status, + updatedAt: metadata.updated_at, + membersCount: metadata.members_count, + size: metadata.size, + startDate: metadata.start_date, + endDate: metadata.end_date, + responsible: metadata.responsible, + } as BazaarProject; +}; + +export const parseMember = (member: any): Member => { + return { + itemId: member.item_id, + userId: member.user_id, + joinDate: member.join_date, + picture: member.picture, + } as Member; +}; + +export const parseBazaarResponse = async (response: any) => { + if (response) { + const metadata = await response.json().then((resp: any) => resp.data[0]); + + if (metadata) { + return parseBazaarProject(metadata); + } + } + return null; +}; diff --git a/plugins/bazaar/src/util/sortMethods.ts b/plugins/bazaar/src/util/sortMethods.ts new file mode 100644 index 0000000000..d75639edca --- /dev/null +++ b/plugins/bazaar/src/util/sortMethods.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2021 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 { BazaarProject, Member } from '../types'; + +export const sortMembers = (m1: Member, m2: Member) => { + return new Date(m2.joinDate!).getTime() - new Date(m1.joinDate!).getTime(); +}; + +export const sortByDate = (a: BazaarProject, b: BazaarProject): number => { + const dateA = new Date(a.updatedAt!).getTime(); + const dateB = new Date(b.updatedAt!).getTime(); + return dateB - dateA; +}; + +export const sortByName = (a: BazaarProject, b: BazaarProject) => { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } + return 0; +}; + +export const sortByMembers = (a: BazaarProject, b: BazaarProject) => { + return b.membersCount - a.membersCount; +}; diff --git a/yarn.lock b/yarn.lock index e300785cd4..b89bf1af9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20239,6 +20239,14 @@ marked@^2.1.1: resolved "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753" integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA== +material-ui-search-bar@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/material-ui-search-bar/-/material-ui-search-bar-1.0.0.tgz#2652dd5bdc4cb043cffb7144d9c296c120702e62" + integrity sha512-lCNuzMLPBVukVAkcnYKLXHneozsuKZREZNOcc8z9S9scXHqxJzhC9hOS3OC3/YJ+NJEB5lZB9zg1gryBaXEu8w== + dependencies: + classnames "^2.2.5" + prop-types "^15.5.8" + math-expression-evaluator@^1.2.14: version "1.2.22" resolved "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.22.tgz#c14dcb3d8b4d150e5dcea9c68c8dad80309b0d5e"