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:
committed by
Frida Jacobsson
parent
32aa6532bf
commit
f7c2855d76
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-bazaar-backend': patch
|
||||
---
|
||||
|
||||
Router now also has endpoint `getLatestProjects` that takes a limit of projects as prop.
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
The "BazaarOverviewCard" can be displayed in Backstage homepage.
|
||||
|
||||

|
||||
|
||||
### Workflow
|
||||
|
||||
To add a project to the bazaar, simply click on the `add-project` button and fill in the form.
|
||||
|
||||
@@ -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 |
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user