Skip to content

Commit a208fa5

Browse files
committed
feat(bulk-import): add fields for annotations, labels and spec input
Signed-off-by: Yi Cai <[email protected]>
1 parent 00fbc7d commit a208fa5

File tree

6 files changed

+261
-10
lines changed

6 files changed

+261
-10
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React, { useState } from 'react';
2+
3+
import { FormHelperText, TextField } from '@material-ui/core';
4+
5+
import { PullRequestPreview, PullRequestPreviewData } from '../../types';
6+
7+
interface KeyValueTextFieldProps {
8+
repoName: string;
9+
label: string;
10+
name: string;
11+
value: string;
12+
onChange: (
13+
event: React.FocusEvent<HTMLTextAreaElement | HTMLInputElement>,
14+
) => void;
15+
formErrors: PullRequestPreviewData;
16+
setFormErrors: (value: React.SetStateAction<PullRequestPreviewData>) => void;
17+
}
18+
19+
const validateKeyValuePairs = (value: string): string | null => {
20+
const keyValuePairs = value.split(';').map(pair => pair.trim());
21+
for (const pair of keyValuePairs) {
22+
if (pair) {
23+
const [key, val] = pair.split(':').map(part => part.trim());
24+
if (!key || !val) {
25+
return 'Each entry must have a key and a value separated by a colon.';
26+
}
27+
}
28+
}
29+
return null;
30+
};
31+
32+
const KeyValueTextField: React.FC<KeyValueTextFieldProps> = ({
33+
repoName,
34+
label,
35+
name,
36+
value,
37+
onChange,
38+
setFormErrors,
39+
formErrors,
40+
}) => {
41+
const [error, setError] = useState<string | null>(null);
42+
const fieldName = name.split('.').pop() ?? '';
43+
44+
const removeError = () => {
45+
const err = { ...formErrors };
46+
if (err[repoName]) {
47+
delete err[repoName][fieldName as keyof PullRequestPreview];
48+
}
49+
setFormErrors(err);
50+
};
51+
52+
const getUpdatedFormError = (
53+
validationError: string,
54+
): PullRequestPreviewData => {
55+
return {
56+
...formErrors,
57+
[repoName]: {
58+
...formErrors[repoName],
59+
[fieldName]: validationError,
60+
},
61+
};
62+
};
63+
64+
const handleChange = (
65+
event: React.FocusEvent<HTMLTextAreaElement | HTMLInputElement, Element>,
66+
) => {
67+
const validationError = validateKeyValuePairs(event.target.value);
68+
if (validationError) {
69+
setError(validationError);
70+
setFormErrors(getUpdatedFormError(validationError));
71+
} else {
72+
setError(null);
73+
removeError();
74+
}
75+
onChange(event);
76+
};
77+
78+
return (
79+
<div>
80+
<TextField
81+
multiline
82+
label={label}
83+
placeholder="key1: value2; key2: value2"
84+
variant="outlined"
85+
margin="normal"
86+
fullWidth
87+
name={name}
88+
value={value}
89+
onChange={handleChange}
90+
error={!!error}
91+
helperText={error}
92+
/>
93+
<FormHelperText style={{ marginLeft: '0.8rem' }}>
94+
Use semicolon to separate {label.toLowerCase()}
95+
</FormHelperText>
96+
</div>
97+
);
98+
};
99+
100+
export default KeyValueTextField;

