Skip to content

Commit 2d93279

Browse files
authored
[TSPS-652] Populate Teaspoons pipeline details using API (#5442)
1 parent 9dd2917 commit 2d93279

File tree

13 files changed

+195
-231
lines changed

13 files changed

+195
-231
lines changed

src/libs/ajax/teaspoons/teaspoons-models.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@ export interface PipelineInput {
99
name: string;
1010
type: 'FILE' | 'STRING' | 'FLOAT';
1111
isRequired: boolean;
12-
fileSuffix?: string; // Only present for FILE types
12+
displayName?: string;
13+
description?: string;
14+
defaultValue?: string;
15+
fileSuffix?: string; // Optional, and only for FILE types
16+
minValue?: number; // Optional, and only for FLOAT types
17+
maxValue?: number; // Optional, and only for FLOAT types
1318
}
1419

1520
export interface PipelineOutput {
1621
name: string;
1722
type: string;
23+
displayName?: string;
24+
description?: string;
1825
}
1926

2027
/* Represents the quota settings for a particular pipeline */

src/pages/scientificServices/pipelines/components/inputs/PipelineFileInput.test.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,9 @@ import { renderWithAppContexts } from 'src/testing/test-utils';
77

88
import { PipelineFileInput, PipelineInputFileUploadState } from './PipelineFileInput';
99

10-
jest.mock('src/pages/scientificServices/pipelines/utils/pipeline-input-utils', () => ({
11-
INPUT_DESCRIPTIONS: {
12-
multiSampleVcf: {
13-
label: 'Select a multi-sample VCF file',
14-
validationRegex: '^[a-zA-Z0-9_.-]+$',
15-
},
16-
},
17-
}));
18-
1910
const mockInput: PipelineInput = {
2011
name: 'multiSampleVcf',
12+
displayName: 'multi-sample VCF file',
2113
type: 'FILE',
2214
isRequired: true,
2315
fileSuffix: '.vcf.gz',

src/pages/scientificServices/pipelines/components/inputs/PipelineFileInput.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import colors from 'src/libs/colors';
66
import { notify } from 'src/libs/notifications';
77
import { formatBytes } from 'src/libs/utils';
88
import { TEASPOONS_MAX_FILE_UPLOAD_SIZE_BYTES } from 'src/pages/scientificServices/pipelines/common/teaspoons-service-constants';
9-
import { INPUT_DESCRIPTIONS } from 'src/pages/scientificServices/pipelines/utils/pipeline-input-utils';
109
import {
1110
resumeUpload,
1211
uploadTimeRemainingDisplayText,
@@ -41,7 +40,8 @@ export const PipelineFileInput: React.FC<PipelineInputSelectorProps> = ({
4140
setUploadState,
4241
}) => {
4342
const fileInputRef = useRef<HTMLInputElement>(null);
44-
const { label, validationRegex } = INPUT_DESCRIPTIONS[input.name] || {};
43+
const { name, displayName, isRequired, fileSuffix } = input;
44+
const FILE_NAME_VALIDATION_REGEX = '^[a-zA-Z0-9_.-]+$';
4545

4646
const validateFile = (file: File | null) => {
4747
// Check if a file is selected, if required
@@ -75,13 +75,13 @@ export const PipelineFileInput: React.FC<PipelineInputSelectorProps> = ({
7575
}
7676

7777
// Validate file type based on suffix
78-
if (input.fileSuffix && !file.name.endsWith(input.fileSuffix)) {
79-
onValidation(`Invalid file type. Please upload a ${input.fileSuffix} file.`);
78+
if (fileSuffix && !file.name.endsWith(fileSuffix)) {
79+
onValidation(`Invalid file type. Please upload a ${fileSuffix} file.`);
8080
return;
8181
}
8282

8383
// Validate file name against regex
84-
if (validationRegex && !new RegExp(validationRegex).test(file.name)) {
84+
if (!new RegExp(FILE_NAME_VALIDATION_REGEX).test(file.name)) {
8585
onValidation('File names may only contain alphanumeric characters, dashes, underscores, and periods.');
8686
return;
8787
}
@@ -142,7 +142,7 @@ export const PipelineFileInput: React.FC<PipelineInputSelectorProps> = ({
142142
return (
143143
<div>
144144
<h3 style={{ marginBottom: '0.5rem' }}>
145-
{label || input.name} {input.isRequired ? <span style={{ color: '#DB3214' }}>*</span> : null}
145+
Select a {displayName || name} {isRequired ? <span style={{ color: '#DB3214' }}>*</span> : null}
146146
</h3>
147147
<div
148148
style={{
@@ -181,7 +181,7 @@ export const PipelineFileInput: React.FC<PipelineInputSelectorProps> = ({
181181
ref={fileInputRef}
182182
type='file'
183183
onChange={handleFileChange}
184-
accept={input.fileSuffix}
184+
accept={fileSuffix}
185185
style={{
186186
position: 'absolute',
187187
top: 0,
@@ -249,7 +249,7 @@ export const PipelineFileInput: React.FC<PipelineInputSelectorProps> = ({
249249
</div>
250250
) : (
251251
<div style={{ fontWeight: 600, paddingTop: '1.25rem', textAlign: 'center' }}>
252-
{dragging ? `Drop ${input.fileSuffix} file here` : `Drop ${input.fileSuffix} file or`}{' '}
252+
{dragging ? `Drop ${fileSuffix} file here` : `Drop ${fileSuffix} file or`}{' '}
253253
{!dragging && (
254254
<button
255255
type='button'
@@ -312,7 +312,7 @@ export const PipelineFileInput: React.FC<PipelineInputSelectorProps> = ({
312312
</ButtonPrimary>
313313
</div>
314314
)}
315-
<div key={input.name} style={{ marginTop: '1rem' }}>
315+
<div key={name} style={{ marginTop: '1rem' }}>
316316
<div
317317
style={{
318318
backgroundColor: '#e4e5e6',

src/pages/scientificServices/pipelines/components/inputs/PipelineFloatInput.test.tsx

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,17 @@ import userEvent from '@testing-library/user-event';
33
import React from 'react';
44
import { PipelineInput } from 'src/libs/ajax/teaspoons/teaspoons-models';
55

6-
import { PipelineFloatInput } from './PipelineFloatInput';
7-
8-
jest.mock('src/pages/scientificServices/pipelines/utils/pipeline-input-utils', () => ({
9-
INPUT_DESCRIPTIONS: {
10-
floatInput: {
11-
label: 'Enter a float value',
12-
placeholder: 'Enter a number',
13-
helpText: 'Must be a valid floating-point number.',
14-
validationRegex: String.raw`^(0(\.\d*)?|1(\.0*)?|\.\d+)$`,
15-
},
16-
},
17-
}));
6+
import { PipelineFloatInput, validatePipelineFloatInput } from './PipelineFloatInput';
187

198
const mockInput: PipelineInput = {
209
name: 'floatInput',
2110
type: 'FLOAT',
2211
isRequired: true,
12+
description: 'Must be a valid floating-point number.',
13+
displayName: 'float input',
14+
defaultValue: '0.0',
15+
minValue: 0,
16+
maxValue: 1,
2317
};
2418

2519
const optionalInput: PipelineInput = {
@@ -40,9 +34,14 @@ describe('PipelineFloatInput', () => {
4034
jest.clearAllMocks();
4135
});
4236

43-
it('renders input label', () => {
37+
it('renders input label with displayName when present', () => {
4438
render(<PipelineFloatInput {...defaultProps} />);
45-
expect(screen.getByText('Enter a float value')).toBeInTheDocument();
39+
expect(screen.getByText('Enter float input')).toBeInTheDocument();
40+
});
41+
42+
it('renders input label with name when displayName is absent', () => {
43+
render(<PipelineFloatInput {...defaultProps} input={{ ...mockInput, displayName: undefined }} />);
44+
expect(screen.getByText('Enter floatInput')).toBeInTheDocument();
4645
});
4746

4847
it('shows required indicator for required inputs', () => {
@@ -55,10 +54,16 @@ describe('PipelineFloatInput', () => {
5554
expect(screen.queryByText('*')).not.toBeInTheDocument();
5655
});
5756

58-
it('renders input placeholder', () => {
57+
it('renders input placeholder with defaultValue when present', () => {
5958
render(<PipelineFloatInput {...defaultProps} />);
6059
const input = screen.getByRole('textbox');
61-
expect(input).toHaveAttribute('placeholder', 'Enter a number');
60+
expect(input).toHaveAttribute('placeholder', '0.0');
61+
});
62+
63+
it('renders input placeholder with text when defaultValue is absent', () => {
64+
render(<PipelineFloatInput {...defaultProps} input={{ ...mockInput, defaultValue: undefined }} />);
65+
const input = screen.getByRole('textbox');
66+
expect(input).toHaveAttribute('placeholder', 'Enter float input');
6267
});
6368

6469
it('renders help text', () => {
@@ -103,7 +108,7 @@ describe('PipelineFloatInput', () => {
103108
fireEvent.change(input, { target: { value: '10' } });
104109

105110
expect(defaultProps.onChange).toHaveBeenCalledWith('10');
106-
expect(defaultProps.onValidation).toHaveBeenCalledWith('Invalid float value');
111+
expect(defaultProps.onValidation).toHaveBeenCalledWith('Value must be between 0 and 1');
107112
});
108113

109114
it('calls onValidation with error for . input', async () => {
@@ -113,7 +118,7 @@ describe('PipelineFloatInput', () => {
113118
fireEvent.change(input, { target: { value: '.' } });
114119

115120
expect(defaultProps.onChange).toHaveBeenCalledWith('.');
116-
expect(defaultProps.onValidation).toHaveBeenCalledWith('Invalid float value');
121+
expect(defaultProps.onValidation).toHaveBeenCalledWith('Enter a valid float value');
117122
});
118123

119124
it('calls onValidation with error for negative float input out of range', async () => {
@@ -123,7 +128,7 @@ describe('PipelineFloatInput', () => {
123128
fireEvent.change(input, { target: { value: '-0.1' } });
124129

125130
expect(defaultProps.onChange).toHaveBeenCalledWith('-0.1');
126-
expect(defaultProps.onValidation).toHaveBeenCalledWith('Invalid float value');
131+
expect(defaultProps.onValidation).toHaveBeenCalledWith('Value must be between 0 and 1');
127132
});
128133

129134
it('does not validate empty input', async () => {
@@ -136,4 +141,38 @@ describe('PipelineFloatInput', () => {
136141

137142
expect(onValidation).not.toHaveBeenCalled();
138143
});
144+
145+
describe('validatePipelineFloatInput', () => {
146+
it('returns undefined for empty input', () => {
147+
expect(validatePipelineFloatInput('', 0, 1)).toBeUndefined();
148+
});
149+
150+
it('returns error for non-numeric input', () => {
151+
expect(validatePipelineFloatInput('abc', 0, 1)).toBe('Enter a valid float value');
152+
});
153+
154+
it('returns error for input below minValue', () => {
155+
expect(validatePipelineFloatInput('-1', 0, 1)).toBe('Value must be between 0 and 1');
156+
});
157+
158+
it('returns error for input above maxValue', () => {
159+
expect(validatePipelineFloatInput('2', 0, 1)).toBe('Value must be between 0 and 1');
160+
});
161+
162+
it('returns undefined for valid input within range', () => {
163+
expect(validatePipelineFloatInput('0.5', 0, 1)).toBeUndefined();
164+
});
165+
166+
it('returns undefined for valid input when min/max are undefined', () => {
167+
expect(validatePipelineFloatInput('100')).toBeUndefined();
168+
});
169+
170+
it('returns error for input below minValue when only minValue is defined', () => {
171+
expect(validatePipelineFloatInput('-10', 0)).toBe('Value must be between 0 and undefined');
172+
});
173+
174+
it('returns error for input above maxValue when only maxValue is defined', () => {
175+
expect(validatePipelineFloatInput('10', undefined, 5)).toBe('Value must be between undefined and 5');
176+
});
177+
});
139178
});
Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, { ReactNode } from 'react';
22
import { ValidatedInput } from 'src/components/input';
33
import { PipelineInput } from 'src/libs/ajax/teaspoons/teaspoons-models';
4-
import { INPUT_DESCRIPTIONS } from 'src/pages/scientificServices/pipelines/utils/pipeline-input-utils';
54

65
interface PipelineFloatInputProps {
76
input: PipelineInput;
@@ -18,37 +17,49 @@ export const PipelineFloatInput: React.FC<PipelineFloatInputProps> = ({
1817
validationError,
1918
onValidation,
2019
}) => {
21-
const { label, placeholder, helpText, validationRegex } = INPUT_DESCRIPTIONS[input.name] || {};
20+
const { name, displayName, description, isRequired, defaultValue, minValue, maxValue } = input;
2221

2322
return (
2423
<>
2524
<h3 style={{ marginBottom: '0.5rem' }}>
26-
{label || input.name} {input.isRequired ? <span style={{ color: '#DB3214' }}> *</span> : null}
25+
Enter {displayName || name} {isRequired ? <span style={{ color: '#DB3214' }}> *</span> : null}
2726
</h3>
2827
<ValidatedInput
2928
width={400}
3029
error={validationError}
3130
inputProps={{
32-
'aria-label': `${input.name} float input`,
31+
'aria-label': `${displayName || name} float input`,
3332
type: 'text',
3433
value: value || '',
35-
placeholder: placeholder || '',
34+
placeholder: defaultValue || `Enter ${displayName || name}`,
3635
onChange: (e) => {
3736
onChange(e);
38-
if (validationRegex) {
39-
const regex = new RegExp(validationRegex);
40-
if (!regex.test(e.trim()) && e.length > 0) {
41-
onValidation('Invalid float value');
42-
} else {
43-
onValidation(undefined);
44-
}
45-
}
37+
onValidation(validatePipelineFloatInput(e, minValue, maxValue));
4638
},
4739
}}
4840
/>
49-
{helpText && (
50-
<div style={{ marginTop: '0.5rem', marginBottom: '2rem', fontStyle: 'italic', maxWidth: 500 }}>{helpText}</div>
41+
{description && (
42+
<div style={{ marginTop: '0.5rem', marginBottom: '2rem', fontStyle: 'italic', maxWidth: 500 }}>
43+
{description}
44+
</div>
5145
)}
5246
</>
5347
);
5448
};
49+
50+
export const validatePipelineFloatInput = (value: string, minValue?: number, maxValue?: number): string | undefined => {
51+
const floatValue = Number.parseFloat(value);
52+
if (value.trim().length === 0) {
53+
return undefined;
54+
}
55+
if (Number.isNaN(floatValue)) {
56+
return 'Enter a valid float value';
57+
}
58+
if (minValue !== undefined && floatValue < minValue) {
59+
return `Value must be between ${minValue} and ${maxValue}`;
60+
}
61+
if (maxValue !== undefined && floatValue > maxValue) {
62+
return `Value must be between ${minValue} and ${maxValue}`;
63+
}
64+
return undefined;
65+
};

src/pages/scientificServices/pipelines/components/inputs/PipelineStringInput.test.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,10 @@ import { PipelineInput } from 'src/libs/ajax/teaspoons/teaspoons-models';
55

66
import { PipelineStringInput } from './PipelineStringInput';
77

8-
jest.mock('src/pages/scientificServices/pipelines/utils/pipeline-input-utils', () => ({
9-
INPUT_DESCRIPTIONS: {
10-
outputBasename: {
11-
label: 'Enter prefix for output file',
12-
placeholder: 'Enter prefix name',
13-
helpText: 'May only contain alphanumeric characters, dashes, and underscores.',
14-
validationRegex: '^[a-zA-Z0-9_-]+$',
15-
},
16-
},
17-
}));
18-
198
const mockInput: PipelineInput = {
209
name: 'outputBasename',
10+
displayName: 'output basename',
11+
description: 'May only contain alphanumeric characters, dashes, and underscores.',
2112
type: 'STRING',
2213
isRequired: true,
2314
};
@@ -43,7 +34,7 @@ describe('PipelineStringInput', () => {
4334
describe('PipelineStringInput', () => {
4435
it('renders input label', () => {
4536
render(<PipelineStringInput {...defaultProps} />);
46-
expect(screen.getByText('Enter prefix for output file')).toBeInTheDocument();
37+
expect(screen.getByText('Enter output basename')).toBeInTheDocument();
4738
});
4839

4940
it('shows required indicator for required inputs', () => {
@@ -59,7 +50,7 @@ describe('PipelineStringInput', () => {
5950
it('renders input placeholder', () => {
6051
render(<PipelineStringInput {...defaultProps} />);
6152
const input = screen.getByRole('textbox');
62-
expect(input).toHaveAttribute('placeholder', 'Enter prefix name');
53+
expect(input).toHaveAttribute('placeholder', 'Enter output basename');
6354
});
6455

6556
it('renders help text', () => {

0 commit comments

Comments
 (0)