diff --git a/packages/javascript/bh-shared-ui/src/components/DetailsAccordion/DetailsAccordion.tsx b/packages/javascript/bh-shared-ui/src/components/DetailsAccordion/DetailsAccordion.tsx index 3ffc620e92d..2f1327a2ac1 100644 --- a/packages/javascript/bh-shared-ui/src/components/DetailsAccordion/DetailsAccordion.tsx +++ b/packages/javascript/bh-shared-ui/src/components/DetailsAccordion/DetailsAccordion.tsx @@ -106,7 +106,8 @@ export const DetailsAccordion = >({ className='bg-neutral-light-2 dark:bg-neutral-dark-2 border-t dark:border-neutral-dark-4 first:border-none' disabled={isDisabled} key={key} - value={String(idx)}> + value={String(idx)} + data-testid={key}> = ({ file_name, errors }) => { - const isSuccess = errors.length === 0; +const FileHeader: React.FC = ({ file_name, errors, warnings }) => { + const status: IndicatorType = (() => { + if (errors.length === 0 && warnings.length === 0) { + return 'good'; + } else if (errors.length === 0) { + return 'pending'; + } + return 'bad'; + })(); + const label = (() => { + if (errors.length === 0 && warnings.length === 0) { + return 'Success'; + } else if (errors.length === 0) { + return 'Partial Success'; + } + return 'Failure'; + })(); return (
{file_name}
- +
); }; -/** Only displays content if ingest had errors */ +/** Only displays content if ingest had errors or warnings */ const FileContent: React.FC = (ingest) => - ingest.errors.length > 0 ? : null; + ingest.errors.length > 0 || ingest.warnings.length > 0 ? : null; -/** Displays file ingest errors */ -const FileErrors: React.FC = ({ errors }) => ( +/** Displays file ingest errors and warnings */ +const FileErrors: React.FC = ({ errors, warnings }) => (
-
Error Message(s):
- {errors.map((error, index) => ( -
- {error} -
- ))} + {errors.length > 0 && ( + + {errors.length === 1 ? 'Error Message:' : 'Error Messages:'} + {errors.map((error, index) => ( +
+ {error} +
+ ))} +
+ )} + {warnings.length > 0 && ( + + {warnings.length === 1 ? 'Warning:' : 'Warnings:'} + {warnings.map((warning, index) => ( +
+ {warning} +
+ ))} +
+ )}
); -const isErrorFree = (ingest: FileIngestCompletedTask | null) => ingest?.errors.length === 0; +const isErrorAndWarningFree = (ingest: FileIngestCompletedTask | null) => + ingest?.errors.length === 0 && ingest?.warnings.length === 0; /** Displays the ingest ID */ const IngestHeader: React.FC = ({ id }) =>
ID {id}
; @@ -62,7 +94,12 @@ const IngestContent: React.FC = (ingest) => { return (
- +
); }; diff --git a/packages/javascript/bh-shared-ui/src/components/FileIngestTable/FileIngestTable.test.tsx b/packages/javascript/bh-shared-ui/src/components/FileIngestTable/FileIngestTable.test.tsx index 5d371288c09..91b9dd504a3 100644 --- a/packages/javascript/bh-shared-ui/src/components/FileIngestTable/FileIngestTable.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/FileIngestTable/FileIngestTable.test.tsx @@ -14,9 +14,11 @@ // // SPDX-License-Identifier: Apache-2.0 -import type { FileIngestJob } from 'js-client-library'; +import userEvent from '@testing-library/user-event'; +import type { FileIngestCompletedTasksResponse, FileIngestJob } from 'js-client-library'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import * as useFileUpload from '../../hooks/useFileUploadQuery/useFileUploadQuery'; import { act, render, screen } from '../../test-utils'; import { FileIngestTable } from './FileIngestTable'; @@ -52,9 +54,33 @@ const MOCK_INGEST_JOB: FileIngestJob = { }, }; +const MOCK_PARTIAL_SUCCESS_INGEST_JOB: FileIngestJob = { + user_id: '1234', + user_email_address: 'spam@example.com', + status: 8, + status_message: 'Partially Completed', + start_time: '2024-08-15T21:25:21.990437Z', + end_time: '2024-08-15T21:26:43.033448Z', + last_ingest: '2024-08-15T21:26:43.033448Z', + id: 9, + total_files: 10, + failed_files: 0, + created_at: '', + updated_at: '', + deleted_at: { + Time: '', + Valid: false, + }, +}; + const MOCK_INGEST_JOBS_RESPONSE = { count: 20, - data: new Array(10).fill(MOCK_INGEST_JOB).map((item, index) => ({ + // fill the array with data + data: Array.from({ length: 10 }, (_, index) => { + if (index % 2 === 0) { + return MOCK_INGEST_JOB; + } else return MOCK_PARTIAL_SUCCESS_INGEST_JOB; + }).map((item, index) => ({ ...item, id: index, status: (index % 10) - 1, @@ -63,6 +89,25 @@ const MOCK_INGEST_JOBS_RESPONSE = { skip: 10, }; +const MOCK_COMPLETED_TASKS_RESPONSE: FileIngestCompletedTasksResponse = { + data: [ + { + file_name: 'generic-with-failed-edges.json', + parent_file_name: '', + errors: [], + warnings: [ + 'skipping invalid relationship. unable to resolve endpoints. source: NON2@EXISTING.NODE, target: NON1@EXISTING.NODE', + ], + id: 9, + created_at: '2026-01-14T00:17:40.255611Z', + updated_at: '2026-01-14T00:17:40.255611Z', + deleted_at: { + Time: '0001-01-01T00:00:00Z', + Valid: false, + }, + }, + ], +}; const server = setupServer( rest.get('/api/v2/file-upload', (req, res, ctx) => res(ctx.json(MOCK_INGEST_JOBS_RESPONSE))), rest.get('/api/v2/features', (req, res, ctx) => { @@ -89,6 +134,9 @@ afterAll(() => { server.resetHandlers(); }); +const useFileUploadQuerySpy = vi.spyOn(useFileUpload, 'useFileUploadQuery'); +useFileUploadQuerySpy.mockReturnValue({ data: MOCK_COMPLETED_TASKS_RESPONSE, isSuccess: true } as any); + describe('FileIngestTable', () => { it('shows a loading state', () => { checkPermissionMock.mockImplementation(() => true); @@ -107,4 +155,27 @@ describe('FileIngestTable', () => { const jobStatus = await screen.findByText('Complete'); expect(jobStatus).toHaveTextContent('Complete'); }); + it('shows a table with partially completed jobs', async () => { + checkPermissionMock.mockImplementation(() => true); + await act(async () => render()); + const user = userEvent.setup(); + + const jobID = await screen.getByRole('button', { name: 'View ingest 9 details' }); + await user.click(jobID); + + const jobsDropdown = await screen.getAllByTestId('0'); + expect(jobsDropdown[0]).toBeInTheDocument(); + + await user.click(jobsDropdown[0]); + + const partiallyCompletedJob = await screen.findByText('generic-with-failed-edges.json'); + expect(partiallyCompletedJob).toBeInTheDocument(); + + await user.click(partiallyCompletedJob); + + const warningText = await screen.findByText( + 'skipping invalid relationship. unable to resolve endpoints. source: NON2@EXISTING.NODE, target: NON1@EXISTING.NODE' + ); + expect(warningText).toBeInTheDocument(); + }); }); diff --git a/packages/javascript/js-client-library/src/types.ts b/packages/javascript/js-client-library/src/types.ts index e9effd0b081..206e76cfe96 100644 --- a/packages/javascript/js-client-library/src/types.ts +++ b/packages/javascript/js-client-library/src/types.ts @@ -584,6 +584,7 @@ export type FileIngestJob = TimestampFields & { export type FileIngestCompletedTask = TimestampFields & { errors: string[]; + warnings: string[]; file_name: string; id: number; parent_file_name: string;