Create "BazaarOverviewCard" that can be added to Backstage homepage.

Change-Id: I6e2565360cb3f7af27f60a0336cd87a75b3018c3
Signed-off-by: Frida Jacobsson <fridaja@axis.com>
Signed-off-by: Frida Jacobsson <fridahelenajacobsson@gmail.com>
This commit is contained in:
Frida Jacobsson
2022-09-23 09:22:18 +02:00
committed by Frida Jacobsson
parent 32aa6532bf
commit f7c2855d76
17 changed files with 292 additions and 34 deletions
+5
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-bazaar-backend': patch
---
Router now also has endpoint `getLatestProjects` that takes a limit of projects as prop.
+1
View File
@@ -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",
+1
View File
@@ -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",
@@ -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');
}
}
+8 -2
View File
@@ -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 });
});
+28
View File
@@ -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 = (
<Page themeId="home">
<Content>
<Grid container spacing={3}>
+ <Grid item xs={12} md={6}>
+ <BazaarOverviewCard variant={'latest'} limit={4} />
+ </Grid>
+ <Grid item xs={12} md={6}>
+ <BazaarOverviewCard variant={'random'} limit={4} />
+ </Grid>
{/* ...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.
+11
View File
@@ -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;
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

+1
View File
@@ -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",
+14 -5
View File
@@ -20,6 +20,7 @@ import {
FetchApi,
IdentityApi,
} from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
export const bazaarApiRef = createApiRef<BazaarApi>({
id: 'bazaar',
@@ -40,7 +41,7 @@ export interface BazaarApi {
addMember(id: number, userId: string): Promise<void>;
getProjects(): Promise<any>;
getProjects(limit?: number, order?: string): Promise<any>;
deleteProject(id: number): Promise<void>;
}
@@ -148,12 +149,20 @@ export class BazaarClient implements BazaarApi {
);
}
async getProjects(): Promise<any> {
async getProjects(limit?: number, order?: string): Promise<any> {
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<void> {
@@ -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<Entity[]>();
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 <ErrorPanel error={catalogEntities.error} />;
}
if (bazaarProjects.error) {
return <ErrorPanel error={bazaarProjects.error} />;
}
return (
<InfoCard
title={
order === 'latest' ? 'Bazaar Latest Projects' : 'Bazaar Random Projects'
}
deepLink={bazaarLink}
>
<ProjectPreview
bazaarProjects={bazaarProjects.value || []}
fetchBazaarProjects={fetchBazaarProjects}
catalogEntities={unlinkedCatalogEntities || []}
useTablePagination={false}
fullHeight={false}
fixedWidth
/>
</InfoCard>
);
};
@@ -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';
@@ -30,6 +30,13 @@ import { DateTime } from 'luxon';
import { HomePageBazaarInfoCard } from '../HomePageBazaarInfoCard';
import { Entity } from '@backstage/catalog-model';
type Props = {
project: BazaarProject;
fetchBazaarProjects: () => Promise<BazaarProject[]>;
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<BazaarProject[]>;
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(),
})}`}
/>
<CardContent className={classes.content}>
<CardContent
className={classes.content}
style={{ height: fullHeight ? '13rem' : '7rem' }}
>
<StatusTag styles={classes.statusTag} status={status} />
<Typography variant="body2" className={classes.memberCount}>
{Number(membersCount) === Number(1)
@@ -25,6 +25,9 @@ type Props = {
bazaarProjects: BazaarProject[];
fetchBazaarProjects: () => Promise<BazaarProject[]>;
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 (
<Grid key={i} item xs={2}>
<Grid
key={i}
item
xs={fixedWidth ? false : 2}
style={{ width: '16rem' }}
>
<ProjectCard
project={bazaarProject}
key={i}
fetchBazaarProjects={fetchBazaarProjects}
catalogEntities={catalogEntities}
fullHeight={fullHeight}
/>
</Grid>
);
})}
</Grid>
<TablePagination
className={classes.pagination}
rowsPerPageOptions={[12, 24, 48, 96]}
count={bazaarProjects?.length}
page={page - 1}
onPageChange={handlePageChange}
rowsPerPage={rows}
onRowsPerPageChange={handleRowChange}
backIconButtonProps={{ disabled: page === 1 }}
nextIconButtonProps={{
disabled: rows * page >= bazaarProjects.length,
}}
/>
{useTablePagination && (
<TablePagination
className={classes.pagination}
rowsPerPageOptions={[12, 24, 48, 96]}
count={bazaarProjects?.length}
page={page - 1}
onPageChange={handlePageChange}
rowsPerPage={rows}
onRowsPerPageChange={handleRowChange}
backIconButtonProps={{ disabled: page === 1 }}
nextIconButtonProps={{
disabled: rows * page >= bazaarProjects.length,
}}
/>
)}
</Content>
);
};
+2
View File
@@ -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';
+3
View File
@@ -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