From 7b1dfe9054d08ccfd72eeac5a987c7aac65764d8 Mon Sep 17 00:00:00 2001 From: debsmita1 Date: Mon, 30 Sep 2024 16:05:05 +0530 Subject: [PATCH] feat(bulk-import): use react query to fetch repositories --- .changeset/witty-socks-cover.md | 5 + .../src/api/BulkImportBackendClient.test.ts | 10 +- .../src/api/BulkImportBackendClient.ts | 6 +- .../AddRepositories/AddRepositories.tsx | 133 +++++++++++ .../AddRepositoriesForm.test.tsx | 26 ++- .../AddRepositories/AddRepositoriesForm.tsx | 215 ++++++++---------- .../AddRepositories/AddRepositoriesPage.tsx | 96 +------- .../src/components/BulkImportPage.tsx | 2 +- .../PreviewFile/PreviewFileSidebar.tsx | 4 +- .../Repositories/AddedRepositoryTableRow.tsx | 31 ++- .../Repositories/CatalogInfoAction.tsx | 51 +++-- .../Repositories/RepositoriesList.tsx | 3 +- .../src/hooks/useAddedRepositories.ts | 3 + .../src/hooks/useRepositories.test.ts | 31 ++- .../bulk-import/src/hooks/useRepositories.ts | 95 ++++---- plugins/bulk-import/src/types/types.ts | 8 + .../src/utils/repository-utils.tsx | 2 +- plugins/bulk-import/tests/bulkImport.spec.ts | 3 + 18 files changed, 419 insertions(+), 305 deletions(-) create mode 100644 .changeset/witty-socks-cover.md create mode 100644 plugins/bulk-import/src/components/AddRepositories/AddRepositories.tsx diff --git a/.changeset/witty-socks-cover.md b/.changeset/witty-socks-cover.md new file mode 100644 index 00000000000..5e049b076f2 --- /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 ca4652927ae..6c844e354ad 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 6691b90db61..7ee11efabc7 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 00000000000..610bbcbe929 --- /dev/null +++ b/plugins/bulk-import/src/components/AddRepositories/AddRepositories.tsx @@ -0,0 +1,133 @@ +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 { 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: { message: string; title: string } | null; +}) => { + 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 && ( +
+ + {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/AddRepositoriesForm.test.tsx b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.test.tsx index ca25db78100..1654dc904b2 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'; @@ -12,7 +13,7 @@ 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 { AddRepositories } from './AddRepositories'; jest.mock('formik', () => ({ ...jest.requireActual('formik'), @@ -44,6 +45,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, @@ -70,6 +81,7 @@ beforeEach(() => { }, setFieldValue: jest.fn(), }); + queryClient = createTestQueryClient(); }); describe('AddRepsositoriesForm', () => { @@ -97,7 +109,9 @@ describe('AddRepsositoriesForm', () => { ], ]} > - + + + , ); @@ -134,9 +148,11 @@ describe('AddRepsositoriesForm', () => { ], ]} > - + + + , ); diff --git a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx index 6b46f7cd4c3..81c76247b6b 100644 --- a/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx +++ b/plugins/bulk-import/src/components/AddRepositories/AddRepositoriesForm.tsx @@ -1,133 +1,112 @@ 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 { 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'; +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 [generalSubmitError, setGeneralSubmitError] = React.useState<{ + message: string; + title: string; + } | null>(null); + 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 = async (importOptions: { + importJobs: CreateImportJobRepository[]; + dryRun?: boolean; + }) => + await bulkImportApi.createImportJobs( + importOptions.importJobs, + importOptions.dryRun, + ); - const closeDrawer = () => { - setOpenDrawer(false); - }; + const mutationCreate = useMutation(createImportJobs, { + onSuccess: (data: ImportJobResponse[] | Response) => { + return data; + }, + onError: (error: Error) => { + setGeneralSubmitError({ + message: + error?.message || + `${get(error, 'error.message')}\n${get(error, 'error.response.url')}` || + 'Failed to create pull request', + title: error?.name || get(error, 'error.name') || 'Error occured', + }); + return error; + }, + }); - 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); + const dryRunResult = await mutationCreate.mutateAsync({ + importJobs: importRepositories, + dryRun: true, }); - setOpenDrawer(false); + const dryRunErrors = getJobErrors(dryRunResult 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 a1a5c26aece..97c8c90c0a9 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/BulkImportPage.tsx b/plugins/bulk-import/src/components/BulkImportPage.tsx index 9c932790f15..39b8ab0320b 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.tsx b/plugins/bulk-import/src/components/Repositories/CatalogInfoAction.tsx index 2bef9b2627d..2af18fa0272 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?.prTemplate + ?.pullRequestUrl || 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 ? null : ( + + {catalogIcon()!.icon} ); }; diff --git a/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx b/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx index df49763a347..1994e2ce94c 100644 --- a/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx +++ b/plugins/bulk-import/src/components/Repositories/RepositoriesList.tsx @@ -42,6 +42,7 @@ export const RepositoriesList = () => { data: importJobs, error: errJobs, loaded: jobsLoaded, + isFetching, refetch, } = useAddedRepositories(pageNumber + 1, rowsPerPage, debouncedSearch); @@ -108,7 +109,7 @@ export const RepositoriesList = () => { Body: () => ( diff --git a/plugins/bulk-import/src/hooks/useAddedRepositories.ts b/plugins/bulk-import/src/hooks/useAddedRepositories.ts index 58aceff3f06..23060d2d03c 100644 --- a/plugins/bulk-import/src/hooks/useAddedRepositories.ts +++ b/plugins/bulk-import/src/hooks/useAddedRepositories.ts @@ -35,6 +35,7 @@ export const useAddedRepositories = ( totalJobs: number; }; error: any; + isFetching: boolean; refetch: () => void; } => { const [addedRepositoriesData, setAddedRepositoriesData] = React.useState<{ @@ -79,6 +80,7 @@ export const useAddedRepositories = ( error, isLoading: loading, refetch, + isFetching, } = useQuery( ['importJobs', pageNumber, rowsPerPage, debouncedSearch], () => fetchAddedRepositories(pageNumber, rowsPerPage, debouncedSearch), @@ -120,6 +122,7 @@ export const useAddedRepositories = ( totalJobs: totalImportJobs, }, loaded, + isFetching, error: { ...(error ?? {}), ...((value as Response)?.statusText diff --git a/plugins/bulk-import/src/hooks/useRepositories.test.ts b/plugins/bulk-import/src/hooks/useRepositories.test.ts index a5b550ff9f7..4e0a59b97ba 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: '', + 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: '', + 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: '', + 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 cc2627020a2..bf1dc7cb690 100644 --- a/plugins/bulk-import/src/hooks/useRepositories.ts +++ b/plugins/bulk-import/src/hooks/useRepositories.ts @@ -7,6 +7,8 @@ import { useApi, } from '@backstage/core-plugin-api'; +import { useQuery } from '@tanstack/react-query'; + import { useDebounceCallback, useDeepCompareMemoize, @@ -15,19 +17,16 @@ import { import { bulkImportApiRef } from '../api/BulkImportBackendClient'; import { AddRepositoryData, + DataFetcherQueryParams, OrgAndRepoResponse, RepositoriesError, Repository, } from '../types'; import { getPRTemplate } 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 }; @@ -51,6 +50,7 @@ export const useRepositories = (options: { }>({}); const identityApi = useApi(identityApiRef); const configApi = useApi(configApiRef); + const bulkImportApi = useApi(bulkImportApiRef); useDebounce( () => { @@ -70,57 +70,66 @@ 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 { + data: value, + error, + isLoading: loading, + } = useQuery( + [ + 'repositories', + options?.page, + options?.querySize, + options?.showOrganizations, + options?.orgName, + debouncedSearch, + ], + () => + fetchRepositories( + options?.page, + options?.querySize, + options?.showOrganizations || false, + options?.orgName || '', + debouncedSearch, + ), + { keepPreviousData: true }, + ); const prepareData = React.useCallback( (result: OrgAndRepoResponse, dataLoading: boolean) => { + setIsLoading(true); const prepareDataForRepositories = () => { const repoData: { [id: string]: AddRepositoryData } = result?.repositories?.reduce((acc, val: Repository) => { - const id = val.id || `${val.organization}/${val.name}`; + const id = val.id; return { ...acc, [id]: { id, repoName: val.name, - defaultBranch: val.defaultBranch, + defaultBranch: val.defaultBranch || 'main', orgName: val.organization, repoUrl: val.url, - organizationUrl: val?.url?.substring( + organizationUrl: val.url?.substring( 0, val.url.indexOf(val?.name || '') - 1, ), diff --git a/plugins/bulk-import/src/types/types.ts b/plugins/bulk-import/src/types/types.ts index 9bc0221c591..2e3061c187b 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.tsx b/plugins/bulk-import/src/utils/repository-utils.tsx index b2b34146cf4..7dc235f5724 100644 --- a/plugins/bulk-import/src/utils/repository-utils.tsx +++ b/plugins/bulk-import/src/utils/repository-utils.tsx @@ -318,7 +318,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 = diff --git a/plugins/bulk-import/tests/bulkImport.spec.ts b/plugins/bulk-import/tests/bulkImport.spec.ts index 8ed2c0db4da..6dd07a28290 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({