diff --git a/.changeset/early-rocks-smell.md b/.changeset/early-rocks-smell.md new file mode 100644 index 0000000000..e35554f03a --- /dev/null +++ b/.changeset/early-rocks-smell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-bazaar': patch +--- + +Added a `Overview Card` for either latest or random projects. Changed `ProjectPreview.tsx` so it take `gridSize` and `useTablePagination` as props. diff --git a/.changeset/rude-tomatoes-behave.md b/.changeset/rude-tomatoes-behave.md new file mode 100644 index 0000000000..880b4dffd5 --- /dev/null +++ b/.changeset/rude-tomatoes-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-bazaar-backend': patch +--- + +Router now also has endpoint `getLatestProjects` that takes a limit of projects as prop. diff --git a/package.json b/package.json index 40e2056e8c..1013b2a049 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ }, "version": "1.7.0-next.1", "dependencies": { + "@backstage/errors": "workspace:^", "@manypkg/get-packages": "^1.1.3", "@microsoft/api-documenter": "^7.17.11", "@microsoft/api-extractor": "^7.23.0", diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index 131959cbd6..31ae4c6cb5 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -25,6 +25,7 @@ "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@types/express": "^4.17.6", "express": "^4.17.1", diff --git a/plugins/bazaar-backend/src/service/DatabaseHandler.ts b/plugins/bazaar-backend/src/service/DatabaseHandler.ts index 352866b4b5..8fbe985a10 100644 --- a/plugins/bazaar-backend/src/service/DatabaseHandler.ts +++ b/plugins/bazaar-backend/src/service/DatabaseHandler.ts @@ -175,14 +175,22 @@ export class DatabaseHandler { return await this.client('metadata').where({ id: id }).del(); } - async getProjects() { + async getProjects(limit?: number, order?: string) { const coalesce = this.client.raw( 'coalesce(count(members.item_id), 0) as members_count', ); - - return await this.client('metadata') + let get = this.client('metadata') .select([...this.columns, coalesce]) - .groupBy(this.columns) - .leftJoin('members', 'metadata.id', '=', 'members.item_id'); + .groupBy(this.columns); + if (limit) { + get = get.limit(limit); + } + if (order === 'latest') { + get = get.orderByRaw('id desc'); + } + if (order === 'random') { + get = get.orderByRaw('RANDOM()'); + } + return await get.leftJoin('members', 'metadata.id', '=', 'members.item_id'); } } diff --git a/plugins/bazaar-backend/src/service/router.ts b/plugins/bazaar-backend/src/service/router.ts index 359945f298..f902d0e8cc 100644 --- a/plugins/bazaar-backend/src/service/router.ts +++ b/plugins/bazaar-backend/src/service/router.ts @@ -92,8 +92,14 @@ export async function createRouter( response.json({ status: 'ok', data: data }); }); - router.get('/projects', async (_, response) => { - const data = await dbHandler.getProjects(); + router.get('/projects', async (request, response) => { + const limit = request.query.limit?.toString(); + const order = request.query.order?.toString(); + + const data = await dbHandler.getProjects( + limit ? parseInt(limit, 10) : undefined, + order, + ); response.json({ status: 'ok', data: data }); }); diff --git a/plugins/bazaar/README.md b/plugins/bazaar/README.md index eeebf6eb19..6168b0c4ba 100644 --- a/plugins/bazaar/README.md +++ b/plugins/bazaar/README.md @@ -64,6 +64,30 @@ const overviewContent = ( {/* ...other entity-cards */} ``` +Add a **Bazaar overview card** to the homepage that displays either the latest projects or random projects. In `packages/app/src/components/home/HomePage.tsx` add: + +```diff ++ import { BazaarOverviewCard } from '@backstage/plugin-bazaar'; + +export const homePage = ( + + + + + ++ ++ ++ + ++ ++ ++ + + {/* ...other homepage items */} +``` + +Specify how many projects you want through the "limit" props. In the example above 4 cards is specified. + ## How does the Bazaar work? ### Layout @@ -72,6 +96,10 @@ The latest modified Bazaar projects are displayed in the Bazaar landing page, lo ![home](media/layout.png) +The "BazaarOverviewCard" can be displayed in Backstage homepage. + +![home](media/overviewCard.png) + ### Workflow To add a project to the bazaar, simply click on the `add-project` button and fill in the form. diff --git a/plugins/bazaar/api-report.md b/plugins/bazaar/api-report.md index c6afde73f2..2e6ebe6746 100644 --- a/plugins/bazaar/api-report.md +++ b/plugins/bazaar/api-report.md @@ -8,6 +8,17 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { RouteRef } from '@backstage/core-plugin-api'; +// @public (undocumented) +export const BazaarOverviewCard: ( + props: BazaarOverviewCardProps, +) => JSX.Element; + +// @public (undocumented) +export type BazaarOverviewCardProps = { + order: 'latest' | 'random'; + limit: number; +}; + // @public (undocumented) export const BazaarPage: () => JSX.Element; diff --git a/plugins/bazaar/media/overviewCard.png b/plugins/bazaar/media/overviewCard.png new file mode 100644 index 0000000000..676764f6e4 Binary files /dev/null and b/plugins/bazaar/media/overviewCard.png differ diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index c88357ef4a..e019784cc5 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -27,6 +27,7 @@ "@backstage/cli": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@date-io/luxon": "1.x", diff --git a/plugins/bazaar/src/api.ts b/plugins/bazaar/src/api.ts index d2486f74dc..33455e69fc 100644 --- a/plugins/bazaar/src/api.ts +++ b/plugins/bazaar/src/api.ts @@ -20,6 +20,7 @@ import { FetchApi, IdentityApi, } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; export const bazaarApiRef = createApiRef({ id: 'bazaar', @@ -40,7 +41,7 @@ export interface BazaarApi { addMember(id: number, userId: string): Promise; - getProjects(): Promise; + getProjects(limit?: number, order?: string): Promise; deleteProject(id: number): Promise; } @@ -148,12 +149,20 @@ export class BazaarClient implements BazaarApi { ); } - async getProjects(): Promise { + async getProjects(limit?: number, order?: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); + const params = { + ...(limit ? { limit: limit.toString() } : {}), + ...(order ? { order } : {}), + }; + const query = new URLSearchParams(params); + const url = `projects?${query.toString()}`; - return await this.fetchApi - .fetch(`${baseUrl}/projects`) - .then(resp => resp.json()); + const data = await this.fetchApi.fetch(`${baseUrl}/${url}`); + if (!data.ok) { + throw await ResponseError.fromResponse(data); + } + return data.json(); } async deleteProject(id: number): Promise { diff --git a/plugins/bazaar/src/components/BazaarOverviewCard/BazaarOverviewCard.tsx b/plugins/bazaar/src/components/BazaarOverviewCard/BazaarOverviewCard.tsx new file mode 100644 index 0000000000..83358c0a05 --- /dev/null +++ b/plugins/bazaar/src/components/BazaarOverviewCard/BazaarOverviewCard.tsx @@ -0,0 +1,141 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect, useState } from 'react'; +import { ProjectPreview } from '../ProjectPreview/ProjectPreview'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import type { BazaarProject } from '../../types'; +import { bazaarApiRef } from '../../api'; +import { fetchCatalogItems } from '../../util/fetchMethods'; +import { parseBazaarProject } from '../../util/parseMethods'; +import { ErrorPanel, InfoCard } from '@backstage/core-components'; +import { bazaarPlugin } from '../../plugin'; + +/** @public */ +export type BazaarOverviewCardProps = { + order: 'latest' | 'random'; + limit: number; +}; + +const getUnlinkedCatalogEntities = ( + bazaarProjects: BazaarProject[], + catalogEntities: Entity[], +) => { + const bazaarProjectRefs = bazaarProjects.map( + (project: BazaarProject) => project.entityRef, + ); + + return catalogEntities.filter((entity: Entity) => { + return !bazaarProjectRefs?.includes(stringifyEntityRef(entity)); + }); +}; + +/** @public */ +export const BazaarOverviewCard = (props: BazaarOverviewCardProps) => { + const { order, limit } = props; + const bazaarApi = useApi(bazaarApiRef); + const catalogApi = useApi(catalogApiRef); + const root = useRouteRef(bazaarPlugin.routes.root); + + const bazaarLink = { + title: 'Go to Bazaar', + link: root.toString(), + }; + + const [unlinkedCatalogEntities, setUnlinkedCatalogEntities] = + useState(); + + const [catalogEntities, fetchCatalogEntities] = useAsyncFn(async () => { + return await fetchCatalogItems(catalogApi); + }); + + const [bazaarProjects, fetchBazaarProjects] = useAsyncFn(async () => { + const response = await bazaarApi.getProjects(limit, order); + return response.data.map(parseBazaarProject) as BazaarProject[]; + }); + + const catalogEntityRefs = catalogEntities.value?.map((project: Entity) => + stringifyEntityRef(project), + ); + + useEffect(() => { + const filterBrokenLinks = () => { + if (catalogEntityRefs) { + bazaarProjects.value?.forEach(async (project: BazaarProject) => { + if (project.entityRef) { + if (!catalogEntityRefs?.includes(project.entityRef)) { + await bazaarApi.updateProject({ + ...project, + entityRef: null, + }); + } + } + }); + } + }; + filterBrokenLinks(); + }, [ + bazaarApi, + bazaarProjects.value, + catalogEntityRefs, + catalogEntities.value, + ]); + + useEffect(() => { + fetchCatalogEntities(); + fetchBazaarProjects(); + }, [fetchBazaarProjects, fetchCatalogEntities]); + + useEffect(() => { + const unlinkedCEntities = getUnlinkedCatalogEntities( + bazaarProjects.value || [], + catalogEntities.value || [], + ); + + if (unlinkedCEntities) { + setUnlinkedCatalogEntities(unlinkedCEntities); + } + }, [bazaarProjects, catalogEntities]); + + if (catalogEntities.error) { + return ; + } + + if (bazaarProjects.error) { + return ; + } + + return ( + + + + ); +}; diff --git a/plugins/bazaar/src/components/BazaarOverviewCard/index.ts b/plugins/bazaar/src/components/BazaarOverviewCard/index.ts new file mode 100644 index 0000000000..06ea623e1b --- /dev/null +++ b/plugins/bazaar/src/components/BazaarOverviewCard/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { BazaarOverviewCard } from './BazaarOverviewCard'; +export type { BazaarOverviewCardProps } from './BazaarOverviewCard'; diff --git a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx index 3aeba305a9..dace9b8185 100644 --- a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx +++ b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx @@ -30,6 +30,13 @@ import { DateTime } from 'luxon'; import { HomePageBazaarInfoCard } from '../HomePageBazaarInfoCard'; import { Entity } from '@backstage/catalog-model'; +type Props = { + project: BazaarProject; + fetchBazaarProjects: () => Promise; + catalogEntities: Entity[]; + fullHeight?: boolean; +}; + const useStyles = makeStyles({ statusTag: { display: 'inline-block', @@ -47,7 +54,7 @@ const useStyles = makeStyles({ float: 'right', }, content: { - height: '13rem', + overflow: 'scroll', }, header: { whiteSpace: 'nowrap', @@ -56,16 +63,11 @@ const useStyles = makeStyles({ }, }); -type Props = { - project: BazaarProject; - fetchBazaarProjects: () => Promise; - catalogEntities: Entity[]; -}; - export const ProjectCard = ({ project, fetchBazaarProjects, catalogEntities, + fullHeight, }: Props) => { const classes = useStyles(); const [openCard, setOpenCard] = useState(false); @@ -101,7 +103,10 @@ export const ProjectCard = ({ base: DateTime.now(), })}`} /> - + {Number(membersCount) === Number(1) diff --git a/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx b/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx index 435d80408d..77c16ef306 100644 --- a/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx +++ b/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx @@ -25,6 +25,9 @@ type Props = { bazaarProjects: BazaarProject[]; fetchBazaarProjects: () => Promise; catalogEntities: Entity[]; + useTablePagination?: boolean; + fullHeight?: boolean; + fixedWidth?: boolean; }; const useStyles = makeStyles({ @@ -51,6 +54,9 @@ export const ProjectPreview = ({ bazaarProjects, fetchBazaarProjects, catalogEntities, + useTablePagination = true, + fullHeight = true, + fixedWidth = false, }: Props) => { const classes = useStyles(); const [page, setPage] = useState(1); @@ -80,31 +86,39 @@ export const ProjectPreview = ({ .slice((page - 1) * rows, rows * page) .map((bazaarProject: BazaarProject, i: number) => { return ( - + ); })} - = bazaarProjects.length, - }} - /> + {useTablePagination && ( + = bazaarProjects.length, + }} + /> + )} ); }; diff --git a/plugins/bazaar/src/index.ts b/plugins/bazaar/src/index.ts index 895469c031..0014c66b6d 100644 --- a/plugins/bazaar/src/index.ts +++ b/plugins/bazaar/src/index.ts @@ -15,5 +15,7 @@ */ export { bazaarPlugin, BazaarPage } from './plugin'; +export { BazaarOverviewCard } from './components/BazaarOverviewCard'; +export type { BazaarOverviewCardProps } from './components/BazaarOverviewCard'; export { EntityBazaarInfoCard } from './components/EntityBazaarInfoCard'; export { SortView } from './components/SortView'; diff --git a/yarn.lock b/yarn.lock index ac27eb8215..c710e002d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4349,6 +4349,7 @@ __metadata: "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@types/express": ^4.17.6 express: ^4.17.1 @@ -4369,6 +4370,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@date-io/luxon": 1.x @@ -35073,6 +35075,7 @@ __metadata: "@backstage/cli": "workspace:*" "@backstage/codemods": "workspace:*" "@backstage/create-app": "workspace:*" + "@backstage/errors": "workspace:^" "@changesets/cli": ^2.14.0 "@manypkg/get-packages": ^1.1.3 "@microsoft/api-documenter": ^7.17.11