From 54e98c1c766e3aca694fb2585b1fc418eb88cf33 Mon Sep 17 00:00:00 2001 From: debsmita1 Date: Tue, 5 Nov 2024 22:18:08 +0530 Subject: [PATCH] feat(bulk-import): use react query to fetch repositories --- .../.changeset/serious-lizards-bathe.md | 5 + .../src/api/BulkImportBackendClient.test.ts | 9 +- .../src/api/BulkImportBackendClient.ts | 6 +- .../AddRepositories/AddRepositories.tsx | 143 +++++++++++++ .../AddRepositories/AddRepositoriesDrawer.tsx | 4 +- .../AddRepositoriesForm.test.tsx | 39 +++- .../AddRepositories/AddRepositoriesForm.tsx | 199 +++++++----------- .../AddRepositories/AddRepositoriesPage.tsx | 96 +-------- .../AddRepositories/RepositoriesTable.tsx | 63 +----- .../src/components/BulkImportPage.test.tsx | 2 +- .../src/components/BulkImportPage.tsx | 2 +- .../Repositories/AddedRepositoryTableRow.tsx | 31 ++- .../Repositories/CatalogInfoAction.test.tsx | 11 + .../Repositories/CatalogInfoAction.tsx | 53 ++--- .../Repositories/RepositoriesList.test.tsx | 2 +- .../Repositories/RepositoriesList.tsx | 6 +- .../src/hooks/useAddedRepositories.test.ts | 7 +- .../src/hooks/useAddedRepositories.ts | 101 +++------ .../src/hooks/useRepositories.test.ts | 31 ++- .../bulk-import/src/hooks/useRepositories.ts | 194 +++++------------ .../plugins/bulk-import/src/types/types.ts | 8 + .../src/utils/repository-utils.test.tsx | 28 --- .../src/utils/repository-utils.tsx | 103 +++++---- .../bulk-import/tests/bulkImport.spec.ts | 3 + 24 files changed, 537 insertions(+), 609 deletions(-) create mode 100644 workspaces/bulk-import/.changeset/serious-lizards-bathe.md create mode 100644 workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositories.tsx diff --git a/workspaces/bulk-import/.changeset/serious-lizards-bathe.md b/workspaces/bulk-import/.changeset/serious-lizards-bathe.md new file mode 100644 index 0000000..f0c5a63 --- /dev/null +++ b/workspaces/bulk-import/.changeset/serious-lizards-bathe.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-bulk-import': minor +--- + +use react query to fetch repositories diff --git a/workspaces/bulk-import/plugins/bulk-import/src/api/BulkImportBackendClient.test.ts b/workspaces/bulk-import/plugins/bulk-import/src/api/BulkImportBackendClient.test.ts index 29fb70c..44d4ec3 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/api/BulkImportBackendClient.test.ts +++ b/workspaces/bulk-import/plugins/bulk-import/src/api/BulkImportBackendClient.test.ts @@ -145,8 +145,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)); @@ -368,6 +371,8 @@ describe('BulkImportBackendClient', () => { expect(response).toEqual({ message: 'Dry run for creating import jobs failed', + ok: false, + status: 404, }); }); }); diff --git a/workspaces/bulk-import/plugins/bulk-import/src/api/BulkImportBackendClient.ts b/workspaces/bulk-import/plugins/bulk-import/src/api/BulkImportBackendClient.ts index 0fe19d0..5946e44 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/api/BulkImportBackendClient.ts +++ b/workspaces/bulk-import/plugins/bulk-import/src/api/BulkImportBackendClient.ts @@ -136,7 +136,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/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositories.tsx b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositories.tsx new file mode 100644 index 0000000..5d68439 --- /dev/null +++ b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositories.tsx @@ -0,0 +1,143 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import React from 'react'; + +import { useDrawer } from '@janus-idp/shared-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 { 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/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesDrawer.tsx b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesDrawer.tsx index 81b95e2..69ae821 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesDrawer.tsx +++ b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesDrawer.tsx @@ -74,7 +74,7 @@ export const AddRepositoriesDrawer = ({ }: { open: boolean; onClose: () => void; - onSelect: (repos: AddedRepositories, drawerOrgName: string) => void; + onSelect: (repos: AddedRepositories) => void; title: string; orgData: AddRepositoryData; }) => { @@ -89,7 +89,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/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.test.tsx b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.test.tsx index 6013b22..92dcec7 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.test.tsx +++ b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.test.tsx @@ -19,13 +19,18 @@ import { configApiRef, identityApiRef } from '@backstage/core-plugin-api'; import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; import { useDrawer } from '@janus-idp/shared-react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen } from '@testing-library/react'; import { useFormikContext } from 'formik'; 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'), @@ -57,6 +62,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, @@ -66,6 +81,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(); @@ -83,9 +105,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, @@ -110,7 +133,9 @@ describe('AddRepsositoriesForm', () => { ], ]} > - + + + , ); @@ -147,9 +172,9 @@ describe('AddRepsositoriesForm', () => { ], ]} > - + + + , ); diff --git a/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx index e6d2530..fb81b09 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx +++ b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx @@ -12,135 +12,96 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ import React from 'react'; + */ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; -import { useDrawer } from '@janus-idp/shared-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 { useApi } from '@backstage/core-plugin-api'; -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'; +import { useMutation } from '@tanstack/react-query'; +import { Formik, FormikHelpers } from 'formik'; -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.setStatus(null); + const importRepositories = prepareDataForSubmission( + values.repositories, + values.approvalTool, + ); + 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); + } else { + formikHelpers.setStatus(dryRunErrors); // to show info messages + const submitResult = await mutationCreate.mutateAsync({ + importJobs: importRepositories, + }); + const createJobErrors = getJobErrors( + submitResult as ImportJobResponse[], + ); + if (Object.keys(createJobErrors?.errors || {}).length > 0) { + formikHelpers.setStatus(createJobErrors); + } else { + navigate(`..`); + } + } + } }; 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/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesPage.tsx b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesPage.tsx index 78ac9e9..27bd667 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesPage.tsx +++ b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesPage.tsx @@ -13,13 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ 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 { DrawerContextProvider } from '@janus-idp/shared-react'; import { Accordion, AccordionDetails, @@ -30,22 +27,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 '@red-hat-developer-hub/backstage-plugin-bulk-import-common'; -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'; @@ -59,76 +44,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 ; @@ -190,15 +118,9 @@ export const AddRepositoriesPage = () => { - - - - - + + + ); } diff --git a/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/RepositoriesTable.tsx b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/RepositoriesTable.tsx index 338b423..ccf3c6f 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/RepositoriesTable.tsx +++ b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/RepositoriesTable.tsx @@ -31,7 +31,6 @@ import { evaluateRowForRepo, filterSelectedForActiveDrawer, getComparator, - getNewOrgsData, updateWithNewSelectedRepositories, } from '../../utils/repository-utils'; import { AddRepositoriesDrawer } from './AddRepositoriesDrawer'; @@ -74,13 +73,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); @@ -97,12 +89,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]); @@ -173,34 +161,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, @@ -234,11 +194,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) => { @@ -278,27 +233,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/workspaces/bulk-import/plugins/bulk-import/src/components/BulkImportPage.test.tsx b/workspaces/bulk-import/plugins/bulk-import/src/components/BulkImportPage.test.tsx index e0d3830..779ac16 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/components/BulkImportPage.test.tsx +++ b/workspaces/bulk-import/plugins/bulk-import/src/components/BulkImportPage.test.tsx @@ -51,7 +51,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/workspaces/bulk-import/plugins/bulk-import/src/components/BulkImportPage.tsx b/workspaces/bulk-import/plugins/bulk-import/src/components/BulkImportPage.tsx index a9c33f3..ed6ad3a 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/components/BulkImportPage.tsx +++ b/workspaces/bulk-import/plugins/bulk-import/src/components/BulkImportPage.tsx @@ -60,7 +60,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/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.test.tsx b/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.test.tsx index f13f6ec..280982e 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.test.tsx +++ b/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.test.tsx @@ -23,6 +23,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', () => ({ @@ -119,8 +120,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/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.tsx b/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.tsx index 826879b..17e0cb2 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.tsx +++ b/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.tsx @@ -77,6 +77,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'); @@ -109,20 +113,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/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/RepositoriesList.test.tsx b/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/RepositoriesList.test.tsx index acb0ab2..44c49ae 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/RepositoriesList.test.tsx +++ b/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/RepositoriesList.test.tsx @@ -64,7 +64,7 @@ const mockIdentityApi = { }; const mockAsyncData = { - loaded: true, + loading: false, data: { addedRepositories: mockGetImportJobs.imports, totalJobs: mockGetImportJobs.imports.length, diff --git a/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx b/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx index 482665a..daf5cd4 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx +++ b/workspaces/bulk-import/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx @@ -54,7 +54,7 @@ export const RepositoriesList = () => { const { data: importJobs, error: errJobs, - loaded: jobsLoaded, + loading, refetch, } = useAddedRepositories(pageNumber + 1, rowsPerPage, debouncedSearch); @@ -105,7 +105,7 @@ export const RepositoriesList = () => { columns={RepositoriesListColumns} onSearchChange={handleSearch} title={ - !jobsLoaded || !importJobs || importJobs.totalJobs === 0 + importJobs?.totalJobs === 0 ? 'Added repositories' : `Added repositories (${importJobs.totalJobs})` } @@ -121,7 +121,7 @@ export const RepositoriesList = () => { Body: () => ( diff --git a/workspaces/bulk-import/plugins/bulk-import/src/hooks/useAddedRepositories.test.ts b/workspaces/bulk-import/plugins/bulk-import/src/hooks/useAddedRepositories.test.ts index a8e2044..8919427 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/hooks/useAddedRepositories.test.ts +++ b/workspaces/bulk-import/plugins/bulk-import/src/hooks/useAddedRepositories.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ 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', () => ({ @@ -38,6 +38,9 @@ jest.mock('formik', () => ({ ...jest.requireActual('formik'), useFormikContext: jest.fn().mockReturnValue({ setFieldValue: jest.fn(), + values: { + repositories: mockGetRepositories, + }, }), })); @@ -45,7 +48,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/workspaces/bulk-import/plugins/bulk-import/src/hooks/useAddedRepositories.ts b/workspaces/bulk-import/plugins/bulk-import/src/hooks/useAddedRepositories.ts index 40a82ea..ea74c06 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/hooks/useAddedRepositories.ts +++ b/workspaces/bulk-import/plugins/bulk-import/src/hooks/useAddedRepositories.ts @@ -12,8 +12,9 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ import React from 'react'; -import { useAsync, useDebounce } from 'react-use'; + */ +import React from 'react'; +import { useAsync } from 'react-use'; import { configApiRef, @@ -21,10 +22,6 @@ import { useApi, } from '@backstage/core-plugin-api'; -import { - useDebounceCallback, - useDeepCompareMemoize, -} from '@janus-idp/shared-react'; import { useQuery } from '@tanstack/react-query'; import { useFormikContext } from 'formik'; @@ -42,20 +39,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 () => { @@ -63,76 +54,48 @@ 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 baseUrl = configApi.getString('app.baseUrl'); 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 @@ -143,5 +106,5 @@ export const useAddedRepositories = ( : {}), }, refetch, - }); + }; }; diff --git a/workspaces/bulk-import/plugins/bulk-import/src/hooks/useRepositories.test.ts b/workspaces/bulk-import/plugins/bulk-import/src/hooks/useRepositories.test.ts index acd7a8f..e8dc2fb 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/hooks/useRepositories.test.ts +++ b/workspaces/bulk-import/plugins/bulk-import/src/hooks/useRepositories.test.ts @@ -12,8 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ import { 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'; @@ -24,12 +23,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({ @@ -46,8 +51,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 }), @@ -61,13 +69,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/workspaces/bulk-import/plugins/bulk-import/src/hooks/useRepositories.ts b/workspaces/bulk-import/plugins/bulk-import/src/hooks/useRepositories.ts index 37adbbd..5a314c7 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/hooks/useRepositories.ts +++ b/workspaces/bulk-import/plugins/bulk-import/src/hooks/useRepositories.ts @@ -12,8 +12,9 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ import React from 'react'; -import { useAsync, useDebounce } from 'react-use'; + */ +import React from 'react'; +import { useAsync } from 'react-use'; import { configApiRef, @@ -21,58 +22,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(); @@ -84,129 +62,57 @@ export const useRepositories = (options: { return url; }); - const bulkImportApi = useApi(bulkImportApiRef); - const { value, loading, error } = useAsync(async () => { - setIsLoading(true); - if (options.showOrganizations) { + const fetchRepositories = async (queryOptions: DataFetcherQueryParams) => { + if (queryOptions?.showOrganizations) { return await bulkImportApi.dataFetcher( - options.page, - options.querySize, - debouncedSearch || '', + queryOptions?.page, + queryOptions?.querySize, + queryOptions?.searchString || '', { fetchOrganizations: true, }, ); } - if (options.orgName) { + if (queryOptions?.orgName) { return await bulkImportApi.dataFetcher( - options.page, - options.querySize, - debouncedSearch || '', + queryOptions?.page, + queryOptions?.querySize, + queryOptions?.searchString || '', { - orgName: options.orgName, + orgName: queryOptions?.orgName, }, ); } return await bulkImportApi.dataFetcher( - options.page, - options.querySize, - debouncedSearch || '', + queryOptions?.page, + queryOptions?.querySize, + queryOptions?.searchString || '', ); - }, [ - options?.page, - options?.querySize, - options?.showOrganizations, - options?.orgName, - debouncedSearch, - ]); + }; - 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(); - } - }, - [ - user, - options?.showOrganizations, - baseUrl, - setRepositoriesData, - setOrganizationsData, - setTotalOrganizations, - setTotalRepositories, - ], + const { + data: value, + error, + isLoading: isQueryLoading, + } = useQuery( + [options?.showOrganizations ? 'organizations' : 'repositories', options], + () => fetchRepositories(options), ); - 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) || @@ -214,5 +120,5 @@ export const useRepositories = (options: { ? { errors: value?.errors || (value as any as Response)?.statusText } : {}), } as RepositoriesError, - }); + }; }; diff --git a/workspaces/bulk-import/plugins/bulk-import/src/types/types.ts b/workspaces/bulk-import/plugins/bulk-import/src/types/types.ts index 8d31597..305e215 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/types/types.ts +++ b/workspaces/bulk-import/plugins/bulk-import/src/types/types.ts @@ -144,3 +144,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/workspaces/bulk-import/plugins/bulk-import/src/utils/repository-utils.test.tsx b/workspaces/bulk-import/plugins/bulk-import/src/utils/repository-utils.test.tsx index 6420673..93e5b29 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/utils/repository-utils.test.tsx +++ b/workspaces/bulk-import/plugins/bulk-import/src/utils/repository-utils.test.tsx @@ -22,7 +22,6 @@ import { componentNameRegex, evaluatePRTemplate, getJobErrors, - getNewOrgsData, getYamlKeyValuePairs, updateWithNewSelectedRepositories, urlHelper, @@ -80,33 +79,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/workspaces/bulk-import/plugins/bulk-import/src/utils/repository-utils.tsx b/workspaces/bulk-import/plugins/bulk-import/src/utils/repository-utils.tsx index 48de992..88d34ea 100644 --- a/workspaces/bulk-import/plugins/bulk-import/src/utils/repository-utils.tsx +++ b/workspaces/bulk-import/plugins/bulk-import/src/utils/repository-utils.tsx @@ -35,7 +35,9 @@ import { ImportStatus, JobErrors, Order, + OrgAndRepoResponse, PullRequestPreview, + Repository, RepositorySelection, RepositoryStatus, } from '../types'; @@ -60,8 +62,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') { @@ -205,37 +207,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 ''; @@ -332,7 +303,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 = @@ -568,13 +539,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 } = @@ -610,5 +636,8 @@ export const prepareDataForAddedRepositories = ( }, }; }, {}); - return repoData; + return { + repoData, + totalJobs: (addedRepositories as ImportJobs)?.totalCount || 0, + }; }; diff --git a/workspaces/bulk-import/plugins/bulk-import/tests/bulkImport.spec.ts b/workspaces/bulk-import/plugins/bulk-import/tests/bulkImport.spec.ts index fde6e96..568d39f 100644 --- a/workspaces/bulk-import/plugins/bulk-import/tests/bulkImport.spec.ts +++ b/workspaces/bulk-import/plugins/bulk-import/tests/bulkImport.spec.ts @@ -117,6 +117,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)' }), @@ -124,6 +125,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({ @@ -139,6 +141,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({