plugins/bulk-import/src/components/PreviewFile/PreviewFile.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,14 +145,14 @@ export const PreviewFileSidebar = ({
145145
{repositoryType === RepositorySelection.Repository ? (
146146
<>
147147
<Typography variant="h5">
148-
{`${data.orgName || data.organizationUrl}/${data.repoName}`}
148+
{`${data.orgName ?? data.organizationUrl}/${data.repoName}`}
149149
</Typography>
150-
<Link to={data.repoUrl || ''}>{data.repoUrl}</Link>
150+
<Link to={data.repoUrl ?? ''}>{data.repoUrl}</Link>
151151
</>
152152
) : (
153153
<>
154154
<Typography variant="h5">{`${data.orgName}`}</Typography>
155-
<Link to={data.repoUrl || ''}>{data.organizationUrl}</Link>
155+
<Link to={data.repoUrl ?? ''}>{data.organizationUrl}</Link>
156156
</>
157157
)}
158158
</div>
@@ -171,7 +171,7 @@ export const PreviewFileSidebar = ({
171171
{repositoryType === RepositorySelection.Repository &&
172172
data.catalogInfoYaml?.prTemplate && (
173173
<PreviewPullRequest
174-
repoName={data.repoName || ''}
174+
repoName={data.repoName ?? ''}
175175
pullRequest={pullRequest}
176176
setPullRequest={setPullRequest}
177177
formErrors={formErrors as PullRequestPreviewData}

plugins/bulk-import/src/components/PreviewFile/PreviewPullRequest.tsx

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import Typography from '@mui/material/Typography';
2323
import { useFormikContext } from 'formik';
2424

2525
import { AddRepositoriesFormValues, PullRequestPreviewData } from '../../types';
26+
import { getYamlKeyValuePairs } from '../../utils/repository-utils';
27+
import KeyValueTextField from './KeyValueTextField';
2628

2729
const useDrawerContentStyles = makeStyles(theme => ({
2830
previewCard: {
@@ -53,7 +55,7 @@ export const PreviewPullRequest = ({
5355
values.approvalTool === 'git' ? 'Pull request' : 'ServiceNow ticket';
5456

5557
const [entityOwner, setEntityOwner] = React.useState<string | null>(
56-
pullRequest[repoName]?.entityOwner || '',
58+
pullRequest[repoName]?.entityOwner ?? '',
5759
);
5860
const { loading: entitiesLoading, value: entities } = useAsync(async () => {
5961
const allEntities = await catalogApi.getEntities({
@@ -64,7 +66,7 @@ export const PreviewPullRequest = ({
6466

6567
return allEntities.items
6668
.map(e => humanizeEntityRef(e, { defaultNamespace: 'true' }))
67-
.sort();
69+
.sort((a, b) => a.localeCompare(b));
6870
});
6971
React.useEffect(() => {
7072
const newFormErrors = {
@@ -158,8 +160,70 @@ export const PreviewPullRequest = ({
158160
setFormErrors(err);
159161
}
160162
}
163+
if (event.target.name.split('.').find(s => s === 'prAnnotations')) {
164+
const annotations = getYamlKeyValuePairs(event.target.value);
165+
setPullRequest({
166+
...pullRequest,
167+
[repoName]: {
168+
...pullRequest[repoName],
169+
prAnnotations: event.target.value,
170+
yaml: {
171+
...pullRequest[repoName]?.yaml,
172+
metadata: {
173+
...pullRequest[repoName]?.yaml.metadata,
174+
annotations: annotations,
175+
},
176+
},
177+
},
178+
});
179+
}
180+
if (event.target.name.split('.').find(s => s === 'prLabels')) {
181+
const labels = getYamlKeyValuePairs(event.target.value);
182+
setPullRequest({
183+
...pullRequest,
184+
[repoName]: {
185+
...pullRequest[repoName],
186+
prLabels: event.target.value,
187+
yaml: {
188+
...pullRequest[repoName]?.yaml,
189+
metadata: {
190+
...pullRequest[repoName]?.yaml.metadata,
191+
labels: labels,
192+
},
193+
},
194+
},
195+
});
196+
}
197+
if (event.target.name.split('.').find(s => s === 'prSpec')) {
198+
const spec = getYamlKeyValuePairs(event.target.value);
199+
setPullRequest({
200+
...pullRequest,
201+
[repoName]: {
202+
...pullRequest[repoName],
203+
prSpec: event.target.value,
204+
yaml: {
205+
...pullRequest[repoName]?.yaml,
206+
spec: spec,
207+
},
208+
},
209+
});
210+
}
161211
};
162212

213+
const keyValueTextFields = [
214+
{
215+
label: 'Annotations',
216+
name: 'prAnnotations',
217+
value: pullRequest?.[repoName]?.prAnnotations,
218+
},
219+
{
220+
label: 'Labels',
221+
name: 'prLabels',
222+
value: pullRequest?.[repoName]?.prLabels,
223+
},
224+
{ label: 'Spec', name: 'prSpec', value: pullRequest?.[repoName]?.prSpec },
225+
];
226+
163227
return (
164228
<>
165229
<Box marginTop={2}>
@@ -214,7 +278,7 @@ export const PreviewPullRequest = ({
214278

215279
<Autocomplete
216280
options={entities || []}
217-
value={entityOwner || ''}
281+
value={entityOwner ?? ''}
218282
loading={entitiesLoading}
219283
loadingText="Loading groups and users"
220284
disableClearable
@@ -224,7 +288,7 @@ export const PreviewPullRequest = ({
224288
...pullRequest,
225289
[repoName]: {
226290
...pullRequest[repoName],
227-
entityOwner: value || '',
291+
entityOwner: value ?? '',
228292
},
229293
});
230294
}}
@@ -304,15 +368,27 @@ export const PreviewPullRequest = ({
304368
WARNING: This may fail if no CODEOWNERS file is found at the target
305369
location.
306370
</FormHelperText>
371+
{keyValueTextFields.map(field => (
372+
<KeyValueTextField
373+
key={field.name}
374+
label={field.label}
375+
name={`repositories.${pullRequest[repoName].componentName}.${field.name}`}
376+
value={field.value ?? ''}
377+
onChange={handleChange}
378+
setFormErrors={setFormErrors}
379+
formErrors={formErrors}
380+
repoName={repoName}
381+
/>
382+
))}
307383
<Box marginTop={2}>
308384
<Typography variant="h6">
309385
Preview {`${approvalTool.toLowerCase()}`}
310386
</Typography>
311387
</Box>
312388

313389
<PreviewPullRequestComponent
314-
title={pullRequest?.[repoName]?.prTitle || ''}
315-
description={pullRequest?.[repoName]?.prDescription || ''}
390+
title={pullRequest?.[repoName]?.prTitle ?? ''}
391+
description={pullRequest?.[repoName]?.prDescription ?? ''}
316392
classes={{
317393
card: contentClasses.previewCard,
318394
cardContent: contentClasses.previewCardContent,

plugins/bulk-import/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export type RepositoriesData = {
1212
export type PullRequestPreview = {
1313
prTitle?: string;
1414
prDescription?: string;
15+
prAnnotations?: string;
16+
prLabels?: string;
17+
prSpec?: string;
1518
componentName?: string;
1619
entityOwner?: string;
1720
useCodeOwnersFile: boolean;

plugins/bulk-import/src/utils/repository-utils.test.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getNewOrgsData,
55
getSelectedRepositories,
66
getSelectedRepositoriesCount,
7+
getYamlKeyValuePairs,
78
updateWithNewSelectedRepositories,
89
urlHelper,
910
} from './repository-utils';
@@ -115,4 +116,58 @@ describe('Repository utils', () => {
115116
getDataForRepositories('user:default/guest').slice(1, 3),
116117
);
117118
});
119+
120+
it('should parse key-value pairs correctly with semicolons', () => {
121+
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`;
122+
123+
const expectedOutput = {
124+
'argocd/app-name': 'guestbook',
125+
'github.com/project-slug': 'janus-idp/backstage-showcase',
126+
'backstage.io/createdAt': '5/12/2021, 07:03:18 AM',
127+
'quay.io/repository-slug': 'janus-idp/backstage-showcase',
128+
'backstage.io/kubernetes-id': 'test-backstage',
129+
};
130+
131+
expect(getYamlKeyValuePairs(prKeyValuePairInput)).toEqual(expectedOutput);
132+
});
133+
134+
it('should handle empty input', () => {
135+
const prKeyValuePairInput = '';
136+
const expectedOutput = {};
137+
expect(getYamlKeyValuePairs(prKeyValuePairInput)).toEqual(expectedOutput);
138+
});
139+
140+
it('should handle input without quotes', () => {
141+
const prKeyValuePairInput = `backstage.io/kubernetes-id: my-kubernetes-component`;
142+
143+
const expectedOutput = {
144+
'backstage.io/kubernetes-id': 'my-kubernetes-component',
145+
};
146+
147+
expect(getYamlKeyValuePairs(prKeyValuePairInput)).toEqual(expectedOutput);
148+
});
149+
150+
it('should handle extra whitespace around key-value pairs', () => {
151+
const prKeyValuePairInput = ` argocd/app-name : 'guestbook' ; github.com/project-slug : janus-idp/backstage-showcase ; backstage.io/createdAt : '5/12/2021, 07:03:18 AM' `;
152+
153+
const expectedOutput = {
154+
'argocd/app-name': 'guestbook',
155+
'github.com/project-slug': 'janus-idp/backstage-showcase',
156+
'backstage.io/createdAt': '5/12/2021, 07:03:18 AM',
157+
};
158+
159+
expect(getYamlKeyValuePairs(prKeyValuePairInput)).toEqual(expectedOutput);
160+
});
161+
162+
it('should parse key-value pairs correctly with semicolon in value', () => {
163+
const prKeyValuePairInput = `type: other; lifecycle: production; owner: user:guest`;
164+
165+
const expectedOutput = {
166+
type: 'other',
167+
lifecycle: 'production',
168+
owner: 'user:guest',
169+
};
170+
171+
expect(getYamlKeyValuePairs(prKeyValuePairInput)).toEqual(expectedOutput);
172+
});
118173
});

plugins/bulk-import/src/utils/repository-utils.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,23 @@ export const getPRTemplate = (componentName: string, entityOwner: string) => {
9090
};
9191
};
9292

93+
export const getYamlKeyValuePairs = (
94+
prYamlInput: string,
95+
): Record<string, string> => {
96+
const keyValuePairs: Record<string, string> = {};
97+
const keyValueEntries = prYamlInput.split(';').map(entry => entry.trim());
98+
99+
keyValueEntries.forEach(entry => {
100+
const [key, ...valueParts] = entry.split(':');
101+
const value = valueParts.join(':').trim();
102+
if (key && value) {
103+
keyValuePairs[key.trim()] = value.replace(/(^['"])|(['"]$)/g, '');
104+
}
105+
});
106+
107+
return keyValuePairs;
108+
};
109+
93110
export const createData = (
94111
id: number,
95112
name: string,

0 commit comments

Comments
 (0)