532 lines
17 KiB
TypeScript
532 lines
17 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import React, { useState, useCallback, useEffect } from "react";
|
||
|
|
import { Card } from "@/components/ui/card";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { toast } from "sonner";
|
||
|
|
import { X, ArrowLeft } from "lucide-react";
|
||
|
|
|
||
|
|
// API import
|
||
|
|
import { saveDataflowRelationship } from "@/lib/api/dataflowSave";
|
||
|
|
|
||
|
|
// 타입 import
|
||
|
|
import {
|
||
|
|
DataConnectionState,
|
||
|
|
DataConnectionActions,
|
||
|
|
DataConnectionDesignerProps,
|
||
|
|
FieldMapping,
|
||
|
|
ValidationResult,
|
||
|
|
TestResult,
|
||
|
|
MappingStats,
|
||
|
|
ActionGroup,
|
||
|
|
SingleAction,
|
||
|
|
} from "./types/redesigned";
|
||
|
|
import { ColumnInfo, Connection, TableInfo } from "@/lib/types/multiConnection";
|
||
|
|
|
||
|
|
// 컴포넌트 import
|
||
|
|
import LeftPanel from "./LeftPanel/LeftPanel";
|
||
|
|
import RightPanel from "./RightPanel/RightPanel";
|
||
|
|
import SaveRelationshipDialog from "./SaveRelationshipDialog";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 🎨 데이터 연결 설정 메인 디자이너
|
||
|
|
* - 좌우 분할 레이아웃 (30% + 70%)
|
||
|
|
* - 상태 관리 및 액션 처리
|
||
|
|
* - 기존 모달 기능을 메인 화면으로 통합
|
||
|
|
*/
|
||
|
|
const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
|
||
|
|
onClose,
|
||
|
|
initialData,
|
||
|
|
showBackButton = false,
|
||
|
|
}) => {
|
||
|
|
// 🔄 상태 관리
|
||
|
|
const [state, setState] = useState<DataConnectionState>(() => ({
|
||
|
|
connectionType: "data_save",
|
||
|
|
currentStep: 1,
|
||
|
|
fieldMappings: [],
|
||
|
|
mappingStats: {
|
||
|
|
totalMappings: 0,
|
||
|
|
validMappings: 0,
|
||
|
|
invalidMappings: 0,
|
||
|
|
missingRequiredFields: 0,
|
||
|
|
estimatedRows: 0,
|
||
|
|
actionType: "INSERT",
|
||
|
|
},
|
||
|
|
// 제어 실행 조건 초기값
|
||
|
|
controlConditions: [],
|
||
|
|
|
||
|
|
// 액션 그룹 초기값 (멀티 액션)
|
||
|
|
actionGroups: [
|
||
|
|
{
|
||
|
|
id: "group_1",
|
||
|
|
name: "기본 액션 그룹",
|
||
|
|
logicalOperator: "AND" as const,
|
||
|
|
actions: [
|
||
|
|
{
|
||
|
|
id: "action_1",
|
||
|
|
name: "액션 1",
|
||
|
|
actionType: "insert" as const,
|
||
|
|
conditions: [],
|
||
|
|
fieldMappings: [],
|
||
|
|
isEnabled: true,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
isEnabled: true,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
|
||
|
|
// 기존 호환성 필드들 (deprecated)
|
||
|
|
actionType: "insert",
|
||
|
|
actionConditions: [],
|
||
|
|
actionFieldMappings: [],
|
||
|
|
isLoading: false,
|
||
|
|
validationErrors: [],
|
||
|
|
...initialData,
|
||
|
|
}));
|
||
|
|
|
||
|
|
// 💾 저장 다이얼로그 상태
|
||
|
|
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
||
|
|
|
||
|
|
// 🔄 초기 데이터 로드
|
||
|
|
useEffect(() => {
|
||
|
|
if (initialData && Object.keys(initialData).length > 1) {
|
||
|
|
console.log("🔄 초기 데이터 로드:", initialData);
|
||
|
|
|
||
|
|
// 로드된 데이터로 state 업데이트
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
connectionType: initialData.connectionType || prev.connectionType,
|
||
|
|
fromConnection: initialData.fromConnection || prev.fromConnection,
|
||
|
|
toConnection: initialData.toConnection || prev.toConnection,
|
||
|
|
fromTable: initialData.fromTable || prev.fromTable,
|
||
|
|
toTable: initialData.toTable || prev.toTable,
|
||
|
|
actionType: initialData.actionType || prev.actionType,
|
||
|
|
controlConditions: initialData.controlConditions || prev.controlConditions,
|
||
|
|
actionConditions: initialData.actionConditions || prev.actionConditions,
|
||
|
|
fieldMappings: initialData.fieldMappings || prev.fieldMappings,
|
||
|
|
currentStep: initialData.fromConnection && initialData.toConnection ? 2 : 1, // 연결 정보가 있으면 2단계부터 시작
|
||
|
|
}));
|
||
|
|
|
||
|
|
console.log("✅ 초기 데이터 로드 완료");
|
||
|
|
}
|
||
|
|
}, [initialData]);
|
||
|
|
|
||
|
|
// 🎯 액션 핸들러들
|
||
|
|
const actions: DataConnectionActions = {
|
||
|
|
// 연결 타입 설정
|
||
|
|
setConnectionType: useCallback((type: "data_save" | "external_call") => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
connectionType: type,
|
||
|
|
// 타입 변경 시 상태 초기화
|
||
|
|
currentStep: 1,
|
||
|
|
fromConnection: undefined,
|
||
|
|
toConnection: undefined,
|
||
|
|
fromTable: undefined,
|
||
|
|
toTable: undefined,
|
||
|
|
fieldMappings: [],
|
||
|
|
validationErrors: [],
|
||
|
|
}));
|
||
|
|
toast.success(`연결 타입이 ${type === "data_save" ? "데이터 저장" : "외부 호출"}로 변경되었습니다.`);
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
// 단계 이동
|
||
|
|
goToStep: useCallback((step: 1 | 2 | 3 | 4) => {
|
||
|
|
setState((prev) => ({ ...prev, currentStep: step }));
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
// 연결 선택
|
||
|
|
selectConnection: useCallback((type: "from" | "to", connection: Connection) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
[type === "from" ? "fromConnection" : "toConnection"]: connection,
|
||
|
|
// 연결 변경 시 테이블과 매핑 초기화
|
||
|
|
[type === "from" ? "fromTable" : "toTable"]: undefined,
|
||
|
|
fieldMappings: [],
|
||
|
|
}));
|
||
|
|
toast.success(`${type === "from" ? "소스" : "대상"} 연결이 선택되었습니다: ${connection.name}`);
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
// 테이블 선택
|
||
|
|
selectTable: useCallback((type: "from" | "to", table: TableInfo) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
[type === "from" ? "fromTable" : "toTable"]: table,
|
||
|
|
// 테이블 변경 시 매핑 초기화
|
||
|
|
fieldMappings: [],
|
||
|
|
}));
|
||
|
|
toast.success(
|
||
|
|
`${type === "from" ? "소스" : "대상"} 테이블이 선택되었습니다: ${table.displayName || table.tableName}`,
|
||
|
|
);
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
// 필드 매핑 생성
|
||
|
|
createMapping: useCallback((fromField: ColumnInfo, toField: ColumnInfo) => {
|
||
|
|
const newMapping: FieldMapping = {
|
||
|
|
id: `${fromField.columnName}_to_${toField.columnName}_${Date.now()}`,
|
||
|
|
fromField,
|
||
|
|
toField,
|
||
|
|
isValid: true, // 기본적으로 유효하다고 가정, 나중에 검증
|
||
|
|
validationMessage: undefined,
|
||
|
|
};
|
||
|
|
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
fieldMappings: [...prev.fieldMappings, newMapping],
|
||
|
|
}));
|
||
|
|
|
||
|
|
toast.success(`매핑이 생성되었습니다: ${fromField.columnName} → ${toField.columnName}`);
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
// 필드 매핑 업데이트
|
||
|
|
updateMapping: useCallback((mappingId: string, updates: Partial<FieldMapping>) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
fieldMappings: prev.fieldMappings.map((mapping) =>
|
||
|
|
mapping.id === mappingId ? { ...mapping, ...updates } : mapping,
|
||
|
|
),
|
||
|
|
}));
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
// 필드 매핑 삭제
|
||
|
|
deleteMapping: useCallback((mappingId: string) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
fieldMappings: prev.fieldMappings.filter((mapping) => mapping.id !== mappingId),
|
||
|
|
}));
|
||
|
|
toast.success("매핑이 삭제되었습니다.");
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
// 매핑 검증
|
||
|
|
validateMappings: useCallback(async (): Promise<ValidationResult> => {
|
||
|
|
setState((prev) => ({ ...prev, isLoading: true }));
|
||
|
|
|
||
|
|
try {
|
||
|
|
// TODO: 실제 검증 로직 구현
|
||
|
|
const result: ValidationResult = {
|
||
|
|
isValid: true,
|
||
|
|
errors: [],
|
||
|
|
warnings: [],
|
||
|
|
};
|
||
|
|
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
validationErrors: result.errors,
|
||
|
|
isLoading: false,
|
||
|
|
}));
|
||
|
|
|
||
|
|
return result;
|
||
|
|
} catch (error) {
|
||
|
|
setState((prev) => ({ ...prev, isLoading: false }));
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
// 제어 조건 관리 (전체 실행 조건)
|
||
|
|
addControlCondition: useCallback(() => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
controlConditions: [
|
||
|
|
...prev.controlConditions,
|
||
|
|
{
|
||
|
|
id: Date.now().toString(),
|
||
|
|
type: "condition",
|
||
|
|
field: "",
|
||
|
|
operator: "=",
|
||
|
|
value: "",
|
||
|
|
dataType: "string",
|
||
|
|
},
|
||
|
|
],
|
||
|
|
}));
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
updateControlCondition: useCallback((index: number, condition: any) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
controlConditions: prev.controlConditions.map((cond, i) => (i === index ? { ...cond, ...condition } : cond)),
|
||
|
|
}));
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
deleteControlCondition: useCallback((index: number) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
controlConditions: prev.controlConditions.filter((_, i) => i !== index),
|
||
|
|
}));
|
||
|
|
toast.success("제어 조건이 삭제되었습니다.");
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
// 액션 설정 관리
|
||
|
|
setActionType: useCallback((type: "insert" | "update" | "delete" | "upsert") => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
actionType: type,
|
||
|
|
// INSERT가 아닌 경우 조건 초기화
|
||
|
|
actionConditions: type === "insert" ? [] : prev.actionConditions,
|
||
|
|
}));
|
||
|
|
toast.success(`액션 타입이 ${type.toUpperCase()}로 변경되었습니다.`);
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
addActionCondition: useCallback(() => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
actionConditions: [
|
||
|
|
...prev.actionConditions,
|
||
|
|
{
|
||
|
|
id: Date.now().toString(),
|
||
|
|
type: "condition",
|
||
|
|
field: "",
|
||
|
|
operator: "=",
|
||
|
|
value: "",
|
||
|
|
dataType: "string",
|
||
|
|
},
|
||
|
|
],
|
||
|
|
}));
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
updateActionCondition: useCallback((index: number, condition: any) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
actionConditions: prev.actionConditions.map((cond, i) => (i === index ? { ...cond, ...condition } : cond)),
|
||
|
|
}));
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
// 🔧 액션 조건 배열 전체 업데이트 (ActionConditionBuilder용)
|
||
|
|
setActionConditions: useCallback((conditions: any[]) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
actionConditions: conditions,
|
||
|
|
}));
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
deleteActionCondition: useCallback((index: number) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
actionConditions: prev.actionConditions.filter((_, i) => i !== index),
|
||
|
|
}));
|
||
|
|
toast.success("조건이 삭제되었습니다.");
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
// 🎯 액션 그룹 관리 (멀티 액션)
|
||
|
|
addActionGroup: useCallback(() => {
|
||
|
|
const newGroupId = `group_${Date.now()}`;
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
actionGroups: [
|
||
|
|
...prev.actionGroups,
|
||
|
|
{
|
||
|
|
id: newGroupId,
|
||
|
|
name: `액션 그룹 ${prev.actionGroups.length + 1}`,
|
||
|
|
logicalOperator: "AND" as const,
|
||
|
|
actions: [
|
||
|
|
{
|
||
|
|
id: `action_${Date.now()}`,
|
||
|
|
name: "액션 1",
|
||
|
|
actionType: "insert" as const,
|
||
|
|
conditions: [],
|
||
|
|
fieldMappings: [],
|
||
|
|
isEnabled: true,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
isEnabled: true,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
}));
|
||
|
|
toast.success("새 액션 그룹이 추가되었습니다.");
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
updateActionGroup: useCallback((groupId: string, updates: Partial<ActionGroup>) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
actionGroups: prev.actionGroups.map((group) => (group.id === groupId ? { ...group, ...updates } : group)),
|
||
|
|
}));
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
deleteActionGroup: useCallback((groupId: string) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
actionGroups: prev.actionGroups.filter((group) => group.id !== groupId),
|
||
|
|
}));
|
||
|
|
toast.success("액션 그룹이 삭제되었습니다.");
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
addActionToGroup: useCallback((groupId: string) => {
|
||
|
|
const newActionId = `action_${Date.now()}`;
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
actionGroups: prev.actionGroups.map((group) =>
|
||
|
|
group.id === groupId
|
||
|
|
? {
|
||
|
|
...group,
|
||
|
|
actions: [
|
||
|
|
...group.actions,
|
||
|
|
{
|
||
|
|
id: newActionId,
|
||
|
|
name: `액션 ${group.actions.length + 1}`,
|
||
|
|
actionType: "insert" as const,
|
||
|
|
conditions: [],
|
||
|
|
fieldMappings: [],
|
||
|
|
isEnabled: true,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
}
|
||
|
|
: group,
|
||
|
|
),
|
||
|
|
}));
|
||
|
|
toast.success("새 액션이 추가되었습니다.");
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
updateActionInGroup: useCallback((groupId: string, actionId: string, updates: Partial<SingleAction>) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
actionGroups: prev.actionGroups.map((group) =>
|
||
|
|
group.id === groupId
|
||
|
|
? {
|
||
|
|
...group,
|
||
|
|
actions: group.actions.map((action) => (action.id === actionId ? { ...action, ...updates } : action)),
|
||
|
|
}
|
||
|
|
: group,
|
||
|
|
),
|
||
|
|
}));
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
deleteActionFromGroup: useCallback((groupId: string, actionId: string) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
actionGroups: prev.actionGroups.map((group) =>
|
||
|
|
group.id === groupId
|
||
|
|
? {
|
||
|
|
...group,
|
||
|
|
actions: group.actions.filter((action) => action.id !== actionId),
|
||
|
|
}
|
||
|
|
: group,
|
||
|
|
),
|
||
|
|
}));
|
||
|
|
toast.success("액션이 삭제되었습니다.");
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
// 매핑 저장 (다이얼로그 표시)
|
||
|
|
saveMappings: useCallback(async () => {
|
||
|
|
setShowSaveDialog(true);
|
||
|
|
}, []),
|
||
|
|
|
||
|
|
// 테스트 실행
|
||
|
|
testExecution: useCallback(async (): Promise<TestResult> => {
|
||
|
|
setState((prev) => ({ ...prev, isLoading: true }));
|
||
|
|
|
||
|
|
try {
|
||
|
|
// TODO: 실제 테스트 로직 구현
|
||
|
|
const result: TestResult = {
|
||
|
|
success: true,
|
||
|
|
message: "테스트가 성공적으로 완료되었습니다.",
|
||
|
|
affectedRows: 10,
|
||
|
|
executionTime: 250,
|
||
|
|
};
|
||
|
|
|
||
|
|
setState((prev) => ({ ...prev, isLoading: false }));
|
||
|
|
toast.success(result.message);
|
||
|
|
|
||
|
|
return result;
|
||
|
|
} catch (error) {
|
||
|
|
setState((prev) => ({ ...prev, isLoading: false }));
|
||
|
|
toast.error("테스트 실행 중 오류가 발생했습니다.");
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}, []),
|
||
|
|
};
|
||
|
|
|
||
|
|
// 💾 실제 저장 함수
|
||
|
|
const handleSaveWithName = useCallback(
|
||
|
|
async (relationshipName: string, description?: string) => {
|
||
|
|
setState((prev) => ({ ...prev, isLoading: true }));
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 실제 저장 로직 구현
|
||
|
|
const saveData = {
|
||
|
|
relationshipName,
|
||
|
|
description,
|
||
|
|
connectionType: state.connectionType,
|
||
|
|
fromConnection: state.fromConnection,
|
||
|
|
toConnection: state.toConnection,
|
||
|
|
fromTable: state.fromTable,
|
||
|
|
toTable: state.toTable,
|
||
|
|
actionType: state.actionType,
|
||
|
|
controlConditions: state.controlConditions,
|
||
|
|
actionConditions: state.actionConditions,
|
||
|
|
fieldMappings: state.fieldMappings,
|
||
|
|
};
|
||
|
|
|
||
|
|
console.log("💾 저장 시작:", saveData);
|
||
|
|
|
||
|
|
// 백엔드 API 호출
|
||
|
|
const result = await saveDataflowRelationship(saveData);
|
||
|
|
|
||
|
|
console.log("✅ 저장 완료:", result);
|
||
|
|
|
||
|
|
setState((prev) => ({ ...prev, isLoading: false }));
|
||
|
|
toast.success(`"${relationshipName}" 관계가 성공적으로 저장되었습니다.`);
|
||
|
|
|
||
|
|
// 저장 후 상위 컴포넌트에 알림 (필요한 경우)
|
||
|
|
if (onClose) {
|
||
|
|
onClose();
|
||
|
|
}
|
||
|
|
} catch (error: any) {
|
||
|
|
setState((prev) => ({ ...prev, isLoading: false }));
|
||
|
|
|
||
|
|
const errorMessage = error.message || "저장 중 오류가 발생했습니다.";
|
||
|
|
toast.error(errorMessage);
|
||
|
|
console.error("❌ 저장 오류:", error);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[state, onClose],
|
||
|
|
);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="overflow-hidden rounded-lg border bg-white shadow-sm">
|
||
|
|
{/* 상단 네비게이션 */}
|
||
|
|
{showBackButton && (
|
||
|
|
<div className="flex-shrink-0 border-b bg-white shadow-sm">
|
||
|
|
<div className="flex items-center justify-between p-4">
|
||
|
|
<div className="flex items-center gap-4">
|
||
|
|
<Button variant="outline" onClick={onClose} className="flex items-center gap-2">
|
||
|
|
<ArrowLeft className="h-4 w-4" />
|
||
|
|
목록으로
|
||
|
|
</Button>
|
||
|
|
<div>
|
||
|
|
<h1 className="text-xl font-bold">🔗 데이터 연결 설정</h1>
|
||
|
|
<p className="text-muted-foreground text-sm">
|
||
|
|
{state.connectionType === "data_save" ? "데이터 저장" : "외부 호출"} 연결 설정
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 메인 컨텐츠 - 좌우 분할 레이아웃 */}
|
||
|
|
<div className="flex h-[calc(100vh-280px)] min-h-[600px] overflow-hidden">
|
||
|
|
{/* 좌측 패널 (30%) */}
|
||
|
|
<div className="flex w-[30%] flex-col border-r bg-white">
|
||
|
|
<LeftPanel state={state} actions={actions} />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 우측 패널 (70%) */}
|
||
|
|
<div className="flex w-[70%] flex-col bg-gray-50">
|
||
|
|
<RightPanel state={state} actions={actions} />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 💾 저장 다이얼로그 */}
|
||
|
|
<SaveRelationshipDialog
|
||
|
|
open={showSaveDialog}
|
||
|
|
onOpenChange={setShowSaveDialog}
|
||
|
|
onSave={handleSaveWithName}
|
||
|
|
actionType={state.actionType}
|
||
|
|
fromTable={state.fromTable?.tableName}
|
||
|
|
toTable={state.toTable?.tableName}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default DataConnectionDesigner;
|