diff --git a/plugins/bulk-import/README.md b/plugins/bulk-import/README.md index 23e060e3e8..9493e6807b 100644 --- a/plugins/bulk-import/README.md +++ b/plugins/bulk-import/README.md @@ -2,6 +2,54 @@ This plugin allows bulk import of multiple catalog entities into the catalog. -## Getting started +--- -Coming soon. +**NOTE** + +The plugin work is still work in progress + +--- + +## For administrators + +### Installation + +#### Procedure + +1. Install the Bulk import UI plugin using the following command: + + ```console + yarn workspace app add @janus-idp/backstage-plugin-bulk-import + ``` + +2. Add Route in `packages/app/src/App.tsx`: + + ```tsx title="packages/app/src/App.tsx" + /* highlight-add-next-line */ + import { BulkImportPage } from '@janus-idp/backstage-plugin-bulk-import’; + + } + /> + } + /> + ``` + +3. Add **Bulk import** Sidebar Item in `packages/app/src/components/Root/Root.tsx`: + + ```tsx title="packages/app/src/components/Root/Root.tsx" + /* highlight-add-next-line */ + import { BulkImportIcon } from '@janus-idp/backstage-plugin-bulk-import’; + + export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + ... + + ... + + ); + ``` diff --git a/plugins/bulk-import/dev/index.tsx b/plugins/bulk-import/dev/index.tsx index af7533dfbc..7682da9417 100644 --- a/plugins/bulk-import/dev/index.tsx +++ b/plugins/bulk-import/dev/index.tsx @@ -9,6 +9,6 @@ createDevApp() .addPage({ element: , title: 'Bulk import', - path: '/bulk-import', + path: '/bulk-import/repositories', }) .render(); diff --git a/plugins/bulk-import/package.json b/plugins/bulk-import/package.json index b9d2d9764e..ce6bdfc0da 100644 --- a/plugins/bulk-import/package.json +++ b/plugins/bulk-import/package.json @@ -4,6 +4,7 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", + "private": true, "publishConfig": { "access": "public", "main": "dist/index.esm.js", @@ -33,6 +34,9 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.61", "@mui/icons-material": "5.14.11", + "@mui/material": "^5.12.2", + "lodash": "^4.17.21", + "formik": "^2.4.5", "react-use": "^17.2.4" }, "peerDependencies": { diff --git a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx new file mode 100644 index 0000000000..37a7acf859 --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx @@ -0,0 +1,99 @@ +import React from 'react'; + +import { makeStyles } from '@material-ui/core'; +import HelpIcon from '@mui/icons-material/HelpOutline'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import { FormikErrors } from 'formik'; + +import { AddRepositoriesFormValues } from '../../types'; +import { AddRepositoriesTable } from './AddRepositoriesTable'; + +const useStyles = makeStyles(theme => ({ + body: { + marginBottom: '50px', + }, + approvalTool: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'left', + alignItems: 'center', + paddingTop: '24px', + paddingBottom: '24px', + paddingLeft: '16px', + backgroundColor: theme.palette.background.paper, + borderBottomStyle: 'groove', + border: theme.palette.divider, + }, + + approvalToolTooltip: { + paddingTop: '4px', + paddingRight: '24px', + paddingLeft: '5px', + }, +})); + +export const AddRepositoriesForm = ({ + values, + setFieldValue, + setapprovalTool, +}: { + values: AddRepositoriesFormValues; + setFieldValue: ( + field: string, + value: any, + shouldValidate?: boolean | undefined, + ) => Promise | Promise>; + setapprovalTool: any; +}) => { + const styles = useStyles(); + + return ( + +
+ + + Approval tool + + + + + + + { + setapprovalTool(value); + setFieldValue('approvalTool', value); + }} + > + } label="Git" /> + } + label="ServiceNow" + /> + + + +
+
+
+ ); +}; diff --git a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesFormFooter.tsx b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesFormFooter.tsx new file mode 100644 index 0000000000..45a8ee649b --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesFormFooter.tsx @@ -0,0 +1,79 @@ +import React, { FormEvent } from 'react'; + +import { Link } from '@backstage/core-components'; + +import { makeStyles } from '@material-ui/core'; +import Button from '@mui/material/Button'; +import Tooltip from '@mui/material/Tooltip'; + +import { AddRepositoriesFormValues } from '../../types'; +import { getRepositoriesSelected } from '../../utils/repository-utils'; + +const useStyles = makeStyles(theme => ({ + createButton: { + marginRight: theme.spacing(1), + }, + illustration: { + flexDirection: 'row', + display: 'flex', + justifyContent: 'space-around', + overflow: 'scroll', + }, + tooltip: { + whiteSpace: 'nowrap', + }, + footer: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'left', + position: 'fixed', + bottom: 0, + paddingTop: '24px', + paddingBottom: '24px', + paddingLeft: '24px', + backgroundColor: theme.palette.background.paper, + width: '100%', + borderTopStyle: 'groove', + border: theme.palette.divider, + }, +})); + +export const AddRepositoriesFormFooter = ({ + approvalTool, + values, + handleSubmit, +}: { + approvalTool: string; + values: AddRepositoriesFormValues; + handleSubmit: (e?: FormEvent | undefined) => void; +}) => { + const styles = useStyles(); + const submitTitle = + (approvalTool === 'git' + ? 'Create pull request' + : 'Create ServiceNow ticket') + + (getRepositoriesSelected(values) > 1 ? 's' : ''); + + return ( +
+ + + + + + + + +
+ ); +}; diff --git a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesPage.tsx b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesPage.tsx new file mode 100644 index 0000000000..fad1e0d50e --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesPage.tsx @@ -0,0 +1,119 @@ +import React from 'react'; + +import { Content, Header, Page } from '@backstage/core-components'; + +import { makeStyles, useTheme } from '@material-ui/core'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import Accordion from '@mui/material/Accordion'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import Typography from '@mui/material/Typography'; +import { useFormik } from 'formik'; + +import { AddRepositoriesFormValues } from '../../types'; +import { AddRepositoriesForm } from './AddRepositoriesForm'; +import { AddRepositoriesFormFooter } from './AddRepositoriesFormFooter'; +import { Illustrations } from './Illustrations'; + +const useStyles = makeStyles(() => ({ + illustration: { + flexDirection: 'row', + display: 'flex', + justifyContent: 'space-around', + overflow: 'scroll', + }, +})); + +export const AddRepositoriesPage = () => { + const styles = useStyles(); + const theme = useTheme(); + const [approvalTool, setApprovalTool] = React.useState<'git' | 'servicenow'>( + 'git', + ); + const initialValues: AddRepositoriesFormValues = { + repositoryType: 'repository', + repositories: [], + organizations: [], + approvalTool: 'git', + }; + + const formik = useFormik({ + enableReinitialize: true, + initialValues, + onSubmit: async (_values: AddRepositoriesFormValues) => {}, + }); + + return ( + +
+ + + } + id="add-repository-summary" + > + + Add repositories to Red Hat Developer Hub in 5 steps + + + + + + + + + + +
+ +
+ + + ); +}; diff --git a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesSearchBar.tsx b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesSearchBar.tsx new file mode 100644 index 0000000000..891b879954 --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesSearchBar.tsx @@ -0,0 +1,60 @@ +import React from 'react'; + +import { makeStyles } from '@material-ui/core'; +import Clear from '@mui/icons-material/Clear'; +import Search from '@mui/icons-material/Search'; +import FormControl from '@mui/material/FormControl'; +import IconButton from '@mui/material/IconButton'; +import Input from '@mui/material/Input'; +import InputAdornment from '@mui/material/InputAdornment'; + +type RepositoriesSearchBarProps = { + value: string; + onChange: (filter: string) => void; +}; + +const useStyles = makeStyles({ + formControl: { + alignItems: 'flex-end', + flexGrow: 1, + paddingLeft: '21px', + }, +}); + +export const RepositoriesSearchBar = ({ + value, + onChange, +}: RepositoriesSearchBarProps) => { + const classes = useStyles(); + + return ( + + onChange(event.target.value)} + value={value} + size="medium" + startAdornment={ + + + + } + endAdornment={ + + onChange('')} + edge="end" + disabled={!value} + data-testid="clear-search" + > + + + + } + /> + + ); +}; diff --git a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesTable.tsx b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesTable.tsx new file mode 100644 index 0000000000..d2e92ec76d --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesTable.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; + +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import { FormikErrors } from 'formik'; + +import { AddRepositoriesFormValues } from '../../types'; +import { AddRepositoriesTableToolbar } from './AddRepositoriesTableToolbar'; +import { RepositoriesTable } from './RepositoriesTable'; + +export const AddRepositoriesTable = ({ + title, + selectedRepositoriesFormData, + setFieldValue, +}: { + title: string; + selectedRepositoriesFormData: AddRepositoriesFormValues; + setFieldValue: ( + field: string, + value: any, + shouldValidate?: boolean, + ) => Promise> | Promise; +}) => { + const [searchString, setSearchString] = React.useState(''); + const [page, setPage] = React.useState(0); + + return ( + + + + {selectedRepositoriesFormData.repositoryType === 'repository' ? ( + + ) : ( + + )} + + + ); +}; diff --git a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesTableToolbar.tsx b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesTableToolbar.tsx new file mode 100644 index 0000000000..328bc21637 --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesTableToolbar.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; + +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import { FormikErrors } from 'formik'; + +import { AddRepositoriesFormValues } from '../../types'; +import { getRepositoriesSelected } from '../../utils/repository-utils'; +import { RepositoriesSearchBar } from './AddRepositoriesSearchBar'; + +export const AddRepositoriesTableToolbar = ({ + title, + setSearchString, + selectedRepositoriesFormData, + setFieldValue, + onPageChange, +}: { + title: string; + setSearchString: (str: string) => void; + selectedRepositoriesFormData: AddRepositoriesFormValues; + setFieldValue: ( + field: string, + value: any, + shouldValidate?: boolean, + ) => Promise> | Promise; + onPageChange: (page: number) => void; +}) => { + const [selection, setSelection] = React.useState('repository'); + const [search, setSearch] = React.useState(''); + const handleToggle = ( + _event: React.MouseEvent, + type: string, + ) => { + if (type) { + setSelection(type); + setFieldValue('repositoryType', type); + onPageChange(0); + } + }; + + const handleSearch = (filter: string) => { + setSearchString(filter); + setSearch(filter); + }; + + return ( + + + {`${title} (${getRepositoriesSelected(selectedRepositoriesFormData)})`} + + + Repositories + Organization + + + + ); +}; diff --git a/plugins/bulk-import/src/components/AddRepositories/Illustrations.tsx b/plugins/bulk-import/src/components/AddRepositories/Illustrations.tsx new file mode 100644 index 0000000000..adc4a7da7e --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/Illustrations.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; + +import { makeStyles } from '@material-ui/core'; + +import { getImageForIconClass } from '../../utils/icons'; + +const useStyles = makeStyles(() => ({ + text: { + maxWidth: '150px', + }, +})); + +export const Illustrations = ({ + iconClassname, + iconText, +}: { + iconClassname: string; + iconText: string; +}) => { + const styles = useStyles(); + return ( +
+ {iconText} +

{iconText}

+
+ ); +}; diff --git a/plugins/bulk-import/src/components/AddRepositories/OrganizationColumnHeader.ts b/plugins/bulk-import/src/components/AddRepositories/OrganizationColumnHeader.ts new file mode 100644 index 0000000000..14cbfc6a77 --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/OrganizationColumnHeader.ts @@ -0,0 +1,20 @@ +import { TableColumn } from '@backstage/core-components'; + +export const OrganizationColumnHeader: TableColumn[] = [ + { + id: 'name', + title: 'Name', + field: 'name', + }, + { id: 'url', title: 'URL', field: 'url' }, + { + id: 'selectedRepositories', + title: 'Selected repositories', + field: 'selectedRepositories', + }, + { + id: 'catalogInfoYaml', + title: 'catalog-info.yaml', + field: 'catalogInfoYaml', + }, +]; diff --git a/plugins/bulk-import/src/components/AddRepositories/OrganizationTableRow.tsx b/plugins/bulk-import/src/components/AddRepositories/OrganizationTableRow.tsx new file mode 100644 index 0000000000..f51eef984b --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/OrganizationTableRow.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; + +import { Link } from '@backstage/core-components'; + +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import TableCell from '@mui/material/TableCell'; +import TableRow from '@mui/material/TableRow'; + +import { AddRepositoriesData } from '../../types'; +import { + getRepositoryStatusForOrg, + getSelectedRepositories, +} from '../../utils/repository-utils'; + +export const OrganizationTableRow = ({ + data, +}: { + data: AddRepositoriesData; +}) => { + return ( + + + {data.name} + + + + <> + {data.url} + + + + + + <>{getSelectedRepositories(data.selectedRepositories)} + + {getRepositoryStatusForOrg(data)} + + ); +}; diff --git a/plugins/bulk-import/src/components/AddRepositories/RepositoriesColumnHeader.ts b/plugins/bulk-import/src/components/AddRepositories/RepositoriesColumnHeader.ts new file mode 100644 index 0000000000..ec1dea25e9 --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/RepositoriesColumnHeader.ts @@ -0,0 +1,24 @@ +import { TableColumn } from '@backstage/core-components'; + +export const RepositoriesColumnHeader: TableColumn[] = [ + { + id: 'name', + title: 'Name', + field: 'name', + }, + { + id: 'url', + title: 'URL', + field: 'url', + }, + { + id: 'organization', + title: 'Organization', + field: 'organization', + }, + { + id: 'catalogInfoYaml', + title: 'catalog-info.yaml', + field: 'catalogInfoYaml', + }, +]; diff --git a/plugins/bulk-import/src/components/AddRepositories/RepositoriesHeader.tsx b/plugins/bulk-import/src/components/AddRepositories/RepositoriesHeader.tsx new file mode 100644 index 0000000000..6ec910ef30 --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/RepositoriesHeader.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; + +import Checkbox from '@mui/material/Checkbox'; +import TableCell from '@mui/material/TableCell'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import TableSortLabel from '@mui/material/TableSortLabel'; + +import { Order } from '../../types'; +import { OrganizationColumnHeader } from './OrganizationColumnHeader'; +import { RepositoriesColumnHeader } from './RepositoriesColumnHeader'; + +export const RepositoriesHeader = ({ + onSelectAllClick, + order, + orderBy, + numSelected, + rowCount, + onRequestSort, + showOrganizations, +}: { + numSelected: number; + onRequestSort: (event: React.MouseEvent, property: any) => void; + onSelectAllClick: (event: React.ChangeEvent) => void; + order: Order; + orderBy: string; + rowCount: number; + showOrganizations: boolean; +}) => { + const createSortHandler = + (property: any) => (event: React.MouseEvent) => { + onRequestSort(event, property); + }; + + return ( + + + {!showOrganizations && ( + + 0 && numSelected < rowCount} + checked={rowCount > 0 && numSelected === rowCount} + onChange={onSelectAllClick} + inputProps={{ + 'aria-label': 'select all repositories', + }} + /> + + )} + {(showOrganizations + ? OrganizationColumnHeader + : RepositoriesColumnHeader + ).map(headCell => ( + + + {headCell.title} + + + ))} + + + ); +}; diff --git a/plugins/bulk-import/src/components/AddRepositories/RepositoriesTable.tsx b/plugins/bulk-import/src/components/AddRepositories/RepositoriesTable.tsx new file mode 100644 index 0000000000..de2a1f265b --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/RepositoriesTable.tsx @@ -0,0 +1,257 @@ +import * as React from 'react'; + +import { makeStyles } from '@material-ui/core'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TablePagination from '@mui/material/TablePagination'; +import TableRow from '@mui/material/TableRow'; +import { FormikErrors } from 'formik'; + +import { + AddRepositoriesData, + AddRepositoriesFormValues, + Order, +} from '../../types'; +import { + getComparator, + getNewSelectedRepositories, +} from '../../utils/repository-utils'; +import { getDataForRepositories } from './mockData'; +import { OrganizationTableRow } from './OrganizationTableRow'; +import { RepositoriesColumnHeader } from './RepositoriesColumnHeader'; +import { RepositoriesHeader } from './RepositoriesHeader'; +import { RepositoryTableRow } from './RepositoryTableRow'; + +const useStyles = makeStyles(theme => ({ + root: { + alignItems: 'start', + padding: theme.spacing(3, 0, 2.5, 2.5), + }, + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, + title: { + display: 'flex', + gap: '20px', + alignItems: 'center', + }, + footer: { + '&:nth-of-type(odd)': { + backgroundColor: `${theme.palette.background.paper}`, + }, + }, +})); + +export const RepositoriesTable = ({ + searchString, + selectedRepositoriesFormData, + page, + setPage, + setFieldValue, + showOrganizations = false, +}: { + searchString: string; + selectedRepositoriesFormData: AddRepositoriesFormValues; + page: number; + setPage: (page: number) => void; + setFieldValue: ( + field: string, + value: any, + shouldValidate?: boolean, + ) => Promise> | Promise; + showOrganizations?: boolean; +}) => { + const classes = useStyles(); + const [order, setOrder] = React.useState('asc'); + const [orderBy, setOrderBy] = React.useState('name'); + const [selected, setSelected] = React.useState([]); + + const [rowsPerPage, setRowsPerPage] = React.useState(5); + + const data: AddRepositoriesData[] = getDataForRepositories(); + const filteredData = React.useMemo(() => { + let repositories = data; + + if (searchString) { + const f = searchString.toUpperCase(); + repositories = repositories.filter((addRepoData: AddRepositoriesData) => { + const n = addRepoData.name?.toUpperCase(); + return n?.includes(f); + }); + } + repositories = repositories.sort(getComparator(order, orderBy)); + + return repositories; + }, [data, searchString, order, orderBy]); + + const handleRequestSort = ( + _event: React.MouseEvent, + property: string, + ) => { + const isAsc = orderBy === property && order === 'asc'; + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(property); + }; + + const handleSelectAllClick = (event: React.ChangeEvent) => { + if (event.target.checked) { + const newSelected = filteredData + .map(n => { + if (n.catalogInfoYaml.status !== 'Exists') { + return n.id; + } + return -1; + }) + .filter(d => d); + setSelected(newSelected); + if (selectedRepositoriesFormData.repositoryType === 'repository') { + setFieldValue( + 'repositories', + getNewSelectedRepositories(filteredData, newSelected), + ); + } else { + setFieldValue( + 'organizations', + getNewSelectedRepositories(filteredData, newSelected), + ); + } + return; + } + if (selectedRepositoriesFormData.repositoryType === 'repository') { + setFieldValue('repositories', []); + } else { + setFieldValue('organizations', []); + } + setSelected([]); + }; + + const handleClick = (_event: React.MouseEvent, id: number) => { + const selectedIndex = selected.indexOf(id); + let newSelected: number[] = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, id); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1), + ); + } + + const repositories = getNewSelectedRepositories(data, newSelected); + setSelected(newSelected); + if (selectedRepositoriesFormData.repositoryType === 'repository') { + setFieldValue('repositories', repositories); + } else { + setFieldValue('organizations', repositories); + } + }; + + const handleChangePage = (_event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = ( + event: React.ChangeEvent, + ) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const isSelected = (id: number) => selected.indexOf(id) !== -1; + + // Avoid a layout jump when reaching the last page with empty rows. + const emptyRows = + page > 0 ? Math.max(0, (1 + page) * rowsPerPage - data.length) : 0; + + const visibleRows = React.useMemo(() => { + return filteredData.slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage, + ); + }, [filteredData, page, rowsPerPage]); + + return ( + <> + + + + {visibleRows?.length > 0 ? ( + + {visibleRows.map(row => { + const isItemSelected = isSelected(row.id); + + return showOrganizations ? ( + + ) : ( + + ); + })} + {emptyRows > 0 && ( + + + + )} + + ) : ( + + + + + + )} +
+
+ No records found +
+
+
+ + + ); +}; diff --git a/plugins/bulk-import/src/components/AddRepositories/RepositoryTableRow.tsx b/plugins/bulk-import/src/components/AddRepositories/RepositoryTableRow.tsx new file mode 100644 index 0000000000..9b2e79d7cf --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/RepositoryTableRow.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; + +import { Link } from '@backstage/core-components'; + +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import Checkbox from '@mui/material/Checkbox'; +import TableCell from '@mui/material/TableCell'; +import TableRow from '@mui/material/TableRow'; + +import { AddRepositoriesData } from '../../types'; +import { getRepositoryStatus } from '../../utils/repository-utils'; + +export const RepositoryTableRow = ({ + handleClick, + isItemSelected, + data, + selectedRepositoryStatus, +}: { + handleClick: (_event: React.MouseEvent, id: number) => void; + isItemSelected: boolean; + data: AddRepositoriesData; + selectedRepositoryStatus: string; +}) => { + return ( + + + handleClick(event, data.id)} + /> + + + {data.name} + + + + <> + {data.url} + + + + + + + <> + {data.organization} + + + + + + {getRepositoryStatus(data.catalogInfoYaml.status, isItemSelected)} + + + ); +}; diff --git a/plugins/bulk-import/src/components/AddRepositories/mockData.ts b/plugins/bulk-import/src/components/AddRepositories/mockData.ts new file mode 100644 index 0000000000..7e171c7448 --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/mockData.ts @@ -0,0 +1,25 @@ +import { createData } from '../../utils/repository-utils'; + +export const getDataForRepositories = () => [ + createData(1, 'Cupcake', 'https://github.com/cupcake', '', 'org/cupcake', 3), + createData(2, 'Donut', 'https://github.com/donut', 'Done', 'org/donut'), + createData(3, 'Eclair', 'https://github.com/eclair', '', 'org/eclair', 2), + createData( + 4, + 'Frozen yoghurt', + 'https://github.com/yogurt', + '', + 'org/yogurt', + 0, + ), + createData( + 5, + 'Gingerbread', + 'https://github.com/gingerbread', + 'Exists', + 'org/gingerbread', + 0, + ), + createData(9, 'KitKat', 'https://github.com/kitkat', '', 'org/kitkat', 0), + createData(13, 'Oreo', 'https://github.com/oreo', '', 'org/oreo', 0), +]; diff --git a/plugins/bulk-import/src/components/BulkImportPage.tsx b/plugins/bulk-import/src/components/BulkImportPage.tsx index 33db95d29e..1e78ca0f6a 100644 --- a/plugins/bulk-import/src/components/BulkImportPage.tsx +++ b/plugins/bulk-import/src/components/BulkImportPage.tsx @@ -8,7 +8,7 @@ export const BulkImportPage = () => (
- + diff --git a/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx b/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx index c16a993543..14ca9dbb66 100644 --- a/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx +++ b/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx @@ -6,6 +6,7 @@ import { makeStyles } from '@material-ui/core'; import { RepositoriesData } from '../../types'; import { columns } from './RepositoriesListColumns'; +import { RepositoriesListToolbar } from './RepositoriesListToolbar'; const useStyles = makeStyles(theme => ({ empty: { @@ -17,20 +18,31 @@ const useStyles = makeStyles(theme => ({ export const RepositoriesList = () => { const classes = useStyles(); + const [addedRepositories, setAddedRepositories] = React.useState< + number | undefined + >(); const data: RepositoriesData[] = []; + const onSearchResultsChange = (searchResults: RepositoriesData[]) => { + setAddedRepositories(searchResults.length); + }; + return ( - - No records found - - } - /> + <> + +
onSearchResultsChange(summary.data)} + columns={columns} + emptyContent={ +
+ No records found +
+ } + /> + ); }; diff --git a/plugins/bulk-import/src/components/Repositories/RepositoriesListToolbar.tsx b/plugins/bulk-import/src/components/Repositories/RepositoriesListToolbar.tsx new file mode 100644 index 0000000000..80fd1a8368 --- /dev/null +++ b/plugins/bulk-import/src/components/Repositories/RepositoriesListToolbar.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { LinkButton } from '@backstage/core-components'; + +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles(theme => ({ + toolbar: { + display: 'flex', + justifyContent: 'end', + marginBottom: '24px', + }, + rbacPreReqLink: { + color: theme.palette.link, + }, + alertTitle: { + fontWeight: 'bold', + }, +})); + +export const RepositoriesListToolbar = () => { + const classes = useStyles(); + return ( +
+ + + Add + + +
+ ); +}; diff --git a/plugins/bulk-import/src/components/Router.test.tsx b/plugins/bulk-import/src/components/Router.test.tsx new file mode 100644 index 0000000000..976991cf20 --- /dev/null +++ b/plugins/bulk-import/src/components/Router.test.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +import { render, screen } from '@testing-library/react'; + +import { Router } from './Router'; + +jest.mock('./BulkImportPage', () => ({ + BulkImportPage: () =>
Bulk Import
, +})); + +jest.mock('./AddRepositories/AddRepositoriesPage', () => ({ + AddRepositoriesPage: () =>
Add Repositories
, +})); + +describe('Router component', () => { + it('renders BulkImportPage when path is "/"', () => { + render( + + + , + ); + expect(screen.queryByText('Bulk Import')).toBeInTheDocument(); + }); + + it('renders Add repositories page when path matches addRepositoriesRouteRef', () => { + render( + + + , + ); + + expect(screen.queryByText('Add Repositories')).toBeInTheDocument(); + }); +}); diff --git a/plugins/bulk-import/src/components/Router.tsx b/plugins/bulk-import/src/components/Router.tsx index 46a2e2f40e..279b624d68 100644 --- a/plugins/bulk-import/src/components/Router.tsx +++ b/plugins/bulk-import/src/components/Router.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { Route, Routes } from 'react-router-dom'; -import { BulkImportPage } from '../plugin'; +import { addRepositoriesRouteRef } from '../routes'; +import { AddRepositoriesPage } from './AddRepositories/AddRepositoriesPage'; +import { BulkImportPage } from './BulkImportPage'; /** * @@ -9,6 +11,10 @@ import { BulkImportPage } from '../plugin'; */ export const Router = () => ( - } /> + } /> + } + /> ); diff --git a/plugins/bulk-import/src/globals.d.ts b/plugins/bulk-import/src/globals.d.ts new file mode 100644 index 0000000000..006534e235 --- /dev/null +++ b/plugins/bulk-import/src/globals.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const content: React.FunctionComponent>; + export default content; +} diff --git a/plugins/bulk-import/src/images/ApprovalTool_Black.svg b/plugins/bulk-import/src/images/ApprovalTool_Black.svg new file mode 100644 index 0000000000..17427607ce --- /dev/null +++ b/plugins/bulk-import/src/images/ApprovalTool_Black.svg @@ -0,0 +1,68 @@ +SOAR security mini illustration - Black +Security, Orchestration, Automation, response, gear, automation + + + + + 2024-01-05T15:47:31.662Z + pending + TRA932ff8c6-d737-49e0-9691-fb1fa82c66c3 + Illustration + 2024-01-05T15:47:31.662Z + true + pending + 2024-01-05T15:47:40.782Z + rhcc-audience:internal + no + Mini illustration + DER932ff8c6-d737-49e0-9691-fb1fa82c66c3 + yes + + + Black + + + image/svg+xml + 2024-02-09T18:53:48.655Z + + + SOAR security mini illustration - Black + + + + + Security, Orchestration, Automation, response, gear, automation + + + Activate + Activate + 2024-02-09T18:58:30.405Z + workflow-process-service + Activate + workflow-process-service + true + 2024-02-09T18:58:30.405Z + workflow-process-service + 2024-02-09T18:58:30.405Z + 1080 + 1080 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/bulk-import/src/images/ApprovalTool_White.svg b/plugins/bulk-import/src/images/ApprovalTool_White.svg new file mode 100644 index 0000000000..bf731e079e --- /dev/null +++ b/plugins/bulk-import/src/images/ApprovalTool_White.svg @@ -0,0 +1,68 @@ +SOAR security mini illustration - White +Security, Orchestration, Automation, response, gear, automation + + + + + 2024-01-05T15:47:32.226Z + pending + TRAc3cb2adb-703d-4d26-a825-f4174fa5cdde + Illustration + 2024-01-05T15:47:32.226Z + true + pending + 2024-01-05T15:47:40.950Z + rhcc-audience:internal + no + Mini illustration + DERc3cb2adb-703d-4d26-a825-f4174fa5cdde + yes + + + White + + + image/svg+xml + 2024-02-09T18:54:08.397Z + + + SOAR security mini illustration - White + + + + + Security, Orchestration, Automation, response, gear, automation + + + Activate + Activate + 2024-02-09T18:59:08.364Z + workflow-process-service + Activate + workflow-process-service + true + 2024-02-09T18:59:08.364Z + workflow-process-service + 2024-02-09T18:59:08.364Z + 1080 + 1080 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/bulk-import/src/images/ChooseRepositories_Black.svg b/plugins/bulk-import/src/images/ChooseRepositories_Black.svg new file mode 100644 index 0000000000..41391036dc --- /dev/null +++ b/plugins/bulk-import/src/images/ChooseRepositories_Black.svg @@ -0,0 +1,68 @@ +Automation mini illustration - Black +automation, app + + + + + 2024-01-05T15:47:12.565Z + pending + TRAf6deb762-8d56-4a81-b19d-7792f121e01f + Illustration + 2024-01-05T15:47:12.565Z + true + pending + 2024-01-05T15:47:24.757Z + rhcc-audience:internal + no + Mini illustration + DERf6deb762-8d56-4a81-b19d-7792f121e01f + yes + + + Black + + + image/svg+xml + 2024-02-09T18:52:17.755Z + + + Automation mini illustration - Black + + + + + automation, app + + + Activate + Activate + 2024-02-09T18:55:12.653Z + workflow-process-service + Activate + workflow-process-service + true + 2024-02-09T18:55:12.653Z + workflow-process-service + 2024-02-09T18:55:12.653Z + 1080 + 1080 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/bulk-import/src/images/ChooseRepositories_White.svg b/plugins/bulk-import/src/images/ChooseRepositories_White.svg new file mode 100644 index 0000000000..f96085067a --- /dev/null +++ b/plugins/bulk-import/src/images/ChooseRepositories_White.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/bulk-import/src/images/EditPullRequest_Black.svg b/plugins/bulk-import/src/images/EditPullRequest_Black.svg new file mode 100644 index 0000000000..4e238ec789 --- /dev/null +++ b/plugins/bulk-import/src/images/EditPullRequest_Black.svg @@ -0,0 +1,132 @@ + + + Application development mini illustration - Black +application, app, development, dev, developer, window, code, cloud + + + + + 2024-03-14T01:55:49.625Z + no + Mini illustration + pending + TRA23159c85-64be-433d-b623-3650bfacff32 + Illustration + 2024-03-14T01:55:49.625Z + DER23159c85-64be-433d-b623-3650bfacff32 + true + pending + 2024-03-14T01:55:58.712Z + rhcc-audience:internal + yes + Black + 1080 + 1080 + image/svg+xml + 2024-03-14T01:56:36.556Z + + + application, app, development, dev, developer, window, code, cloud + + + + + Application development mini illustration - Black + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/bulk-import/src/images/EditPullRequest_White.svg b/plugins/bulk-import/src/images/EditPullRequest_White.svg new file mode 100644 index 0000000000..dab1d1bf79 --- /dev/null +++ b/plugins/bulk-import/src/images/EditPullRequest_White.svg @@ -0,0 +1,216 @@ + + + +Application development mini illustration - White +application, app, development, dev, developer, window, code, cloud + + + + + 2024-03-14T01:55:49.037Z + no + Mini illustration + pending + TRAb55b00f5-fa4c-4433-9fb1-701917a6774b + Illustration + 2024-03-14T01:55:49.037Z + DERb55b00f5-fa4c-4433-9fb1-701917a6774b + true + pending + 2024-03-14T01:56:04.863Z + rhcc-audience:internal + yes + White + 1080 + 1080 + image/svg+xml + 2024-03-14T01:56:36.266Z + + + application, app, development, dev, developer, window, code, cloud + + + + + Application development mini illustration - White + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/bulk-import/src/images/GenerateCatalogInfo_Black.svg b/plugins/bulk-import/src/images/GenerateCatalogInfo_Black.svg new file mode 100644 index 0000000000..102164517e --- /dev/null +++ b/plugins/bulk-import/src/images/GenerateCatalogInfo_Black.svg @@ -0,0 +1,68 @@ +Digital transformation mini illustration - Black +digital transformation, collaboration, bubbles, code, technology + + + + + 2024-01-05T15:47:01.194Z + pending + TRA96548f2e-65dd-4486-9299-929a817f72cf + Illustration + 2024-01-05T15:47:01.194Z + true + pending + 2024-01-05T15:47:13.635Z + rhcc-audience:internal + no + Mini illustration + DER96548f2e-65dd-4486-9299-929a817f72cf + yes + + + Black + + + image/svg+xml + 2024-02-09T18:52:46.120Z + + + Digital transformation mini illustration - Black + + + + + digital transformation, collaboration, bubbles, code, technology + + + Activate + Activate + 2024-02-09T18:56:53.741Z + workflow-process-service + Activate + workflow-process-service + true + 2024-02-09T18:56:53.741Z + workflow-process-service + 2024-02-09T18:56:53.741Z + 1080 + 1080 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/bulk-import/src/images/GenerateCatalogInfo_White.svg b/plugins/bulk-import/src/images/GenerateCatalogInfo_White.svg new file mode 100644 index 0000000000..1107e75c53 --- /dev/null +++ b/plugins/bulk-import/src/images/GenerateCatalogInfo_White.svg @@ -0,0 +1,68 @@ +Digital transformation mini illustration - White +digital transformation, collaboration, bubbles, code, technology + + + + + 2024-01-05T15:47:01.549Z + pending + TRA0b529eab-8c9d-4e7f-8bd0-d2eb57d2708b + Illustration + 2024-01-05T15:47:01.549Z + true + pending + 2024-01-05T15:47:12.759Z + rhcc-audience:internal + no + Mini illustration + DER0b529eab-8c9d-4e7f-8bd0-d2eb57d2708b + yes + + + White + + + image/svg+xml + 2024-02-09T18:52:47.286Z + + + Digital transformation mini illustration - White + + + + + digital transformation, collaboration, bubbles, code, technology + + + Activate + Activate + 2024-02-09T18:56:54.975Z + workflow-process-service + Activate + workflow-process-service + true + 2024-02-09T18:56:54.975Z + workflow-process-service + 2024-02-09T18:56:54.975Z + 1080 + 1080 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/bulk-import/src/images/TrackStatus_Black.svg b/plugins/bulk-import/src/images/TrackStatus_Black.svg new file mode 100644 index 0000000000..0adf9bcd18 --- /dev/null +++ b/plugins/bulk-import/src/images/TrackStatus_Black.svg @@ -0,0 +1,68 @@ +Laptop mini illustration - Black +laptop, computer, workstation, device + + + + + 2024-01-05T15:47:02.277Z + pending + TRA61bbea5a-150f-4aaf-91e8-c3e333c249aa + Illustration + 2024-01-05T15:47:02.277Z + true + pending + 2024-01-05T15:47:13.362Z + rhcc-audience:internal + no + Mini illustration + DER61bbea5a-150f-4aaf-91e8-c3e333c249aa + yes + + + Black + + + image/svg+xml + 2024-02-09T18:53:16.693Z + + + Laptop mini illustration - Black + + + + + laptop, computer, workstation, device + + + Activate + Activate + 2024-02-09T18:57:54.798Z + workflow-process-service + Activate + workflow-process-service + true + 2024-02-09T18:57:54.798Z + workflow-process-service + 2024-02-09T18:57:54.798Z + 1080 + 1080 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/bulk-import/src/images/TrackStatus_White.svg b/plugins/bulk-import/src/images/TrackStatus_White.svg new file mode 100644 index 0000000000..602091deb6 --- /dev/null +++ b/plugins/bulk-import/src/images/TrackStatus_White.svg @@ -0,0 +1,68 @@ +Laptop mini illustration - White +laptop, computer, workstation, device + + + + + 2024-01-05T15:47:02.727Z + pending + TRA331e80e0-812a-41ff-bbda-8ab252cb46c2 + Illustration + 2024-01-05T15:47:02.727Z + true + pending + 2024-01-05T15:47:13.431Z + rhcc-audience:internal + no + Mini illustration + DER331e80e0-812a-41ff-bbda-8ab252cb46c2 + yes + + + White + + + image/svg+xml + 2024-02-09T18:53:24.431Z + + + Laptop mini illustration - White + + + + + laptop, computer, workstation, device + + + Activate + Activate + 2024-02-09T18:58:08.173Z + workflow-process-service + Activate + workflow-process-service + true + 2024-02-09T18:58:08.173Z + workflow-process-service + 2024-02-09T18:58:08.173Z + 1080 + 1080 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/bulk-import/src/plugin.ts b/plugins/bulk-import/src/plugin.ts index cd5593de8c..9c17dda86a 100644 --- a/plugins/bulk-import/src/plugin.ts +++ b/plugins/bulk-import/src/plugin.ts @@ -4,19 +4,20 @@ import { createRoutableExtension, } from '@backstage/core-plugin-api'; -import { rootRouteRef } from './routes'; +import { addRepositoriesRouteRef, rootRouteRef } from './routes'; export const bulkImportPlugin = createPlugin({ id: 'bulk-import', routes: { root: rootRouteRef, + addRepositories: addRepositoriesRouteRef, }, }); export const BulkImportPage = bulkImportPlugin.provide( createRoutableExtension({ name: 'BulkImportPage', - component: () => import('./components').then(m => m.BulkImportPage), + component: () => import('./components').then(m => m.Router), mountPoint: rootRouteRef, }), ); diff --git a/plugins/bulk-import/src/routes.ts b/plugins/bulk-import/src/routes.ts index fd7daf4037..512ec81967 100644 --- a/plugins/bulk-import/src/routes.ts +++ b/plugins/bulk-import/src/routes.ts @@ -1,5 +1,11 @@ -import { createRouteRef } from '@backstage/core-plugin-api'; +import { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api'; export const rootRouteRef = createRouteRef({ id: 'bulk-import', }); + +export const addRepositoriesRouteRef = createSubRouteRef({ + id: 'bulk-import-repositories-add', + parent: rootRouteRef, + path: '/add', +}); diff --git a/plugins/bulk-import/src/types.ts b/plugins/bulk-import/src/types.ts index 5feb4efa94..0bf21cfeb3 100644 --- a/plugins/bulk-import/src/types.ts +++ b/plugins/bulk-import/src/types.ts @@ -5,3 +5,26 @@ export type RepositoriesData = { status: string; lastUpdated: string; }; + +export type AddRepositoriesData = { + id: number; + name: string; + url: string; + organization?: string; + selectedRepositories?: number; + catalogInfoYaml: { + yaml: string; + status: string; + }; +}; + +export type Order = 'asc' | 'desc'; + +export type RepositoryType = 'repository' | 'organization'; + +export type AddRepositoriesFormValues = { + repositoryType: 'repository' | 'organzation'; + repositories?: AddRepositoriesData[]; + organizations?: AddRepositoriesData[]; + approvalTool: 'git' | 'servicenow'; +}; diff --git a/plugins/bulk-import/src/utils/icons.ts b/plugins/bulk-import/src/utils/icons.ts new file mode 100644 index 0000000000..1fdcd3e437 --- /dev/null +++ b/plugins/bulk-import/src/utils/icons.ts @@ -0,0 +1,26 @@ +import approvalToolBlackImg from '../images/ApprovalTool_Black.svg'; +import approvalToolWhiteImg from '../images/ApprovalTool_White.svg'; +import chooseRepositoriesBlackImg from '../images/ChooseRepositories_Black.svg'; +import chooseRepositoriesWhiteImg from '../images/ChooseRepositories_White.svg'; +import editPullRequestBlackImg from '../images/EditPullRequest_Black.svg'; +import editPullRequestWhiteImg from '../images/EditPullRequest_White.svg'; +import generateCatalogInfoBlackImg from '../images/GenerateCatalogInfo_Black.svg'; +import generateCatalogInfoWhiteImg from '../images/GenerateCatalogInfo_White.svg'; +import trackStatusBlackImg from '../images/TrackStatus_Black.svg'; +import trackStatusWhiteImg from '../images/TrackStatus_White.svg'; + +const logos = new Map() + .set('icon-edit-pullrequest-black', editPullRequestBlackImg) + .set('icon-generate-cataloginfo-black', generateCatalogInfoBlackImg) + .set('icon-track-status-black', trackStatusBlackImg) + .set('icon-choose-repositories-black', chooseRepositoriesBlackImg) + .set('icon-approval-tool-black', approvalToolBlackImg) + .set('icon-edit-pullrequest-white', editPullRequestWhiteImg) + .set('icon-choose-repositories-white', chooseRepositoriesWhiteImg) + .set('icon-generate-cataloginfo-white', generateCatalogInfoWhiteImg) + .set('icon-track-status-white', trackStatusWhiteImg) + .set('icon-approval-tool-white', approvalToolWhiteImg); + +export const getImageForIconClass = (iconClass: string): string => { + return logos.get(iconClass); +}; diff --git a/plugins/bulk-import/src/utils/repository-utils.tsx b/plugins/bulk-import/src/utils/repository-utils.tsx new file mode 100644 index 0000000000..8eb5dde110 --- /dev/null +++ b/plugins/bulk-import/src/utils/repository-utils.tsx @@ -0,0 +1,136 @@ +import React from 'react'; + +import { Link } from '@backstage/core-components'; + +import ReadyIcon from '@mui/icons-material/CheckOutlined'; +import { get } from 'lodash'; + +import { + AddRepositoriesData, + AddRepositoriesFormValues, + Order, +} from '../types'; + +export const getRepositoryStatus = ( + status: string, + isItemSelected?: boolean, +) => { + if (isItemSelected) { + return ( + + + Ready + + ); + } + if (status === 'Exists') { + return Repository already added; + } + + return Not generated; +}; + +export const getRepositoryStatusForOrg = (data: AddRepositoriesData) => { + if (data.selectedRepositories && data.selectedRepositories > 0) { + return getRepositoryStatus(data.catalogInfoYaml.status); + } + + return Not generated; +}; + +const descendingComparator = ( + a: AddRepositoriesData, + b: AddRepositoriesData, + orderBy: string, +) => { + const value1 = get(a, orderBy); + const value2 = get(b, orderBy); + + if (value2 < value1) { + return -1; + } + if (value2 > value1) { + return 1; + } + return 0; +}; + +export const getComparator = ( + order: Order, + orderBy: string, +): ((a: AddRepositoriesData, b: AddRepositoriesData) => number) => { + return order === 'desc' + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); +}; + +export const createData = ( + id: number, + name: string, + url: string, + catalogInfoYaml: string, + organization?: string, + selectedRepositories?: number, +): AddRepositoriesData => { + return { + id, + name, + url, + organization, + selectedRepositories, + catalogInfoYaml: { + status: catalogInfoYaml, + yaml: '', + }, + }; +}; + +export const getSelectedRepositories = (repositories: number | undefined) => { + if (!repositories || repositories === 0) { + return ( + <> + None{' '} + {}} to=""> + Select + + + ); + } + return ( + <> + {repositories}{' '} + {}} to=""> + Edit + + + ); +}; + +export const getNewSelectedRepositories = ( + data: AddRepositoriesData[], + selectedIds: number[], +) => { + return data + .map(d => { + if (selectedIds.find((id: number) => id === d.id)) { + return d; + } + return null; + }) + .filter(repo => repo); +}; + +export const getRepositoriesSelected = (data: AddRepositoriesFormValues) => { + if (data.repositoryType === 'repository') { + return data.repositories?.length || 0; + } + return ( + data.organizations?.reduce((acc, org) => { + const repos = acc + (org.selectedRepositories || 0); + return repos; + }, 0) || 0 + ); +}; diff --git a/plugins/bulk-import/tests/bulkImport.spec.ts b/plugins/bulk-import/tests/bulkImport.spec.ts index 998d3bf9ff..6119c6810c 100644 --- a/plugins/bulk-import/tests/bulkImport.spec.ts +++ b/plugins/bulk-import/tests/bulkImport.spec.ts @@ -27,6 +27,54 @@ test.describe('Bulk import plugin', () => { ]; const thead = page.locator('thead'); + for (const col of columns) { + await expect(thead.getByText(col)).toBeVisible(); + } + }); + test('Add button is shown', async () => { + await page.locator(`a`).filter({ hasText: 'Add' }).click(); + await expect( + page.getByRole('heading', { name: 'Add repositories', exact: true }), + ).toBeVisible({ + timeout: 20000, + }); + }); + + test('Add repositories page is shown', async () => { + await expect( + page.getByRole('heading', { + name: 'Add repositories to Red Hat Developer Hub in 5 steps', + }), + ).toBeVisible({ + timeout: 20000, + }); + await page.mouse.wheel(0, 200); + await expect( + page.getByRole('heading', { name: 'Selected repositories (0)' }), + ).toBeVisible({ + timeout: 20000, + }); + let columns = ['Name', 'URL', 'Organization', 'catalog-info.yaml']; + let thead = page.locator('thead'); + + for (const col of columns) { + await expect(thead.getByText(col)).toBeVisible(); + } + await page.click('input[aria-label="select all repositories"]'); + await expect( + page.getByRole('heading', { name: 'Selected repositories (6)' }), + ).toBeVisible({ + timeout: 20000, + }); + await page.locator(`button`).filter({ hasText: 'Organization' }).click(); + await expect( + page.getByRole('heading', { name: 'Selected repositories (0)' }), + ).toBeVisible({ + timeout: 20000, + }); + columns = ['Name', 'URL', 'Selected repositories', 'catalog-info.yaml']; + thead = page.locator('thead'); + for (const col of columns) { await expect(thead.getByText(col)).toBeVisible(); }