Skip to content

Commit 1e40a1e

Browse files
authored
add frontend for fetching type conversion (#2987)
Create frontend for [custom column type conversion.](#2989). It creates a dropdown of possible conversions determined by the backend. Right now only numeric -> string is supported. If there are no available conversion, the option for `use custom column destination type` will be hidden. Test: locally created a table that contains column with numeric type, create ClickPipe via UI, and see CH table get created with String type.
1 parent 0dd02fc commit 1e40a1e

4 files changed

Lines changed: 382 additions & 8 deletions

File tree

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
'use client';
2+
import { Divider } from '@tremor/react';
3+
import {
4+
Dispatch,
5+
SetStateAction,
6+
useCallback,
7+
useEffect,
8+
useMemo,
9+
useState,
10+
} from 'react';
11+
import ReactSelect from 'react-select';
12+
13+
import { TableMapRow } from '@/app/dto/MirrorsDTO';
14+
import SelectTheme from '@/app/styles/select';
15+
import { DBType } from '@/grpc_generated/peers';
16+
import { ColumnsItem } from '@/grpc_generated/route';
17+
import { Button } from '@/lib/Button';
18+
import { Checkbox } from '@/lib/Checkbox';
19+
import { Icon } from '@/lib/Icon';
20+
import { Label } from '@/lib/Label';
21+
import { RowWithCheckbox } from '@/lib/Layout';
22+
import { fetchAllTypeConversions } from '../handlers';
23+
import { columnBoxDividerStyle, engineOptionStyles } from './styles';
24+
interface CustomColumnTypeProps {
25+
columns: ColumnsItem[];
26+
tableRow: TableMapRow;
27+
rows: TableMapRow[];
28+
setRows: Dispatch<SetStateAction<TableMapRow[]>>;
29+
peerType: DBType;
30+
}
31+
32+
export default function CustomColumnType({
33+
columns,
34+
tableRow,
35+
rows,
36+
setRows,
37+
peerType,
38+
}: CustomColumnTypeProps) {
39+
const [useCustom, setUseCustom] = useState(false);
40+
const [selectedColumnName, setSelectedColumnName] = useState<string>('');
41+
const [destinationTypeMapping, setDestinationTypeMapping] = useState<
42+
Record<string, { value: string; label: string }[]>
43+
>({});
44+
45+
const selectedColumns = useMemo(() => {
46+
return columns.filter((col) => !tableRow.exclude.has(col.name));
47+
}, [columns, tableRow.exclude]);
48+
49+
const columnsWithDstTypes = useMemo(() => {
50+
const columnsWithDstTypes = selectedColumns.filter(
51+
(col) => destinationTypeMapping[col.name] !== undefined
52+
);
53+
if (columnsWithDstTypes.length == 0) {
54+
setUseCustom(false);
55+
}
56+
return columnsWithDstTypes;
57+
}, [selectedColumns, destinationTypeMapping]);
58+
59+
const customColumnTypeList = useMemo(() => {
60+
const currentRow = rows.find((r) => r.source === tableRow.source);
61+
if (!currentRow) return [];
62+
63+
return currentRow.columns
64+
.filter((col) => col.destinationType)
65+
.map((col) => ({
66+
columnName: col.sourceName,
67+
destinationType: col.destinationType,
68+
}));
69+
}, [rows, tableRow.source]);
70+
71+
const remainingColumnsWithDstTypes = columnsWithDstTypes.filter(
72+
(col) =>
73+
!customColumnTypeList.some((mapping) => mapping.columnName === col.name)
74+
);
75+
76+
const destinationTypeOptions = useCallback(
77+
(col: string): { value: string; label: string }[] => {
78+
return destinationTypeMapping[col] ?? [];
79+
},
80+
[destinationTypeMapping]
81+
);
82+
83+
useEffect(() => {
84+
const fetchTypeMappings = async () => {
85+
const columnNameToQvalueKind = selectedColumns.map((col) => [
86+
col.name,
87+
col.qkind,
88+
]) as [string, string][];
89+
90+
try {
91+
const typeConversions = await fetchAllTypeConversions(peerType);
92+
const columnNameToDestinationTypes = {} as Record<
93+
string,
94+
{ value: string; label: string }[]
95+
>;
96+
for (const [columnName, qkind] of columnNameToQvalueKind) {
97+
for (const tc of typeConversions) {
98+
if (tc.qkind === qkind) {
99+
columnNameToDestinationTypes[columnName] =
100+
tc.destinationTypes.map((type: string) => ({
101+
value: type,
102+
label: type,
103+
})) ?? [];
104+
break;
105+
}
106+
}
107+
}
108+
setDestinationTypeMapping(columnNameToDestinationTypes);
109+
} catch (error) {
110+
console.error('Error fetching type conversions:', error);
111+
}
112+
};
113+
114+
fetchTypeMappings();
115+
}, [peerType, selectedColumns]);
116+
117+
const handleUseCustomDstType = useCallback(
118+
(state: boolean) => {
119+
setUseCustom(state);
120+
if (!state) {
121+
setSelectedColumnName('');
122+
123+
// clear custom column types already set
124+
setRows((prev) =>
125+
prev.map((row) => {
126+
if (row.source !== tableRow.source) return row;
127+
return {
128+
...row,
129+
columns: row.columns.map((col) => ({
130+
...col,
131+
destinationType: '',
132+
})),
133+
};
134+
})
135+
);
136+
}
137+
},
138+
[tableRow.source, setRows]
139+
);
140+
141+
const handleCreateColumn = useCallback(
142+
(value: string) => {
143+
if (selectedColumnName) {
144+
setRows((prev) =>
145+
prev.map((row) => {
146+
if (row.source !== tableRow.source) return row;
147+
148+
const columnExistsInRow = row.columns.some(
149+
(col) => col.sourceName === selectedColumnName
150+
);
151+
if (!columnExistsInRow) {
152+
return {
153+
...row,
154+
columns: [
155+
...row.columns,
156+
{
157+
sourceName: selectedColumnName,
158+
destinationName: '',
159+
destinationType: value,
160+
ordering: 0,
161+
nullableEnabled: false,
162+
},
163+
],
164+
};
165+
} else {
166+
return {
167+
...row,
168+
columns: row.columns.map((col) =>
169+
col.sourceName === selectedColumnName
170+
? { ...col, destinationType: value }
171+
: col
172+
),
173+
};
174+
}
175+
})
176+
);
177+
setSelectedColumnName('');
178+
}
179+
},
180+
[tableRow.source, setRows, selectedColumnName]
181+
);
182+
183+
const handleUpdateColumn = useCallback(
184+
(entry: { columnName: string; destinationType: string }, value: string) => {
185+
setRows((prev) =>
186+
prev.map((row) => {
187+
if (row.source !== tableRow.source) return row;
188+
189+
if (value === '') {
190+
return {
191+
...row,
192+
columns: row.columns.filter(
193+
(col) => col.sourceName !== entry.columnName
194+
),
195+
};
196+
} else {
197+
return {
198+
...row,
199+
columns: row.columns.map((col) =>
200+
col.sourceName === entry.columnName
201+
? { ...col, destinationType: value }
202+
: col
203+
),
204+
};
205+
}
206+
})
207+
);
208+
},
209+
[tableRow.source, setRows]
210+
);
211+
212+
return (
213+
columnsWithDstTypes.length > 0 && (
214+
<div
215+
style={{
216+
display: 'flex',
217+
flexDirection: 'column',
218+
alignContent: 'center',
219+
rowGap: '0.5rem',
220+
}}
221+
>
222+
<Divider
223+
style={{
224+
...columnBoxDividerStyle,
225+
marginTop: '0.5rem',
226+
}}
227+
/>
228+
<RowWithCheckbox
229+
label={
230+
<Label as='label' style={{ fontSize: 13 }}>
231+
Use custom destination column type
232+
</Label>
233+
}
234+
action={
235+
<Checkbox
236+
style={{ marginLeft: 0 }}
237+
checked={useCustom}
238+
onCheckedChange={handleUseCustomDstType}
239+
/>
240+
}
241+
/>
242+
{useCustom && (
243+
<div
244+
style={{
245+
display: 'flex',
246+
flexDirection: 'column',
247+
gap: '0.5rem',
248+
}}
249+
>
250+
{customColumnTypeList.length > 0 && (
251+
<div
252+
style={{
253+
display: 'flex',
254+
gap: '0.5rem',
255+
flexDirection: 'column',
256+
}}
257+
>
258+
{customColumnTypeList.map((mapping) => (
259+
<div
260+
key={mapping.columnName}
261+
style={{
262+
display: 'flex',
263+
alignItems: 'center',
264+
gap: '0.5rem',
265+
}}
266+
>
267+
<div style={{ width: '30%' }}>
268+
<ReactSelect
269+
value={{
270+
value: mapping.columnName,
271+
label: mapping.columnName,
272+
}}
273+
theme={SelectTheme}
274+
styles={engineOptionStyles}
275+
/>
276+
</div>
277+
<span></span>
278+
<div style={{ width: '30%' }}>
279+
<ReactSelect
280+
value={{
281+
value: mapping.destinationType,
282+
label: mapping.destinationType,
283+
}}
284+
onChange={(val) =>
285+
val?.value && handleUpdateColumn(mapping, val.value)
286+
}
287+
theme={SelectTheme}
288+
styles={engineOptionStyles}
289+
options={destinationTypeOptions(mapping.columnName)}
290+
/>
291+
</div>
292+
<Button
293+
variant='normalBorderless'
294+
onClick={() => handleUpdateColumn(mapping, '')}
295+
>
296+
<Icon name='close' />
297+
</Button>
298+
</div>
299+
))}
300+
</div>
301+
)}
302+
303+
{remainingColumnsWithDstTypes.length > 0 && (
304+
<div
305+
style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}
306+
>
307+
<div style={{ width: '30%' }}>
308+
<ReactSelect
309+
placeholder='Column'
310+
value={
311+
selectedColumnName
312+
? {
313+
value: selectedColumnName,
314+
label: selectedColumnName,
315+
}
316+
: null
317+
}
318+
onChange={(val) =>
319+
val?.value && setSelectedColumnName(val.value)
320+
}
321+
options={remainingColumnsWithDstTypes.map((col) => ({
322+
value: col.name,
323+
label: col.name,
324+
}))}
325+
theme={SelectTheme}
326+
styles={engineOptionStyles}
327+
/>
328+
</div>
329+
<span></span>
330+
<div style={{ width: '30%' }}>
331+
<ReactSelect
332+
placeholder='Destination type'
333+
value={null}
334+
onChange={(val) =>
335+
val?.value && handleCreateColumn(val.value)
336+
}
337+
options={
338+
selectedColumnName
339+
? destinationTypeOptions(selectedColumnName)
340+
: []
341+
}
342+
theme={SelectTheme}
343+
styles={engineOptionStyles}
344+
/>
345+
</div>
346+
</div>
347+
)}
348+
</div>
349+
)}
350+
</div>
351+
)
352+
);
353+
}

0 commit comments

Comments
 (0)