diff --git a/.changeset/angry-tables-invite.md b/.changeset/angry-tables-invite.md new file mode 100644 index 0000000000..a187acda78 --- /dev/null +++ b/.changeset/angry-tables-invite.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-bazaar': patch +'@backstage/plugin-bazaar-backend': patch +--- + +Add Bazaar plugin to marketplace and some minor refactoring diff --git a/microsite/data/plugins/bazaar.yaml b/microsite/data/plugins/bazaar.yaml new file mode 100644 index 0000000000..6e27692d78 --- /dev/null +++ b/microsite/data/plugins/bazaar.yaml @@ -0,0 +1,8 @@ +title: Bazaar +author: Axis Communications AB +authorUrl: https://www.axis.com +category: Discovery +description: A marketplace where engineers can propose projects suitable for inner sourcing +documentation: https://github.com/backstage/backstage/blob/master/plugins/bazaar/README.md +iconUrl: img/bazaar.svg +npmPackageName: '@backstage/plugin-bazaar' diff --git a/microsite/static/img/bazaar.svg b/microsite/static/img/bazaar.svg new file mode 100644 index 0000000000..66c53003fe --- /dev/null +++ b/microsite/static/img/bazaar.svg @@ -0,0 +1 @@ + diff --git a/plugins/bazaar-backend/src/service/router.ts b/plugins/bazaar-backend/src/service/router.ts index 730d958fcd..3915c8c609 100644 --- a/plugins/bazaar-backend/src/service/router.ts +++ b/plugins/bazaar-backend/src/service/router.ts @@ -69,21 +69,19 @@ export async function createRouter( } }); - router.get('/projects/id/:id', async (request, response) => { - const id = decodeURIComponent(request.params.id); + router.get('/projects/:idOrRef', async (request, response) => { + const idOrRef = decodeURIComponent(request.params.idOrRef); + let data; - const data = await dbHandler.getMetadataById(parseInt(id, 10)); + if (/^-?\d+$/.test(idOrRef)) { + data = await dbHandler.getMetadataById(parseInt(idOrRef, 10)); + } else { + data = await dbHandler.getMetadataByRef(idOrRef); + } response.json({ status: 'ok', data: data }); }); - router.get('/projects/ref/:ref', async (request, response) => { - const ref = decodeURIComponent(request.params.ref); - - const data = await dbHandler.getMetadataByRef(ref); - response.json({ status: 'ok', data: data }); - }); - router.get('/projects', async (_, response) => { const data = await dbHandler.getProjects(); diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 03d6090f01..d081a220e7 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -21,6 +21,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/catalog-client": "^0.5.3", "@backstage/catalog-model": "^0.9.7", "@backstage/cli": "^0.10.5", "@backstage/core-components": "^0.8.3", diff --git a/plugins/bazaar/src/api.ts b/plugins/bazaar/src/api.ts index 9f5010ec43..cae890ea4a 100644 --- a/plugins/bazaar/src/api.ts +++ b/plugins/bazaar/src/api.ts @@ -85,9 +85,12 @@ export class BazaarClient implements BazaarApi { async getProjectById(id: number): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - const response = await fetch(`${baseUrl}/projects/id/${id}`, { - method: 'GET', - }); + const response = await fetch( + `${baseUrl}/projects/${encodeURIComponent(id)}`, + { + method: 'GET', + }, + ); return response.ok ? response : null; } @@ -96,7 +99,7 @@ export class BazaarClient implements BazaarApi { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); const response = await fetch( - `${baseUrl}/projects/ref/${encodeURIComponent(entityRef)}`, + `${baseUrl}/projects/${encodeURIComponent(entityRef)}`, { method: 'GET', }, @@ -108,32 +111,45 @@ export class BazaarClient implements BazaarApi { async getMembers(id: number): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - return await fetch(`${baseUrl}/projects/${id}/members`, { - method: 'GET', - }).then(resp => resp.json()); + return await fetch( + `${baseUrl}/projects/${encodeURIComponent(id)}/members`, + { + method: 'GET', + }, + ).then(resp => resp.json()); } async addMember(id: number, userId: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - await fetch(`${baseUrl}/projects/${id}/member/${userId}`, { - method: 'PUT', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', + await fetch( + `${baseUrl}/projects/${encodeURIComponent( + id, + )}/member/${encodeURIComponent(userId)}`, + { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + picture: (await this.identityApi.getProfileInfo()).picture, + }), }, - body: JSON.stringify({ - picture: this.identityApi.getProfile()?.picture, - }), - }); + ); } async deleteMember(id: number, userId: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - await fetch(`${baseUrl}/projects/${id}/member/${userId}`, { - method: 'DELETE', - }); + await fetch( + `${baseUrl}/projects/${encodeURIComponent( + id, + )}/member/${encodeURIComponent(userId)}`, + { + method: 'DELETE', + }, + ); } async getProjects(): Promise { @@ -147,7 +163,7 @@ export class BazaarClient implements BazaarApi { async deleteProject(id: number): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - await fetch(`${baseUrl}/projects/${id}`, { + await fetch(`${baseUrl}/projects/${encodeURIComponent(id)}`, { method: 'DELETE', }); } diff --git a/plugins/bazaar/src/components/About/About.tsx b/plugins/bazaar/src/components/About/About.tsx index d9c529a209..68525ab642 100644 --- a/plugins/bazaar/src/components/About/About.tsx +++ b/plugins/bazaar/src/components/About/About.tsx @@ -15,8 +15,8 @@ */ import React from 'react'; -import { Grid, Typography, Link, makeStyles } from '@material-ui/core'; -import { InfoCard } from '@backstage/core-components'; +import { Grid, Typography, makeStyles } from '@material-ui/core'; +import { InfoCard, Link } from '@backstage/core-components'; const useStyles = makeStyles({ subheader: { @@ -39,7 +39,7 @@ export const About = () => { internal projects suitable for{' '} Inner Sourcing diff --git a/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx b/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx index 4333ff11fd..749fcf3cff 100644 --- a/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx +++ b/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx @@ -57,7 +57,10 @@ export const AddProjectDialog = ({ setSelectedEntity(entity); }; - const handleSubmit: any = async ( + const handleSubmit: ( + getValues: UseFormGetValues, + reset: UseFormReset, + ) => Promise = async ( getValues: UseFormGetValues, reset: UseFormReset, ) => { diff --git a/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx b/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx index 5145dc54b8..6c6ce1bb3e 100644 --- a/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx +++ b/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx @@ -21,10 +21,9 @@ import { Card, CardContent, Typography, - Link, GridSize, } from '@material-ui/core'; -import { Avatar } from '@backstage/core-components'; +import { Avatar, Link } from '@backstage/core-components'; import { AboutField } from '@backstage/plugin-catalog'; import { StatusTag } from '../StatusTag'; import { Member, BazaarProject } from '../../types'; @@ -32,6 +31,7 @@ import { Member, BazaarProject } from '../../types'; const useStyles = makeStyles({ break: { wordBreak: 'break-word', + textAlign: 'justify', }, }); @@ -111,7 +111,7 @@ export const CardContentFields = ({ /> {member?.userId} diff --git a/plugins/bazaar/src/components/EditProjectDialog/EditProjectDialog.tsx b/plugins/bazaar/src/components/EditProjectDialog/EditProjectDialog.tsx index 0b2a8ef391..5073bd0cd9 100644 --- a/plugins/bazaar/src/components/EditProjectDialog/EditProjectDialog.tsx +++ b/plugins/bazaar/src/components/EditProjectDialog/EditProjectDialog.tsx @@ -36,6 +36,11 @@ const useStyles = makeStyles({ marginLeft: '0', marginRight: 'auto', }, + wordBreak: { + wordBreak: 'break-all', + whiteSpace: 'normal', + margin: '-0.25rem 0', + }, }); export const EditProjectDialog = ({ @@ -76,9 +81,9 @@ export const EditProjectDialog = ({ }); }, [bazaarProject]); - const handleEditSubmit: any = async ( + const handleEditSubmit: ( getValues: UseFormGetValues, - ) => { + ) => Promise = async (getValues: UseFormGetValues) => { const formValues = getValues(); const updateResponse = await bazaarApi.updateProject({ @@ -101,7 +106,9 @@ export const EditProjectDialog = ({ handleClose={handleDeleteClose} message={[ 'Are you sure you want to delete ', - {bazaarProject.name}, + + {bazaarProject.name} + , ' from the Bazaar?', ]} type="delete" diff --git a/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx b/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx index 470dc824b6..0c9da1ed24 100644 --- a/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx +++ b/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx @@ -15,7 +15,7 @@ */ import React, { useState, useEffect } from 'react'; -import { CardHeader, Divider, IconButton } from '@material-ui/core'; +import { CardHeader, Divider, IconButton, makeStyles } from '@material-ui/core'; import { HeaderIconLinkRow, IconLinkVerticalProps, @@ -37,6 +37,14 @@ import { ConfirmationDialog } from '../ConfirmationDialog'; import { CardContentFields } from '../CardContentFields'; import { fetchProjectMembers } from '../../util/fetchMethods'; +const useStyles = makeStyles({ + wordBreak: { + wordBreak: 'break-all', + whiteSpace: 'normal', + margin: '-0.25rem 0', + }, +}); + type Props = { bazaarProject: BazaarProject | null | undefined; fetchBazaarProject: () => Promise; @@ -46,6 +54,7 @@ export const EntityBazaarInfoContent = ({ bazaarProject, fetchBazaarProject, }: Props) => { + const classes = useStyles(); const bazaarApi = useApi(bazaarApiRef); const identity = useApi(identityApiRef); const [openEdit, setOpenEdit] = useState(false); @@ -160,9 +169,11 @@ export const EntityBazaarInfoContent = ({ handleClose={handleUnlinkClose} message={[ 'Are you sure you want to unlink ', - {parseEntityRef(bazaarProject.entityRef!).name}, + + {parseEntityRef(bazaarProject.entityRef!).name} + , ' from ', - {bazaarProject.name}, + {bazaarProject.name}, ' ?', ]} type="unlink" @@ -171,7 +182,7 @@ export const EntityBazaarInfoContent = ({ )} {bazaarProject?.name!}

} action={
void; @@ -70,6 +84,7 @@ export const HomePageBazaarInfoCard = ({ handleClose, initEntity, }: Props) => { + const classes = useStyles(); const catalogLink = useRouteRef(catalogRouteRef); const bazaarApi = useApi(bazaarApiRef); const identity = useApi(identityApiRef); @@ -222,9 +237,11 @@ export const HomePageBazaarInfoCard = ({ handleClose={() => setOpenUnlink(false)} message={[ 'Are you sure you want to unlink ', - {parseEntityRef(bazaarProject.value?.entityRef!).name}, + + {parseEntityRef(bazaarProject.value?.entityRef!).name} + , ' from ', - {bazaarProject.value?.name}, + {bazaarProject.value?.name}, ' ?', ]} type="unlink" @@ -242,7 +259,11 @@ export const HomePageBazaarInfoCard = ({ /> + {bazaarProject.value?.name || initProject.name} +

+ } action={
setOpenEdit(true)}> diff --git a/plugins/bazaar/src/components/InputField/InputField.tsx b/plugins/bazaar/src/components/InputField/InputField.tsx index 76eb28da6c..cae22a1313 100644 --- a/plugins/bazaar/src/components/InputField/InputField.tsx +++ b/plugins/bazaar/src/components/InputField/InputField.tsx @@ -15,13 +15,18 @@ */ import React from 'react'; -import { Controller, Control, FieldError } from 'react-hook-form'; +import { + Controller, + Control, + FieldError, + ValidationRule, +} from 'react-hook-form'; import { TextField } from '@material-ui/core'; import { FormValues } from '../../types'; type Rules = { required: boolean; - pattern?: any; + pattern?: ValidationRule | undefined; }; type Props = { diff --git a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx index a558d347a5..3aeba305a9 100644 --- a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx +++ b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx @@ -41,12 +41,19 @@ const useStyles = makeStyles({ WebkitLineClamp: 7, WebkitBoxOrient: 'vertical', overflow: 'hidden', - backgroundColor: '', + textAlign: 'justify', }, memberCount: { float: 'right', }, - content: { height: '13rem', marginBottom: '-0.5rem' }, + content: { + height: '13rem', + }, + header: { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, }); type Props = { @@ -82,7 +89,12 @@ export const ProjectCard = ({ setOpenCard(true)}> + {name} + + } subtitle={`updated ${DateTime.fromISO( new Date(updatedAt!).toISOString(), ).toRelative({ diff --git a/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx b/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx index 08c43d1c2e..2f8b9e9cb9 100644 --- a/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx +++ b/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx @@ -16,12 +16,7 @@ import React from 'react'; import { Button, Dialog } from '@material-ui/core'; -import { - useForm, - SubmitHandler, - UseFormReset, - UseFormGetValues, -} from 'react-hook-form'; +import { useForm, UseFormReset, UseFormGetValues } from 'react-hook-form'; import { InputField } from '../InputField/InputField'; import { InputSelector } from '../InputSelector/InputSelector'; import { FormValues } from '../../types'; @@ -36,7 +31,7 @@ type Props = { handleSave: ( getValues: UseFormGetValues, reset: UseFormReset, - ) => SubmitHandler; + ) => Promise; isAddForm: boolean; title: string; defaultValues: FormValues; diff --git a/plugins/bazaar/src/components/SortMethodSelector/SortMethodSelector.tsx b/plugins/bazaar/src/components/SortMethodSelector/SortMethodSelector.tsx index 93d5649a14..d29211dc44 100644 --- a/plugins/bazaar/src/components/SortMethodSelector/SortMethodSelector.tsx +++ b/plugins/bazaar/src/components/SortMethodSelector/SortMethodSelector.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { ChangeEvent, ReactNode } from 'react'; import { FormControl, MenuItem, Select, makeStyles } from '@material-ui/core'; const useStyles = makeStyles({ @@ -27,7 +27,12 @@ const useStyles = makeStyles({ type Props = { sortMethodNbr: number; - handleSortMethodChange: any; + handleSortMethodChange: + | (( + event: ChangeEvent<{ name?: string | undefined; value: unknown }>, + child: ReactNode, + ) => void) + | undefined; }; export const SortMethodSelector = ({ diff --git a/plugins/bazaar/src/components/SortView/SortView.tsx b/plugins/bazaar/src/components/SortView/SortView.tsx index a7f99625e5..fa97ac69ee 100644 --- a/plugins/bazaar/src/components/SortView/SortView.tsx +++ b/plugins/bazaar/src/components/SortView/SortView.tsx @@ -33,7 +33,7 @@ import { fetchCatalogItems } from '../../util/fetchMethods'; import { parseBazaarProject } from '../../util/parseMethods'; const useStyles = makeStyles({ - button: { width: '12rem' }, + button: { minWidth: '11rem' }, container: { marginTop: '2rem', }, @@ -141,7 +141,9 @@ export const SortView = () => { } }, [bazaarProjects, catalogEntities]); - const handleSortMethodChange = (event: ChangeEvent) => { + const handleSortMethodChange = ( + event: ChangeEvent<{ name?: string | undefined; value: unknown }>, + ) => { setSortMethodNbr( typeof event.target.value === 'number' ? event.target.value : 0, ); diff --git a/plugins/bazaar/src/util/fetchMethods.ts b/plugins/bazaar/src/util/fetchMethods.ts index 015abae210..f4bdb0ae38 100644 --- a/plugins/bazaar/src/util/fetchMethods.ts +++ b/plugins/bazaar/src/util/fetchMethods.ts @@ -19,6 +19,7 @@ import { parseMember } from './parseMethods'; import { BazaarProject, Member } from '../types'; import { BazaarApi } from '../api'; import { Entity } from '@backstage/catalog-model'; +import { CatalogApi } from '@backstage/plugin-catalog-react'; export const fetchProjectMembers = async ( bazaarApi: BazaarApi, @@ -37,7 +38,9 @@ export const fetchProjectMembers = async ( return []; }; -export const fetchCatalogItems = async (catalogApi: any): Promise => { +export const fetchCatalogItems = async ( + catalogApi: CatalogApi, +): Promise => { const entities = await catalogApi.getEntities({ filter: { kind: ['Component', 'Resource'],