diff --git a/.changeset/witty-socks-cover.md b/.changeset/witty-socks-cover.md new file mode 100644 index 0000000000..5e049b076f --- /dev/null +++ b/.changeset/witty-socks-cover.md @@ -0,0 +1,5 @@ +--- +"@janus-idp/backstage-plugin-bulk-import": minor +--- + +use react query to fetch repositories diff --git a/plugins/bulk-import/src/api/BulkImportBackendClient.test.ts b/plugins/bulk-import/src/api/BulkImportBackendClient.test.ts index ca4652927a..6c844e354a 100644 --- a/plugins/bulk-import/src/api/BulkImportBackendClient.test.ts +++ b/plugins/bulk-import/src/api/BulkImportBackendClient.test.ts @@ -131,8 +131,11 @@ const handlers = [ ) ) { return res( - ctx.status(400), - ctx.json({ message: 'Dry run for creating import jobs failed' }), + ctx.json({ + message: 'Dry run for creating import jobs failed', + ok: false, + status: 404, + }), ); } return res(ctx.json(jobs)); @@ -351,9 +354,10 @@ describe('BulkImportBackendClient', () => { ), true, ); - expect(response).toEqual({ message: 'Dry run for creating import jobs failed', + ok: false, + status: 404, }); }); }); diff --git a/plugins/bulk-import/src/api/BulkImportBackendClient.ts b/plugins/bulk-import/src/api/BulkImportBackendClient.ts index 6691b90db6..7ee11efabc 100644 --- a/plugins/bulk-import/src/api/BulkImportBackendClient.ts +++ b/plugins/bulk-import/src/api/BulkImportBackendClient.ts @@ -122,7 +122,11 @@ export class BulkImportBackendClient implements BulkImportAPI { body: JSON.stringify(importRepositories), }, ); - return jsonResponse.json(); + if (!jsonResponse.ok) { + const errorResponse = await jsonResponse.json(); + throw errorResponse; + } + return jsonResponse.status === 204 ? null : await jsonResponse.json(); } async deleteImportAction(repo: string, defaultBranch: string) { diff --git a/plugins/bulk-import/src/components/AddRepositories/AddRepositories.tsx b/plugins/bulk-import/src/components/AddRepositories/AddRepositories.tsx new file mode 100644 index 0000000000..de8c52db25 --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/AddRepositories.tsx @@ -0,0 +1,130 @@ +import React from 'react'; + +import { makeStyles } from '@material-ui/core'; +import { Alert, AlertTitle } from '@material-ui/lab'; +import FormControl from '@mui/material/FormControl'; +import { useFormikContext } from 'formik'; +import { get } from 'lodash'; + +import { useDrawer } from '@janus-idp/shared-react'; + +import { AddRepositoriesFormValues, PullRequestPreviewData } from '../../types'; +import { PreviewFileSidebar } from '../PreviewFile/PreviewFileSidebar'; +// import HelpIcon from '@mui/icons-material/HelpOutline'; +// 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 { useFormikContext } from 'formik'; +// import { AddRepositoriesFormValues } from '../../types'; +import { AddRepositoriesFormFooter } from './AddRepositoriesFormFooter'; +import { AddRepositoriesTable } from './AddRepositoriesTable'; + +const useStyles = makeStyles(theme => ({ + body: { + marginBottom: '50px', + padding: '24px', + }, + 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 AddRepositories = ({ error }: { error: any }) => { + const styles = useStyles(); + const { openDrawer, setOpenDrawer, drawerData } = useDrawer(); + const { setFieldValue, values } = + useFormikContext(); + + const closeDrawer = () => { + setOpenDrawer(false); + }; + + const handleSave = (pullRequest: PullRequestPreviewData, _event: any) => { + Object.keys(pullRequest).forEach(pr => { + setFieldValue( + `repositories.${pr}.catalogInfoYaml.prTemplate`, + pullRequest[pr], + ); + }); + setOpenDrawer(false); + }; + + return ( + <> + +
+ {error && ( +
+ + {get(error, 'name') || 'Error occured'} + {get(error, 'err') || 'Failed to create pull request'} + +
+ )} + {/* + // Enable this when ServiceNow approval tool is supported + + + Approval tool + + + + + + + { + setFieldValue('approvalTool', value); + }} + > + } label="Git" /> + } + label="ServiceNow" + disabled + /> + + */} + +
+
+
+ + {openDrawer && ( + + )} + + ); +}; diff --git a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesDrawer.tsx b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesDrawer.tsx index 9fd1ffc3d1..44db107e6c 100644 --- a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesDrawer.tsx +++ b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesDrawer.tsx @@ -60,7 +60,7 @@ export const AddRepositoriesDrawer = ({ }: { open: boolean; onClose: () => void; - onSelect: (repos: AddedRepositories, drawerOrgName: string) => void; + onSelect: (repos: AddedRepositories) => void; title: string; orgData: AddRepositoryData; }) => { @@ -75,7 +75,7 @@ export const AddRepositoriesDrawer = ({ }; const handleSelectRepoFromDrawer = (selected: AddedRepositories) => { - onSelect(selected, orgData?.orgName || ''); + onSelect(selected); const newStatus = { ...(status?.errors || {}) }; Object.keys(newStatus).forEach(s => { if (!Object.keys(selected).find(sel => sel === s)) { diff --git a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.test.tsx b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.test.tsx index ca25db7810..b1b24634c6 100644 --- a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.test.tsx +++ b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.test.tsx @@ -4,6 +4,7 @@ import { BrowserRouter as Router } from 'react-router-dom'; import { configApiRef, identityApiRef } from '@backstage/core-plugin-api'; import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen } from '@testing-library/react'; import { useFormikContext } from 'formik'; @@ -11,8 +12,12 @@ import { useDrawer } from '@janus-idp/shared-react'; import { bulkImportApiRef } from '../../api/BulkImportBackendClient'; import { mockGetImportJobs, mockGetRepositories } from '../../mocks/mockData'; -import { ImportJobStatus, RepositorySelection } from '../../types'; -import { AddRepositoriesForm } from './AddRepositoriesForm'; +import { + ImportJobStatus, + OrgAndRepoResponse, + RepositorySelection, +} from '../../types'; +import { AddRepositories } from './AddRepositories'; jest.mock('formik', () => ({ ...jest.requireActual('formik'), @@ -44,6 +49,16 @@ jest.mock('@material-ui/core', () => ({ }, })); +const createTestQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, // Disable retries for testing + }, + }, + }); +let queryClient: QueryClient; + class MockBulkImportApi { async getImportAction( repo: string, @@ -53,6 +68,13 @@ class MockBulkImportApi { i => i.repository.url === repo, ) as ImportJobStatus; } + async dataFetcher( + _pageNo: number, + _size: number, + _searchString: string, + ): Promise { + return mockGetRepositories; + } } const mockBulkImportApi = new MockBulkImportApi(); @@ -70,9 +92,10 @@ beforeEach(() => { }, setFieldValue: jest.fn(), }); + queryClient = createTestQueryClient(); }); -describe('AddRepsositoriesForm', () => { +describe('AddRepositoriesForm', () => { it('should render the repositories list with the footer', async () => { (useDrawer as jest.Mock).mockImplementation(initial => ({ initial, @@ -97,7 +120,9 @@ describe('AddRepsositoriesForm', () => { ], ]} > - + + + , ); @@ -134,9 +159,9 @@ describe('AddRepsositoriesForm', () => { ], ]} > - + + + , ); diff --git a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx index 6b46f7cd4c..b602a18a68 100644 --- a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx +++ b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx @@ -1,133 +1,99 @@ import React from 'react'; +import { useNavigate } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core'; -import { Alert, AlertTitle } from '@material-ui/lab'; -import FormControl from '@mui/material/FormControl'; -import { useFormikContext } from 'formik'; +import { useApi } from '@backstage/core-plugin-api'; -import { useDrawer } from '@janus-idp/shared-react'; +import { useMutation } from '@tanstack/react-query'; +import { Formik, FormikHelpers } from 'formik'; -import { AddRepositoriesFormValues, PullRequestPreviewData } from '../../types'; -import { PreviewFileSidebar } from '../PreviewFile/PreviewFileSidebar'; -// import HelpIcon from '@mui/icons-material/HelpOutline'; -// 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 { useFormikContext } from 'formik'; -// import { AddRepositoriesFormValues } from '../../types'; -import { AddRepositoriesFormFooter } from './AddRepositoriesFormFooter'; -import { AddRepositoriesTable } from './AddRepositoriesTable'; +import { DrawerContextProvider } from '@janus-idp/shared-react'; -const useStyles = makeStyles(theme => ({ - body: { - marginBottom: '50px', - padding: '24px', - }, - 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, - }, +import { bulkImportApiRef } from '../../api/BulkImportBackendClient'; +import { + AddRepositoriesFormValues, + ApprovalTool, + CreateImportJobRepository, + ImportJobResponse, + RepositorySelection, +} from '../../types'; +import { + getJobErrors, + prepareDataForSubmission, +} from '../../utils/repository-utils'; +import { AddRepositories } from './AddRepositories'; - approvalToolTooltip: { - paddingTop: '4px', - paddingRight: '24px', - paddingLeft: '5px', - }, -})); +export const AddRepositoriesForm = () => { + const bulkImportApi = useApi(bulkImportApiRef); + const navigate = useNavigate(); + const initialValues: AddRepositoriesFormValues = { + repositoryType: RepositorySelection.Repository, + repositories: {}, + excludedRepositories: {}, + approvalTool: ApprovalTool.Git, + }; -export const AddRepositoriesForm = ({ - error, -}: { - error: { message: string; title: string } | null; -}) => { - const styles = useStyles(); - const { openDrawer, setOpenDrawer, drawerData } = useDrawer(); - const { setFieldValue, values } = - useFormikContext(); + const createImportJobs = (importOptions: { + importJobs: CreateImportJobRepository[]; + dryRun?: boolean; + }) => + bulkImportApi.createImportJobs( + importOptions.importJobs, + importOptions.dryRun, + ); - const closeDrawer = () => { - setOpenDrawer(false); - }; + const mutationCreate = useMutation(createImportJobs); - const handleSave = (pullRequest: PullRequestPreviewData, _event: any) => { - Object.keys(pullRequest).forEach(pr => { - setFieldValue( - `repositories.${pr}.catalogInfoYaml.prTemplate`, - pullRequest[pr], - ); + const handleSubmit = async ( + values: AddRepositoriesFormValues, + formikHelpers: FormikHelpers, + ) => { + formikHelpers.setSubmitting(true); + formikHelpers.setStatus(null); + const importRepositories = prepareDataForSubmission( + values.repositories, + values.approvalTool, + ); + formikHelpers.setSubmitting(true); + mutationCreate.mutate({ + importJobs: importRepositories, + dryRun: true, }); - setOpenDrawer(false); + if (!mutationCreate.isError) { + const dryRunErrors = getJobErrors( + mutationCreate.data as ImportJobResponse[], + ); + if (Object.keys(dryRunErrors?.errors || {}).length > 0) { + formikHelpers.setStatus(dryRunErrors); + formikHelpers.setSubmitting(false); + } else { + formikHelpers.setStatus(dryRunErrors); // to show info messages + const submitResult = await mutationCreate.mutateAsync({ + importJobs: importRepositories, + }); + formikHelpers.setSubmitting(true); + const createJobErrors = getJobErrors( + submitResult as ImportJobResponse[], + ); + if (Object.keys(createJobErrors?.errors || {}).length > 0) { + formikHelpers.setStatus(createJobErrors); + } else { + navigate(`..`); + } + } + } + + formikHelpers.setSubmitting(false); }; return ( - <> - -
- {error && ( -
- - {error?.title} - {error?.message} - -
- )} - {/* - // Enable this when ServiceNow approval tool is supported - - - Approval tool - - - - - - - { - setFieldValue('approvalTool', value); - }} - > - } label="Git" /> - } - label="ServiceNow" - disabled - /> - - */} - -
-
-
- - {openDrawer && ( - - )} - + + + + + ); }; diff --git a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesPage.tsx b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesPage.tsx index a1a5c26aec..97c8c90c0a 100644 --- a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesPage.tsx +++ b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesPage.tsx @@ -1,8 +1,6 @@ import React from 'react'; -import { useNavigate } from 'react-router-dom'; import { Content, Header, Page, Progress } from '@backstage/core-components'; -import { useApi } from '@backstage/core-plugin-api'; import { usePermission } from '@backstage/plugin-permission-react'; import { @@ -15,23 +13,10 @@ import { import { Alert, AlertTitle } from '@material-ui/lab'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import Typography from '@mui/material/Typography'; -import { Formik, FormikHelpers } from 'formik'; -import { get } from 'lodash'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { bulkImportPermission } from '@janus-idp/backstage-plugin-bulk-import-common'; -import { DrawerContextProvider } from '@janus-idp/shared-react'; -import { bulkImportApiRef } from '../../api/BulkImportBackendClient'; -import { - AddRepositoriesFormValues, - ApprovalTool, - ImportJobResponse, - RepositorySelection, -} from '../../types'; -import { - getJobErrors, - prepareDataForSubmission, -} from '../../utils/repository-utils'; import { AddRepositoriesForm } from './AddRepositoriesForm'; import { Illustrations } from './Illustrations'; @@ -45,76 +30,19 @@ const useStyles = makeStyles(() => ({ })); export const AddRepositoriesPage = () => { + const queryClientRef = React.useRef(); const theme = useTheme(); const classes = useStyles(); - const bulkImportApi = useApi(bulkImportApiRef); - const navigate = useNavigate(); - const [generalSubmitError, setGeneralSubmitError] = React.useState<{ - message: string; - title: string; - } | null>(null); - const initialValues: AddRepositoriesFormValues = { - repositoryType: RepositorySelection.Repository, - repositories: {}, - excludedRepositories: {}, - approvalTool: ApprovalTool.Git, - }; + + if (!queryClientRef.current) { + queryClientRef.current = new QueryClient(); + } const bulkImportViewPermissionResult = usePermission({ permission: bulkImportPermission, resourceRef: bulkImportPermission.resourceType, }); - const handleSubmit = async ( - values: AddRepositoriesFormValues, - formikHelpers: FormikHelpers, - ) => { - formikHelpers.setSubmitting(true); - formikHelpers.setStatus(null); - const importRepositories = prepareDataForSubmission( - values.repositories, - values.approvalTool, - ); - try { - formikHelpers.setSubmitting(true); - const dryrunResponse: ImportJobResponse[] = - await bulkImportApi.createImportJobs(importRepositories, true); - const dryRunErrors = getJobErrors(dryrunResponse); - if (Object.keys(dryRunErrors?.errors || {}).length > 0) { - formikHelpers.setStatus(dryRunErrors); - formikHelpers.setSubmitting(false); - } else { - formikHelpers.setStatus(dryRunErrors); // to show info messages - const createJobResponse: ImportJobResponse[] | Response = - await bulkImportApi.createImportJobs(importRepositories); - formikHelpers.setSubmitting(true); - if (!Array.isArray(createJobResponse)) { - setGeneralSubmitError({ - message: - get(createJobResponse, 'error.message') || - 'Failed to create pull request', - title: get(createJobResponse, 'error.name') || 'Error occured', - }); - formikHelpers.setSubmitting(false); - } else { - const createJobErrors = getJobErrors(createJobResponse); - if (Object.keys(createJobErrors?.errors || {}).length > 0) { - formikHelpers.setStatus(createJobErrors); - formikHelpers.setSubmitting(false); - } else { - navigate(`..`); - } - } - } - } catch (error: any) { - setGeneralSubmitError({ - message: error?.message || 'Error occured', - title: error?.name, - }); - formikHelpers.setSubmitting(false); - } - }; - const showContent = () => { if (bulkImportViewPermissionResult.loading) { return ; @@ -176,15 +104,9 @@ export const AddRepositoriesPage = () => { - - - - - + + + ); } diff --git a/plugins/bulk-import/src/components/AddRepositories/RepositoriesTable.tsx b/plugins/bulk-import/src/components/AddRepositories/RepositoriesTable.tsx index 83e241f0fc..1153e262cb 100644 --- a/plugins/bulk-import/src/components/AddRepositories/RepositoriesTable.tsx +++ b/plugins/bulk-import/src/components/AddRepositories/RepositoriesTable.tsx @@ -17,7 +17,6 @@ import { evaluateRowForRepo, filterSelectedForActiveDrawer, getComparator, - getNewOrgsData, updateWithNewSelectedRepositories, } from '../../utils/repository-utils'; import { AddRepositoriesDrawer } from './AddRepositoriesDrawer'; @@ -60,13 +59,6 @@ export const RepositoriesTable = ({ searchString, }); - const [orgsData, setOrgsData] = React.useState<{ - [name: string]: AddRepositoryData; - }>({}); - const [, setReposData] = React.useState<{ - [name: string]: AddRepositoryData; - }>({}); - React.useEffect(() => { if (drawerOrganization) { setDrawerPage(0); @@ -83,12 +75,8 @@ export const RepositoriesTable = ({ React.useEffect(() => { if (showOrganizations) { - setOrgsData(data?.organizations || {}); - setTableData(Object.values(data?.organizations || {})); } else { - setReposData(data?.repositories || {}); - setTableData(Object.values(data?.repositories || {})); } }, [data, showOrganizations]); @@ -159,34 +147,6 @@ export const RepositoriesTable = ({ } else { updateSelectedRepositories(newSelectedRows); } - - const newOrgsData = Object.values(orgsData)?.reduce((orgAcc, org) => { - return { - ...orgAcc, - [org.orgName as string]: { - ...org, - selectedRepositories: Object.values(newSelectedRows)?.reduce( - (acc, row) => { - if (row.orgName === org.orgName) { - return { - ...acc, - [row.id]: { - ...row, - catalogInfoYaml: { - ...row.catalogInfoYaml, - status: RepositoryStatus.Ready, - }, - }, - }; - } - return acc; - }, - {}, - ), - }, - }; - }, {}); - setOrgsData(newOrgsData); }; const handleSelectAllClick = ( _event: React.ChangeEvent, @@ -220,11 +180,6 @@ export const RepositoriesTable = ({ newSelected = { ...selected, [repo.id]: repo }; } updateSelection(newSelected); - // handle non drawer selection click - if (!drawerOrganization) { - const newOrgsData = getNewOrgsData(orgsData, repo); - setOrgsData(newOrgsData); - } }; const handleChangePage = (_event: unknown, newPage: number) => { @@ -264,27 +219,13 @@ export const RepositoriesTable = ({ }, [setIsOpen]); const handleUpdatesFromDrawer = React.useCallback( - (drawerSelected: AddedRepositories, drawerOrgId: string) => { + (drawerSelected: AddedRepositories) => { if (drawerSelected) { setSelected(drawerSelected); updateSelectedRepositories(drawerSelected); - - const newOrgsData = Object.values(orgsData).reduce((acc, org) => { - if (org.id === drawerOrgId) { - return { - ...acc, - [org.orgName as string]: { - ...org, - selectedRepositories: drawerSelected, - }, - }; - } - return acc; - }, {}); - setOrgsData(newOrgsData); } }, - [updateSelectedRepositories, orgsData, setOrgsData, setSelected], + [updateSelectedRepositories, setSelected], ); const selectedForActiveDrawer = React.useMemo( diff --git a/plugins/bulk-import/src/components/BulkImportPage.test.tsx b/plugins/bulk-import/src/components/BulkImportPage.test.tsx index 4bdef1c9ca..1880280028 100644 --- a/plugins/bulk-import/src/components/BulkImportPage.test.tsx +++ b/plugins/bulk-import/src/components/BulkImportPage.test.tsx @@ -37,7 +37,7 @@ describe('BulkImport Page', () => { RequirePermissionMock.mockImplementation(props => <>{props.children}); mockUsePermission.mockReturnValue({ loading: false, allowed: true }); mockUseAddedRepositories.mockReturnValue({ - loaded: true, + loading: false, data: { addedRepositories: [], totalJobs: 0 }, refetch: jest.fn(), error: undefined, diff --git a/plugins/bulk-import/src/components/BulkImportPage.tsx b/plugins/bulk-import/src/components/BulkImportPage.tsx index 9c932790f1..39b8ab0320 100644 --- a/plugins/bulk-import/src/components/BulkImportPage.tsx +++ b/plugins/bulk-import/src/components/BulkImportPage.tsx @@ -46,7 +46,7 @@ export const BulkImportPage = () => { } if (bulkImportViewPermissionResult.allowed) { return ( - + + {data.repoName} - - {urlHelper(data?.repoUrl || '')} - - + {data?.repoUrl ? ( + + {urlHelper(data.repoUrl)} + + + ) : ( + <>- + )} - - {urlHelper(data?.organizationUrl || '')} - - + {data?.organizationUrl ? ( + + {urlHelper(data.organizationUrl)} + + + ) : ( + <>- + )} - diff --git a/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.test.tsx b/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.test.tsx index 1bc0b7758a..7deb63de44 100644 --- a/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.test.tsx +++ b/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.test.tsx @@ -9,6 +9,7 @@ import { useFormikContext } from 'formik'; import { mockGetImportJobs, mockGetRepositories } from '../../mocks/mockData'; import { RepositoryStatus } from '../../types'; +import { getPRTemplate } from '../../utils/repository-utils'; import CatalogInfoAction from './CatalogInfoAction'; jest.mock('@backstage/plugin-permission-react', () => ({ @@ -105,8 +106,18 @@ describe('CatalogInfoAction', () => { repositories: { ['org/dessert/cupcake']: { ...mockGetImportJobs.imports[0], + repoUrl: 'https://github.com/org/dessert/cupcake', + status: RepositoryStatus.ADDED, catalogInfoYaml: { status: RepositoryStatus.ADDED, + prTemplate: getPRTemplate( + 'org/dessert/cupcake', + 'org/dessert', + 'user:default/guest', + 'https://localhost:3001', + 'https://github.com/org/dessert/cupcake', + 'main', + ), }, }, }, diff --git a/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.tsx b/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.tsx index 2bef9b2627..0eb1b58404 100644 --- a/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.tsx +++ b/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.tsx @@ -63,6 +63,10 @@ const CatalogInfoAction = ({ data }: { data: AddRepositoryData }) => { values.repositories[data.id]?.catalogInfoYaml?.status === RepositoryStatus.WAIT_PR_APPROVAL; + const canView = + values?.repositories?.[data.id]?.catalogInfoYaml?.status === + RepositoryStatus.ADDED && values?.repositories?.[data.id]?.repoUrl; + const removeQueryParams = () => { searchParams.delete('repository'); searchParams.delete('defaultBranch'); @@ -95,20 +99,11 @@ const CatalogInfoAction = ({ data }: { data: AddRepositoryData }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [repoUrl, defaultBranch, value?.status, values?.repositories, loading]); - return ( - - - {hasPermissionToEdit ? ( + const catalogIcon = () => { + if (hasPermissionToEdit) { + return { + tooltip: 'Edit catalog-info.yaml pull request', + icon: ( { > - ) : ( + ), + dataTestId: 'edit-catalog-info', + }; + } + if (canView) { + return { + tooltip: 'View catalog-info.yaml file', + icon: ( - )} - + ), + dataTestId: 'view-catalog-info', + }; + } + return null; + }; + + return catalogIcon()?.tooltip ? ( + + {catalogIcon()?.icon} - ); + ) : null; }; export default CatalogInfoAction; diff --git a/plugins/bulk-import/src/components/Repositories/RepositoriesList.test.tsx b/plugins/bulk-import/src/components/Repositories/RepositoriesList.test.tsx index 5596bd2237..cfffbc9abe 100644 --- a/plugins/bulk-import/src/components/Repositories/RepositoriesList.test.tsx +++ b/plugins/bulk-import/src/components/Repositories/RepositoriesList.test.tsx @@ -50,7 +50,7 @@ const mockIdentityApi = { }; const mockAsyncData = { - loaded: true, + loading: false, data: { addedRepositories: mockGetImportJobs.imports, totalJobs: mockGetImportJobs.imports.length, diff --git a/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx b/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx index df49763a34..e3582a90b8 100644 --- a/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx +++ b/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx @@ -41,7 +41,7 @@ export const RepositoriesList = () => { const { data: importJobs, error: errJobs, - loaded: jobsLoaded, + loading, refetch, } = useAddedRepositories(pageNumber + 1, rowsPerPage, debouncedSearch); @@ -92,7 +92,7 @@ export const RepositoriesList = () => { columns={RepositoriesListColumns} onSearchChange={handleSearch} title={ - !jobsLoaded || !importJobs || importJobs.totalJobs === 0 + importJobs?.totalJobs === 0 ? 'Added repositories' : `Added repositories (${importJobs.totalJobs})` } @@ -108,7 +108,7 @@ export const RepositoriesList = () => { Body: () => ( diff --git a/plugins/bulk-import/src/hooks/useAddedRepositories.test.ts b/plugins/bulk-import/src/hooks/useAddedRepositories.test.ts index 5fb06c7efe..b93eabad07 100644 --- a/plugins/bulk-import/src/hooks/useAddedRepositories.test.ts +++ b/plugins/bulk-import/src/hooks/useAddedRepositories.test.ts @@ -1,6 +1,6 @@ import { renderHook, waitFor } from '@testing-library/react'; -import { mockGetImportJobs } from '../mocks/mockData'; +import { mockGetImportJobs, mockGetRepositories } from '../mocks/mockData'; import { useAddedRepositories } from './useAddedRepositories'; jest.mock('@backstage/core-plugin-api', () => ({ @@ -24,6 +24,9 @@ jest.mock('formik', () => ({ ...jest.requireActual('formik'), useFormikContext: jest.fn().mockReturnValue({ setFieldValue: jest.fn(), + values: { + repositories: mockGetRepositories, + }, }), })); @@ -31,7 +34,7 @@ describe('useAddedRepositories', () => { it('should return import jobs', async () => { const { result } = renderHook(() => useAddedRepositories(1, 5, '')); await waitFor(() => { - expect(result.current.loaded).toBeTruthy(); + expect(result.current.loading).toBeFalsy(); expect(result.current.data.totalJobs).toBe(4); }); }); diff --git a/plugins/bulk-import/src/hooks/useAddedRepositories.ts b/plugins/bulk-import/src/hooks/useAddedRepositories.ts index 58aceff3f0..c52703b6b9 100644 --- a/plugins/bulk-import/src/hooks/useAddedRepositories.ts +++ b/plugins/bulk-import/src/hooks/useAddedRepositories.ts @@ -1,5 +1,5 @@ import React from 'react'; -import { useAsync, useDebounce } from 'react-use'; +import { useAsync } from 'react-use'; import { configApiRef, @@ -10,11 +10,6 @@ import { import { useQuery } from '@tanstack/react-query'; import { useFormikContext } from 'formik'; -import { - useDebounceCallback, - useDeepCompareMemoize, -} from '@janus-idp/shared-react'; - import { bulkImportApiRef } from '../api/BulkImportBackendClient'; import { AddRepositoriesFormValues, @@ -29,20 +24,14 @@ export const useAddedRepositories = ( searchString: string, pollInterval?: number, ): { - loaded: boolean; data: { addedRepositories: AddRepositoryData[]; totalJobs: number; }; error: any; + loading: boolean; refetch: () => void; } => { - const [addedRepositoriesData, setAddedRepositoriesData] = React.useState<{ - [id: string]: AddRepositoryData; - }>({}); - const [totalImportJobs, setTotalImportJobs] = React.useState(0); - const [loaded, setLoaded] = React.useState(false); - const [debouncedSearch, setDebouncedSearch] = React.useState(searchString); const identityApi = useApi(identityApiRef); const configApi = useApi(configApiRef); const { value: user } = useAsync(async () => { @@ -50,76 +39,51 @@ export const useAddedRepositories = ( return identityRef.userEntityRef; }); - useDebounce( - () => { - setDebouncedSearch(searchString); - }, - 200, - [searchString], - ); - const { value: baseUrl } = useAsync(async () => { const url = configApi.getString('app.baseUrl'); return url; }); const bulkImportApi = useApi(bulkImportApiRef); - const { setFieldValue } = useFormikContext(); + const { setFieldValue, values } = + useFormikContext(); const fetchAddedRepositories = async ( page: number, size: number, searchStr: string, - ) => { - const response = await bulkImportApi.getImportJobs(page, size, searchStr); - return response; - }; + ) => await bulkImportApi.getImportJobs(page, size, searchStr); const { data: value, error, - isLoading: loading, + isLoading: isLoadingTable, refetch, } = useQuery( - ['importJobs', pageNumber, rowsPerPage, debouncedSearch], - () => fetchAddedRepositories(pageNumber, rowsPerPage, debouncedSearch), - { keepPreviousData: true, refetchInterval: pollInterval || 60000 }, + ['importJobs', pageNumber, rowsPerPage, searchString], + () => fetchAddedRepositories(pageNumber, rowsPerPage, searchString), + { refetchInterval: pollInterval || 60000 }, ); - const prepareData = React.useCallback( - (addedRepositories: ImportJobs | Response, isLoading: boolean) => { - if (!isLoading) { - const repoData = prepareDataForAddedRepositories( - addedRepositories, - user as string, - baseUrl as string, - ); - setAddedRepositoriesData(repoData); - setFieldValue(`repositories`, repoData); - setTotalImportJobs((addedRepositories as ImportJobs)?.totalCount || 0); - setLoaded(true); - } - }, - [ - user, - baseUrl, - setFieldValue, - setAddedRepositoriesData, - setTotalImportJobs, - ], - ); - - const debouncedUpdateResources = useDebounceCallback(prepareData, 250); - - React.useEffect(() => { - debouncedUpdateResources?.(value, loading); - }, [debouncedUpdateResources, value, loading]); + const prepareData = React.useMemo(() => { + const repoData = prepareDataForAddedRepositories( + value as ImportJobs | Response, + user as string, + baseUrl as string, + ); + if ( + Object.values(repoData.repoData).length !== + Object.values(values.repositories).length + ) + setFieldValue(`repositories`, repoData.repoData); + return { + addedRepositories: Object.values(repoData.repoData), + totalJobs: repoData.totalJobs, + }; + }, [value, user, baseUrl, values.repositories, setFieldValue]); - return useDeepCompareMemoize({ - data: { - addedRepositories: Object.values(addedRepositoriesData), - totalJobs: totalImportJobs, - }, - loaded, + return { + data: prepareData, + loading: isLoadingTable, error: { ...(error ?? {}), ...((value as Response)?.statusText @@ -130,5 +94,5 @@ export const useAddedRepositories = ( : {}), }, refetch, - }); + }; }; diff --git a/plugins/bulk-import/src/hooks/useRepositories.test.ts b/plugins/bulk-import/src/hooks/useRepositories.test.ts index a5b550ff9f..6561ae948b 100644 --- a/plugins/bulk-import/src/hooks/useRepositories.test.ts +++ b/plugins/bulk-import/src/hooks/useRepositories.test.ts @@ -1,5 +1,4 @@ -import { useApi } from '@backstage/core-plugin-api'; - +import { useQuery } from '@tanstack/react-query'; import { renderHook, waitFor } from '@testing-library/react'; import { mockGetOrganizations, mockGetRepositories } from '../mocks/mockData'; @@ -10,12 +9,18 @@ jest.mock('@backstage/core-plugin-api', () => ({ useApi: jest.fn(), })); -const mockUseApi = useApi as jest.MockedFunction; +jest.mock('@tanstack/react-query', () => ({ + ...jest.requireActual('@tanstack/react-query'), + useQuery: jest.fn(), +})); describe('useRepositories', () => { it('should return repositories', async () => { - mockUseApi.mockReturnValue({ - dataFetcher: jest.fn().mockReturnValue(mockGetRepositories), + (useQuery as jest.Mock).mockReturnValue({ + data: mockGetRepositories, + isLoading: false, + error: null, + refetch: jest.fn(), }); const { result } = renderHook(() => useRepositories({ @@ -32,8 +37,11 @@ describe('useRepositories', () => { }); it('should return organizations', async () => { - mockUseApi.mockReturnValue({ - dataFetcher: jest.fn().mockReturnValue(mockGetOrganizations), + (useQuery as jest.Mock).mockReturnValue({ + data: mockGetOrganizations, + isLoading: false, + error: null, + refetch: jest.fn(), }); const { result } = renderHook(() => useRepositories({ page: 1, querySize: 10, showOrganizations: true }), @@ -47,13 +55,16 @@ describe('useRepositories', () => { }); it('should return repositories in an organization', async () => { - mockUseApi.mockReturnValue({ - dataFetcher: jest.fn().mockReturnValue({ + (useQuery as jest.Mock).mockReturnValue({ + data: { ...mockGetRepositories, repositories: mockGetRepositories.repositories?.filter( r => r.organization === 'org/dessert', ), - }), + }, + isLoading: false, + error: null, + refetch: jest.fn(), }); const { result } = renderHook(() => useRepositories({ page: 1, querySize: 10, orgName: 'org/dessert' }), diff --git a/plugins/bulk-import/src/hooks/useRepositories.ts b/plugins/bulk-import/src/hooks/useRepositories.ts index cc2627020a..3fedfb88e4 100644 --- a/plugins/bulk-import/src/hooks/useRepositories.ts +++ b/plugins/bulk-import/src/hooks/useRepositories.ts @@ -1,5 +1,5 @@ import React from 'react'; -import { useAsync, useDebounce } from 'react-use'; +import { useAsync } from 'react-use'; import { configApiRef, @@ -7,58 +7,35 @@ import { useApi, } from '@backstage/core-plugin-api'; -import { - useDebounceCallback, - useDeepCompareMemoize, -} from '@janus-idp/shared-react'; +import { useQuery } from '@tanstack/react-query'; import { bulkImportApiRef } from '../api/BulkImportBackendClient'; import { AddRepositoryData, + DataFetcherQueryParams, OrgAndRepoResponse, RepositoriesError, - Repository, } from '../types'; -import { getPRTemplate } from '../utils/repository-utils'; +import { + prepareDataForOrganizations, + prepareDataForRepositories, +} from '../utils/repository-utils'; -export const useRepositories = (options: { - page: number; - querySize: number; - showOrganizations?: boolean; - orgName?: string; - searchString?: string; -}): { +export const useRepositories = ( + options: DataFetcherQueryParams, +): { loading: boolean; data: { repositories?: { [id: string]: AddRepositoryData }; organizations?: { [id: string]: AddRepositoryData }; - totalRepositories: number; - totalOrganizations: number; + totalRepositories?: number; + totalOrganizations?: number; } | null; error: RepositoriesError | undefined; } => { - const [repositoriesData, setRepositoriesData] = React.useState<{ - [id: string]: AddRepositoryData; - }>({}); - const [totalOrganizations, setTotalOrganizations] = React.useState(0); - const [isLoading, setIsLoading] = React.useState(true); - const [debouncedSearch, setDebouncedSearch] = React.useState( - options?.searchString, - ); - const [totalRepositories, setTotalRepositories] = React.useState(0); - const [organizationsData, setOrganizationsData] = React.useState<{ - [id: string]: AddRepositoryData; - }>({}); const identityApi = useApi(identityApiRef); const configApi = useApi(configApiRef); - - useDebounce( - () => { - setDebouncedSearch(options?.searchString); - }, - 500, - [options?.searchString], - ); + const bulkImportApi = useApi(bulkImportApiRef); const { value: user } = useAsync(async () => { const identityRef = await identityApi.getBackstageIdentity(); @@ -70,129 +47,63 @@ export const useRepositories = (options: { return url; }); - const bulkImportApi = useApi(bulkImportApiRef); - const { value, loading, error } = useAsync(async () => { - setIsLoading(true); - if (options.showOrganizations) { - return await bulkImportApi.dataFetcher( - options.page, - options.querySize, - debouncedSearch || '', - { - fetchOrganizations: true, - }, - ); + const fetchRepositories = async ( + pageNo: number, + size: number, + showOrganizations: boolean, + orgName: string, + searchStr: string = '', + ) => { + if (showOrganizations) { + return await bulkImportApi.dataFetcher(pageNo, size, searchStr, { + fetchOrganizations: true, + }); } - if (options.orgName) { - return await bulkImportApi.dataFetcher( - options.page, - options.querySize, - debouncedSearch || '', - { - orgName: options.orgName, - }, - ); + if (orgName) { + return await bulkImportApi.dataFetcher(pageNo, size, searchStr, { + orgName, + }); } - return await bulkImportApi.dataFetcher( - options.page, - options.querySize, - debouncedSearch || '', - ); - }, [ - options?.page, - options?.querySize, - options?.showOrganizations, - options?.orgName, - debouncedSearch, - ]); + return await bulkImportApi.dataFetcher(pageNo, size, searchStr); + }; - const prepareData = React.useCallback( - (result: OrgAndRepoResponse, dataLoading: boolean) => { - const prepareDataForRepositories = () => { - const repoData: { [id: string]: AddRepositoryData } = - result?.repositories?.reduce((acc, val: Repository) => { - const id = val.id || `${val.organization}/${val.name}`; - return { - ...acc, - [id]: { - id, - repoName: val.name, - defaultBranch: val.defaultBranch, - orgName: val.organization, - repoUrl: val.url, - organizationUrl: val?.url?.substring( - 0, - val.url.indexOf(val?.name || '') - 1, - ), - catalogInfoYaml: { - prTemplate: getPRTemplate( - val.name || '', - val.organization || '', - user as string, - baseUrl as string, - val.url || '', - val.defaultBranch || 'main', - ), - }, - }, - }; - }, {}) || {}; - setRepositoriesData(repoData); - setTotalRepositories(result?.totalCount); - setIsLoading(false); - }; - const prepareDataForOrganizations = () => { - const orgData: { [id: string]: AddRepositoryData } = - result?.organizations?.reduce( - (acc: { [id: string]: AddRepositoryData }, val: Repository) => { - return { - ...acc, - [val.id]: { - id: val.id, - orgName: val.name, - organizationUrl: `https://github.com/${val?.name}`, - totalReposInOrg: val.totalRepoCount, - }, - }; - }, - {}, - ) || {}; - setOrganizationsData(orgData); - setTotalOrganizations(result?.totalCount); - setIsLoading(false); - }; - if (!dataLoading) - if (options?.showOrganizations) { - prepareDataForOrganizations(); - } else { - prepareDataForRepositories(); - } - }, + const { + data: value, + error, + isLoading: isQueryLoading, + } = useQuery( [ - user, + options?.showOrganizations ? 'organizations' : 'repositories', + options?.page, + options?.querySize, options?.showOrganizations, - baseUrl, - setRepositoriesData, - setOrganizationsData, - setTotalOrganizations, - setTotalRepositories, + options?.orgName, + options?.searchString, ], + () => + fetchRepositories( + options?.page, + options?.querySize, + options?.showOrganizations || false, + options?.orgName || '', + options?.searchString, + ), ); - const debouncedUpdateResources = useDebounceCallback(prepareData, 200); - - React.useEffect(() => { - debouncedUpdateResources?.(value, loading); - }, [debouncedUpdateResources, value, loading]); + const prepareData = React.useMemo(() => { + if (options?.showOrganizations) { + return prepareDataForOrganizations(value as OrgAndRepoResponse); + } + return prepareDataForRepositories( + value as OrgAndRepoResponse, + user || 'user:default/guest', + baseUrl || '', + ); + }, [options?.showOrganizations, value, user, baseUrl]); - return useDeepCompareMemoize({ - loading: isLoading, - data: { - repositories: repositoriesData, - organizations: organizationsData, - totalOrganizations: totalOrganizations, - totalRepositories: totalRepositories, - }, + return { + loading: isQueryLoading, + data: prepareData, error: { ...(error ?? {}), ...((value?.errors && value.errors.length > 0) || @@ -200,5 +111,5 @@ export const useRepositories = (options: { ? { errors: value?.errors || (value as any as Response)?.statusText } : {}), } as RepositoriesError, - }); + }; }; diff --git a/plugins/bulk-import/src/types/types.ts b/plugins/bulk-import/src/types/types.ts index 9bc0221c59..2e3061c187 100644 --- a/plugins/bulk-import/src/types/types.ts +++ b/plugins/bulk-import/src/types/types.ts @@ -130,3 +130,11 @@ export type JobErrors = { export interface RepositoriesError extends Error { errors?: string[]; } + +export type DataFetcherQueryParams = { + page: number; + querySize: number; + showOrganizations?: boolean; + orgName?: string; + searchString?: string; +}; diff --git a/plugins/bulk-import/src/utils/repository-utils.test.tsx b/plugins/bulk-import/src/utils/repository-utils.test.tsx index c85787d0a8..3bb7cd4bb8 100644 --- a/plugins/bulk-import/src/utils/repository-utils.test.tsx +++ b/plugins/bulk-import/src/utils/repository-utils.test.tsx @@ -8,7 +8,6 @@ import { componentNameRegex, evaluatePRTemplate, getJobErrors, - getNewOrgsData, getYamlKeyValuePairs, updateWithNewSelectedRepositories, urlHelper, @@ -66,33 +65,6 @@ describe('Repository utils', () => { expect(url).toBe('-'); }); - it('should update organization data when repositories are selected', () => { - const newOrgsData = getNewOrgsData( - { - 'org/dessert': { - id: '1234', - orgName: 'org/dessert', - organizationUrl: 'https://github.com/org/dessert', - }, - 'org/food': { - id: '1235', - orgName: 'org/food', - organizationUrl: 'https://github.com/org/food', - }, - 'org/pet-store-boston': { - id: '1236', - orgName: 'org/pet-store-boston', - organizationUrl: 'https://github.com/org/pet-store-boston', - }, - }, - mockGetRepositories.repositories[1], - ); - expect( - Object.values(newOrgsData).find(o => o.orgName === 'org/dessert') - ?.selectedRepositories, - ).toEqual({ 'org/dessert/donut': mockGetRepositories.repositories[1] }); - }); - it('should parse key-value pairs correctly with semicolons', () => { const prKeyValuePairInput = `argocd/app-name: 'guestbook'; github.com/project-slug: janus-idp/backstage-showcase; backstage.io/createdAt: '5/12/2021, 07:03:18 AM'; quay.io/repository-slug: janus-idp/backstage-showcase; backstage.io/kubernetes-id: test-backstage`; diff --git a/plugins/bulk-import/src/utils/repository-utils.tsx b/plugins/bulk-import/src/utils/repository-utils.tsx index b2b34146cf..26d293fb70 100644 --- a/plugins/bulk-import/src/utils/repository-utils.tsx +++ b/plugins/bulk-import/src/utils/repository-utils.tsx @@ -21,7 +21,9 @@ import { ImportStatus, JobErrors, Order, + OrgAndRepoResponse, PullRequestPreview, + Repository, RepositorySelection, RepositoryStatus, } from '../types'; @@ -46,8 +48,8 @@ export const descendingComparator = ( }; if (orderBy === 'selectedRepositories') { - value1 = value1?.length; - value2 = value2?.length; + value1 = Object.keys(value1)?.length; + value2 = Object.values(value2)?.length; } if (orderBy === 'catalogInfoYaml.status') { @@ -191,37 +193,6 @@ export const urlHelper = (url: string) => { return url.split('https://')[1] || url; }; -export const getNewOrgsData = ( - orgsData: { [name: string]: AddRepositoryData }, - repo: AddRepositoryData, -): { [name: string]: AddRepositoryData } => { - const org = Object.values(orgsData)?.find(o => o.orgName === repo.orgName); - - let selectedRepositories = { ...(org?.selectedRepositories || {}) }; - selectedRepositories = selectedRepositories[repo.id] - ? Object.keys(selectedRepositories).reduce( - (acc, sr) => (sr === repo.id ? { ...acc, [repo.id]: repo } : acc), - {}, - ) - : { ...selectedRepositories, [repo.id]: repo }; - - const newOrgsData = - org && - Object.values(orgsData)?.reduce((acc, od) => { - if (od.orgName === org.orgName) { - return { - ...acc, - [org.orgName as string]: { - ...org, - selectedRepositories: selectedRepositories || [], - }, - }; - } - return acc; - }, {}); - return newOrgsData || []; -}; - export const getImportStatus = (status: string, showIcon?: boolean) => { if (!status) { return ''; @@ -318,7 +289,7 @@ export const areAllRowsSelected = ( export const getJobErrors = ( createJobResponse: ImportJobResponse[], ): JobErrors => { - return createJobResponse.reduce( + return createJobResponse?.reduce( (acc: JobErrors, res: ImportJobResponse) => { if (res.errors?.length > 0) { const errs = @@ -554,13 +525,68 @@ export const evaluatePRTemplate = ( } }; +export const prepareDataForOrganizations = (result: OrgAndRepoResponse) => { + const orgData: { [id: string]: AddRepositoryData } = + result?.organizations?.reduce( + (acc: { [id: string]: AddRepositoryData }, val: Repository) => { + return { + ...acc, + [val.id]: { + id: val.id, + orgName: val.name, + organizationUrl: `https://github.com/${val?.name}`, + totalReposInOrg: val.totalRepoCount, + }, + }; + }, + {}, + ) || {}; + return { organizations: orgData, totalOrganizations: result?.totalCount }; +}; + +export const prepareDataForRepositories = ( + result: OrgAndRepoResponse, + user: string, + baseUrl: string, +) => { + const repoData: { [id: string]: AddRepositoryData } = + result?.repositories?.reduce((acc, val: Repository) => { + const id = val.id; + return { + ...acc, + [id]: { + id, + repoName: val.name, + defaultBranch: val.defaultBranch || 'main', + orgName: val.organization, + repoUrl: val.url, + organizationUrl: val.url?.substring( + 0, + val.url.indexOf(val?.name || '') - 1, + ), + catalogInfoYaml: { + prTemplate: getPRTemplate( + val.name || '', + val.organization || '', + user, + baseUrl || '', + val.url || '', + val.defaultBranch || 'main', + ), + }, + }, + }; + }, {}) || {}; + return { repositories: repoData, totalRepositories: result?.totalCount }; +}; + export const prepareDataForAddedRepositories = ( addedRepositories: ImportJobs | Response | undefined, user: string, baseUrl: string, -) => { +): { repoData: AddedRepositories; totalJobs: number } => { if (!Array.isArray((addedRepositories as ImportJobs)?.imports)) { - return {}; + return { repoData: {}, totalJobs: 0 }; } const importJobs = addedRepositories as ImportJobs; const repoData: { [id: string]: AddRepositoryData } = @@ -596,5 +622,8 @@ export const prepareDataForAddedRepositories = ( }, }; }, {}); - return repoData; + return { + repoData, + totalJobs: (addedRepositories as ImportJobs)?.totalCount || 0, + }; }; diff --git a/plugins/bulk-import/tests/bulkImport.spec.ts b/plugins/bulk-import/tests/bulkImport.spec.ts index 8ed2c0db4d..6dd07a2829 100644 --- a/plugins/bulk-import/tests/bulkImport.spec.ts +++ b/plugins/bulk-import/tests/bulkImport.spec.ts @@ -103,6 +103,7 @@ test.describe('Bulk import plugin', () => { timeout: 20000, }); await page.locator('button[aria-label="Next page"]').click(); + await page.waitForTimeout(2000); await page.click('input[aria-label="select all repositories"]'); await expect( page.getByRole('heading', { name: 'Selected repositories (9)' }), @@ -110,6 +111,7 @@ test.describe('Bulk import plugin', () => { timeout: 20000, }); await page.locator(`button`).filter({ hasText: 'Organization' }).click(); + await page.waitForTimeout(2000); await expect( page.getByRole('heading', { name: 'Selected repositories (9)' }), ).toBeVisible({ @@ -125,6 +127,7 @@ test.describe('Bulk import plugin', () => { test('Select Repositories side panel is shown', async () => { await page.locator('button[type="button"][value="repository"]').click(); + await page.waitForTimeout(2000); await expect( page.getByRole('heading', { name: 'Selected repositories (9)' }), ).toBeVisible({