Merge pull request #8496 from lykkeaxlin/bazaar-workflow-changes
[Bazaar] Optional linkage to an Entity
This commit is contained in:
@@ -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
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
|
||||
+10
-32
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
### 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)
|
||||

|
||||
|
||||
## 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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 287 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 357 KiB |
@@ -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"
|
||||
|
||||
@@ -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
|
||||
/// <reference types="react" />
|
||||
|
||||
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<undefined>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityBazaarInfoCard: () => JSX.Element;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
+52
-38
@@ -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<BazaarApi>({
|
||||
id: 'bazaar',
|
||||
@@ -28,19 +26,23 @@ export const bazaarApiRef = createApiRef<BazaarApi>({
|
||||
});
|
||||
|
||||
export interface BazaarApi {
|
||||
updateMetadata(bazaarProject: BazaarProject): Promise<any>;
|
||||
updateProject(bazaarProject: any): Promise<any>;
|
||||
|
||||
getMetadata(entity: Entity): Promise<any>;
|
||||
addProject(bazaarProject: any): Promise<any>;
|
||||
|
||||
getMembers(entity: Entity): Promise<any>;
|
||||
getProjectById(id: number): Promise<any>;
|
||||
|
||||
deleteMember(entity: Entity): Promise<void>;
|
||||
getProjectByRef(entityRef: string): Promise<any>;
|
||||
|
||||
addMember(entity: Entity): Promise<void>;
|
||||
getMembers(id: number): Promise<any>;
|
||||
|
||||
getEntities(): Promise<any>;
|
||||
deleteMember(id: number, userId: string): Promise<void>;
|
||||
|
||||
deleteEntity(bazaarProject: BazaarProject): Promise<void>;
|
||||
addMember(id: number, userId: string): Promise<void>;
|
||||
|
||||
getProjects(): Promise<any>;
|
||||
|
||||
deleteProject(id: number): Promise<void>;
|
||||
}
|
||||
|
||||
export class BazaarClient implements BazaarApi {
|
||||
@@ -55,10 +57,10 @@ export class BazaarClient implements BazaarApi {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
}
|
||||
|
||||
async updateMetadata(bazaarProject: BazaarProject): Promise<any> {
|
||||
async updateProject(bazaarProject: any): Promise<any> {
|
||||
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<any> {
|
||||
async addProject(bazaarProject: any): Promise<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
async getMembers(id: number): Promise<any> {
|
||||
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<void> {
|
||||
async addMember(id: number, userId: string): Promise<void> {
|
||||
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<void> {
|
||||
async deleteMember(id: number, userId: string): Promise<void> {
|
||||
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<any> {
|
||||
async getProjects(): Promise<any> {
|
||||
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<void> {
|
||||
async deleteProject(id: number): Promise<void> {
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<Entity | null>(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<FormValues>,
|
||||
reset: UseFormReset<FormValues>,
|
||||
) => {
|
||||
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 (
|
||||
<ProjectDialog
|
||||
handleSave={handleSave}
|
||||
handleSave={handleSubmit}
|
||||
title="Add project"
|
||||
isAddForm
|
||||
defaultValues={defaultValues}
|
||||
open={open}
|
||||
projectSelector={
|
||||
<ProjectSelector
|
||||
value={selectedEntity?.metadata?.name || ''}
|
||||
onChange={handleListItemClick}
|
||||
isFormInvalid={selectedEntity === null}
|
||||
entities={catalogEntities || []}
|
||||
onChange={handleEntityClick}
|
||||
catalogEntities={catalogEntities || []}
|
||||
disableClearable={false}
|
||||
defaultValue={null}
|
||||
label="Select a project"
|
||||
/>
|
||||
}
|
||||
handleClose={handleCloseDialog}
|
||||
handleClose={handleClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<Snackbar
|
||||
open={open}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
>
|
||||
<Alert
|
||||
severity="error"
|
||||
action={
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={handleClose}
|
||||
data-testid="error-button-close"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
{message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<div>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Grid container>
|
||||
<Grid item xs={descriptionSize}>
|
||||
<AboutField label="Description">
|
||||
{bazaarProject.description
|
||||
.split('\n')
|
||||
.map((str: string, i: number) => (
|
||||
<Typography
|
||||
key={i}
|
||||
variant="body2"
|
||||
paragraph
|
||||
className={classes.break}
|
||||
>
|
||||
{str}
|
||||
</Typography>
|
||||
))}
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
item
|
||||
xs={membersSize}
|
||||
>
|
||||
<AboutField label="Latest members">
|
||||
{members.length ? (
|
||||
members.slice(0, 7).map((member: Member) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
backgroundColor: '',
|
||||
marginBottom: '0.3rem',
|
||||
marginTop: '0.3rem',
|
||||
display: 'block',
|
||||
}}
|
||||
key={member.userId}
|
||||
>
|
||||
<Avatar
|
||||
displayName={member.userId}
|
||||
customStyles={{
|
||||
width: '19px',
|
||||
height: '19px',
|
||||
fontSize: '8px',
|
||||
float: 'left',
|
||||
marginRight: '0.3rem',
|
||||
marginTop: '0rem',
|
||||
marginBottom: '0rem',
|
||||
alignItems: 'left',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
picture={member.picture}
|
||||
/>
|
||||
<Link
|
||||
className={classes.break}
|
||||
href={`http://github.com/${member.userId}`}
|
||||
target="_blank"
|
||||
>
|
||||
{member?.userId}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="Status">
|
||||
<StatusTag status={bazaarProject.status} />
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="size">
|
||||
<Typography variant="body2">{bazaarProject.size}</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="Start date">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject.startDate?.substring(0, 10) || ''}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="End date">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject.endDate?.substring(0, 10) || ''}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={4}>
|
||||
<AboutField label="Responsible">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject.responsible || ''}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { AlertBanner } from './AlertBanner';
|
||||
export { CardContentFields } from './CardContentFields';
|
||||
@@ -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 (
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
onClose={handleClose}
|
||||
aria-labelledby="customized-dialog-title"
|
||||
open={open}
|
||||
>
|
||||
<CustomDialogTitle id="customized-dialog-title" onClose={handleClose}>
|
||||
{type.charAt(0).toLocaleUpperCase('en-US') + type.slice(1)} project
|
||||
</CustomDialogTitle>
|
||||
|
||||
<DialogContent dividers>{message}</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleSubmit} color="primary" type="submit">
|
||||
{type}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { DeleteProjectDialog } from './DeleteProjectDialog';
|
||||
export { ConfirmationDialog } from './ConfirmationDialog';
|
||||
@@ -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<typeof styles> {
|
||||
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 (
|
||||
<MuiDialogTitle disableTypography className={classes.root} {...other}>
|
||||
<Typography variant="h6">{children}</Typography>
|
||||
{onClose ? (
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
className={classes.closeButton}
|
||||
onClick={onClose}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</MuiDialogTitle>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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';
|
||||
@@ -45,7 +45,6 @@ export const DateSelector = ({ name, control, setValue }: Props) => {
|
||||
<FormControl>
|
||||
<MuiPickersUtilsProvider utils={LuxonUtils}>
|
||||
<KeyboardDatePicker
|
||||
disablePast
|
||||
disableToolbar
|
||||
format="dd-MM-yyyy"
|
||||
label={label}
|
||||
|
||||
@@ -1,139 +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, { Dispatch, SetStateAction } from 'react';
|
||||
import {
|
||||
createStyles,
|
||||
Theme,
|
||||
withStyles,
|
||||
WithStyles,
|
||||
} from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import MuiDialogTitle from '@material-ui/core/DialogTitle';
|
||||
import MuiDialogContent from '@material-ui/core/DialogContent';
|
||||
import MuiDialogActions from '@material-ui/core/DialogActions';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { bazaarApiRef } from '../../api';
|
||||
import { BazaarProject } from '../../types';
|
||||
|
||||
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<typeof styles> {
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DialogTitle = withStyles(styles)((props: DialogTitleProps) => {
|
||||
const { children, classes, onClose, ...other } = props;
|
||||
return (
|
||||
<MuiDialogTitle disableTypography className={classes.root} {...other}>
|
||||
<Typography variant="h6">{children}</Typography>
|
||||
{onClose ? (
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
className={classes.closeButton}
|
||||
onClick={onClose}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</MuiDialogTitle>
|
||||
);
|
||||
});
|
||||
|
||||
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<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
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 (
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
onClose={handleCloseAndClear}
|
||||
aria-labelledby="customized-dialog-title"
|
||||
open={openDelete}
|
||||
>
|
||||
<DialogTitle id="customized-dialog-title" onClose={handleCloseAndClear}>
|
||||
Delete project
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent dividers>
|
||||
Are you sure you want to delete this project from the Bazaar?
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleSubmit} color="primary" type="submit">
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -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<FormValues, object>;
|
||||
setValue: UseFormSetValue<FormValues>;
|
||||
};
|
||||
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.25rem',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
float: 'left',
|
||||
}}
|
||||
>
|
||||
<div className={classes.container}>
|
||||
<div className={classes.startDate}>
|
||||
<DateSelector name="startDate" control={control} setValue={setValue} />
|
||||
</div>
|
||||
|
||||
<Typography
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: '1.5rem',
|
||||
margin: '0 1rem',
|
||||
}}
|
||||
>
|
||||
-
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
float: 'right',
|
||||
}}
|
||||
>
|
||||
<Typography className={classes.dash}>-</Typography>
|
||||
<div className={classes.endDate}>
|
||||
<DateSelector name="endDate" control={control} setValue={setValue} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<BazaarProject | null>;
|
||||
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<FormValues>({
|
||||
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<FormValues>) => {
|
||||
const handleEditSubmit: any = async (
|
||||
getValues: UseFormGetValues<FormValues>,
|
||||
) => {
|
||||
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 (
|
||||
<ProjectDialog
|
||||
title="Edit project"
|
||||
handleSave={handleSave}
|
||||
isAddForm={false}
|
||||
defaultValues={defaultValues}
|
||||
open={open}
|
||||
handleClose={handleClose}
|
||||
/>
|
||||
<div>
|
||||
<ConfirmationDialog
|
||||
open={openDelete}
|
||||
handleClose={handleDeleteClose}
|
||||
message={[
|
||||
'Are you sure you want to delete ',
|
||||
<b>{bazaarProject.name}</b>,
|
||||
' from the Bazaar?',
|
||||
]}
|
||||
type="delete"
|
||||
handleSubmit={handleDeleteSubmit}
|
||||
/>
|
||||
|
||||
<ProjectDialog
|
||||
title="Edit project"
|
||||
handleSave={handleEditSubmit}
|
||||
isAddForm={false}
|
||||
defaultValues={defaultValues}
|
||||
open={openEdit}
|
||||
handleClose={handleEditClose}
|
||||
deleteButton={
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
className={classes.button}
|
||||
onClick={() => {
|
||||
setOpenDelete(true);
|
||||
}}
|
||||
>
|
||||
Delete project
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<HTMLButtonElement>();
|
||||
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<HTMLButtonElement>) => {
|
||||
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 ? <ExitToAppIcon /> : <PersonAddIcon />,
|
||||
href: '',
|
||||
onClick: async () => {
|
||||
handleMembersClick();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Community',
|
||||
icon: <ChatIcon />,
|
||||
href: bazaarProject?.value?.community,
|
||||
disabled: !bazaarProject?.value?.community || !isMember,
|
||||
},
|
||||
];
|
||||
|
||||
if (!isBazaar) {
|
||||
return null;
|
||||
} else if (bazaarProject.loading || members.loading) {
|
||||
return <Progress />;
|
||||
} else if (bazaarProject.error) {
|
||||
return <Alert severity="error">{bazaarProject?.error?.message}</Alert>;
|
||||
} else if (members.error) {
|
||||
return <Alert severity="error">{members?.error?.message}</Alert>;
|
||||
}
|
||||
return (
|
||||
<Card>
|
||||
{bazaarProject?.value && (
|
||||
<EditProjectDialog
|
||||
open={open}
|
||||
entity={entity}
|
||||
if (isBazaar) {
|
||||
return (
|
||||
<Card>
|
||||
<EntityBazaarInfoContent
|
||||
bazaarProject={bazaarProject.value}
|
||||
fetchBazaarProject={fetchBazaarProject}
|
||||
handleClose={closeEdit}
|
||||
isAddForm={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{bazaarProject?.value && (
|
||||
<DeleteProjectDialog
|
||||
bazaarProject={bazaarProject.value}
|
||||
openDelete={openDelete}
|
||||
handleClose={closeDelete}
|
||||
setIsBazaar={setIsBazaar}
|
||||
/>
|
||||
)}
|
||||
<CardHeader
|
||||
title="Bazaar"
|
||||
action={
|
||||
<IconButton onClick={onOpen}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
}
|
||||
subheader={<HeaderIconLinkRow links={links} />}
|
||||
/>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Popover
|
||||
open={popoverOpen}
|
||||
onClose={popoverCloseHandler}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
setPopoverOpen(false);
|
||||
}}
|
||||
>
|
||||
<EditIcon className={classes.icon} />
|
||||
<ListItemText primary="Edit project" />
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setOpenDelete(true);
|
||||
setPopoverOpen(false);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon className={classes.icon} />
|
||||
<ListItemText primary="Remove from Bazaar" />
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Popover>
|
||||
<Grid container>
|
||||
<Grid item xs={10}>
|
||||
<AboutField label="Announcement">
|
||||
{bazaarProject?.value?.announcement
|
||||
? bazaarProject?.value?.announcement
|
||||
.split('\n')
|
||||
.map((str: string, i: number) => (
|
||||
<Typography
|
||||
key={i}
|
||||
variant="body2"
|
||||
paragraph
|
||||
className={classes.description}
|
||||
>
|
||||
{str}
|
||||
</Typography>
|
||||
))
|
||||
: 'No announcement'}
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid style={{ marginRight: '0rem' }}>
|
||||
{' '}
|
||||
<AboutField label="Latest members">
|
||||
{members?.value?.length ? (
|
||||
members.value.slice(0, 7).map((member: Member) => {
|
||||
return (
|
||||
<div key={member.userId}>
|
||||
<Avatar
|
||||
displayName={member.userId}
|
||||
customStyles={{
|
||||
width: '19px',
|
||||
height: '19px',
|
||||
fontSize: '8px',
|
||||
float: 'left',
|
||||
marginRight: '0.3rem',
|
||||
marginBottom: '0.25rem',
|
||||
}}
|
||||
picture={member.picture}
|
||||
/>
|
||||
<Link
|
||||
className={classes.memberLink}
|
||||
href={`http://github.com/${member.userId}`}
|
||||
target="_blank"
|
||||
>
|
||||
{member?.userId}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="Status">
|
||||
<StatusTag status={bazaarProject?.value?.status || 'proposed'} />
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="size">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject?.value?.size}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="Start date">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject?.value?.startDate?.substring(0, 10) || ''}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="End date">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject?.value?.endDate?.substring(0, 10) || ''}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="Responsible">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject?.value?.responsible || ''}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -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<BazaarProject | null>;
|
||||
};
|
||||
|
||||
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: <DashboardIcon />,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: 'Unlink project',
|
||||
icon: <LinkOffIcon />,
|
||||
disabled: false,
|
||||
onClick: () => {
|
||||
setOpenUnlink(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: isMember ? 'Leave' : 'Join',
|
||||
icon: isMember ? <ExitToAppIcon /> : <PersonAddIcon />,
|
||||
href: '',
|
||||
onClick: async () => {
|
||||
handleMembersClick();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Community',
|
||||
icon: <ChatIcon />,
|
||||
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 <Alert severity="error">{members?.error?.message}</Alert>;
|
||||
}
|
||||
|
||||
if (bazaarProject) {
|
||||
return (
|
||||
<div>
|
||||
<EditProjectDialog
|
||||
bazaarProject={bazaarProject!}
|
||||
openEdit={openEdit}
|
||||
handleEditClose={handleEditClose}
|
||||
fetchBazaarProject={fetchBazaarProject}
|
||||
/>
|
||||
|
||||
{openUnlink && (
|
||||
<ConfirmationDialog
|
||||
open={openUnlink}
|
||||
handleClose={handleUnlinkClose}
|
||||
message={[
|
||||
'Are you sure you want to unlink ',
|
||||
<b>{parseEntityRef(bazaarProject.entityRef!).name}</b>,
|
||||
' from ',
|
||||
<b>{bazaarProject.name}</b>,
|
||||
' ?',
|
||||
]}
|
||||
type="unlink"
|
||||
handleSubmit={handleUnlinkSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CardHeader
|
||||
title={bazaarProject?.name!}
|
||||
action={
|
||||
<div>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setOpenEdit(true);
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
}
|
||||
subheader={<HeaderIconLinkRow links={links} />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<CardContentFields
|
||||
bazaarProject={bazaarProject}
|
||||
members={members.value || []}
|
||||
descriptionSize={10}
|
||||
membersSize={2}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -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';
|
||||
@@ -36,11 +36,7 @@ export const HomePage = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header
|
||||
data-testid="bazaar-header"
|
||||
title="Bazaar"
|
||||
subtitle="Marketplace for inner source projects"
|
||||
/>
|
||||
<Header title="Bazaar" subtitle="Marketplace for inner source projects" />
|
||||
<RoutedTabs routes={tabContent} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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: <DashboardIcon />,
|
||||
href: bazaarProject.value?.entityRef ? getEntityPageLink() : '',
|
||||
disabled: bazaarProject.value?.entityRef === null,
|
||||
},
|
||||
{
|
||||
label: bazaarProject.value?.entityRef ? 'Unlink project' : 'Link project',
|
||||
icon: bazaarProject.value?.entityRef ? (
|
||||
<LinkOffIcon />
|
||||
) : (
|
||||
<InsertLinkIcon />
|
||||
),
|
||||
onClick: handleLink,
|
||||
},
|
||||
{
|
||||
label: isMember ? 'Leave' : 'Join',
|
||||
icon: isMember ? <ExitToAppIcon /> : <PersonAddIcon />,
|
||||
href: '',
|
||||
onClick: async () => {
|
||||
handleMembersClick();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Community',
|
||||
icon: <ChatIcon />,
|
||||
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 <Alert severity="error">{bazaarProject?.error?.message}</Alert>;
|
||||
} else if (members.error) {
|
||||
return <Alert severity="error">{members?.error?.message}</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LinkProjectDialog
|
||||
openProjectSelector={openProjectSelector}
|
||||
handleProjectSelectorClose={() => setOpenProjectSelector(false)}
|
||||
catalogEntities={catalogEntities.value || []}
|
||||
bazaarProject={bazaarProject.value || initProject}
|
||||
fetchBazaarProject={fetchBazaarProject}
|
||||
initEntity={initEntity}
|
||||
/>
|
||||
|
||||
{openUnlink && (
|
||||
<ConfirmationDialog
|
||||
open={openUnlink}
|
||||
handleClose={() => setOpenUnlink(false)}
|
||||
message={[
|
||||
'Are you sure you want to unlink ',
|
||||
<b>{parseEntityRef(bazaarProject.value?.entityRef!).name}</b>,
|
||||
' from ',
|
||||
<b>{bazaarProject.value?.name}</b>,
|
||||
' ?',
|
||||
]}
|
||||
type="unlink"
|
||||
handleSubmit={handleUnlinkSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<EditProjectDialog
|
||||
bazaarProject={bazaarProject.value || initProject}
|
||||
openEdit={openEdit}
|
||||
handleEditClose={() => setOpenEdit(false)}
|
||||
handleCardClose={handleClose}
|
||||
fetchBazaarProject={fetchBazaarProject}
|
||||
/>
|
||||
|
||||
<CardHeader
|
||||
title={bazaarProject.value?.name || initProject.name}
|
||||
action={
|
||||
<div>
|
||||
<IconButton onClick={() => setOpenEdit(true)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={handleClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
}
|
||||
subheader={<HeaderIconLinkRow links={links} />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<CardContentFields
|
||||
bazaarProject={bazaarProject.value || initProject}
|
||||
members={members.value || []}
|
||||
descriptionSize={9}
|
||||
membersSize={3}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -25,7 +25,7 @@ type Rules = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
inputType: 'announcement' | 'community' | 'responsible';
|
||||
inputType: 'description' | 'community' | 'responsible' | 'name';
|
||||
error?: FieldError | undefined;
|
||||
control: Control<FormValues, object>;
|
||||
helperText?: string;
|
||||
|
||||
@@ -60,12 +60,7 @@ export const InputSelector = ({ name, options, control, error }: Props) => {
|
||||
>
|
||||
{options.map(option => {
|
||||
return (
|
||||
<MenuItem
|
||||
data-testid="menu-item"
|
||||
button
|
||||
key={option}
|
||||
value={option}
|
||||
>
|
||||
<MenuItem button key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -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<BazaarProject | null>;
|
||||
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 (
|
||||
<Dialog onClose={handleProjectSelectorClose} open={openProjectSelector}>
|
||||
<CustomDialogTitle
|
||||
id="customized-dialog-title"
|
||||
onClose={handleProjectSelectorClose}
|
||||
>
|
||||
Select entity
|
||||
</CustomDialogTitle>
|
||||
<DialogContent className={classes.content} dividers>
|
||||
<ProjectSelector
|
||||
label=""
|
||||
onChange={handleEntityClick}
|
||||
catalogEntities={catalogEntities || []}
|
||||
disableClearable
|
||||
defaultValue={catalogEntities[0] || null}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleSubmit} color="primary" type="submit">
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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<BazaarProject[]>;
|
||||
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 (
|
||||
<Card key={entityRef as string}>
|
||||
<CardActionArea
|
||||
style={{
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
}}
|
||||
component={RouterLink}
|
||||
to={`${catalogLink()}/${namespace}/${kind}/${name}`}
|
||||
>
|
||||
<ItemCardHeader
|
||||
title={name}
|
||||
subtitle={`updated ${DateTime.fromISO(
|
||||
new Date(updatedAt!).toISOString(),
|
||||
).toRelative({
|
||||
base: DateTime.now(),
|
||||
})}`}
|
||||
<div>
|
||||
<Dialog fullWidth onClose={handleClose} open={openCard}>
|
||||
<HomePageBazaarInfoCard
|
||||
initProject={project}
|
||||
handleClose={handleClose}
|
||||
initEntity={catalogEntities[0] || null}
|
||||
/>
|
||||
<CardContent style={{ height: '12rem' }}>
|
||||
<StatusTag styles={classes.statusTag} status={status} />
|
||||
<Typography variant="body2" className={classes.memberCount}>
|
||||
{membersCount === 1
|
||||
? `${membersCount} member`
|
||||
: `${membersCount} members`}
|
||||
</Typography>
|
||||
<div style={{ minHeight: '6.5rem', maxHeight: '6.5rem' }}>
|
||||
<Typography variant="body2" className={classes.announcement}>
|
||||
{announcement}
|
||||
</Dialog>
|
||||
|
||||
<Card key={id}>
|
||||
<CardActionArea onClick={() => setOpenCard(true)}>
|
||||
<ItemCardHeader
|
||||
title={name}
|
||||
subtitle={`updated ${DateTime.fromISO(
|
||||
new Date(updatedAt!).toISOString(),
|
||||
).toRelative({
|
||||
base: DateTime.now(),
|
||||
})}`}
|
||||
/>
|
||||
<CardContent className={classes.content}>
|
||||
<StatusTag styles={classes.statusTag} status={status} />
|
||||
<Typography variant="body2" className={classes.memberCount}>
|
||||
{Number(membersCount) === Number(1)
|
||||
? `${membersCount} member`
|
||||
: `${membersCount} members`}
|
||||
</Typography>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
<Typography variant="body2" className={classes.description}>
|
||||
{description}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<typeof styles> {
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DialogTitle = withStyles(styles)((props: DialogTitleProps) => {
|
||||
const { children, classes, onClose, ...other } = props;
|
||||
return (
|
||||
<MuiDialogTitle disableTypography className={classes.root} {...other}>
|
||||
<Typography variant="h6">{children}</Typography>
|
||||
{onClose ? (
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
className={classes.closeButton}
|
||||
onClick={onClose}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</MuiDialogTitle>
|
||||
);
|
||||
});
|
||||
|
||||
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<FormValues>({
|
||||
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 = ({
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
onClose={handleCloseAndClear}
|
||||
onClose={handleCloseDialog}
|
||||
aria-labelledby="customized-dialog-title"
|
||||
open={open}
|
||||
>
|
||||
<DialogTitle id="customized-dialog-title" onClose={handleCloseAndClear}>
|
||||
<CustomDialogTitle
|
||||
id="customized-dialog-title"
|
||||
onClose={handleCloseDialog}
|
||||
>
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{isAddForm && projectSelector}
|
||||
</CustomDialogTitle>
|
||||
<DialogContent style={{ padding: '1rem', paddingTop: '0rem' }} dividers>
|
||||
<InputField
|
||||
error={errors.name}
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: RegExp('^[a-zA-Z0-9_-]*$'),
|
||||
}}
|
||||
inputType="name"
|
||||
helperText="please enter a url safe project name"
|
||||
/>
|
||||
|
||||
<InputField
|
||||
error={errors.announcement}
|
||||
error={errors.description}
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
}}
|
||||
inputType="announcement"
|
||||
helperText="please enter an announcement"
|
||||
placeholder="Describe who you are and what skills you are looking for"
|
||||
inputType="description"
|
||||
helperText="please enter a description"
|
||||
/>
|
||||
|
||||
<InputSelector
|
||||
@@ -184,7 +137,7 @@ export const ProjectDialog = ({
|
||||
placeholder="Contact person of the project"
|
||||
/>
|
||||
|
||||
<DoubleDateSelector setValue={setValue} control={control} />
|
||||
{isAddForm && projectSelector}
|
||||
|
||||
<InputField
|
||||
error={errors.community}
|
||||
@@ -197,11 +150,14 @@ export const ProjectDialog = ({
|
||||
helperText="please enter a link starting with http/https"
|
||||
placeholder="Community link to e.g. Teams or Discord"
|
||||
/>
|
||||
|
||||
<DoubleDateSelector setValue={setValue} control={control} />
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
{!isAddForm && deleteButton}
|
||||
<Button
|
||||
onClick={handleSubmit(handleSaveProject)}
|
||||
onClick={handleSubmit(handleSaveForm)}
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
|
||||
@@ -14,15 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { ChangeEvent, useState } from 'react';
|
||||
import { Content } from '@backstage/core-components';
|
||||
import { ProjectCard } from '../ProjectCard/ProjectCard';
|
||||
import { makeStyles, Grid, TablePagination } from '@material-ui/core';
|
||||
import { BazaarProject } from '../../types';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
type Props = {
|
||||
bazaarProjects: BazaarProject[];
|
||||
sortingMethod: (arg0: BazaarProject, arg1: BazaarProject) => number;
|
||||
fetchBazaarProjects: () => Promise<BazaarProject[]>;
|
||||
catalogEntities: Entity[];
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@@ -32,75 +34,75 @@ const useStyles = makeStyles({
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
},
|
||||
item: {
|
||||
minWidth: '20.5rem',
|
||||
maxWidth: '20.5rem',
|
||||
width: '20%',
|
||||
empty: {
|
||||
height: '10rem',
|
||||
textAlign: 'center',
|
||||
verticalAlign: 'middle',
|
||||
lineHeight: '10rem',
|
||||
},
|
||||
pagination: {
|
||||
marginTop: '1rem',
|
||||
marginLeft: 'auto',
|
||||
marginRight: '0',
|
||||
},
|
||||
});
|
||||
|
||||
export const ProjectPreview = ({ bazaarProjects, sortingMethod }: Props) => {
|
||||
export const ProjectPreview = ({
|
||||
bazaarProjects,
|
||||
fetchBazaarProjects,
|
||||
catalogEntities,
|
||||
}: Props) => {
|
||||
const classes = useStyles();
|
||||
const [page, setPage] = useState(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [rows, setRows] = useState(12);
|
||||
|
||||
const handleChangePage = (_: any, newPage: number) => {
|
||||
const handlePageChange = (_: any, newPage: number) => {
|
||||
setPage(newPage + 1);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
const handleRowChange = (
|
||||
event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setRows(parseInt(event.target.value, 10));
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
if (!bazaarProjects.length) {
|
||||
return (
|
||||
<div
|
||||
data-testid="empty-bazaar"
|
||||
style={{
|
||||
height: '10rem',
|
||||
textAlign: 'center',
|
||||
verticalAlign: 'middle',
|
||||
lineHeight: '10rem',
|
||||
}}
|
||||
>
|
||||
Please add projects to the Bazaar.
|
||||
</div>
|
||||
<div className={classes.empty}>Please add projects to the Bazaar.</div>
|
||||
);
|
||||
}
|
||||
|
||||
bazaarProjects.sort(sortingMethod);
|
||||
|
||||
return (
|
||||
<Content className={classes.content} noPadding>
|
||||
<Grid wrap="wrap" container spacing={3}>
|
||||
{bazaarProjects
|
||||
.slice((page - 1) * rowsPerPage, rowsPerPage * page)
|
||||
.slice((page - 1) * rows, rows * page)
|
||||
.map((bazaarProject: BazaarProject, i: number) => {
|
||||
return (
|
||||
<Grid key={i} className={classes.item} item xs={3}>
|
||||
<ProjectCard bazaarProject={bazaarProject} key={i} />
|
||||
<Grid key={i} item xs={2}>
|
||||
<ProjectCard
|
||||
project={bazaarProject}
|
||||
key={i}
|
||||
fetchBazaarProjects={fetchBazaarProjects}
|
||||
catalogEntities={catalogEntities}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
<TablePagination
|
||||
className={classes.pagination}
|
||||
rowsPerPageOptions={[12, 24, 48, 96]}
|
||||
count={bazaarProjects?.length}
|
||||
page={page - 1}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
onPageChange={handlePageChange}
|
||||
rowsPerPage={rows}
|
||||
onRowsPerPageChange={handleRowChange}
|
||||
backIconButtonProps={{ disabled: page === 1 }}
|
||||
nextIconButtonProps={{
|
||||
disabled: rowsPerPage * page >= bazaarProjects.length,
|
||||
}}
|
||||
style={{
|
||||
marginTop: '1rem',
|
||||
marginLeft: 'auto',
|
||||
marginRight: '0',
|
||||
disabled: rows * page >= bazaarProjects.length,
|
||||
}}
|
||||
/>
|
||||
</Content>
|
||||
|
||||
@@ -17,38 +17,44 @@
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Autocomplete } from '@material-ui/lab';
|
||||
import { TextField } from '@material-ui/core';
|
||||
import { TextField, makeStyles } from '@material-ui/core';
|
||||
|
||||
type Props = {
|
||||
entities: Entity[];
|
||||
value: string;
|
||||
catalogEntities: Entity[];
|
||||
onChange: (entity: Entity) => void;
|
||||
isFormInvalid: boolean;
|
||||
disableClearable: boolean;
|
||||
defaultValue: Entity | null | undefined;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: { width: '100%', minWidth: '22rem' },
|
||||
autocomplete: { overflow: 'hidden' },
|
||||
});
|
||||
|
||||
export const ProjectSelector = ({
|
||||
entities,
|
||||
value,
|
||||
catalogEntities,
|
||||
onChange,
|
||||
isFormInvalid,
|
||||
disableClearable,
|
||||
defaultValue,
|
||||
label,
|
||||
}: Props) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Autocomplete
|
||||
defaultValue={entities[0]}
|
||||
options={entities}
|
||||
getOptionLabel={option => option?.metadata?.name}
|
||||
renderOption={option => <span>{option?.metadata?.name}</span>}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
error={isFormInvalid && value === ''}
|
||||
helperText={
|
||||
isFormInvalid && value === '' ? 'Please select a project' : ''
|
||||
}
|
||||
{...params}
|
||||
label="Select a project *"
|
||||
/>
|
||||
)}
|
||||
onChange={(_, data) => onChange(data!)}
|
||||
/>
|
||||
<div className={classes.container}>
|
||||
<Autocomplete
|
||||
className={classes.autocomplete}
|
||||
fullWidth
|
||||
disableClearable={disableClearable}
|
||||
defaultValue={defaultValue}
|
||||
options={catalogEntities}
|
||||
getOptionLabel={option => option?.metadata?.name}
|
||||
renderOption={option => <span>{option?.metadata?.name}</span>}
|
||||
renderInput={params => <TextField {...params} label={label} />}
|
||||
onChange={(_, data) => {
|
||||
onChange(data!);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 { FormControl, MenuItem, Select, makeStyles } from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
select: {
|
||||
fontSize: 'xx-large',
|
||||
fontWeight: 'bold',
|
||||
width: '16rem',
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
sortMethodNbr: number;
|
||||
handleSortMethodChange: any;
|
||||
};
|
||||
|
||||
export const SortMethodSelector = ({
|
||||
sortMethodNbr,
|
||||
handleSortMethodChange,
|
||||
}: Props) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<FormControl fullWidth>
|
||||
<Select
|
||||
className={classes.select}
|
||||
disableUnderline
|
||||
value={sortMethodNbr}
|
||||
onChange={handleSortMethodChange}
|
||||
>
|
||||
<MenuItem value={0}>Latest updated</MenuItem>
|
||||
<MenuItem value={1}>A-Z</MenuItem>
|
||||
<MenuItem value={2}>Most members</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
@@ -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 { SortMethodSelector } from './SortMethodSelector';
|
||||
@@ -14,17 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
Progress,
|
||||
} from '@backstage/core-components';
|
||||
import React, { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { Content, SupportButton } from '@backstage/core-components';
|
||||
import { AddProjectDialog } from '../AddProjectDialog';
|
||||
import { AlertBanner } from '../AlertBanner';
|
||||
import { ProjectPreview } from '../ProjectPreview/ProjectPreview';
|
||||
import { Button, makeStyles, Link } from '@material-ui/core';
|
||||
import { Button, makeStyles } from '@material-ui/core';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
@@ -32,14 +26,34 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { BazaarProject } from '../../types';
|
||||
import { bazaarApiRef } from '../../api';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import SearchBar from 'material-ui-search-bar';
|
||||
import { sortByDate, sortByMembers, sortByName } from '../../util/sortMethods';
|
||||
import { SortMethodSelector } from '../SortMethodSelector';
|
||||
import { fetchCatalogItems } from '../../util/fetchMethods';
|
||||
import { parseBazaarProject } from '../../util/parseMethods';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
button: { width: '12rem' },
|
||||
container: {
|
||||
marginTop: '2rem',
|
||||
},
|
||||
header: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
margin: '0 auto',
|
||||
marginBottom: '1.2rem',
|
||||
},
|
||||
search: {
|
||||
marginRight: '1rem',
|
||||
height: '2.5rem',
|
||||
width: '35rem',
|
||||
},
|
||||
});
|
||||
|
||||
const filterCatalogEntities = (
|
||||
const getUnlinkedCatalogEntities = (
|
||||
bazaarProjects: BazaarProject[],
|
||||
catalogEntities: Entity[],
|
||||
) => {
|
||||
@@ -47,86 +61,91 @@ const filterCatalogEntities = (
|
||||
(project: BazaarProject) => project.entityRef,
|
||||
);
|
||||
|
||||
const filtered = catalogEntities.filter((entity: Entity) => {
|
||||
return catalogEntities.filter((entity: Entity) => {
|
||||
return !bazaarProjectRefs?.includes(stringifyEntityRef(entity));
|
||||
});
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
export const SortView = () => {
|
||||
const classes = useStyles();
|
||||
const [openAdd, setOpenAdd] = useState(false);
|
||||
const [openNoProjects, setOpenNoProjects] = useState(false);
|
||||
const bazaarApi = useApi(bazaarApiRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [filteredCatalogEntites, setFilteredCatalogEntities] =
|
||||
const classes = useStyles();
|
||||
const sortMethods = [sortByDate, sortByName, sortByMembers];
|
||||
const [sortMethodNbr, setSortMethodNbr] = useState(0);
|
||||
const [openAdd, setOpenAdd] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [unlinkedCatalogEntities, setUnlinkedCatalogEntities] =
|
||||
useState<Entity[]>();
|
||||
|
||||
const compareProjectsByDate = (
|
||||
a: BazaarProject,
|
||||
b: BazaarProject,
|
||||
): number => {
|
||||
const dateA = new Date(a.updatedAt!).getTime();
|
||||
const dateB = new Date(b.updatedAt!).getTime();
|
||||
return dateB - dateA;
|
||||
};
|
||||
|
||||
const handleCloseNoProjects = () => {
|
||||
setOpenNoProjects(false);
|
||||
};
|
||||
|
||||
const [catalogEntities, fetchCatalogEntities] = useAsyncFn(async () => {
|
||||
const entities = await catalogApi.getEntities({
|
||||
filter: {
|
||||
kind: ['Component', 'Resource'],
|
||||
},
|
||||
fields: ['kind', 'metadata.name', 'metadata.namespace'],
|
||||
});
|
||||
|
||||
return entities.items;
|
||||
return await fetchCatalogItems(catalogApi);
|
||||
});
|
||||
|
||||
const [bazaarProjects, fetchBazaarProjects] = useAsyncFn(async () => {
|
||||
const response = await bazaarApi.getEntities();
|
||||
const response = await bazaarApi.getProjects();
|
||||
const dbProjects: BazaarProject[] = [];
|
||||
|
||||
response.data.forEach((project: any) => {
|
||||
dbProjects.push({
|
||||
entityRef: project.entity_ref,
|
||||
name: project.name,
|
||||
status: project.status,
|
||||
announcement: project.announcement,
|
||||
community: project.community,
|
||||
updatedAt: project.updated_at,
|
||||
membersCount: project.members_count,
|
||||
size: project.size,
|
||||
startDate: project.startDate,
|
||||
endDate: project.endDate,
|
||||
responsible: project.responsible,
|
||||
});
|
||||
dbProjects.push(parseBazaarProject(project));
|
||||
});
|
||||
|
||||
return dbProjects;
|
||||
});
|
||||
|
||||
const catalogEntityRefs = catalogEntities.value?.map((project: Entity) =>
|
||||
stringifyEntityRef(project),
|
||||
);
|
||||
|
||||
const getSearchResults = () => {
|
||||
return bazaarProjects.value
|
||||
?.filter(project => project.name.includes(searchValue))
|
||||
.sort(sortMethods[sortMethodNbr]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const filterBrokenLinks = () => {
|
||||
if (catalogEntityRefs) {
|
||||
bazaarProjects.value?.forEach(async (project: BazaarProject) => {
|
||||
if (project.entityRef) {
|
||||
if (!catalogEntityRefs?.includes(project.entityRef as string)) {
|
||||
await bazaarApi.updateProject({
|
||||
...project,
|
||||
entityRef: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
filterBrokenLinks();
|
||||
}, [
|
||||
bazaarApi,
|
||||
bazaarProjects.value,
|
||||
catalogEntityRefs,
|
||||
catalogEntities.value,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCatalogEntities();
|
||||
fetchBazaarProjects();
|
||||
}, [fetchBazaarProjects, fetchCatalogEntities]);
|
||||
|
||||
useEffect(() => {
|
||||
const filteredCatalogEntities = filterCatalogEntities(
|
||||
const unlinkedCEntities = getUnlinkedCatalogEntities(
|
||||
bazaarProjects.value || [],
|
||||
catalogEntities.value || [],
|
||||
);
|
||||
|
||||
if (filteredCatalogEntities) {
|
||||
setFilteredCatalogEntities(filteredCatalogEntities);
|
||||
if (unlinkedCEntities) {
|
||||
setUnlinkedCatalogEntities(unlinkedCEntities);
|
||||
}
|
||||
}, [bazaarProjects, catalogEntities]);
|
||||
|
||||
if (catalogEntities.loading || bazaarProjects.loading) return <Progress />;
|
||||
const handleSortMethodChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setSortMethodNbr(
|
||||
typeof event.target.value === 'number' ? event.target.value : 0,
|
||||
);
|
||||
};
|
||||
|
||||
if (catalogEntities.error)
|
||||
return <Alert severity="error">{catalogEntities.error.message}</Alert>;
|
||||
@@ -136,38 +155,34 @@ export const SortView = () => {
|
||||
|
||||
return (
|
||||
<Content noPadding>
|
||||
<AlertBanner
|
||||
open={openNoProjects}
|
||||
message={
|
||||
<div>
|
||||
No project available. Please{' '}
|
||||
<Link
|
||||
style={{ color: 'inherit', fontWeight: 'bold' }}
|
||||
href="/create"
|
||||
>
|
||||
create a project
|
||||
</Link>{' '}
|
||||
from a template first.
|
||||
</div>
|
||||
}
|
||||
handleClose={handleCloseNoProjects}
|
||||
/>
|
||||
<ContentHeader title="Latest updated">
|
||||
<div className={classes.header}>
|
||||
<SortMethodSelector
|
||||
sortMethodNbr={sortMethodNbr}
|
||||
handleSortMethodChange={handleSortMethodChange}
|
||||
/>
|
||||
|
||||
<SearchBar
|
||||
className={classes.search}
|
||||
value={searchValue}
|
||||
onChange={newSortMethod => {
|
||||
setSearchValue(newSortMethod);
|
||||
}}
|
||||
onCancelSearch={() => {
|
||||
setSearchValue('');
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className={classes.button}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (filteredCatalogEntites?.length !== 0) {
|
||||
setOpenAdd(true);
|
||||
} else {
|
||||
setOpenNoProjects(true);
|
||||
}
|
||||
setOpenAdd(true);
|
||||
}}
|
||||
>
|
||||
Add project
|
||||
</Button>
|
||||
<AddProjectDialog
|
||||
catalogEntities={filteredCatalogEntites || []}
|
||||
catalogEntities={unlinkedCatalogEntities || []}
|
||||
handleClose={() => {
|
||||
setOpenAdd(false);
|
||||
}}
|
||||
@@ -176,10 +191,11 @@ export const SortView = () => {
|
||||
fetchCatalogEntities={fetchCatalogEntities}
|
||||
/>
|
||||
<SupportButton />
|
||||
</ContentHeader>
|
||||
</div>
|
||||
<ProjectPreview
|
||||
bazaarProjects={bazaarProjects.value || []}
|
||||
sortingMethod={compareProjectsByDate}
|
||||
bazaarProjects={getSearchResults() || []}
|
||||
fetchBazaarProjects={fetchBazaarProjects}
|
||||
catalogEntities={unlinkedCatalogEntities || []}
|
||||
/>
|
||||
<Content noPadding className={classes.container} />
|
||||
</Content>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Member[]> => {
|
||||
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<Entity[]> => {
|
||||
const entities = await catalogApi.getEntities({
|
||||
filter: {
|
||||
kind: ['Component', 'Resource'],
|
||||
},
|
||||
fields: ['kind', 'metadata.name', 'metadata.namespace'],
|
||||
});
|
||||
|
||||
return entities.items;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user