second draft of the Bazaar plugin
signed-off-by: lykkeaxlin <lykkeaxlin@hotmail.com> Co-authored-by: klaraab <klarabroman@live.se> Signed-off-by: Lykke Axlin <lykkeaxlin@hotmail.com>
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
# @backstage/plugin-bazaar
|
||||
|
||||
### What is the Bazaar?
|
||||
|
||||
The Bazaar is a place where teams can propose projects for cross-functional team development. Essentially a marketplace for internal projects suitable for [Inner Sourcing](https://en.wikipedia.org/wiki/Inner_source). With "Inner Sourcing", we mean projects that are developed internally within a company, but with Open Source best practices.
|
||||
|
||||
### Why?
|
||||
|
||||
Many companies today are of high need to increase the ease of cross-team cooperation. In large organizations, engineers often have limited ways of discovering or announcing the projects which could benefit from a wider development effort in terms of different expertise, experiences, and teams spread across the organization. With no good way to find these existing internal projects to join, the possibility of working with Inner Sourcing practices suffers.
|
||||
|
||||
### How?
|
||||
|
||||
The Bazaar allows engineers and teams to open up and announce their new and exciting projects for transparent cooperation in other parts of larger organizations. The Bazaar ensures that new Inner Sourcing friendly projects gain visibility through Backstage and a way for interested engineers to show their interest and in the future contribute with their specific skill set. The Bazaar also provides an easy way to manage, catalog, and browse these Inner Sourcing friendly projects and components.
|
||||
|
||||
## Getting Started
|
||||
|
||||
First install the plugin into your app:
|
||||
|
||||
```sh
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-bazaar
|
||||
```
|
||||
|
||||
Modify your app routes in `packages/app/src/App.tsx` to include the `Bazaar` component exported from the plugin, for example:
|
||||
|
||||
```diff
|
||||
+ import { BazaarPage } from '@backstage/plugin-bazaar';
|
||||
|
||||
const routes = (
|
||||
|
||||
<FlatRoutes>
|
||||
...
|
||||
+ <Route path="bazaar" element={<BazaarPage />} />
|
||||
{/* other routes... */}
|
||||
|
||||
```
|
||||
|
||||
Add a **Bazaar icon** to the Sidebar to easily access the Bazaar. In `packages/app/src/components/Root.tsx` add:
|
||||
|
||||
```diff
|
||||
+ import StorefrontIcon from '@material-ui/icons/Storefront';
|
||||
|
||||
<SidebarDivider />
|
||||
<SidebarScrollWrapper>
|
||||
+ <SidebarItem icon={StorefrontIcon} to="bazaar" text="Bazaar" />
|
||||
{/* ...other sidebar-items */}
|
||||
```
|
||||
|
||||
Add a **Bazaar card** to the overview tab on the `packages/app/src/components/catalog/EntityPage.tsx` add:
|
||||
|
||||
```diff
|
||||
const overviewContent = (
|
||||
|
||||
<Grid item md={8} xs={12}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
</Grid>
|
||||
|
||||
+ <Grid item sm={4}>
|
||||
+ <EntityBazaarInfoCard />
|
||||
+ </Grid>
|
||||
|
||||
{/* ...other entity-cards */}
|
||||
```
|
||||
|
||||
## How does the Bazaar work?
|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
### 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. You will be asked to add an announcement for new team members. The purpose of the announcement is for you to present your ideas and what skills you are looking for. Further you need to provide the status of the project.
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
### 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)
|
||||
|
||||
**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
|
||||
|
||||
- Bazaar landing page
|
||||
|
||||
- Add a tab 'My page', where your personal data is displayed. For example: your projects and its latest activities, projects or tags you are following etc.
|
||||
- Make it possible to sort the project based on the number of members
|
||||
|
||||
- Bazaar card
|
||||
|
||||
- Make it possible to follow tags/projects
|
||||
|
||||
- Bazaar tab on the EntityPage
|
||||
|
||||
- Fill Bazaar-tab with more content, for example images and achievements
|
||||
- Show all the members that have joined the project
|
||||
|
||||
- Dialogues
|
||||
|
||||
- Extend the dialogue for adding a project with more fields, e.g. chat link and the possibility to add images
|
||||
|
||||
- Testing
|
||||
- Add tests to all the components
|
||||
@@ -0,0 +1,24 @@
|
||||
## 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)
|
||||
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { createDevApp } from '@backstage/dev-utils';
|
||||
import { bazaarPlugin, BazaarPage } from '../src/plugin';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(bazaarPlugin)
|
||||
.addPage({
|
||||
element: <BazaarPage />,
|
||||
title: 'Root Page',
|
||||
})
|
||||
.render();
|
||||
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 3.8 MiB |
|
After Width: | Height: | Size: 389 KiB |
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 388 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 88 KiB |
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@backstage/plugin-bazaar",
|
||||
"version": "0.1.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"start": "backstage-cli plugin:serve",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"diff": "backstage-cli plugin:diff",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.9.0",
|
||||
"@backstage/cli": "^0.7.2",
|
||||
"@backstage/core": "^0.7.14",
|
||||
"@backstage/core-api": "^0.2.23",
|
||||
"@backstage/core-components": "^0.4.1",
|
||||
"@backstage/core-plugin-api": "^0.1.3",
|
||||
"@backstage/plugin-catalog": "^0.6.6",
|
||||
"@backstage/plugin-catalog-react": "^0.4.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"moment": "^2.29.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-hook-form": "^7.13.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.7.2",
|
||||
"@backstage/dev-utils": "^0.2.6",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"cross-fetch": "^3.0.6"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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, Typography, Link, makeStyles } from '@material-ui/core';
|
||||
import { InfoCard } from '@backstage/core';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
subheader: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
export const About = () => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Grid container spacing={4}>
|
||||
<Grid item xs={5}>
|
||||
<InfoCard title="About Bazaar">
|
||||
<Typography className={classes.subheader} variant="body1">
|
||||
What is the Bazaar?
|
||||
</Typography>
|
||||
<Typography paragraph>
|
||||
The Bazaar is a place where teams can propose projects for
|
||||
cross-functional team development. Essentially a marketplace for
|
||||
internal projects suitable for{' '}
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://en.wikipedia.org/wiki/Inner_source"
|
||||
>
|
||||
Inner Sourcing
|
||||
</Link>
|
||||
. With "Inner Sourcing", we mean projects that are developed
|
||||
internally within a company, but with Open Source best practices.
|
||||
</Typography>
|
||||
<Typography className={classes.subheader} variant="body1">
|
||||
Why?
|
||||
</Typography>
|
||||
<Typography paragraph>
|
||||
Many companies today are of high need to increase the ease of
|
||||
cross-team cooperation. In large organizations, engineers often have
|
||||
limited ways of discovering or announcing the projects which could
|
||||
benefit from a wider development effort in terms of different
|
||||
expertise, experiences, and teams spread across the organization.
|
||||
With no good way to find these existing internal projects to join,
|
||||
the possibility of working with Inner Sourcing practices suffers.
|
||||
</Typography>
|
||||
<Typography className={classes.subheader} variant="body1">
|
||||
How?
|
||||
</Typography>
|
||||
<Typography paragraph>
|
||||
The Bazaar allows engineers and teams to open up and announce their
|
||||
new and exciting projects for transparent cooperation in other parts
|
||||
of larger organizations. The Bazaar ensures that new Inner Sourcing
|
||||
friendly projects gain visibility through Backstage and a way for
|
||||
interested engineers to show their interest and in the future
|
||||
contribute with their specific skill set. The Bazaar also provides
|
||||
an easy way to manage, catalog, and browse these Inner Sourcing
|
||||
friendly projects and components.
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -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 { About } from './About';
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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, Dispatch, SetStateAction } from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { SubmitHandler } from 'react-hook-form';
|
||||
import { updateMetadata } from '../../util/dbRequests';
|
||||
import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
import { ProjectDialog } from '../ProjectDialog';
|
||||
import { ProjectSelector } from '../ProjectSelector';
|
||||
import { BazaarProject, FormValues, Status } from '../../util/types';
|
||||
|
||||
type Props = {
|
||||
catalogEntities: Entity[];
|
||||
open: boolean;
|
||||
handleClose: () => void;
|
||||
setBazaarProjects: Dispatch<SetStateAction<BazaarProject[]>>;
|
||||
setCatalogEntities: Dispatch<SetStateAction<Entity[]>>;
|
||||
};
|
||||
|
||||
export const AddProjectDialog = ({
|
||||
catalogEntities,
|
||||
open,
|
||||
handleClose,
|
||||
setBazaarProjects,
|
||||
setCatalogEntities,
|
||||
}: Props) => {
|
||||
const baseUrl = useApi(configApiRef)
|
||||
.getConfig('backend')
|
||||
.getString('baseUrl');
|
||||
const [selectedEntity, setSelectedEntity] = useState(
|
||||
catalogEntities ? catalogEntities[0] : null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedEntity(catalogEntities ? catalogEntities[0] : null);
|
||||
}, [catalogEntities]);
|
||||
|
||||
const defaultValues = {
|
||||
title: 'Add project',
|
||||
announcement: '',
|
||||
status: 'proposed' as Status,
|
||||
};
|
||||
|
||||
const handleListItemClick = (entity: Entity) => {
|
||||
setSelectedEntity(entity);
|
||||
};
|
||||
|
||||
const handleSave: SubmitHandler<FormValues> = async (
|
||||
getValues: any,
|
||||
reset: any,
|
||||
) => {
|
||||
const formValues = getValues();
|
||||
|
||||
const bazaarProject: BazaarProject = {
|
||||
entityRef: `${selectedEntity!.metadata.namespace}/${
|
||||
selectedEntity!.kind
|
||||
}/${selectedEntity!.metadata.name}`,
|
||||
name: selectedEntity!.metadata.name,
|
||||
announcement: formValues.announcement,
|
||||
status: formValues.status,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setBazaarProjects((oldProjects: BazaarProject[]) => {
|
||||
return [...oldProjects, bazaarProject];
|
||||
});
|
||||
|
||||
setCatalogEntities((oldEntities: Entity[]) => {
|
||||
return oldEntities.filter(entity => entity !== selectedEntity);
|
||||
});
|
||||
|
||||
await updateMetadata(
|
||||
selectedEntity!,
|
||||
selectedEntity!.metadata.name,
|
||||
formValues.announcement,
|
||||
formValues.status,
|
||||
baseUrl,
|
||||
);
|
||||
|
||||
handleClose();
|
||||
reset(defaultValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<ProjectDialog
|
||||
handleSave={handleSave}
|
||||
title="Add project"
|
||||
isAddForm
|
||||
defaultValues={defaultValues}
|
||||
open={open}
|
||||
projectSelector={
|
||||
<ProjectSelector
|
||||
value={selectedEntity?.metadata.name || ''}
|
||||
onChange={handleListItemClick}
|
||||
isFormInvalid={false}
|
||||
entities={catalogEntities || []}
|
||||
/>
|
||||
}
|
||||
handleClose={handleClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 { AddProjectDialog } from './AddProjectDialog';
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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,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 { AlertBanner } from './AlertBanner';
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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 { Entity } from '@backstage/catalog-model';
|
||||
import { deleteEntity } from '../../util/dbRequests';
|
||||
import {
|
||||
useApi,
|
||||
configApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
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 = {
|
||||
entity: Entity;
|
||||
openDelete: boolean;
|
||||
handleClose: () => void;
|
||||
setIsBazaar: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const DeleteProjectDialog = ({
|
||||
entity,
|
||||
openDelete,
|
||||
handleClose,
|
||||
setIsBazaar,
|
||||
}: Props) => {
|
||||
const baseUrl = useApi(configApiRef)
|
||||
.getConfig('backend')
|
||||
.getString('baseUrl');
|
||||
|
||||
const handleCloseAndClear = () => {
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const identity = useApi(identityApiRef);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await deleteEntity(baseUrl, entity, identity);
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -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 { DeleteProjectDialog } from './DeleteProjectDialog';
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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, Dispatch, SetStateAction } from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { updateMetadata } from '../../util/dbRequests';
|
||||
import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
import { ProjectDialog } from '../ProjectDialog';
|
||||
import { BazaarProject, FormValues } from '../../util/types';
|
||||
|
||||
type Props = {
|
||||
entity: Entity;
|
||||
bazaarProject: BazaarProject;
|
||||
setBazaarProject: Dispatch<SetStateAction<BazaarProject>>;
|
||||
open: boolean;
|
||||
handleClose: () => void;
|
||||
isAddForm: boolean;
|
||||
};
|
||||
|
||||
export const EditProjectDialog = ({
|
||||
entity,
|
||||
bazaarProject,
|
||||
setBazaarProject,
|
||||
open,
|
||||
handleClose,
|
||||
}: Props) => {
|
||||
const baseUrl = useApi(configApiRef)
|
||||
.getConfig('backend')
|
||||
.getString('baseUrl');
|
||||
|
||||
const [defaultValues, setDefaultValues] = useState<FormValues>({
|
||||
announcement: bazaarProject.announcement,
|
||||
status: bazaarProject.status,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setDefaultValues({
|
||||
announcement: bazaarProject.announcement,
|
||||
status: bazaarProject.status,
|
||||
});
|
||||
}, [bazaarProject]);
|
||||
|
||||
const handleSave: any = async (getValues: any, _: any) => {
|
||||
const formValues = getValues();
|
||||
|
||||
const updateResponse = await updateMetadata(
|
||||
entity!,
|
||||
entity.metadata.name,
|
||||
formValues.announcement,
|
||||
formValues.status,
|
||||
baseUrl,
|
||||
);
|
||||
|
||||
if (updateResponse.status === 'ok')
|
||||
setBazaarProject((oldProject: BazaarProject) => {
|
||||
return {
|
||||
...oldProject,
|
||||
announcement: formValues.announcement,
|
||||
status: formValues.status,
|
||||
};
|
||||
});
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ProjectDialog
|
||||
title="Edit project"
|
||||
handleSave={handleSave}
|
||||
isAddForm={false}
|
||||
defaultValues={defaultValues}
|
||||
open={open}
|
||||
handleClose={handleClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 { EditProjectDialog } from './EditProjectDialog';
|
||||
@@ -0,0 +1,369 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 {
|
||||
Grid,
|
||||
makeStyles,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Typography,
|
||||
Divider,
|
||||
IconButton,
|
||||
Popover,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
ListItemText,
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
Progress,
|
||||
HeaderIconLinkRow,
|
||||
IconLinkVerticalProps,
|
||||
Avatar,
|
||||
} from '@backstage/core-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
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,
|
||||
configApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { getEntityRef } from '../../util/dbRequests';
|
||||
import { useAsync } from 'react-use';
|
||||
import { Member, BazaarProject } from '../../util/types';
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
export const EntityBazaarInfoCard = () => {
|
||||
const { entity } = useEntity();
|
||||
const classes = useStyles();
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const [openDelete, setOpenDelete] = useState(false);
|
||||
const identity = useApi(identityApiRef);
|
||||
const [isMember, setIsMember] = useState(false);
|
||||
const [members, setMembers] = useState<Member[]>([]);
|
||||
const [bazaarProject, setBazaarProject] = useState<BazaarProject>({
|
||||
entityRef: '',
|
||||
name: '',
|
||||
announcement: '',
|
||||
status: 'proposed',
|
||||
updatedAt: '',
|
||||
});
|
||||
const [isBazaar, setIsBazaar] = useState(false);
|
||||
|
||||
const baseUrl = useApi(configApiRef)
|
||||
.getConfig('backend')
|
||||
.getString('baseUrl');
|
||||
|
||||
const getInitMemberStatus = async () => {
|
||||
const response = await fetch(`${baseUrl}/api/bazaar/members`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
entity_ref: getEntityRef(entity),
|
||||
},
|
||||
}).then(resp => resp.json());
|
||||
|
||||
const dbMembers = response.data.map((obj: any) => {
|
||||
const member: Member = {
|
||||
userId: obj.user_id,
|
||||
entityRef: obj.entity_ref,
|
||||
joinDate: obj.join_date,
|
||||
};
|
||||
|
||||
return member;
|
||||
});
|
||||
|
||||
dbMembers.sort(sortMembers);
|
||||
|
||||
setMembers(dbMembers);
|
||||
setIsMember(
|
||||
dbMembers
|
||||
.map((member: Member) => member.userId)
|
||||
.indexOf(identity.getUserId()) >= 0,
|
||||
);
|
||||
};
|
||||
|
||||
const getMetadata = async () => {
|
||||
const response = await fetch(`${baseUrl}/api/bazaar/metadata`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
entity_ref: getEntityRef(entity),
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status !== 404) {
|
||||
setIsBazaar(true);
|
||||
const data = await response.json().then(resp => resp.data);
|
||||
|
||||
setBazaarProject({
|
||||
entityRef: data[0].entityRef,
|
||||
name: data[0].name,
|
||||
announcement: data[0].announcement,
|
||||
status: data[0].status,
|
||||
updatedAt: data[0].updatedAt,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { loading } = useAsync(async () => {
|
||||
await getInitMemberStatus();
|
||||
await getMetadata();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
setIsMember(!isMember);
|
||||
|
||||
const newMember: Member = {
|
||||
userId: identity.getUserId(),
|
||||
entityRef: getEntityRef(entity),
|
||||
joinDate: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const headers = {
|
||||
user_id: newMember.userId,
|
||||
entity_ref: newMember.entityRef,
|
||||
join_date: newMember.joinDate,
|
||||
};
|
||||
|
||||
if (!isMember) {
|
||||
setMembers((prevMembers: Member[]) => {
|
||||
const newMembers: Member[] = [...prevMembers, newMember];
|
||||
newMembers.sort(sortMembers);
|
||||
return newMembers;
|
||||
});
|
||||
await fetch(`${baseUrl}/api/bazaar/members/add`, {
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
});
|
||||
} else {
|
||||
setMembers(
|
||||
members.filter(
|
||||
(member: Member) => member.userId !== identity.getUserId(),
|
||||
),
|
||||
);
|
||||
await fetch(`${baseUrl}/api/bazaar/members/remove`, {
|
||||
method: 'DELETE',
|
||||
headers: headers,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const links: IconLinkVerticalProps[] = [
|
||||
{
|
||||
label: 'Community',
|
||||
icon: <ChatIcon />,
|
||||
},
|
||||
{
|
||||
label: isMember ? 'Leave' : 'Join',
|
||||
icon: isMember ? <ExitToAppIcon /> : <PersonAddIcon />,
|
||||
href: '',
|
||||
onClick: async () => {
|
||||
handleMembersClick();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (!isBazaar) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title="Bazaar" style={{ paddingBottom: '1rem' }} />
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Typography variant="body1">
|
||||
This project is not in the Bazaar. Go to the{' '}
|
||||
<Link className={classes.link} to="/bazaar">
|
||||
Bazaar
|
||||
</Link>{' '}
|
||||
to add the project or to{' '}
|
||||
<Link className={classes.link} to="/bazaar/about">
|
||||
read more
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card>
|
||||
<EditProjectDialog
|
||||
open={open}
|
||||
entity={entity}
|
||||
bazaarProject={bazaarProject}
|
||||
setBazaarProject={setBazaarProject}
|
||||
handleClose={closeEdit}
|
||||
isAddForm={false}
|
||||
/>
|
||||
|
||||
<DeleteProjectDialog
|
||||
entity={entity}
|
||||
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="Suggest changes" />
|
||||
</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={12}>
|
||||
<AboutField label="Announcement">
|
||||
{bazaarProject.announcement
|
||||
? bazaarProject.announcement.split('\n').map((str: string) => (
|
||||
<Typography
|
||||
key={Math.floor(Math.random() * 1000)}
|
||||
variant="body2"
|
||||
paragraph
|
||||
className={classes.description}
|
||||
>
|
||||
{str}
|
||||
</Typography>
|
||||
))
|
||||
: 'No announcement'}
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<AboutField label="Status">
|
||||
<StatusTag status={bazaarProject.status} />
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
{' '}
|
||||
<AboutField label="Latest members">
|
||||
{members.length ? (
|
||||
members.slice(0, 3).map(member => {
|
||||
return (
|
||||
<div key={member.userId}>
|
||||
<Avatar
|
||||
displayName={member.userId}
|
||||
customStyles={{
|
||||
width: '19px',
|
||||
height: '19px',
|
||||
fontSize: '8px',
|
||||
float: 'left',
|
||||
marginRight: '0.3rem',
|
||||
}}
|
||||
/>
|
||||
<Link
|
||||
className={classes.memberLink}
|
||||
to={`http://github.com/${member.userId}`}
|
||||
>
|
||||
{member?.userId}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</AboutField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { EntityBazaarInfoCard } from './EntityBazaarInfoCard';
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 { Header, RoutedTabs } from '@backstage/core-components';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { SortView } from '../SortView';
|
||||
import { About } from '../About';
|
||||
|
||||
export const HomePage = () => {
|
||||
const tabContent = [
|
||||
{
|
||||
path: '/',
|
||||
title: 'Home',
|
||||
children: <Route path="/" element={<SortView />} />,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
title: 'About',
|
||||
children: <Route path="/about" element={<About />} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header
|
||||
data-testid="bazaar-header"
|
||||
title="Bazaar"
|
||||
subtitle="Marketplace for inner source projects"
|
||||
/>
|
||||
<RoutedTabs routes={tabContent} />
|
||||
</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 { HomePage } from './HomePage';
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 { Controller, Control, FieldError } from 'react-hook-form';
|
||||
import { TextField } from '@material-ui/core';
|
||||
import { FormValues } from '../../util/types';
|
||||
|
||||
type Props = {
|
||||
inputType: string;
|
||||
error: FieldError | undefined;
|
||||
control: Control<FormValues, object>;
|
||||
name: 'announcement' | 'status';
|
||||
helperText: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export const InputField = ({
|
||||
inputType,
|
||||
error,
|
||||
control,
|
||||
name,
|
||||
helperText,
|
||||
placeholder,
|
||||
}: Props) => {
|
||||
const label = inputType.charAt(0).toUpperCase() + inputType.slice(1);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
margin="dense"
|
||||
multiline
|
||||
id="title"
|
||||
type="text"
|
||||
fullWidth
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
error={!!error}
|
||||
helperText={error && helperText}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 { InputField } from './InputField';
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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 InputLabel from '@material-ui/core/InputLabel';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import { Controller, Control, FieldError } from 'react-hook-form';
|
||||
import { FormValues } from '../../util/types';
|
||||
|
||||
type Props = {
|
||||
options: string[];
|
||||
control: Control<FormValues, object>;
|
||||
name: 'announcement' | 'status';
|
||||
error?: FieldError | undefined;
|
||||
};
|
||||
|
||||
export const InputSelector = ({ name, options, control, error }: Props) => {
|
||||
const label = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel
|
||||
htmlFor="demo-simple-select-outlined"
|
||||
id="demo-simple-select-outlined-label"
|
||||
>
|
||||
{label}
|
||||
</InputLabel>
|
||||
<Select
|
||||
{...field}
|
||||
required
|
||||
labelId="demo-simple-select-outlined-label"
|
||||
id="demo-simple-select-outlined"
|
||||
label={label}
|
||||
error={!!error}
|
||||
>
|
||||
{options.map(option => {
|
||||
return (
|
||||
<MenuItem
|
||||
data-testid="menu-item"
|
||||
button
|
||||
key={option}
|
||||
value={option}
|
||||
>
|
||||
{option}
|
||||
</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 { InputSelector } from './InputSelector';
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 { ItemCardHeader } from '@backstage/core-components';
|
||||
import {
|
||||
Card,
|
||||
CardActionArea,
|
||||
CardContent,
|
||||
makeStyles,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { StatusTag } from '../StatusTag/StatusTag';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import moment from 'moment';
|
||||
import { catalogRouteRef } from '@backstage/plugin-catalog-react';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { BazaarProject } from '../../util/types';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
statusTag: {
|
||||
display: 'inline-block',
|
||||
whiteSpace: 'nowrap',
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
announcement: {
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 5,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
marginBottom: '0.8rem',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
memberCount: {
|
||||
float: 'right',
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
bazaarProject: BazaarProject;
|
||||
memberCount: number;
|
||||
};
|
||||
|
||||
export const ProjectCard = ({ bazaarProject, memberCount }: Props) => {
|
||||
const classes = useStyles();
|
||||
const { entityRef, name, status, updatedAt, announcement } = bazaarProject;
|
||||
const catalogLink = useRouteRef(catalogRouteRef);
|
||||
|
||||
return (
|
||||
<Card key={entityRef}>
|
||||
<CardActionArea
|
||||
style={{
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
}}
|
||||
component={RouterLink}
|
||||
to={`${catalogLink()}/${entityRef}`}
|
||||
>
|
||||
<ItemCardHeader
|
||||
title={name}
|
||||
subtitle={`updated ${moment(updatedAt).fromNow()}`}
|
||||
/>
|
||||
<CardContent style={{ height: '12rem' }}>
|
||||
<StatusTag styles={classes.statusTag} status={status} />
|
||||
<Typography variant="body2" className={classes.memberCount}>
|
||||
{memberCount === 1
|
||||
? `${memberCount} member`
|
||||
: `${memberCount} members`}
|
||||
</Typography>
|
||||
<div style={{ minHeight: '6.5rem', maxHeight: '6.5rem' }}>
|
||||
<Typography variant="body2" className={classes.announcement}>
|
||||
{announcement}
|
||||
</Typography>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -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 { ProjectCard } from './ProjectCard';
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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 {
|
||||
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 { useForm, SubmitHandler } from 'react-hook-form';
|
||||
import { InputField } from '../InputField/InputField';
|
||||
import { InputSelector } from '../InputSelector/InputSelector';
|
||||
import { FormValues } from '../../util/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 = {
|
||||
handleSave: (getValues: any, reset: any) => SubmitHandler<FormValues>;
|
||||
isAddForm: boolean;
|
||||
title: string;
|
||||
defaultValues: FormValues;
|
||||
open: boolean;
|
||||
projectSelector?: JSX.Element;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const ProjectDialog = ({
|
||||
handleSave,
|
||||
isAddForm,
|
||||
title,
|
||||
defaultValues,
|
||||
open,
|
||||
projectSelector,
|
||||
handleClose,
|
||||
}: Props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
control,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
} = useForm<FormValues>({
|
||||
mode: 'onChange',
|
||||
defaultValues: defaultValues,
|
||||
});
|
||||
|
||||
const handleCloseAndClear = () => {
|
||||
handleClose();
|
||||
reset(defaultValues);
|
||||
};
|
||||
|
||||
const handleSaveProject = () => {
|
||||
handleSave(getValues, reset);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
onClose={handleCloseAndClear}
|
||||
aria-labelledby="customized-dialog-title"
|
||||
open={open}
|
||||
>
|
||||
<DialogTitle id="customized-dialog-title" onClose={handleCloseAndClear}>
|
||||
{title}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent dividers>
|
||||
{isAddForm && projectSelector}
|
||||
|
||||
<InputField
|
||||
error={errors.announcement}
|
||||
control={control}
|
||||
inputType="announcement"
|
||||
helperText="please enter an announcement"
|
||||
placeholder="Describe who you are and what skills you are looking for"
|
||||
name="announcement"
|
||||
/>
|
||||
|
||||
<InputSelector
|
||||
control={control}
|
||||
name="status"
|
||||
options={['proposed', 'ongoing']}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={handleSubmit(handleSaveProject)}
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</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 { ProjectDialog } from './ProjectDialog';
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 { Content } from '@backstage/core-components';
|
||||
import { ProjectCard } from '../ProjectCard/ProjectCard';
|
||||
import { makeStyles, Grid } from '@material-ui/core';
|
||||
import Pagination from '@material-ui/lab/Pagination';
|
||||
import { BazaarProject } from '../../util/types';
|
||||
|
||||
type Props = {
|
||||
bazaarProjects: BazaarProject[];
|
||||
sortingMethod: (arg0: BazaarProject, arg1: BazaarProject) => number;
|
||||
bazaarMembers: Map<string, number>;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
content: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
},
|
||||
item: {
|
||||
minWidth: '20.5rem',
|
||||
maxWidth: '20.5rem',
|
||||
width: '20%',
|
||||
},
|
||||
});
|
||||
|
||||
export const ProjectPreview = ({
|
||||
bazaarProjects,
|
||||
sortingMethod,
|
||||
bazaarMembers,
|
||||
}: Props) => {
|
||||
const classes = useStyles();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 10;
|
||||
const pageCount = Math.ceil(bazaarProjects.length / pageSize);
|
||||
|
||||
const handlePageClick = (_: any, pageIndex: number) => {
|
||||
setCurrentPage(pageIndex);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
bazaarProjects.sort(sortingMethod);
|
||||
|
||||
return (
|
||||
<Content className={classes.content} noPadding>
|
||||
<Grid wrap="wrap" container spacing={3}>
|
||||
{bazaarProjects
|
||||
.slice(pageSize * (currentPage - 1), pageSize * currentPage)
|
||||
.map((bazaarProject: BazaarProject) => {
|
||||
const entityRef = bazaarProject.entityRef;
|
||||
|
||||
return (
|
||||
<Grid key={entityRef || ''} className={classes.item} item xs={3}>
|
||||
<ProjectCard
|
||||
bazaarProject={bazaarProject}
|
||||
key={Math.random()}
|
||||
memberCount={bazaarMembers.get(entityRef) || 0}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
<Pagination
|
||||
showFirstButton
|
||||
showLastButton
|
||||
siblingCount={2}
|
||||
style={{
|
||||
marginTop: '1rem',
|
||||
marginLeft: 'auto',
|
||||
marginRight: '0',
|
||||
}}
|
||||
count={pageCount}
|
||||
page={currentPage}
|
||||
onChange={handlePageClick}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -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 { ProjectPreview } from './ProjectPreview';
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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 InputLabel from '@material-ui/core/InputLabel';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
type Props = {
|
||||
entities: Entity[];
|
||||
value: string;
|
||||
onChange: (entity: Entity) => void;
|
||||
isFormInvalid: boolean;
|
||||
};
|
||||
|
||||
export const ProjectSelector = ({
|
||||
entities,
|
||||
value,
|
||||
onChange,
|
||||
isFormInvalid,
|
||||
}: Props) => {
|
||||
return (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="demo-simple-select-outlined-label">
|
||||
Select a project
|
||||
</InputLabel>
|
||||
<Select
|
||||
required
|
||||
labelId="demo-simple-select-outlined-label"
|
||||
id="demo-simple-select-outlined"
|
||||
value={value}
|
||||
error={isFormInvalid && value === ''}
|
||||
label="Project"
|
||||
>
|
||||
{entities?.map(entity => {
|
||||
const projectName = entity.metadata.name;
|
||||
return (
|
||||
<MenuItem
|
||||
button
|
||||
onClick={() => onChange(entity)}
|
||||
key={projectName}
|
||||
value={projectName}
|
||||
>
|
||||
{projectName}
|
||||
</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 { ProjectSelector } from './ProjectSelector';
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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 {
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
Progress,
|
||||
} 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 { useAsync } from 'react-use';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { getBazaarMembers } from '../../util/dbRequests';
|
||||
import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
catalogApiRef,
|
||||
CATALOG_FILTER_EXISTS,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { BazaarProject } from '../../util/types';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
marginTop: '2rem',
|
||||
},
|
||||
});
|
||||
|
||||
export const SortView = () => {
|
||||
const classes = useStyles();
|
||||
const [openAdd, setOpenAdd] = useState(false);
|
||||
const [openNoProjects, setOpenNoProjects] = useState(false);
|
||||
const [catalogEntities, setCatalogEntities] = useState<Entity[]>([]);
|
||||
const [bazaarMembers, setBazaarMembers] = useState<Map<string, number>>(
|
||||
new Map(),
|
||||
);
|
||||
const [bazaarProjects, setBazaarProjects] = useState<BazaarProject[]>([]);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const baseUrl = useApi(configApiRef)
|
||||
.getConfig('backend')
|
||||
.getString('baseUrl');
|
||||
|
||||
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 { loading } = useAsync(async () => {
|
||||
const entities = await catalogApi.getEntities({
|
||||
filter: {
|
||||
kind: 'Component',
|
||||
'metadata.annotations.backstage.io/edit-url': CATALOG_FILTER_EXISTS,
|
||||
},
|
||||
fields: ['apiVersion', 'kind', 'metadata', 'spec'],
|
||||
});
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/bazaar/entities`, {
|
||||
method: 'GET',
|
||||
}).then(resp => resp.json());
|
||||
|
||||
const dbProjects: BazaarProject[] = [];
|
||||
const bazaarProjectRefs: string[] = [];
|
||||
|
||||
response.data.forEach((project: any) => {
|
||||
dbProjects.push({
|
||||
entityRef: project.entity_ref,
|
||||
name: project.name,
|
||||
status: project.status,
|
||||
announcement: project.announcement,
|
||||
updatedAt: project.updated_at,
|
||||
});
|
||||
|
||||
bazaarProjectRefs.push(project.entity_ref);
|
||||
});
|
||||
|
||||
setBazaarMembers(await getBazaarMembers(dbProjects, baseUrl));
|
||||
setBazaarProjects(dbProjects);
|
||||
setCatalogEntities(
|
||||
entities.items.filter((entity: Entity) => {
|
||||
const catalogEntityRef = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
|
||||
|
||||
return !bazaarProjectRefs.includes(catalogEntityRef);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
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">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (catalogEntities.length !== 0) {
|
||||
setOpenAdd(true);
|
||||
} else {
|
||||
setOpenNoProjects(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add project
|
||||
</Button>
|
||||
<AddProjectDialog
|
||||
catalogEntities={catalogEntities}
|
||||
handleClose={() => {
|
||||
setOpenAdd(false);
|
||||
}}
|
||||
open={openAdd}
|
||||
setBazaarProjects={setBazaarProjects}
|
||||
setCatalogEntities={setCatalogEntities}
|
||||
/>
|
||||
<SupportButton />
|
||||
</ContentHeader>
|
||||
<ProjectPreview
|
||||
bazaarProjects={bazaarProjects || []}
|
||||
sortingMethod={compareProjectsByDate}
|
||||
bazaarMembers={bazaarMembers}
|
||||
/>
|
||||
<Content noPadding className={classes.container} />
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -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 { SortView } from './SortView';
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 { StatusOK, StatusWarning } from '@backstage/core-components';
|
||||
import { Status } from '../../util/types';
|
||||
|
||||
interface StatusComponent {
|
||||
[key: string]: JSX.Element | undefined;
|
||||
}
|
||||
|
||||
const statuses: StatusComponent = {
|
||||
proposed: <StatusWarning>proposed</StatusWarning>,
|
||||
ongoing: <StatusOK>ongoing</StatusOK>,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
status: Status;
|
||||
styles?: string;
|
||||
};
|
||||
|
||||
export const StatusTag = ({ status, styles }: Props) => {
|
||||
return <div className={styles}>{statuses[status]}</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 { StatusTag } from './StatusTag';
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 { bazaarPlugin, BazaarPage } from './plugin';
|
||||
export { EntityBazaarInfoCard } from './components/EntityBazaarInfoCard';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 { bazaarPlugin } from './plugin';
|
||||
|
||||
describe('bazaar', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(bazaarPlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 {
|
||||
createPlugin,
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import { rootRouteRef } from './routes';
|
||||
|
||||
export const bazaarPlugin = createPlugin({
|
||||
id: 'bazaar',
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
},
|
||||
});
|
||||
|
||||
export const BazaarPage = bazaarPlugin.provide(
|
||||
createRoutableExtension({
|
||||
component: () => import('./components/HomePage').then(m => m.HomePage),
|
||||
mountPoint: rootRouteRef,
|
||||
}),
|
||||
);
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
title: 'bazaar',
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 '@testing-library/jest-dom';
|
||||
import 'cross-fetch/polyfill';
|
||||
@@ -0,0 +1,11 @@
|
||||
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
||||
// It should be published with your NPM package. It should not be tracked by Git.
|
||||
{
|
||||
"tsdocVersion": "0.12",
|
||||
"toolPackages": [
|
||||
{
|
||||
"packageName": "@microsoft/api-extractor",
|
||||
"packageVersion": "7.18.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 { Entity } from '@backstage/catalog-model';
|
||||
import { IdentityApi } from '@backstage/core-api';
|
||||
import { BazaarProject, Status } from './types';
|
||||
|
||||
export const getEntityRef = (entity: Entity) => {
|
||||
return `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
|
||||
};
|
||||
|
||||
export const updateMetadata = async (
|
||||
entity: Entity,
|
||||
name: string,
|
||||
announcement: string,
|
||||
status: Status,
|
||||
baseUrl: string,
|
||||
) => {
|
||||
return await fetch(`${baseUrl}/api/bazaar/metadata`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
entity_ref: getEntityRef(entity),
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
announcement: announcement,
|
||||
status: status,
|
||||
}),
|
||||
}).then(resp => resp.json());
|
||||
};
|
||||
|
||||
export const getBazaarMembers = async (
|
||||
bazaarProjects: BazaarProject[],
|
||||
baseUrl: string,
|
||||
) => {
|
||||
const bazaarMembers = new Map<string, number>();
|
||||
for (const project of bazaarProjects) {
|
||||
const response = await fetch(`${baseUrl}/api/bazaar/members`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
entity_ref: project.entityRef,
|
||||
},
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
const nbrOfMembers = await json.data.length;
|
||||
bazaarMembers.set(project.entityRef, nbrOfMembers);
|
||||
}
|
||||
return bazaarMembers;
|
||||
};
|
||||
|
||||
export const deleteEntity = async (
|
||||
baseUrl: string,
|
||||
entity: Entity,
|
||||
identity: IdentityApi,
|
||||
) => {
|
||||
await fetch(`${baseUrl}/api/bazaar/metadata`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
entity_ref: getEntityRef(entity),
|
||||
},
|
||||
});
|
||||
|
||||
await fetch(`${baseUrl}/api/bazaar/members/remove`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
user_id: identity.getUserId(),
|
||||
entity_ref: getEntityRef(entity),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 type Member = {
|
||||
entityRef: string;
|
||||
userId: string;
|
||||
joinDate: string;
|
||||
};
|
||||
|
||||
export type Status = 'ongoing' | 'proposed';
|
||||
|
||||
export type BazaarProject = {
|
||||
name: string;
|
||||
entityRef: string;
|
||||
status: Status;
|
||||
announcement: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type FormValues = {
|
||||
announcement: string;
|
||||
status: string;
|
||||
};
|
||||