made link to entity optional
Signed-off-by: Lykke Axlin <lykkeaxlin@hotmail.com> Co-authored-by: klaraab <klarabroman@live.se>
This commit is contained in:
+52
-38
@@ -14,13 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import {
|
||||
createApiRef,
|
||||
DiscoveryApi,
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { BazaarProject } from './types';
|
||||
|
||||
export const bazaarApiRef = createApiRef<BazaarApi>({
|
||||
id: 'bazaar',
|
||||
@@ -28,19 +26,23 @@ export const bazaarApiRef = createApiRef<BazaarApi>({
|
||||
});
|
||||
|
||||
export interface BazaarApi {
|
||||
updateMetadata(bazaarProject: BazaarProject): Promise<any>;
|
||||
updateProject(bazaarProject: any): Promise<any>;
|
||||
|
||||
getMetadata(entity: Entity): Promise<any>;
|
||||
addProject(bazaarProject: any): Promise<any>;
|
||||
|
||||
getMembers(entity: Entity): Promise<any>;
|
||||
getProjectById(id: number): Promise<any>;
|
||||
|
||||
deleteMember(entity: Entity): Promise<void>;
|
||||
getProjectByRef(entityRef: string): Promise<any>;
|
||||
|
||||
addMember(entity: Entity): Promise<void>;
|
||||
getMembers(id: number): Promise<any>;
|
||||
|
||||
getEntities(): Promise<any>;
|
||||
deleteMember(id: number, userId: string): Promise<void>;
|
||||
|
||||
deleteEntity(bazaarProject: BazaarProject): Promise<void>;
|
||||
addMember(id: number, userId: string): Promise<void>;
|
||||
|
||||
getProjects(): Promise<any>;
|
||||
|
||||
deleteProject(id: number): Promise<void>;
|
||||
}
|
||||
|
||||
export class BazaarClient implements BazaarApi {
|
||||
@@ -55,10 +57,10 @@ export class BazaarClient implements BazaarApi {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
}
|
||||
|
||||
async updateMetadata(bazaarProject: BazaarProject): Promise<any> {
|
||||
async updateProject(bazaarProject: any): Promise<any> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('bazaar');
|
||||
|
||||
return await fetch(`${baseUrl}/metadata`, {
|
||||
return await fetch(`${baseUrl}/projects`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -68,11 +70,34 @@ export class BazaarClient implements BazaarApi {
|
||||
}).then(resp => resp.json());
|
||||
}
|
||||
|
||||
async getMetadata(entity: Entity): Promise<any> {
|
||||
async addProject(bazaarProject: any): Promise<any> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('bazaar');
|
||||
|
||||
return await fetch(`${baseUrl}/projects`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(bazaarProject),
|
||||
}).then(resp => resp.json());
|
||||
}
|
||||
|
||||
async getProjectById(id: number): Promise<any> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('bazaar');
|
||||
|
||||
const response = await fetch(`${baseUrl}/projects/id/${id}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
return response.ok ? response : null;
|
||||
}
|
||||
|
||||
async getProjectByRef(entityRef: string): Promise<any> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('bazaar');
|
||||
|
||||
const response = await fetch(
|
||||
`${baseUrl}/metadata/${encodeURIComponent(stringifyEntityRef(entity))}`,
|
||||
`${baseUrl}/projects/ref/${encodeURIComponent(entityRef)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
@@ -81,60 +106,49 @@ export class BazaarClient implements BazaarApi {
|
||||
return response.ok ? response : null;
|
||||
}
|
||||
|
||||
async getMembers(entity: Entity): Promise<any> {
|
||||
async getMembers(id: number): Promise<any> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('bazaar');
|
||||
|
||||
return await fetch(
|
||||
`${baseUrl}/members/${encodeURIComponent(stringifyEntityRef(entity))}`,
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
).then(resp => resp.json());
|
||||
return await fetch(`${baseUrl}/projects/${id}/members`, {
|
||||
method: 'GET',
|
||||
}).then(resp => resp.json());
|
||||
}
|
||||
|
||||
async addMember(entity: Entity): Promise<void> {
|
||||
async addMember(id: number, userId: string): Promise<void> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('bazaar');
|
||||
|
||||
await fetch(`${baseUrl}/member`, {
|
||||
await fetch(`${baseUrl}/projects/${id}/member/${userId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
entity_ref: stringifyEntityRef(entity),
|
||||
user_id: this.identityApi.getUserId(),
|
||||
picture: this.identityApi.getProfile()?.picture,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteMember(entity: Entity): Promise<void> {
|
||||
async deleteMember(id: number, userId: string): Promise<void> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('bazaar');
|
||||
|
||||
await fetch(
|
||||
`${baseUrl}/member/${encodeURIComponent(
|
||||
stringifyEntityRef(entity),
|
||||
)}/${this.identityApi.getUserId()}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
},
|
||||
);
|
||||
await fetch(`${baseUrl}/projects/${id}/member/${userId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
async getEntities(): Promise<any> {
|
||||
async getProjects(): Promise<any> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('bazaar');
|
||||
|
||||
return await fetch(`${baseUrl}/entities`, {
|
||||
return await fetch(`${baseUrl}/projects`, {
|
||||
method: 'GET',
|
||||
}).then(resp => resp.json());
|
||||
}
|
||||
|
||||
async deleteEntity(bazaarProject: BazaarProject): Promise<void> {
|
||||
async deleteProject(id: number): Promise<void> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('bazaar');
|
||||
const entityRef = bazaarProject.entityRef as string;
|
||||
|
||||
await fetch(`${baseUrl}/metadata/${encodeURIComponent(entityRef)}`, {
|
||||
await fetch(`${baseUrl}/projects/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { UseFormReset, UseFormGetValues } from 'react-hook-form';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
@@ -39,18 +39,13 @@ export const AddProjectDialog = ({
|
||||
fetchCatalogEntities,
|
||||
}: Props) => {
|
||||
const bazaarApi = useApi(bazaarApiRef);
|
||||
const [selectedEntity, setSelectedEntity] = useState(
|
||||
catalogEntities ? catalogEntities[0] : null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedEntity(catalogEntities ? catalogEntities[0] : null);
|
||||
}, [catalogEntities]);
|
||||
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
|
||||
|
||||
const defaultValues = {
|
||||
name: '',
|
||||
title: 'Add project',
|
||||
community: '',
|
||||
announcement: '',
|
||||
description: '',
|
||||
status: 'proposed' as Status,
|
||||
size: 'medium' as Size,
|
||||
responsible: '',
|
||||
@@ -58,59 +53,48 @@ export const AddProjectDialog = ({
|
||||
endDate: null,
|
||||
};
|
||||
|
||||
const handleListItemClick = (entity: Entity) => {
|
||||
const handleEntityClick = (entity: Entity) => {
|
||||
setSelectedEntity(entity);
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setSelectedEntity(catalogEntities ? catalogEntities[0] : null);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleSave: any = async (
|
||||
const handleSubmit: any = async (
|
||||
getValues: UseFormGetValues<FormValues>,
|
||||
reset: UseFormReset<FormValues>,
|
||||
) => {
|
||||
const formValues = getValues();
|
||||
const response = await bazaarApi.addProject({
|
||||
...formValues,
|
||||
entityRef: selectedEntity ? stringifyEntityRef(selectedEntity) : null,
|
||||
startDate: formValues.startDate ?? null,
|
||||
endDate: formValues.endDate ?? null,
|
||||
} as BazaarProject);
|
||||
|
||||
if (selectedEntity) {
|
||||
await bazaarApi.updateMetadata({
|
||||
name: selectedEntity.metadata.name,
|
||||
entityRef: stringifyEntityRef(selectedEntity),
|
||||
announcement: formValues.announcement,
|
||||
status: formValues.status,
|
||||
community: formValues.community,
|
||||
membersCount: 0,
|
||||
size: formValues.size,
|
||||
startDate: formValues.startDate ?? null,
|
||||
endDate: formValues.endDate ?? null,
|
||||
responsible: formValues.responsible,
|
||||
} as BazaarProject);
|
||||
|
||||
if (response.status === 'ok') {
|
||||
fetchBazaarProjects();
|
||||
fetchCatalogEntities();
|
||||
|
||||
handleClose();
|
||||
reset(defaultValues);
|
||||
}
|
||||
|
||||
handleClose();
|
||||
reset(defaultValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<ProjectDialog
|
||||
handleSave={handleSave}
|
||||
handleSave={handleSubmit}
|
||||
title="Add project"
|
||||
isAddForm
|
||||
defaultValues={defaultValues}
|
||||
open={open}
|
||||
projectSelector={
|
||||
<ProjectSelector
|
||||
value={selectedEntity?.metadata?.name || ''}
|
||||
onChange={handleListItemClick}
|
||||
isFormInvalid={selectedEntity === null}
|
||||
entities={catalogEntities || []}
|
||||
onChange={handleEntityClick}
|
||||
catalogEntities={catalogEntities || []}
|
||||
disableClearable={false}
|
||||
defaultValue={null}
|
||||
label="Select a project"
|
||||
/>
|
||||
}
|
||||
handleClose={handleCloseDialog}
|
||||
handleClose={handleClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* 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,168 @@
|
||||
/*
|
||||
* 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,
|
||||
makeStyles,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Link,
|
||||
GridSize,
|
||||
} from '@material-ui/core';
|
||||
import { Avatar } from '@backstage/core-components';
|
||||
import { AboutField } from '@backstage/plugin-catalog';
|
||||
import { StatusTag } from '../StatusTag';
|
||||
import { Member, BazaarProject } from '../../types';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
break: {
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
bazaarProject: BazaarProject;
|
||||
members: Member[];
|
||||
descriptionSize: GridSize;
|
||||
membersSize: GridSize;
|
||||
};
|
||||
|
||||
export const CardContentFields = ({
|
||||
bazaarProject,
|
||||
members,
|
||||
descriptionSize,
|
||||
membersSize,
|
||||
}: Props) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Grid container>
|
||||
<Grid item xs={descriptionSize}>
|
||||
<AboutField label="Description">
|
||||
{bazaarProject.description
|
||||
.split('\n')
|
||||
.map((str: string, i: number) => (
|
||||
<Typography
|
||||
key={i}
|
||||
variant="body2"
|
||||
paragraph
|
||||
className={classes.break}
|
||||
>
|
||||
{str}
|
||||
</Typography>
|
||||
))}
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
item
|
||||
xs={membersSize}
|
||||
>
|
||||
<AboutField label="Latest members">
|
||||
{members.length ? (
|
||||
members.slice(0, 7).map((member: Member) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
backgroundColor: '',
|
||||
marginBottom: '0.3rem',
|
||||
marginTop: '0.3rem',
|
||||
display: 'block',
|
||||
}}
|
||||
key={member.userId}
|
||||
>
|
||||
<Avatar
|
||||
displayName={member.userId}
|
||||
customStyles={{
|
||||
width: '19px',
|
||||
height: '19px',
|
||||
fontSize: '8px',
|
||||
float: 'left',
|
||||
marginRight: '0.3rem',
|
||||
marginTop: '0rem',
|
||||
marginBottom: '0rem',
|
||||
alignItems: 'left',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
picture={member.picture}
|
||||
/>
|
||||
<Link
|
||||
className={classes.break}
|
||||
href={`http://github.com/${member.userId}`}
|
||||
target="_blank"
|
||||
>
|
||||
{member?.userId}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="Status">
|
||||
<StatusTag status={bazaarProject.status} />
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="size">
|
||||
<Typography variant="body2">{bazaarProject.size}</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="Start date">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject.startDate?.substring(0, 10) || ''}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="End date">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject.endDate?.substring(0, 10) || ''}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={4}>
|
||||
<AboutField label="Responsible">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject.responsible || ''}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { AlertBanner } from './AlertBanner';
|
||||
export { CardContentFields } from './CardContentFields';
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 Button from '@material-ui/core/Button';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import {
|
||||
CustomDialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
} from '../CustomDialogTitle';
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
handleClose: () => void;
|
||||
message: (string | JSX.Element)[];
|
||||
type: 'delete' | 'unlink';
|
||||
handleSubmit: () => void;
|
||||
};
|
||||
|
||||
export const ConfirmationDialog = ({
|
||||
open,
|
||||
handleClose,
|
||||
message,
|
||||
type,
|
||||
handleSubmit,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
onClose={handleClose}
|
||||
aria-labelledby="customized-dialog-title"
|
||||
open={open}
|
||||
>
|
||||
<CustomDialogTitle id="customized-dialog-title" onClose={handleClose}>
|
||||
{type.charAt(0).toLocaleUpperCase('en-US') + type.slice(1)} project
|
||||
</CustomDialogTitle>
|
||||
|
||||
<DialogContent dividers>{message}</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleSubmit} color="primary" type="submit">
|
||||
{type}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { DeleteProjectDialog } from './DeleteProjectDialog';
|
||||
export { ConfirmationDialog } from './ConfirmationDialog';
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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 IconButton from '@material-ui/core/IconButton';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import MuiDialogTitle from '@material-ui/core/DialogTitle';
|
||||
import {
|
||||
Theme,
|
||||
WithStyles,
|
||||
withStyles,
|
||||
createStyles,
|
||||
} from '@material-ui/core/styles';
|
||||
import MuiDialogContent from '@material-ui/core/DialogContent';
|
||||
import MuiDialogActions from '@material-ui/core/DialogActions';
|
||||
|
||||
/*
|
||||
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 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],
|
||||
},
|
||||
});
|
||||
|
||||
export const DialogContent = withStyles((theme: Theme) => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
}))(MuiDialogContent);
|
||||
|
||||
export const DialogActions = withStyles((theme: Theme) => ({
|
||||
root: {
|
||||
margin: 0,
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
}))(MuiDialogActions);
|
||||
|
||||
export const CustomDialogTitle = 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export {
|
||||
CustomDialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from './CustomDialogTitle';
|
||||
@@ -45,7 +45,6 @@ export const DateSelector = ({ name, control, setValue }: Props) => {
|
||||
<FormControl>
|
||||
<MuiPickersUtilsProvider utils={LuxonUtils}>
|
||||
<KeyboardDatePicker
|
||||
disablePast
|
||||
disableToolbar
|
||||
format="dd-MM-yyyy"
|
||||
label={label}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
* 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 { useApi } from '@backstage/core-plugin-api';
|
||||
import { bazaarApiRef } from '../../api';
|
||||
import { BazaarProject } from '../../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 = {
|
||||
bazaarProject: BazaarProject;
|
||||
openDelete: boolean;
|
||||
handleClose: () => void;
|
||||
setIsBazaar: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const DeleteProjectDialog = ({
|
||||
bazaarProject,
|
||||
openDelete,
|
||||
handleClose,
|
||||
setIsBazaar,
|
||||
}: Props) => {
|
||||
const handleCloseAndClear = () => {
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const bazaarApi = useApi(bazaarApiRef);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await bazaarApi.deleteEntity(bazaarProject);
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -17,49 +17,48 @@
|
||||
import React from 'react';
|
||||
import { Control, UseFormSetValue } from 'react-hook-form';
|
||||
import { FormValues } from '../../types';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import { DateSelector } from '../DateSelector/DateSelector';
|
||||
import { Typography, makeStyles } from '@material-ui/core';
|
||||
|
||||
type Props = {
|
||||
control: Control<FormValues, object>;
|
||||
setValue: UseFormSetValue<FormValues>;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
marginTop: '0.25rem',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
startDate: {
|
||||
float: 'left',
|
||||
},
|
||||
endDate: {
|
||||
float: 'right',
|
||||
},
|
||||
dash: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: '1.5rem',
|
||||
margin: '0 1rem',
|
||||
},
|
||||
});
|
||||
|
||||
export const DoubleDateSelector = ({ control, setValue }: Props) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.25rem',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
float: 'left',
|
||||
}}
|
||||
>
|
||||
<div className={classes.container}>
|
||||
<div className={classes.startDate}>
|
||||
<DateSelector name="startDate" control={control} setValue={setValue} />
|
||||
</div>
|
||||
|
||||
<Typography
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: '1.5rem',
|
||||
margin: '0 1rem',
|
||||
}}
|
||||
>
|
||||
-
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
float: 'right',
|
||||
}}
|
||||
>
|
||||
<Typography className={classes.dash}>-</Typography>
|
||||
<div className={classes.endDate}>
|
||||
<DateSelector name="endDate" control={control} setValue={setValue} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,81 +15,121 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { ProjectDialog } from '../ProjectDialog';
|
||||
import { BazaarProject, FormValues, Size, Status } from '../../types';
|
||||
import { BazaarProject, FormValues } from '../../types';
|
||||
import { bazaarApiRef } from '../../api';
|
||||
import { UseFormGetValues } from 'react-hook-form';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { Button, makeStyles } from '@material-ui/core';
|
||||
|
||||
type Props = {
|
||||
entity: Entity;
|
||||
bazaarProject: BazaarProject;
|
||||
openEdit: boolean;
|
||||
handleEditClose: () => void;
|
||||
handleCardClose?: () => void;
|
||||
fetchBazaarProject: () => Promise<BazaarProject | null>;
|
||||
open: boolean;
|
||||
handleClose: () => void;
|
||||
isAddForm: boolean;
|
||||
fetchBazaarProjects?: () => void;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
button: {
|
||||
marginLeft: '0',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
export const EditProjectDialog = ({
|
||||
entity,
|
||||
bazaarProject,
|
||||
openEdit,
|
||||
handleEditClose,
|
||||
handleCardClose,
|
||||
fetchBazaarProject,
|
||||
open,
|
||||
handleClose,
|
||||
fetchBazaarProjects,
|
||||
}: Props) => {
|
||||
const classes = useStyles();
|
||||
const bazaarApi = useApi(bazaarApiRef);
|
||||
const [openDelete, setOpenDelete] = useState(false);
|
||||
const [defaultValues, setDefaultValues] = useState<FormValues>({
|
||||
announcement: bazaarProject.announcement,
|
||||
community: bazaarProject.community,
|
||||
status: bazaarProject.status,
|
||||
size: bazaarProject.size,
|
||||
startDate: bazaarProject?.startDate ?? null,
|
||||
endDate: bazaarProject?.endDate ?? null,
|
||||
responsible: bazaarProject.responsible,
|
||||
...bazaarProject,
|
||||
startDate: bazaarProject.startDate ?? null,
|
||||
endDate: bazaarProject.endDate ?? null,
|
||||
});
|
||||
|
||||
const bazaarApi = useApi(bazaarApiRef);
|
||||
const handleDeleteClose = () => {
|
||||
setOpenDelete(false);
|
||||
handleEditClose();
|
||||
|
||||
if (handleCardClose) handleCardClose();
|
||||
};
|
||||
|
||||
const handleDeleteSubmit = async () => {
|
||||
await bazaarApi.deleteProject(bazaarProject.id);
|
||||
|
||||
handleDeleteClose();
|
||||
if (fetchBazaarProjects) fetchBazaarProjects();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDefaultValues({
|
||||
announcement: bazaarProject.announcement,
|
||||
community: bazaarProject.community,
|
||||
status: bazaarProject.status,
|
||||
size: bazaarProject.size,
|
||||
startDate: bazaarProject?.startDate ?? null,
|
||||
endDate: bazaarProject?.endDate ?? null,
|
||||
responsible: bazaarProject.responsible,
|
||||
...bazaarProject,
|
||||
startDate: bazaarProject.startDate ?? null,
|
||||
endDate: bazaarProject.endDate ?? null,
|
||||
});
|
||||
}, [bazaarProject]);
|
||||
|
||||
const handleSave: any = async (getValues: UseFormGetValues<FormValues>) => {
|
||||
const handleEditSubmit: any = async (
|
||||
getValues: UseFormGetValues<FormValues>,
|
||||
) => {
|
||||
const formValues = getValues();
|
||||
|
||||
const updateResponse = await bazaarApi.updateMetadata({
|
||||
name: entity.metadata.name,
|
||||
entityRef: stringifyEntityRef(entity),
|
||||
announcement: formValues.announcement,
|
||||
status: formValues.status as Status,
|
||||
community: formValues.community,
|
||||
const updateResponse = await bazaarApi.updateProject({
|
||||
...formValues,
|
||||
id: bazaarProject.id,
|
||||
entityRef: bazaarProject.entityRef,
|
||||
membersCount: bazaarProject.membersCount,
|
||||
size: formValues.size as Size,
|
||||
startDate: formValues?.startDate ?? null,
|
||||
endDate: formValues?.endDate ?? null,
|
||||
responsible: formValues.responsible,
|
||||
});
|
||||
|
||||
if (updateResponse.status === 'ok') fetchBazaarProject();
|
||||
handleClose();
|
||||
handleEditClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ProjectDialog
|
||||
title="Edit project"
|
||||
handleSave={handleSave}
|
||||
isAddForm={false}
|
||||
defaultValues={defaultValues}
|
||||
open={open}
|
||||
handleClose={handleClose}
|
||||
/>
|
||||
<div>
|
||||
<ConfirmationDialog
|
||||
open={openDelete}
|
||||
handleClose={handleDeleteClose}
|
||||
message={[
|
||||
'Are you sure you want to delete ',
|
||||
<b>{bazaarProject.name}</b>,
|
||||
' from the Bazaar?',
|
||||
]}
|
||||
type="delete"
|
||||
handleSubmit={handleDeleteSubmit}
|
||||
/>
|
||||
|
||||
<ProjectDialog
|
||||
title="Edit project"
|
||||
handleSave={handleEditSubmit}
|
||||
isAddForm={false}
|
||||
defaultValues={defaultValues}
|
||||
open={openEdit}
|
||||
handleClose={handleEditClose}
|
||||
deleteButton={
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
className={classes.button}
|
||||
onClick={() => {
|
||||
setOpenDelete(true);
|
||||
}}
|
||||
>
|
||||
Delete project
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,347 +15,48 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Grid,
|
||||
makeStyles,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Typography,
|
||||
Divider,
|
||||
IconButton,
|
||||
Popover,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
ListItemText,
|
||||
Link,
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
Progress,
|
||||
HeaderIconLinkRow,
|
||||
IconLinkVerticalProps,
|
||||
Avatar,
|
||||
} from '@backstage/core-components';
|
||||
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 } from '@backstage/core-plugin-api';
|
||||
import { Member, BazaarProject } from '../../types';
|
||||
import { bazaarApiRef } from '../../api';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
|
||||
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();
|
||||
};
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { bazaarApiRef } from '../../api';
|
||||
import { EntityBazaarInfoContent } from '../EntityBazaarInfoContent';
|
||||
import { Card } from '@material-ui/core';
|
||||
import { parseBazaarResponse } from '../../util/parseMethods';
|
||||
|
||||
export const EntityBazaarInfoCard = () => {
|
||||
const { entity } = useEntity();
|
||||
const classes = useStyles();
|
||||
const bazaarApi = useApi(bazaarApiRef);
|
||||
const identity = useApi(identityApiRef);
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const [openDelete, setOpenDelete] = useState(false);
|
||||
const [isMember, setIsMember] = useState(false);
|
||||
const [isBazaar, setIsBazaar] = useState(false);
|
||||
const [members, fetchMembers] = useAsyncFn(async () => {
|
||||
const response = await bazaarApi.getMembers(entity);
|
||||
|
||||
const dbMembers = response.data.map((obj: any) => {
|
||||
const member: Member = {
|
||||
userId: obj.user_id,
|
||||
entityRef: obj.entity_ref,
|
||||
joinDate: obj.join_date,
|
||||
picture: obj.picture,
|
||||
};
|
||||
|
||||
return member;
|
||||
});
|
||||
|
||||
dbMembers.sort(sortMembers);
|
||||
|
||||
return dbMembers;
|
||||
});
|
||||
|
||||
const [bazaarProject, fetchBazaarProject] = useAsyncFn(async () => {
|
||||
const response = await bazaarApi.getMetadata(entity);
|
||||
const response = await bazaarApi.getProjectByRef(
|
||||
stringifyEntityRef(entity),
|
||||
);
|
||||
|
||||
if (response) {
|
||||
const metadata = await response.json().then((resp: any) => resp.data[0]);
|
||||
|
||||
if (metadata) {
|
||||
return {
|
||||
entityRef: metadata.entity_ref,
|
||||
name: metadata.name,
|
||||
community: metadata.community,
|
||||
announcement: metadata.announcement,
|
||||
status: metadata.status,
|
||||
updatedAt: metadata.updated_at,
|
||||
membersCount: metadata.members_count,
|
||||
size: metadata.size,
|
||||
startDate: metadata.start_date,
|
||||
endDate: metadata.end_date,
|
||||
responsible: metadata.responsible,
|
||||
} as BazaarProject;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return await parseBazaarResponse(response);
|
||||
});
|
||||
|
||||
const [isBazaar, setIsBazaar] = useState(bazaarProject.value ?? false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMembers();
|
||||
fetchBazaarProject();
|
||||
}, [fetchMembers, fetchBazaarProject]);
|
||||
}, [fetchBazaarProject]);
|
||||
|
||||
useEffect(() => {
|
||||
const isBazaarMember =
|
||||
members?.value
|
||||
?.map((member: Member) => member.userId)
|
||||
.indexOf(identity.getUserId()) >= 0;
|
||||
const isBazaarProject = bazaarProject.value !== null;
|
||||
const isBazaarProject = bazaarProject.value !== undefined;
|
||||
|
||||
setIsMember(isBazaarMember);
|
||||
setIsBazaar(isBazaarProject);
|
||||
}, [bazaarProject, members, identity]);
|
||||
}, [bazaarProject.value]);
|
||||
|
||||
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 () => {
|
||||
if (!isMember) {
|
||||
await bazaarApi.addMember(entity);
|
||||
} else {
|
||||
await bazaarApi.deleteMember(entity);
|
||||
}
|
||||
|
||||
fetchMembers();
|
||||
fetchBazaarProject();
|
||||
};
|
||||
|
||||
const links: IconLinkVerticalProps[] = [
|
||||
{
|
||||
label: isMember ? 'Leave' : 'Join',
|
||||
icon: isMember ? <ExitToAppIcon /> : <PersonAddIcon />,
|
||||
href: '',
|
||||
onClick: async () => {
|
||||
handleMembersClick();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Community',
|
||||
icon: <ChatIcon />,
|
||||
href: bazaarProject?.value?.community,
|
||||
disabled: !bazaarProject?.value?.community || !isMember,
|
||||
},
|
||||
];
|
||||
|
||||
if (!isBazaar) {
|
||||
return null;
|
||||
} else if (bazaarProject.loading || members.loading) {
|
||||
return <Progress />;
|
||||
} else if (bazaarProject.error) {
|
||||
return <Alert severity="error">{bazaarProject?.error?.message}</Alert>;
|
||||
} else if (members.error) {
|
||||
return <Alert severity="error">{members?.error?.message}</Alert>;
|
||||
}
|
||||
return (
|
||||
<Card>
|
||||
{bazaarProject?.value && (
|
||||
<EditProjectDialog
|
||||
open={open}
|
||||
entity={entity}
|
||||
if (isBazaar) {
|
||||
return (
|
||||
<Card>
|
||||
<EntityBazaarInfoContent
|
||||
bazaarProject={bazaarProject.value}
|
||||
fetchBazaarProject={fetchBazaarProject}
|
||||
handleClose={closeEdit}
|
||||
isAddForm={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{bazaarProject?.value && (
|
||||
<DeleteProjectDialog
|
||||
bazaarProject={bazaarProject.value}
|
||||
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="Edit project" />
|
||||
</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={10}>
|
||||
<AboutField label="Announcement">
|
||||
{bazaarProject?.value?.announcement
|
||||
? bazaarProject?.value?.announcement
|
||||
.split('\n')
|
||||
.map((str: string, i: number) => (
|
||||
<Typography
|
||||
key={i}
|
||||
variant="body2"
|
||||
paragraph
|
||||
className={classes.description}
|
||||
>
|
||||
{str}
|
||||
</Typography>
|
||||
))
|
||||
: 'No announcement'}
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid style={{ marginRight: '0rem' }}>
|
||||
{' '}
|
||||
<AboutField label="Latest members">
|
||||
{members?.value?.length ? (
|
||||
members.value.slice(0, 7).map((member: Member) => {
|
||||
return (
|
||||
<div key={member.userId}>
|
||||
<Avatar
|
||||
displayName={member.userId}
|
||||
customStyles={{
|
||||
width: '19px',
|
||||
height: '19px',
|
||||
fontSize: '8px',
|
||||
float: 'left',
|
||||
marginRight: '0.3rem',
|
||||
marginBottom: '0.25rem',
|
||||
}}
|
||||
picture={member.picture}
|
||||
/>
|
||||
<Link
|
||||
className={classes.memberLink}
|
||||
href={`http://github.com/${member.userId}`}
|
||||
target="_blank"
|
||||
>
|
||||
{member?.userId}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="Status">
|
||||
<StatusTag status={bazaarProject?.value?.status || 'proposed'} />
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="size">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject?.value?.size}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="Start date">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject?.value?.startDate?.substring(0, 10) || ''}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="End date">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject?.value?.endDate?.substring(0, 10) || ''}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<AboutField label="Responsible">
|
||||
<Typography variant="body2">
|
||||
{bazaarProject?.value?.responsible || ''}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
import { CardHeader, Divider, IconButton } from '@material-ui/core';
|
||||
import {
|
||||
HeaderIconLinkRow,
|
||||
IconLinkVerticalProps,
|
||||
} from '@backstage/core-components';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import ChatIcon from '@material-ui/icons/Chat';
|
||||
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
||||
import DashboardIcon from '@material-ui/icons/Dashboard';
|
||||
import LinkOffIcon from '@material-ui/icons/LinkOff';
|
||||
import { EditProjectDialog } from '../EditProjectDialog';
|
||||
import { useApi, identityApiRef } from '@backstage/core-plugin-api';
|
||||
import { BazaarProject, Member } from '../../types';
|
||||
import { bazaarApiRef } from '../../api';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
|
||||
import { parseEntityRef } from '@backstage/catalog-model';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { CardContentFields } from '../CardContentFields';
|
||||
import { fetchProjectMembers } from '../../util/fetchMethods';
|
||||
|
||||
type Props = {
|
||||
bazaarProject: BazaarProject | null | undefined;
|
||||
fetchBazaarProject: () => Promise<BazaarProject | null>;
|
||||
};
|
||||
|
||||
export const EntityBazaarInfoContent = ({
|
||||
bazaarProject,
|
||||
fetchBazaarProject,
|
||||
}: Props) => {
|
||||
const bazaarApi = useApi(bazaarApiRef);
|
||||
const identity = useApi(identityApiRef);
|
||||
const [openEdit, setOpenEdit] = useState(false);
|
||||
const [isMember, setIsMember] = useState(false);
|
||||
const [openUnlink, setOpenUnlink] = useState(false);
|
||||
const [members, fetchMembers] = useAsyncFn(async () => {
|
||||
return bazaarProject
|
||||
? await fetchProjectMembers(bazaarApi, bazaarProject)
|
||||
: [];
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchMembers();
|
||||
}, [fetchMembers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (members.value) {
|
||||
const isBazaarMember =
|
||||
members.value
|
||||
?.map((member: Member) => member.userId)
|
||||
.indexOf(identity.getUserId()) >= 0;
|
||||
|
||||
setIsMember(isBazaarMember);
|
||||
}
|
||||
}, [bazaarProject, members, identity]);
|
||||
|
||||
const handleMembersClick = async () => {
|
||||
if (!isMember) {
|
||||
await bazaarApi.addMember(bazaarProject?.id!, identity.getUserId());
|
||||
} else {
|
||||
await bazaarApi.deleteMember(bazaarProject!.id, identity.getUserId());
|
||||
}
|
||||
setIsMember(!isMember);
|
||||
fetchMembers();
|
||||
};
|
||||
|
||||
const links: IconLinkVerticalProps[] = [
|
||||
{
|
||||
label: 'Entity page',
|
||||
icon: <DashboardIcon />,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: 'Unlink project',
|
||||
icon: <LinkOffIcon />,
|
||||
disabled: false,
|
||||
onClick: () => {
|
||||
setOpenUnlink(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: isMember ? 'Leave' : 'Join',
|
||||
icon: isMember ? <ExitToAppIcon /> : <PersonAddIcon />,
|
||||
href: '',
|
||||
onClick: async () => {
|
||||
handleMembersClick();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Community',
|
||||
icon: <ChatIcon />,
|
||||
href: bazaarProject?.community,
|
||||
disabled: bazaarProject?.community === '' || !isMember,
|
||||
},
|
||||
];
|
||||
|
||||
const handleEditClose = () => {
|
||||
setOpenEdit(false);
|
||||
};
|
||||
|
||||
const handleUnlinkClose = () => {
|
||||
setOpenUnlink(false);
|
||||
};
|
||||
|
||||
const handleUnlinkSubmit = async () => {
|
||||
const updateResponse = await bazaarApi.updateProject({
|
||||
...bazaarProject,
|
||||
entityRef: null,
|
||||
});
|
||||
|
||||
if (updateResponse.status === 'ok') {
|
||||
handleUnlinkClose();
|
||||
fetchBazaarProject();
|
||||
}
|
||||
};
|
||||
|
||||
if (members.error) {
|
||||
return <Alert severity="error">{members?.error?.message}</Alert>;
|
||||
}
|
||||
|
||||
if (bazaarProject) {
|
||||
return (
|
||||
<div>
|
||||
<EditProjectDialog
|
||||
bazaarProject={bazaarProject!}
|
||||
openEdit={openEdit}
|
||||
handleEditClose={handleEditClose}
|
||||
fetchBazaarProject={fetchBazaarProject}
|
||||
/>
|
||||
|
||||
{openUnlink && (
|
||||
<ConfirmationDialog
|
||||
open={openUnlink}
|
||||
handleClose={handleUnlinkClose}
|
||||
message={[
|
||||
'Are you sure you want to unlink ',
|
||||
<b>{parseEntityRef(bazaarProject.entityRef!).name}</b>,
|
||||
' from ',
|
||||
<b>{bazaarProject.name}</b>,
|
||||
' ?',
|
||||
]}
|
||||
type="unlink"
|
||||
handleSubmit={handleUnlinkSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CardHeader
|
||||
title={bazaarProject?.name!}
|
||||
action={
|
||||
<div>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setOpenEdit(true);
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
}
|
||||
subheader={<HeaderIconLinkRow links={links} />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<CardContentFields
|
||||
bazaarProject={bazaarProject}
|
||||
members={members.value || []}
|
||||
descriptionSize={10}
|
||||
membersSize={2}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -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 { EntityBazaarInfoContent } from './EntityBazaarInfoContent';
|
||||
@@ -36,11 +36,7 @@ export const HomePage = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header
|
||||
data-testid="bazaar-header"
|
||||
title="Bazaar"
|
||||
subtitle="Marketplace for inner source projects"
|
||||
/>
|
||||
<Header title="Bazaar" subtitle="Marketplace for inner source projects" />
|
||||
<RoutedTabs routes={tabContent} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
import { Card, CardHeader, Divider, IconButton } from '@material-ui/core';
|
||||
import {
|
||||
HeaderIconLinkRow,
|
||||
IconLinkVerticalProps,
|
||||
} from '@backstage/core-components';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import ChatIcon from '@material-ui/icons/Chat';
|
||||
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
||||
import InsertLinkIcon from '@material-ui/icons/InsertLink';
|
||||
import DashboardIcon from '@material-ui/icons/Dashboard';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import LinkOffIcon from '@material-ui/icons/LinkOff';
|
||||
import { EditProjectDialog } from '../EditProjectDialog';
|
||||
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
|
||||
import {
|
||||
useApi,
|
||||
identityApiRef,
|
||||
useRouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { Member, BazaarProject } from '../../types';
|
||||
import { bazaarApiRef } from '../../api';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
import {
|
||||
catalogApiRef,
|
||||
catalogRouteRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
|
||||
import {
|
||||
parseEntityName,
|
||||
stringifyEntityRef,
|
||||
Entity,
|
||||
parseEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog/ConfirmationDialog';
|
||||
import { CardContentFields } from '../CardContentFields/CardContentFields';
|
||||
|
||||
import { LinkProjectDialog } from '../LinkProjectDialog';
|
||||
import {
|
||||
fetchCatalogItems,
|
||||
fetchProjectMembers,
|
||||
} from '../../util/fetchMethods';
|
||||
import { parseBazaarResponse } from '../../util/parseMethods';
|
||||
|
||||
type Props = {
|
||||
initProject: BazaarProject;
|
||||
fetchBazaarProjects: () => Promise<BazaarProject[]>;
|
||||
handleClose: () => void;
|
||||
initEntity: Entity;
|
||||
};
|
||||
|
||||
export const HomePageBazaarInfoCard = ({
|
||||
initProject,
|
||||
fetchBazaarProjects,
|
||||
handleClose,
|
||||
initEntity,
|
||||
}: Props) => {
|
||||
const catalogLink = useRouteRef(catalogRouteRef);
|
||||
const bazaarApi = useApi(bazaarApiRef);
|
||||
const identity = useApi(identityApiRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [openEdit, setOpenEdit] = useState(false);
|
||||
const [openProjectSelector, setOpenProjectSelector] = useState(false);
|
||||
const [openUnlink, setOpenUnlink] = useState(false);
|
||||
const [isMember, setIsMember] = useState(false);
|
||||
|
||||
const [catalogEntities, fetchCatalogEntities] = useAsyncFn(async () => {
|
||||
const entities = await fetchCatalogItems(catalogApi);
|
||||
const bazaarProjects = await bazaarApi.getProjects();
|
||||
const bazaarLinkedRefs: string[] = bazaarProjects.data
|
||||
.filter((entity: any) => entity.entity_ref !== null)
|
||||
.map((entity: any) => entity.entity_ref);
|
||||
|
||||
return entities.filter(
|
||||
(entity: Entity) =>
|
||||
!bazaarLinkedRefs.includes(stringifyEntityRef(entity)),
|
||||
);
|
||||
});
|
||||
|
||||
const [bazaarProject, fetchBazaarProject] = useAsyncFn(async () => {
|
||||
const response = await bazaarApi.getProjectById(initProject.id);
|
||||
return await parseBazaarResponse(response);
|
||||
});
|
||||
|
||||
const [members, fetchMembers] = useAsyncFn(async () => {
|
||||
return fetchProjectMembers(bazaarApi, bazaarProject.value ?? initProject);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchMembers();
|
||||
fetchBazaarProject();
|
||||
fetchCatalogEntities();
|
||||
}, [fetchMembers, fetchBazaarProject, fetchCatalogEntities]);
|
||||
|
||||
useEffect(() => {
|
||||
if (members.value) {
|
||||
const isBazaarMember =
|
||||
members?.value
|
||||
?.map((member: Member) => member.userId)
|
||||
.indexOf(identity.getUserId()) >= 0;
|
||||
setIsMember(isBazaarMember);
|
||||
}
|
||||
}, [bazaarProject.value, members, identity]);
|
||||
|
||||
const handleMembersClick = async () => {
|
||||
if (!isMember) {
|
||||
await bazaarApi.addMember(bazaarProject.value!.id, identity.getUserId());
|
||||
} else {
|
||||
await bazaarApi.deleteMember(
|
||||
bazaarProject.value!.id,
|
||||
identity.getUserId(),
|
||||
);
|
||||
}
|
||||
setIsMember(!isMember);
|
||||
fetchMembers();
|
||||
};
|
||||
|
||||
const getEntityPageLink = () => {
|
||||
if (bazaarProject?.value?.entityRef) {
|
||||
const { name, kind, namespace } = parseEntityName(
|
||||
bazaarProject.value.entityRef,
|
||||
);
|
||||
return `${catalogLink()}/${namespace}/${kind}/${name}`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const handleLink = () => {
|
||||
if (bazaarProject.value?.entityRef) {
|
||||
setOpenUnlink(true);
|
||||
} else {
|
||||
fetchCatalogEntities();
|
||||
setOpenProjectSelector(true);
|
||||
}
|
||||
};
|
||||
|
||||
const links: IconLinkVerticalProps[] = [
|
||||
{
|
||||
label: 'Entity page',
|
||||
icon: <DashboardIcon />,
|
||||
href: bazaarProject.value?.entityRef ? getEntityPageLink() : '',
|
||||
disabled: bazaarProject.value?.entityRef === null,
|
||||
},
|
||||
{
|
||||
label: bazaarProject.value?.entityRef ? 'Unlink project' : 'Link project',
|
||||
icon: bazaarProject.value?.entityRef ? (
|
||||
<LinkOffIcon />
|
||||
) : (
|
||||
<InsertLinkIcon />
|
||||
),
|
||||
onClick: handleLink,
|
||||
},
|
||||
{
|
||||
label: isMember ? 'Leave' : 'Join',
|
||||
icon: isMember ? <ExitToAppIcon /> : <PersonAddIcon />,
|
||||
href: '',
|
||||
onClick: async () => {
|
||||
handleMembersClick();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Community',
|
||||
icon: <ChatIcon />,
|
||||
href: bazaarProject.value?.community,
|
||||
disabled: !bazaarProject.value?.community || !isMember,
|
||||
},
|
||||
];
|
||||
|
||||
const handleUnlinkSubmit = async () => {
|
||||
const updateResponse = await bazaarApi.updateProject({
|
||||
...bazaarProject.value,
|
||||
entityRef: null,
|
||||
});
|
||||
|
||||
if (updateResponse.status === 'ok') {
|
||||
setOpenUnlink(false);
|
||||
fetchBazaarProject();
|
||||
}
|
||||
};
|
||||
|
||||
if (bazaarProject.error) {
|
||||
return <Alert severity="error">{bazaarProject?.error?.message}</Alert>;
|
||||
} else if (members.error) {
|
||||
return <Alert severity="error">{members?.error?.message}</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LinkProjectDialog
|
||||
openProjectSelector={openProjectSelector}
|
||||
handleProjectSelectorClose={() => setOpenProjectSelector(false)}
|
||||
catalogEntities={catalogEntities.value || []}
|
||||
bazaarProject={bazaarProject.value || initProject}
|
||||
fetchBazaarProject={fetchBazaarProject}
|
||||
initEntity={initEntity}
|
||||
/>
|
||||
|
||||
{openUnlink && (
|
||||
<ConfirmationDialog
|
||||
open={openUnlink}
|
||||
handleClose={() => setOpenUnlink(false)}
|
||||
message={[
|
||||
'Are you sure you want to unlink ',
|
||||
<b>{parseEntityRef(bazaarProject.value?.entityRef!).name}</b>,
|
||||
' from ',
|
||||
<b>{bazaarProject.value?.name}</b>,
|
||||
' ?',
|
||||
]}
|
||||
type="unlink"
|
||||
handleSubmit={handleUnlinkSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<EditProjectDialog
|
||||
bazaarProject={bazaarProject.value || initProject}
|
||||
openEdit={openEdit}
|
||||
handleEditClose={() => setOpenEdit(false)}
|
||||
handleCardClose={handleClose}
|
||||
fetchBazaarProject={fetchBazaarProject}
|
||||
fetchBazaarProjects={fetchBazaarProjects}
|
||||
/>
|
||||
|
||||
<CardHeader
|
||||
title={bazaarProject.value?.name || initProject.name}
|
||||
action={
|
||||
<div>
|
||||
<IconButton onClick={() => setOpenEdit(true)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={handleClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
}
|
||||
subheader={<HeaderIconLinkRow links={links} />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<CardContentFields
|
||||
bazaarProject={bazaarProject.value || initProject}
|
||||
members={members.value || []}
|
||||
descriptionSize={9}
|
||||
membersSize={3}
|
||||
/>
|
||||
</Card>
|
||||
</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 { HomePageBazaarInfoCard } from './HomePageBazaarInfoCard';
|
||||
@@ -25,7 +25,7 @@ type Rules = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
inputType: 'announcement' | 'community' | 'responsible';
|
||||
inputType: 'description' | 'community' | 'responsible' | 'name';
|
||||
error?: FieldError | undefined;
|
||||
control: Control<FormValues, object>;
|
||||
helperText?: string;
|
||||
|
||||
@@ -60,12 +60,7 @@ export const InputSelector = ({ name, options, control, error }: Props) => {
|
||||
>
|
||||
{options.map(option => {
|
||||
return (
|
||||
<MenuItem
|
||||
data-testid="menu-item"
|
||||
button
|
||||
key={option}
|
||||
value={option}
|
||||
>
|
||||
<MenuItem button key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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 {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
Button,
|
||||
DialogContent,
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import { ProjectSelector } from '../ProjectSelector';
|
||||
import { CustomDialogTitle } from '../CustomDialogTitle';
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
import { bazaarApiRef } from '../../api';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
import { BazaarProject } from '../../types';
|
||||
|
||||
type Props = {
|
||||
openProjectSelector: boolean;
|
||||
handleProjectSelectorClose: () => void;
|
||||
catalogEntities: Entity[];
|
||||
bazaarProject: BazaarProject;
|
||||
fetchBazaarProject: () => Promise<BazaarProject | null>;
|
||||
initEntity: Entity;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
content: { padding: '0 1rem' },
|
||||
});
|
||||
|
||||
export const LinkProjectDialog = ({
|
||||
openProjectSelector,
|
||||
handleProjectSelectorClose,
|
||||
catalogEntities,
|
||||
bazaarProject,
|
||||
fetchBazaarProject,
|
||||
initEntity,
|
||||
}: Props) => {
|
||||
const classes = useStyles();
|
||||
const bazaarApi = useApi(bazaarApiRef);
|
||||
const [selectedEntity, setSelectedEntity] = useState(initEntity);
|
||||
const handleEntityClick = (entity: Entity) => {
|
||||
setSelectedEntity(entity);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
handleProjectSelectorClose();
|
||||
|
||||
const updateResponse = await bazaarApi.updateProject({
|
||||
...bazaarProject,
|
||||
entityRef: stringifyEntityRef(selectedEntity!),
|
||||
});
|
||||
if (updateResponse.status === 'ok') fetchBazaarProject();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog onClose={handleProjectSelectorClose} open={openProjectSelector}>
|
||||
<CustomDialogTitle
|
||||
id="customized-dialog-title"
|
||||
onClose={handleProjectSelectorClose}
|
||||
>
|
||||
Select entity
|
||||
</CustomDialogTitle>
|
||||
<DialogContent className={classes.content} dividers>
|
||||
<ProjectSelector
|
||||
label=""
|
||||
onChange={handleEntityClick}
|
||||
catalogEntities={catalogEntities || []}
|
||||
disableClearable
|
||||
defaultValue={catalogEntities[0] || null}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleSubmit} color="primary" type="submit">
|
||||
OK
|
||||
</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 { LinkProjectDialog } from './LinkProjectDialog';
|
||||
@@ -14,85 +14,95 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { ItemCardHeader } from '@backstage/core-components';
|
||||
import {
|
||||
Card,
|
||||
CardActionArea,
|
||||
CardContent,
|
||||
Dialog,
|
||||
makeStyles,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { StatusTag } from '../StatusTag/StatusTag';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { catalogRouteRef } from '@backstage/plugin-catalog-react';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { BazaarProject } from '../../types';
|
||||
import { parseEntityName } from '@backstage/catalog-model';
|
||||
import { DateTime } from 'luxon';
|
||||
import { HomePageBazaarInfoCard } from '../HomePageBazaarInfoCard';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
statusTag: {
|
||||
display: 'inline-block',
|
||||
whiteSpace: 'nowrap',
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
announcement: {
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 5,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
marginBottom: '0.8rem',
|
||||
},
|
||||
description: {
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 7,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '',
|
||||
},
|
||||
memberCount: {
|
||||
float: 'right',
|
||||
},
|
||||
content: { height: '13rem', marginBottom: '-0.5rem' },
|
||||
});
|
||||
|
||||
type Props = {
|
||||
bazaarProject: BazaarProject;
|
||||
project: BazaarProject;
|
||||
fetchBazaarProjects: () => Promise<BazaarProject[]>;
|
||||
catalogEntities: Entity[];
|
||||
};
|
||||
|
||||
export const ProjectCard = ({ bazaarProject }: Props) => {
|
||||
export const ProjectCard = ({
|
||||
project,
|
||||
fetchBazaarProjects,
|
||||
catalogEntities,
|
||||
}: Props) => {
|
||||
const classes = useStyles();
|
||||
const { entityRef, name, status, updatedAt, announcement, membersCount } =
|
||||
bazaarProject;
|
||||
const catalogLink = useRouteRef(catalogRouteRef);
|
||||
const { namespace, kind } = parseEntityName(entityRef);
|
||||
const [openCard, setOpenCard] = useState(false);
|
||||
const { id, name, status, updatedAt, description, membersCount } = project;
|
||||
|
||||
const handleClose = () => {
|
||||
setOpenCard(false);
|
||||
fetchBazaarProjects();
|
||||
};
|
||||
|
||||
return (
|
||||
<Card key={entityRef as string}>
|
||||
<CardActionArea
|
||||
style={{
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
}}
|
||||
component={RouterLink}
|
||||
to={`${catalogLink()}/${namespace}/${kind}/${name}`}
|
||||
>
|
||||
<ItemCardHeader
|
||||
title={name}
|
||||
subtitle={`updated ${DateTime.fromISO(
|
||||
new Date(updatedAt!).toISOString(),
|
||||
).toRelative({
|
||||
base: DateTime.now(),
|
||||
})}`}
|
||||
<div>
|
||||
<Dialog fullWidth onClose={handleClose} open={openCard}>
|
||||
<HomePageBazaarInfoCard
|
||||
initProject={project}
|
||||
fetchBazaarProjects={fetchBazaarProjects}
|
||||
handleClose={handleClose}
|
||||
initEntity={catalogEntities[0] || null}
|
||||
/>
|
||||
<CardContent style={{ height: '12rem' }}>
|
||||
<StatusTag styles={classes.statusTag} status={status} />
|
||||
<Typography variant="body2" className={classes.memberCount}>
|
||||
{membersCount === 1
|
||||
? `${membersCount} member`
|
||||
: `${membersCount} members`}
|
||||
</Typography>
|
||||
<div style={{ minHeight: '6.5rem', maxHeight: '6.5rem' }}>
|
||||
<Typography variant="body2" className={classes.announcement}>
|
||||
{announcement}
|
||||
</Dialog>
|
||||
|
||||
<Card key={id}>
|
||||
<CardActionArea onClick={() => setOpenCard(true)}>
|
||||
<ItemCardHeader
|
||||
title={name}
|
||||
subtitle={`updated ${DateTime.fromISO(
|
||||
new Date(updatedAt!).toISOString(),
|
||||
).toRelative({
|
||||
base: DateTime.now(),
|
||||
})}`}
|
||||
/>
|
||||
<CardContent className={classes.content}>
|
||||
<StatusTag styles={classes.statusTag} status={status} />
|
||||
<Typography variant="body2" className={classes.memberCount}>
|
||||
{Number(membersCount) === Number(1)
|
||||
? `${membersCount} member`
|
||||
: `${membersCount} members`}
|
||||
</Typography>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
<Typography variant="body2" className={classes.description}>
|
||||
{description}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,17 +15,7 @@
|
||||
*/
|
||||
|
||||
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 { Button, Dialog } from '@material-ui/core';
|
||||
import {
|
||||
useForm,
|
||||
SubmitHandler,
|
||||
@@ -36,61 +26,11 @@ import { InputField } from '../InputField/InputField';
|
||||
import { InputSelector } from '../InputSelector/InputSelector';
|
||||
import { FormValues } from '../../types';
|
||||
import { DoubleDateSelector } from '../DoubleDateSelector/DoubleDateSelector';
|
||||
|
||||
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);
|
||||
import {
|
||||
CustomDialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
} from '../CustomDialogTitle';
|
||||
|
||||
type Props = {
|
||||
handleSave: (
|
||||
@@ -102,6 +42,7 @@ type Props = {
|
||||
defaultValues: FormValues;
|
||||
open: boolean;
|
||||
projectSelector?: JSX.Element;
|
||||
deleteButton?: JSX.Element;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
@@ -112,6 +53,7 @@ export const ProjectDialog = ({
|
||||
defaultValues,
|
||||
open,
|
||||
projectSelector,
|
||||
deleteButton,
|
||||
handleClose,
|
||||
}: Props) => {
|
||||
const {
|
||||
@@ -123,16 +65,16 @@ export const ProjectDialog = ({
|
||||
setValue,
|
||||
} = useForm<FormValues>({
|
||||
mode: 'onChange',
|
||||
defaultValues: defaultValues,
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const handleCloseAndClear = () => {
|
||||
handleClose();
|
||||
reset(defaultValues);
|
||||
const handleSaveForm = () => {
|
||||
handleSave(getValues, reset);
|
||||
};
|
||||
|
||||
const handleSaveProject = () => {
|
||||
handleSave(getValues, reset);
|
||||
const handleCloseDialog = () => {
|
||||
handleClose();
|
||||
reset(defaultValues);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -140,24 +82,36 @@ export const ProjectDialog = ({
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
onClose={handleCloseAndClear}
|
||||
onClose={handleCloseDialog}
|
||||
aria-labelledby="customized-dialog-title"
|
||||
open={open}
|
||||
>
|
||||
<DialogTitle id="customized-dialog-title" onClose={handleCloseAndClear}>
|
||||
<CustomDialogTitle
|
||||
id="customized-dialog-title"
|
||||
onClose={handleCloseDialog}
|
||||
>
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{isAddForm && projectSelector}
|
||||
</CustomDialogTitle>
|
||||
<DialogContent style={{ padding: '1rem', paddingTop: '0rem' }} dividers>
|
||||
<InputField
|
||||
error={errors.name}
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: RegExp('^[a-zA-Z0-9_-]*$'),
|
||||
}}
|
||||
inputType="name"
|
||||
helperText="please enter a url safe project name"
|
||||
/>
|
||||
|
||||
<InputField
|
||||
error={errors.announcement}
|
||||
error={errors.description}
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
}}
|
||||
inputType="announcement"
|
||||
helperText="please enter an announcement"
|
||||
inputType="description"
|
||||
helperText="please enter a description"
|
||||
placeholder="Describe who you are and what skills you are looking for"
|
||||
/>
|
||||
|
||||
@@ -184,7 +138,7 @@ export const ProjectDialog = ({
|
||||
placeholder="Contact person of the project"
|
||||
/>
|
||||
|
||||
<DoubleDateSelector setValue={setValue} control={control} />
|
||||
{isAddForm && projectSelector}
|
||||
|
||||
<InputField
|
||||
error={errors.community}
|
||||
@@ -197,11 +151,14 @@ export const ProjectDialog = ({
|
||||
helperText="please enter a link starting with http/https"
|
||||
placeholder="Community link to e.g. Teams or Discord"
|
||||
/>
|
||||
|
||||
<DoubleDateSelector setValue={setValue} control={control} />
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
{!isAddForm && deleteButton}
|
||||
<Button
|
||||
onClick={handleSubmit(handleSaveProject)}
|
||||
onClick={handleSubmit(handleSaveForm)}
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
|
||||
@@ -14,15 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { ChangeEvent, useState } from 'react';
|
||||
import { Content } from '@backstage/core-components';
|
||||
import { ProjectCard } from '../ProjectCard/ProjectCard';
|
||||
import { makeStyles, Grid, TablePagination } from '@material-ui/core';
|
||||
import { BazaarProject } from '../../types';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
type Props = {
|
||||
bazaarProjects: BazaarProject[];
|
||||
sortingMethod: (arg0: BazaarProject, arg1: BazaarProject) => number;
|
||||
fetchBazaarProjects: () => Promise<BazaarProject[]>;
|
||||
catalogEntities: Entity[];
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@@ -32,75 +34,75 @@ const useStyles = makeStyles({
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
},
|
||||
item: {
|
||||
minWidth: '20.5rem',
|
||||
maxWidth: '20.5rem',
|
||||
width: '20%',
|
||||
empty: {
|
||||
height: '10rem',
|
||||
textAlign: 'center',
|
||||
verticalAlign: 'middle',
|
||||
lineHeight: '10rem',
|
||||
},
|
||||
pagination: {
|
||||
marginTop: '1rem',
|
||||
marginLeft: 'auto',
|
||||
marginRight: '0',
|
||||
},
|
||||
});
|
||||
|
||||
export const ProjectPreview = ({ bazaarProjects, sortingMethod }: Props) => {
|
||||
export const ProjectPreview = ({
|
||||
bazaarProjects,
|
||||
fetchBazaarProjects,
|
||||
catalogEntities,
|
||||
}: Props) => {
|
||||
const classes = useStyles();
|
||||
const [page, setPage] = useState(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [rows, setRows] = useState(12);
|
||||
|
||||
const handleChangePage = (_: any, newPage: number) => {
|
||||
const handlePageChange = (_: any, newPage: number) => {
|
||||
setPage(newPage + 1);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
const handleRowChange = (
|
||||
event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setRows(parseInt(event.target.value, 10));
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
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>
|
||||
<div className={classes.empty}>Please add projects to the Bazaar.</div>
|
||||
);
|
||||
}
|
||||
|
||||
bazaarProjects.sort(sortingMethod);
|
||||
|
||||
return (
|
||||
<Content className={classes.content} noPadding>
|
||||
<Grid wrap="wrap" container spacing={3}>
|
||||
{bazaarProjects
|
||||
.slice((page - 1) * rowsPerPage, rowsPerPage * page)
|
||||
.slice((page - 1) * rows, rows * page)
|
||||
.map((bazaarProject: BazaarProject, i: number) => {
|
||||
return (
|
||||
<Grid key={i} className={classes.item} item xs={3}>
|
||||
<ProjectCard bazaarProject={bazaarProject} key={i} />
|
||||
<Grid key={i} item xs={2}>
|
||||
<ProjectCard
|
||||
project={bazaarProject}
|
||||
key={i}
|
||||
fetchBazaarProjects={fetchBazaarProjects}
|
||||
catalogEntities={catalogEntities}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
<TablePagination
|
||||
className={classes.pagination}
|
||||
rowsPerPageOptions={[12, 24, 48, 96]}
|
||||
count={bazaarProjects?.length}
|
||||
page={page - 1}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
onPageChange={handlePageChange}
|
||||
rowsPerPage={rows}
|
||||
onRowsPerPageChange={handleRowChange}
|
||||
backIconButtonProps={{ disabled: page === 1 }}
|
||||
nextIconButtonProps={{
|
||||
disabled: rowsPerPage * page >= bazaarProjects.length,
|
||||
}}
|
||||
style={{
|
||||
marginTop: '1rem',
|
||||
marginLeft: 'auto',
|
||||
marginRight: '0',
|
||||
disabled: rows * page >= bazaarProjects.length,
|
||||
}}
|
||||
/>
|
||||
</Content>
|
||||
|
||||
@@ -17,38 +17,44 @@
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Autocomplete } from '@material-ui/lab';
|
||||
import { TextField } from '@material-ui/core';
|
||||
import { TextField, makeStyles } from '@material-ui/core';
|
||||
|
||||
type Props = {
|
||||
entities: Entity[];
|
||||
value: string;
|
||||
catalogEntities: Entity[];
|
||||
onChange: (entity: Entity) => void;
|
||||
isFormInvalid: boolean;
|
||||
disableClearable: boolean;
|
||||
defaultValue: Entity | null | undefined;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: { width: '100%', minWidth: '22rem' },
|
||||
autocomplete: { overflow: 'hidden' },
|
||||
});
|
||||
|
||||
export const ProjectSelector = ({
|
||||
entities,
|
||||
value,
|
||||
catalogEntities,
|
||||
onChange,
|
||||
isFormInvalid,
|
||||
disableClearable,
|
||||
defaultValue,
|
||||
label,
|
||||
}: Props) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Autocomplete
|
||||
defaultValue={entities[0]}
|
||||
options={entities}
|
||||
getOptionLabel={option => option?.metadata?.name}
|
||||
renderOption={option => <span>{option?.metadata?.name}</span>}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
error={isFormInvalid && value === ''}
|
||||
helperText={
|
||||
isFormInvalid && value === '' ? 'Please select a project' : ''
|
||||
}
|
||||
{...params}
|
||||
label="Select a project *"
|
||||
/>
|
||||
)}
|
||||
onChange={(_, data) => onChange(data!)}
|
||||
/>
|
||||
<div className={classes.container}>
|
||||
<Autocomplete
|
||||
className={classes.autocomplete}
|
||||
fullWidth
|
||||
disableClearable={disableClearable}
|
||||
defaultValue={defaultValue}
|
||||
options={catalogEntities}
|
||||
getOptionLabel={option => option?.metadata?.name}
|
||||
renderOption={option => <span>{option?.metadata?.name}</span>}
|
||||
renderInput={params => <TextField {...params} label={label} />}
|
||||
onChange={(_, data) => {
|
||||
onChange(data!);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 { FormControl, MenuItem, Select, makeStyles } from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
select: {
|
||||
fontSize: 'xx-large',
|
||||
fontWeight: 'bold',
|
||||
width: '16rem',
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
sortMethodNbr: number;
|
||||
handleSortMethodChange: any;
|
||||
};
|
||||
|
||||
export const SortMethodSelector = ({
|
||||
sortMethodNbr,
|
||||
handleSortMethodChange,
|
||||
}: Props) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<FormControl fullWidth>
|
||||
<Select
|
||||
className={classes.select}
|
||||
disableUnderline
|
||||
value={sortMethodNbr}
|
||||
onChange={handleSortMethodChange}
|
||||
>
|
||||
<MenuItem value={0}>Latest updated</MenuItem>
|
||||
<MenuItem value={1}>A-Z</MenuItem>
|
||||
<MenuItem value={2}>Most members</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 { SortMethodSelector } from './SortMethodSelector';
|
||||
@@ -14,17 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
Progress,
|
||||
} from '@backstage/core-components';
|
||||
import React, { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { Content, SupportButton } 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 { Button, makeStyles } from '@material-ui/core';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
@@ -32,14 +26,34 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { BazaarProject } from '../../types';
|
||||
import { bazaarApiRef } from '../../api';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import SearchBar from 'material-ui-search-bar';
|
||||
import { sortByDate, sortByMembers, sortByName } from '../../util/sortMethods';
|
||||
import { SortMethodSelector } from '../SortMethodSelector';
|
||||
import { fetchCatalogItems } from '../../util/fetchMethods';
|
||||
import { parseBazaarProject } from '../../util/parseMethods';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
button: { width: '12rem' },
|
||||
container: {
|
||||
marginTop: '2rem',
|
||||
},
|
||||
header: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
margin: '0 auto',
|
||||
marginBottom: '1.2rem',
|
||||
},
|
||||
search: {
|
||||
marginRight: '1rem',
|
||||
height: '2.5rem',
|
||||
width: '35rem',
|
||||
},
|
||||
});
|
||||
|
||||
const filterCatalogEntities = (
|
||||
const getUnlinkedCatalogEntities = (
|
||||
bazaarProjects: BazaarProject[],
|
||||
catalogEntities: Entity[],
|
||||
) => {
|
||||
@@ -47,86 +61,91 @@ const filterCatalogEntities = (
|
||||
(project: BazaarProject) => project.entityRef,
|
||||
);
|
||||
|
||||
const filtered = catalogEntities.filter((entity: Entity) => {
|
||||
return catalogEntities.filter((entity: Entity) => {
|
||||
return !bazaarProjectRefs?.includes(stringifyEntityRef(entity));
|
||||
});
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
export const SortView = () => {
|
||||
const classes = useStyles();
|
||||
const [openAdd, setOpenAdd] = useState(false);
|
||||
const [openNoProjects, setOpenNoProjects] = useState(false);
|
||||
const bazaarApi = useApi(bazaarApiRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [filteredCatalogEntites, setFilteredCatalogEntities] =
|
||||
const classes = useStyles();
|
||||
const sortMethods = [sortByDate, sortByName, sortByMembers];
|
||||
const [sortMethodNbr, setSortMethodNbr] = useState(0);
|
||||
const [openAdd, setOpenAdd] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [unlinkedCatalogEntities, setUnlinkedCatalogEntities] =
|
||||
useState<Entity[]>();
|
||||
|
||||
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 [catalogEntities, fetchCatalogEntities] = useAsyncFn(async () => {
|
||||
const entities = await catalogApi.getEntities({
|
||||
filter: {
|
||||
kind: ['Component', 'Resource'],
|
||||
},
|
||||
fields: ['kind', 'metadata.name', 'metadata.namespace'],
|
||||
});
|
||||
|
||||
return entities.items;
|
||||
return await fetchCatalogItems(catalogApi);
|
||||
});
|
||||
|
||||
const [bazaarProjects, fetchBazaarProjects] = useAsyncFn(async () => {
|
||||
const response = await bazaarApi.getEntities();
|
||||
const response = await bazaarApi.getProjects();
|
||||
const dbProjects: BazaarProject[] = [];
|
||||
|
||||
response.data.forEach((project: any) => {
|
||||
dbProjects.push({
|
||||
entityRef: project.entity_ref,
|
||||
name: project.name,
|
||||
status: project.status,
|
||||
announcement: project.announcement,
|
||||
community: project.community,
|
||||
updatedAt: project.updated_at,
|
||||
membersCount: project.members_count,
|
||||
size: project.size,
|
||||
startDate: project.startDate,
|
||||
endDate: project.endDate,
|
||||
responsible: project.responsible,
|
||||
});
|
||||
dbProjects.push(parseBazaarProject(project));
|
||||
});
|
||||
|
||||
return dbProjects;
|
||||
});
|
||||
|
||||
const catalogEntityRefs = catalogEntities.value?.map((project: Entity) =>
|
||||
stringifyEntityRef(project),
|
||||
);
|
||||
|
||||
const getSearchResults = () => {
|
||||
return bazaarProjects.value
|
||||
?.filter(project => project.name.includes(searchValue))
|
||||
.sort(sortMethods[sortMethodNbr]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const filterBrokenLinks = () => {
|
||||
if (catalogEntityRefs) {
|
||||
bazaarProjects.value?.forEach(async (project: BazaarProject) => {
|
||||
if (project.entityRef) {
|
||||
if (!catalogEntityRefs?.includes(project.entityRef as string)) {
|
||||
await bazaarApi.updateProject({
|
||||
...project,
|
||||
entityRef: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
filterBrokenLinks();
|
||||
}, [
|
||||
bazaarApi,
|
||||
bazaarProjects.value,
|
||||
catalogEntityRefs,
|
||||
catalogEntities.value,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCatalogEntities();
|
||||
fetchBazaarProjects();
|
||||
}, [fetchBazaarProjects, fetchCatalogEntities]);
|
||||
|
||||
useEffect(() => {
|
||||
const filteredCatalogEntities = filterCatalogEntities(
|
||||
const unlinkedCEntities = getUnlinkedCatalogEntities(
|
||||
bazaarProjects.value || [],
|
||||
catalogEntities.value || [],
|
||||
);
|
||||
|
||||
if (filteredCatalogEntities) {
|
||||
setFilteredCatalogEntities(filteredCatalogEntities);
|
||||
if (unlinkedCEntities) {
|
||||
setUnlinkedCatalogEntities(unlinkedCEntities);
|
||||
}
|
||||
}, [bazaarProjects, catalogEntities]);
|
||||
|
||||
if (catalogEntities.loading || bazaarProjects.loading) return <Progress />;
|
||||
const handleSortMethodChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setSortMethodNbr(
|
||||
typeof event.target.value === 'number' ? event.target.value : 0,
|
||||
);
|
||||
};
|
||||
|
||||
if (catalogEntities.error)
|
||||
return <Alert severity="error">{catalogEntities.error.message}</Alert>;
|
||||
@@ -136,38 +155,34 @@ export const SortView = () => {
|
||||
|
||||
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">
|
||||
<div className={classes.header}>
|
||||
<SortMethodSelector
|
||||
sortMethodNbr={sortMethodNbr}
|
||||
handleSortMethodChange={handleSortMethodChange}
|
||||
/>
|
||||
|
||||
<SearchBar
|
||||
className={classes.search}
|
||||
value={searchValue}
|
||||
onChange={newSortMethod => {
|
||||
setSearchValue(newSortMethod);
|
||||
}}
|
||||
onCancelSearch={() => {
|
||||
setSearchValue('');
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className={classes.button}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (filteredCatalogEntites?.length !== 0) {
|
||||
setOpenAdd(true);
|
||||
} else {
|
||||
setOpenNoProjects(true);
|
||||
}
|
||||
setOpenAdd(true);
|
||||
}}
|
||||
>
|
||||
Add project
|
||||
</Button>
|
||||
<AddProjectDialog
|
||||
catalogEntities={filteredCatalogEntites || []}
|
||||
catalogEntities={unlinkedCatalogEntities || []}
|
||||
handleClose={() => {
|
||||
setOpenAdd(false);
|
||||
}}
|
||||
@@ -176,10 +191,11 @@ export const SortView = () => {
|
||||
fetchCatalogEntities={fetchCatalogEntities}
|
||||
/>
|
||||
<SupportButton />
|
||||
</ContentHeader>
|
||||
</div>
|
||||
<ProjectPreview
|
||||
bazaarProjects={bazaarProjects.value || []}
|
||||
sortingMethod={compareProjectsByDate}
|
||||
bazaarProjects={getSearchResults() || []}
|
||||
fetchBazaarProjects={fetchBazaarProjects}
|
||||
catalogEntities={unlinkedCatalogEntities || []}
|
||||
/>
|
||||
<Content noPadding className={classes.container} />
|
||||
</Content>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { EntityRef } from '@backstage/catalog-model';
|
||||
|
||||
export type Member = {
|
||||
entityRef: EntityRef;
|
||||
itemId: number;
|
||||
userId: string;
|
||||
joinDate?: string;
|
||||
picture?: string;
|
||||
@@ -29,10 +29,11 @@ export type Size = 'small' | 'medium' | 'large';
|
||||
|
||||
export type BazaarProject = {
|
||||
name: string;
|
||||
entityRef: EntityRef;
|
||||
id: number;
|
||||
entityRef?: EntityRef;
|
||||
community: string;
|
||||
status: Status;
|
||||
announcement: string;
|
||||
description: string;
|
||||
updatedAt?: string;
|
||||
membersCount: number;
|
||||
size: Size;
|
||||
@@ -42,7 +43,8 @@ export type BazaarProject = {
|
||||
};
|
||||
|
||||
export type FormValues = {
|
||||
announcement: string;
|
||||
name: string;
|
||||
description: string;
|
||||
community: string;
|
||||
status: string;
|
||||
size: Size;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 { sortMembers } from './sortMethods';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { parseMember } from './parseMethods';
|
||||
import { BazaarProject, Member } from '../types';
|
||||
import { BazaarApi } from '../api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
export const fetchProjectMembers = async (
|
||||
bazaarApi: BazaarApi,
|
||||
project: BazaarProject,
|
||||
): Promise<Member[]> => {
|
||||
const response = await bazaarApi.getMembers(project.id);
|
||||
|
||||
if (response.data.length > 0) {
|
||||
const dbMembers = response.data.map((member: any) => {
|
||||
return parseMember(member);
|
||||
});
|
||||
|
||||
dbMembers.sort(sortMembers);
|
||||
return dbMembers;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const fetchCatalogItems = async (
|
||||
catalogApi: CatalogApi,
|
||||
): Promise<Entity[]> => {
|
||||
const entities = await catalogApi.getEntities({
|
||||
filter: {
|
||||
kind: ['Component', 'Resource'],
|
||||
},
|
||||
fields: ['kind', 'metadata.name', 'metadata.namespace'],
|
||||
});
|
||||
|
||||
return entities.items;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 { BazaarProject, Member } from '../types';
|
||||
|
||||
export const parseBazaarProject = (metadata: any): BazaarProject => {
|
||||
return {
|
||||
id: metadata.id,
|
||||
entityRef: metadata.entity_ref,
|
||||
name: metadata.name,
|
||||
community: metadata.community,
|
||||
description: metadata.description,
|
||||
status: metadata.status,
|
||||
updatedAt: metadata.updated_at,
|
||||
membersCount: metadata.members_count,
|
||||
size: metadata.size,
|
||||
startDate: metadata.start_date,
|
||||
endDate: metadata.end_date,
|
||||
responsible: metadata.responsible,
|
||||
} as BazaarProject;
|
||||
};
|
||||
|
||||
export const parseMember = (member: any): Member => {
|
||||
return {
|
||||
itemId: member.item_id,
|
||||
userId: member.user_id,
|
||||
joinDate: member.join_date,
|
||||
picture: member.picture,
|
||||
} as Member;
|
||||
};
|
||||
|
||||
export const parseBazaarResponse = async (response: any) => {
|
||||
if (response) {
|
||||
const metadata = await response.json().then((resp: any) => resp.data[0]);
|
||||
|
||||
if (metadata) {
|
||||
return parseBazaarProject(metadata);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 { BazaarProject, Member } from '../types';
|
||||
|
||||
export const sortMembers = (m1: Member, m2: Member) => {
|
||||
return new Date(m2.joinDate!).getTime() - new Date(m1.joinDate!).getTime();
|
||||
};
|
||||
|
||||
export const sortByDate = (a: BazaarProject, b: BazaarProject): number => {
|
||||
const dateA = new Date(a.updatedAt!).getTime();
|
||||
const dateB = new Date(b.updatedAt!).getTime();
|
||||
return dateB - dateA;
|
||||
};
|
||||
|
||||
export const sortByName = (a: BazaarProject, b: BazaarProject) => {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
} else if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const sortByMembers = (a: BazaarProject, b: BazaarProject) => {
|
||||
return b.membersCount - a.membersCount;
|
||||
};
|
||||
Reference in New Issue
Block a user