Skip to content

Commit 05ec617

Browse files
Chris Jonesclaude
andcommitted
fix(autorag): resolve merge conflicts and fix AutoragConfigurePage tests
- Update MockEvaluationSelect to defer test_data_key setValue after AutoragConfigure's sync/clear effects settle (setTimeout(0)) - Fix upload test to assert on useS3FileUploadMutation's mutateAsync instead of the directly imported uploadFileToS3 function - Fix arrow-body-style lint error in AutoragConfigure.spec.tsx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 parents 25a84a9 + c7cfd0c commit 05ec617

45 files changed

Lines changed: 2170 additions & 453 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/automl/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,6 @@ $RECYCLE.BIN/
353353
# Windows shortcuts
354354
*.lnk
355355

356+
# Claude
357+
CLAUDE.md
358+
.claude/settings.local.json

packages/automl/bff/internal/integrations/s3/client_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package s3
22

33
import (
44
"errors"
5+
"fmt"
56
"testing"
67

78
"github.com/stretchr/testify/assert"
@@ -207,3 +208,50 @@ func TestCountLines(t *testing.T) {
207208
assert.Equal(t, 1, countLines([]byte("a\nb")))
208209
assert.Equal(t, 3, countLines([]byte("a\nb\nc\n")))
209210
}
211+
212+
// mockS3CodedError simulates AWS SDK errors that implement ErrorCode().
213+
type mockS3CodedError struct {
214+
msg string
215+
code string
216+
}
217+
218+
func (e mockS3CodedError) Error() string {
219+
if e.msg != "" {
220+
return e.msg
221+
}
222+
return e.code
223+
}
224+
225+
func (e mockS3CodedError) ErrorCode() string {
226+
return e.code
227+
}
228+
229+
func TestIsS3ConditionalCreateConflict(t *testing.T) {
230+
t.Parallel()
231+
tests := []struct {
232+
name string
233+
err error
234+
want bool
235+
}{
236+
{name: "PreconditionFailed", err: mockS3CodedError{code: "PreconditionFailed"}, want: true},
237+
{name: "ConditionalRequestConflict", err: mockS3CodedError{code: "ConditionalRequestConflict"}, want: true},
238+
{
239+
name: "wrapped PreconditionFailed",
240+
err: fmt.Errorf("upload failed: %w", mockS3CodedError{code: "PreconditionFailed"}),
241+
want: true,
242+
},
243+
{
244+
name: "wrapped ConditionalRequestConflict",
245+
err: fmt.Errorf("upload failed: %w", mockS3CodedError{code: "ConditionalRequestConflict"}),
246+
want: true,
247+
},
248+
{name: "other ErrorCode", err: mockS3CodedError{code: "NoSuchKey"}, want: false},
249+
{name: "plain error", err: errors.New("failed"), want: false},
250+
{name: "nil", err: nil, want: false},
251+
}
252+
for _, tt := range tests {
253+
t.Run(tt.name, func(t *testing.T) {
254+
assert.Equal(t, tt.want, isS3ConditionalCreateConflict(tt.err))
255+
})
256+
}
257+
}

packages/automl/frontend/src/app/components/configure/AutomlConfigure.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ import {
2626
SplitItem,
2727
Stack,
2828
StackItem,
29+
Tooltip,
2930
} from '@patternfly/react-core';
30-
import { CubesIcon } from '@patternfly/react-icons';
31+
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
32+
import { CubesIcon, TimesIcon } from '@patternfly/react-icons';
3133
import { useQueryClient } from '@tanstack/react-query';
3234
import { findKey } from 'es-toolkit';
3335
import React, { useEffect, useRef, useState } from 'react';
@@ -36,6 +38,7 @@ import { Navigate, useParams } from 'react-router';
3638
import AutomlConnectionModal from '~/app/components/common/AutomlConnectionModal';
3739
import ConfigureFormGroup from '~/app/components/common/ConfigureFormGroup';
3840
import S3FileExplorer from '~/app/components/common/S3FileExplorer/S3FileExplorer.tsx';
41+
import type { File } from '~/app/components/common/FileExplorer/FileExplorer.tsx';
3942
import SecretSelector, { SecretSelection } from '~/app/components/common/SecretSelector';
4043
import { useS3GetFileSchemaQuery } from '~/app/hooks/queries';
4144
import { ConfigureSchema, MAX_TOP_N, MIN_TOP_N } from '~/app/schemas/configure.schema';
@@ -106,6 +109,8 @@ function AutomlConfigure(): React.JSX.Element {
106109
const secretsRefreshRef = useRef<(() => Promise<SecretListItem[] | undefined>) | null>(null);
107110
const previousFileKeyRef = useRef<string | undefined>();
108111

112+
const [selectedTrainingDataFile, setSelectedTrainingDataFile] = useState<File | undefined>();
113+
109114
const form = useFormContext<ConfigureSchema>();
110115

111116
const {
@@ -159,6 +164,7 @@ function AutomlConfigure(): React.JSX.Element {
159164
// reset selected file values if secret or bucket changes
160165
useEffect(() => {
161166
setValue('train_data_file_key', '', { shouldValidate: true });
167+
setSelectedTrainingDataFile(undefined);
162168
}, [trainDataSecretName, trainDataBucketName, setValue]);
163169

164170
// reset all column-related form fields when file selection changes
@@ -311,6 +317,39 @@ function AutomlConfigure(): React.JSX.Element {
311317
</StackItem>
312318
</>
313319
)}
320+
{selectedTrainingDataFile && (
321+
<StackItem>
322+
<Table aria-label="Selected training data file" variant="compact">
323+
<Thead>
324+
<Tr>
325+
<Th>Name</Th>
326+
<Th>Type</Th>
327+
<Th />
328+
</Tr>
329+
</Thead>
330+
<Tbody>
331+
<Tr>
332+
<Td dataLabel="Name">{selectedTrainingDataFile.name}</Td>
333+
<Td dataLabel="Type">{selectedTrainingDataFile.type}</Td>
334+
<Td isActionCell>
335+
<Tooltip content="Remove selection">
336+
<Button
337+
size="sm"
338+
variant="plain"
339+
aria-label="Remove selection"
340+
icon={<TimesIcon />}
341+
onClick={() => {
342+
setSelectedTrainingDataFile(undefined);
343+
setValue('train_data_file_key', '', { shouldValidate: true });
344+
}}
345+
/>
346+
</Tooltip>
347+
</Td>
348+
</Tr>
349+
</Tbody>
350+
</Table>
351+
</StackItem>
352+
)}
314353
</Stack>
315354
</CardBody>
316355
</div>
@@ -496,6 +535,7 @@ function AutomlConfigure(): React.JSX.Element {
496535
const file = files[0];
497536
const filePath = file.path.replace(/^\//, '');
498537
setValue('train_data_file_key', filePath, { shouldValidate: true });
538+
setSelectedTrainingDataFile(file);
499539
}
500540
}}
501541
selectableExtensions={['csv']}

packages/automl/frontend/src/app/components/configure/__tests__/AutomlConfigure.spec.tsx

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import React from 'react';
77
import { FormProvider, useForm } from 'react-hook-form';
88
import { useNavigate, useParams } from 'react-router';
99
import AutomlConfigure from '~/app/components/configure/AutomlConfigure';
10+
import type { Files } from '~/app/components/common/FileExplorer/FileExplorer';
1011
import { useS3GetFileSchemaQuery } from '~/app/hooks/queries';
1112
import { createConfigureSchema } from '~/app/schemas/configure.schema';
1213

@@ -17,7 +18,33 @@ jest.mock('react-router', () => ({
1718
}));
1819

1920
jest.mock('~/app/hooks/queries');
20-
jest.mock('~/app/components/common/S3FileExplorer/S3FileExplorer', () => () => null);
21+
22+
// Mock S3FileExplorer component
23+
jest.mock('~/app/components/common/S3FileExplorer/S3FileExplorer', () => ({
24+
__esModule: true,
25+
default: ({
26+
isOpen,
27+
onSelectFiles,
28+
onClose,
29+
}: {
30+
isOpen: boolean;
31+
onSelectFiles: (files: Files) => void;
32+
onClose: () => void;
33+
}) =>
34+
isOpen ? (
35+
<div data-testid="file-explorer-modal">
36+
<button
37+
data-testid="file-explorer-select-file"
38+
onClick={() => {
39+
onSelectFiles([{ path: '/data.csv', name: 'data.csv', type: 'csv' }]);
40+
onClose();
41+
}}
42+
>
43+
Select File
44+
</button>
45+
</div>
46+
) : null,
47+
}));
2148

2249
// Mock SecretSelector component
2350
jest.mock('~/app/components/common/SecretSelector', () => ({
@@ -262,6 +289,56 @@ describe('AutomlConfigure', () => {
262289
});
263290
});
264291

292+
describe('selected training data file table', () => {
293+
it('should NOT display the selected file table when no file is selected', () => {
294+
renderComponent();
295+
296+
// Select a secret so the "Select files" button appears
297+
fireEvent.click(screen.getByTestId('aws-secret-selector-select-secret-1'));
298+
299+
expect(
300+
screen.queryByRole('grid', { name: 'Selected training data file' }),
301+
).not.toBeInTheDocument();
302+
});
303+
304+
it('should display the selected file table after selecting a file', () => {
305+
renderComponent();
306+
307+
// Select a secret
308+
fireEvent.click(screen.getByTestId('aws-secret-selector-select-secret-1'));
309+
310+
// Open file explorer and select a file
311+
fireEvent.click(screen.getByRole('button', { name: 'Select files' }));
312+
fireEvent.click(screen.getByTestId('file-explorer-select-file'));
313+
314+
// Verify the table appears with correct content
315+
const table = screen.getByRole('grid', { name: 'Selected training data file' });
316+
expect(table).toBeInTheDocument();
317+
expect(screen.getByText('data.csv')).toBeInTheDocument();
318+
expect(screen.getByText('csv')).toBeInTheDocument();
319+
});
320+
321+
it('should remove the selected file when the remove button is clicked', () => {
322+
renderComponent();
323+
324+
// Select a secret and a file
325+
fireEvent.click(screen.getByTestId('aws-secret-selector-select-secret-1'));
326+
fireEvent.click(screen.getByRole('button', { name: 'Select files' }));
327+
fireEvent.click(screen.getByTestId('file-explorer-select-file'));
328+
329+
// Verify the table is shown
330+
expect(screen.getByRole('grid', { name: 'Selected training data file' })).toBeInTheDocument();
331+
332+
// Click the remove button
333+
fireEvent.click(screen.getByRole('button', { name: 'Remove selection' }));
334+
335+
// Table should be removed
336+
expect(
337+
screen.queryByRole('grid', { name: 'Selected training data file' }),
338+
).not.toBeInTheDocument();
339+
});
340+
});
341+
265342
describe('invalid secret selection', () => {
266343
it('should disable "Select files" button when selected secret is invalid', () => {
267344
renderComponent();
Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import React from 'react';
22
import { Link } from 'react-router-dom';
33

4-
import {
5-
EmptyState,
6-
EmptyStateBody,
7-
EmptyStateFooter,
8-
EmptyStateActions,
9-
Button,
10-
} from '@patternfly/react-core';
11-
import { PlusCircleIcon } from '@patternfly/react-icons';
4+
import EmptyDetailsView from '@odh-dashboard/internal/components/EmptyDetailsView';
5+
import { ProjectObjectType, typedEmptyImage } from '@odh-dashboard/internal/concepts/design/utils';
6+
import { Button } from '@patternfly/react-core';
127

8+
/**
9+
* Empty State B — pipeline server and managed AutoML pipelines are OK; zero runs.
10+
* Shown only after successful loads (`!loadError && loaded && totalSize === 0`).
11+
*/
1312
interface EmptyExperimentsStateProps {
1413
createExperimentRoute: string;
1514
dataTestId?: string;
@@ -19,29 +18,23 @@ const EmptyExperimentsState: React.FC<EmptyExperimentsStateProps> = ({
1918
createExperimentRoute,
2019
dataTestId = 'empty-experiments-state',
2120
}) => (
22-
<EmptyState
23-
data-testid={dataTestId}
24-
titleText="No experiments yet"
25-
icon={PlusCircleIcon}
26-
headingLevel="h2"
27-
>
28-
<EmptyStateBody>
29-
Test different model configurations to find the best-performing solution for classification,
30-
regression, and time series problems.
31-
</EmptyStateBody>
32-
33-
<EmptyStateFooter>
34-
<EmptyStateActions>
21+
<div data-testid={dataTestId}>
22+
<EmptyDetailsView
23+
title="Create an AutoML optimization run"
24+
description="Test different model configurations to find the best-performing solution for classification, regression, and time series problems."
25+
iconImage={typedEmptyImage(ProjectObjectType.pipeline, 'MissingModel')}
26+
imageAlt=""
27+
createButton={
3528
<Button
3629
data-testid="create-experiment-button"
3730
variant="primary"
3831
component={(props) => <Link {...props} to={createExperimentRoute} />}
3932
>
40-
Create AutoML optimization run
33+
Create experiment
4134
</Button>
42-
</EmptyStateActions>
43-
</EmptyStateFooter>
44-
</EmptyState>
35+
}
36+
/>
37+
</div>
4538
);
4639

4740
export default EmptyExperimentsState;

packages/automl/frontend/src/app/components/empty-states/NoPipelineServer.tsx

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
1-
import { EmptyState, EmptyStateBody } from '@patternfly/react-core';
2-
import { ServerIcon } from '@patternfly/react-icons';
1+
/**
2+
* Empty State A — no compatible pipeline server and/or managed AutoML pipelines unavailable.
3+
* Directs users to the Pipelines page to configure DSPA and enable AutoML / AutoRAG pipelines.
4+
*/
5+
import EmptyDetailsView from '@odh-dashboard/internal/components/EmptyDetailsView';
6+
import { ProjectObjectType, typedEmptyImage } from '@odh-dashboard/internal/concepts/design/utils';
7+
import { pipelinesBaseRoute } from '@odh-dashboard/internal/routes/pipelines/global';
8+
import { Button } from '@patternfly/react-core';
39
import * as React from 'react';
10+
import { Link } from 'react-router-dom';
411

512
type NoPipelineServerProps = {
613
namespace?: string;
714
};
815

916
function NoPipelineServer({ namespace }: NoPipelineServerProps): React.JSX.Element {
1017
return (
11-
<EmptyState
12-
titleText="No Pipeline Server in this namespace"
13-
headingLevel="h4"
14-
icon={ServerIcon}
15-
>
16-
<EmptyStateBody>
17-
{namespace ? (
18-
<>
19-
No Data Science Pipelines (DSPipelineApplication) was found in namespace{' '}
20-
<strong>{namespace}</strong>. Install Data Science Pipelines in your project to use
21-
AutoML experiments, or select a different project.
22-
</>
23-
) : (
24-
<>
25-
No Data Science Pipelines (DSPipelineApplication) was found. Install Data Science
26-
Pipelines in your project to use AutoML experiments.
27-
</>
28-
)}
29-
</EmptyStateBody>
30-
</EmptyState>
18+
<EmptyDetailsView
19+
title="Configure a compatible pipeline server"
20+
description="To use AutoML, you need access to a pipeline server with AutoML and AutoRAG enabled. Create or edit a pipeline server on the Pipelines page."
21+
iconImage={typedEmptyImage(ProjectObjectType.pipeline, 'MissingModel')}
22+
imageAlt=""
23+
createButton={
24+
<Button
25+
variant="link"
26+
isInline
27+
data-testid="go-to-pipelines-link"
28+
component={(props) => <Link {...props} to={pipelinesBaseRoute(namespace)} />}
29+
>
30+
Go to Pipelines
31+
</Button>
32+
}
33+
/>
3134
);
3235
}
3336

0 commit comments

Comments
 (0)