Skip to content

Commit a3dd3ba

Browse files
data importer with format fixes
1 parent eac85ef commit a3dd3ba

File tree

10 files changed

+482
-0
lines changed

10 files changed

+482
-0
lines changed

frontend/src/components/Layout/PageLayout.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import PredefinedSchemaDialog from '../Popups/GraphEnhancementDialog/EnitityExtr
2323
import { SKIP_AUTH } from '../../utils/Constants';
2424
import { useNavigate } from 'react-router';
2525
import { deduplicateByFullPattern, deduplicateNodeByValue } from '../../utils/Utils';
26+
import DataImporterSchemaDailog from '../Popups/GraphEnhancementDialog/EnitityExtraction/DataImporter';
2627

2728
const GCSModal = lazy(() => import('../DataSources/GCS/GCSModal'));
2829
const S3Modal = lazy(() => import('../DataSources/AWS/S3Modal'));
@@ -193,6 +194,11 @@ const PageLayout: React.FC = () => {
193194
allPatterns,
194195
selectedNodes,
195196
selectedRels,
197+
dataImporterSchemaDialog,
198+
setDataImporterSchemaDialog,
199+
setImporterPattern,
200+
setImporterNodes,
201+
setImporterRels,
196202
} = useFileContext();
197203
const navigate = useNavigate();
198204
const { user, isAuthenticated } = useAuth0();
@@ -465,6 +471,45 @@ const PageLayout: React.FC = () => {
465471
[]
466472
);
467473

474+
const handleImporterApply = useCallback(
475+
(
476+
newPatterns: string[],
477+
nodes: OptionType[],
478+
rels: OptionType[],
479+
updatedSource: OptionType[],
480+
updatedTarget: OptionType[],
481+
updatedType: OptionType[]
482+
) => {
483+
setImporterPattern((prevPatterns: string[]) => {
484+
const uniquePatterns = Array.from(new Set([...newPatterns, ...prevPatterns]));
485+
return uniquePatterns;
486+
});
487+
setCombinedPatternsVal((prevPatterns: string[]) => {
488+
const uniquePatterns = Array.from(new Set([...newPatterns, ...prevPatterns]));
489+
return uniquePatterns;
490+
});
491+
setDataImporterSchemaDialog({
492+
triggeredFrom: 'importerSchemaApply',
493+
show: true,
494+
});
495+
setSchemaView('importer');
496+
setImporterNodes(nodes);
497+
setCombinedNodesVal((prevNodes: OptionType[]) => {
498+
const combined = [...nodes, ...prevNodes];
499+
return deduplicateNodeByValue(combined);
500+
});
501+
setImporterRels(rels);
502+
setCombinedRelsVal((prevRels: OptionType[]) => {
503+
const combined = [...rels, ...prevRels];
504+
return deduplicateByFullPattern(combined);
505+
});
506+
localStorage.setItem(LOCAL_KEYS.source, JSON.stringify(updatedSource));
507+
localStorage.setItem(LOCAL_KEYS.type, JSON.stringify(updatedType));
508+
localStorage.setItem(LOCAL_KEYS.target, JSON.stringify(updatedTarget));
509+
},
510+
[]
511+
);
512+
468513
const openPredefinedSchema = useCallback(() => {
469514
setPredefinedSchemaDialog({ triggeredFrom: 'predefinedDialog', show: true });
470515
}, []);
@@ -477,6 +522,10 @@ const PageLayout: React.FC = () => {
477522
setShowTextFromSchemaDialog({ triggeredFrom: 'schemadialog', show: true });
478523
}, []);
479524

525+
const openDataImporterSchema = useCallback(() => {
526+
setShowTextFromSchemaDialog({ triggeredFrom: 'schemadialog', show: true });
527+
}, []);
528+
480529
const openChatBot = useCallback(() => setShowChatBot(true), []);
481530

482531
return (
@@ -565,6 +614,20 @@ const PageLayout: React.FC = () => {
565614
}}
566615
onApply={handlePredinedApply}
567616
></PredefinedSchemaDialog>
617+
<DataImporterSchemaDailog
618+
open={dataImporterSchemaDialog.show}
619+
onClose={() => {
620+
setDataImporterSchemaDialog({ triggeredFrom: '', show: false });
621+
switch (dataImporterSchemaDialog.triggeredFrom) {
622+
case 'enhancementtab':
623+
toggleEnhancementDialog();
624+
break;
625+
default:
626+
break;
627+
}
628+
}}
629+
onApply={handleImporterApply}
630+
></DataImporterSchemaDailog>
568631
{isLargeDesktop ? (
569632
<div
570633
className={`layout-wrapper ${!isLeftExpanded ? 'drawerdropzoneclosed' : ''} ${
@@ -596,6 +659,7 @@ const PageLayout: React.FC = () => {
596659
openTextSchema={openTextSchema}
597660
openLoadSchema={openLoadSchema}
598661
openPredefinedSchema={openPredefinedSchema}
662+
openDataImporterSchema={openDataImporterSchema}
599663
showEnhancementDialog={showEnhancementDialog}
600664
toggleEnhancementDialog={toggleEnhancementDialog}
601665
setOpenConnection={setOpenConnection}
@@ -670,6 +734,7 @@ const PageLayout: React.FC = () => {
670734
openTextSchema={openTextSchema}
671735
openLoadSchema={openLoadSchema}
672736
openPredefinedSchema={openPredefinedSchema}
737+
openDataImporterSchema={openDataImporterSchema}
673738
showEnhancementDialog={showEnhancementDialog}
674739
toggleEnhancementDialog={toggleEnhancementDialog}
675740
setOpenConnection={setOpenConnection}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { Button, Dialog } from '@neo4j-ndl/react';
2+
import { useState } from 'react';
3+
import { OptionType, TupleType } from '../../../../types';
4+
import { extractOptions, updateSourceTargetTypeOptions } from '../../../../utils/Utils';
5+
import { useFileContext } from '../../../../context/UsersFiles';
6+
import ImporterInput from './ImporterInput';
7+
import SchemaViz from '../../../Graph/SchemaViz';
8+
import PatternContainer from './PatternContainer';
9+
import UploadJsonData from './UploadJsonData';
10+
11+
interface DataImporterDialogProps {
12+
open: boolean;
13+
onClose: () => void;
14+
onApply: (
15+
patterns: string[],
16+
nodeLabels: OptionType[],
17+
relationshipLabels: OptionType[],
18+
updatedSource: OptionType[],
19+
updatedTarget: OptionType[],
20+
updatedType: OptionType[]
21+
) => void;
22+
}
23+
24+
const DataImporterSchemaDailog = ({ open, onClose, onApply }: DataImporterDialogProps) => {
25+
const {
26+
importerPattern,
27+
setImporterPattern,
28+
importerNodes,
29+
setImporterNodes,
30+
importerRels,
31+
setImporterRels,
32+
sourceOptions,
33+
setSourceOptions,
34+
targetOptions,
35+
setTargetOptions,
36+
typeOptions,
37+
setTypeOptions,
38+
} = useFileContext();
39+
40+
const [openGraphView, setOpenGraphView] = useState<boolean>(false);
41+
const [viewPoint, setViewPoint] = useState<string>('');
42+
const handleCancel = () => {
43+
onClose();
44+
setImporterPattern([]);
45+
setImporterNodes([]);
46+
setImporterRels([]);
47+
};
48+
49+
const handleImporterCheck = async () => {
50+
const [newSourceOptions, newTargetOptions, newTypeOptions] = await updateSourceTargetTypeOptions({
51+
patterns: importerPattern.map((label) => ({ label, value: label })),
52+
currentSourceOptions: sourceOptions,
53+
currentTargetOptions: targetOptions,
54+
currentTypeOptions: typeOptions,
55+
setSourceOptions,
56+
setTargetOptions,
57+
setTypeOptions,
58+
});
59+
onApply(importerPattern, importerNodes, importerRels, newSourceOptions, newTargetOptions, newTypeOptions);
60+
onClose();
61+
};
62+
63+
const handleRemovePattern = (patternToRemove: string) => {
64+
const updatedPatterns = importerPattern.filter((p) => p !== patternToRemove);
65+
if (updatedPatterns.length === 0) {
66+
setImporterPattern([]);
67+
setImporterNodes([]);
68+
setImporterRels([]);
69+
return;
70+
}
71+
const updatedTuples: TupleType[] = updatedPatterns
72+
.map((item: string) => {
73+
const matchResult = item.match(/^(.+?)-\[:([A-Z_]+)\]->(.+)$/);
74+
if (matchResult) {
75+
const [source, rel, target] = matchResult.slice(1).map((s) => s.trim());
76+
return {
77+
value: `${source},${rel},${target}`,
78+
label: `${source} -[:${rel}]-> ${target}`,
79+
source,
80+
target,
81+
type: rel,
82+
};
83+
}
84+
return null;
85+
})
86+
.filter(Boolean) as TupleType[];
87+
const { nodeLabelOptions, relationshipTypeOptions } = extractOptions(updatedTuples);
88+
setImporterPattern(updatedPatterns);
89+
setImporterNodes(nodeLabelOptions);
90+
setImporterRels(relationshipTypeOptions);
91+
};
92+
93+
const handleSchemaView = () => {
94+
setOpenGraphView(true);
95+
setViewPoint('showSchemaView');
96+
};
97+
98+
return (
99+
<>
100+
<Dialog isOpen={open} onClose={handleCancel}>
101+
<Dialog.Header>Entity Graph Extraction Settings</Dialog.Header>
102+
<Dialog.Content className='n-flex n-flex-col n-gap-token-6 p-6'>
103+
<ImporterInput />
104+
<UploadJsonData
105+
onSchemaExtracted={({ nodeLabels, relationshipTypes, relationshipObjectTypes, nodeObjectTypes }) => {
106+
const nodeLabelMap = Object.fromEntries(nodeLabels.map((n) => [n.$id, n.token]));
107+
const relTypeMap = Object.fromEntries(relationshipTypes.map((r) => [r.$id, r.token]));
108+
const nodeIdToLabel: Record<string, string> = {};
109+
nodeObjectTypes.forEach((nodeObj: any) => {
110+
const labelRef = nodeObj.labels?.[0]?.$ref;
111+
if (labelRef && nodeLabelMap[labelRef.slice(1)]) {
112+
nodeIdToLabel[nodeObj.$id] = nodeLabelMap[labelRef.slice(1)];
113+
}
114+
});
115+
116+
const patterns = relationshipObjectTypes.map((relObj) => {
117+
const fromId = relObj.from.$ref.slice(1);
118+
const toId = relObj.to.$ref.slice(1);
119+
const relId = relObj.type.$ref.slice(1);
120+
const fromLabel = nodeIdToLabel[fromId] || 'source';
121+
const toLabel = nodeIdToLabel[toId] || 'target';
122+
const relLabel = relTypeMap[relId] || 'type';
123+
const pattern = `${fromLabel} -[:${relLabel}]-> ${toLabel}`;
124+
return pattern;
125+
});
126+
127+
const importerTuples = patterns
128+
.map((p) => {
129+
const match = p.match(/^(.+?) -\[:(.+?)\]-> (.+)$/);
130+
if (!match) {
131+
return null;
132+
}
133+
const [_, source, type, target] = match;
134+
return {
135+
label: `${source} -[:${type}]-> ${target}`,
136+
value: `${source},${type},${target}`,
137+
source,
138+
target,
139+
type,
140+
};
141+
})
142+
.filter(Boolean) as TupleType[];
143+
const { nodeLabelOptions, relationshipTypeOptions } = extractOptions(importerTuples);
144+
setImporterNodes(nodeLabelOptions);
145+
setImporterRels(relationshipTypeOptions);
146+
setImporterPattern(patterns);
147+
}}
148+
/>
149+
<PatternContainer
150+
pattern={importerPattern}
151+
handleRemove={handleRemovePattern}
152+
handleSchemaView={handleSchemaView}
153+
nodes={importerNodes}
154+
rels={importerRels}
155+
/>
156+
<Dialog.Actions className='n-flex n-justify-end n-gap-token-4 pt-4'>
157+
<Button onClick={handleCancel} isDisabled={importerPattern.length === 0}>
158+
Cancel
159+
</Button>
160+
<Button onClick={handleImporterCheck} isDisabled={importerPattern.length === 0}>
161+
Apply
162+
</Button>
163+
</Dialog.Actions>
164+
</Dialog.Content>
165+
</Dialog>
166+
{openGraphView && (
167+
<SchemaViz
168+
open={openGraphView}
169+
setGraphViewOpen={setOpenGraphView}
170+
viewPoint={viewPoint}
171+
nodeValues={importerNodes ?? []}
172+
relationshipValues={importerRels ?? []}
173+
/>
174+
)}
175+
</>
176+
);
177+
};
178+
179+
export default DataImporterSchemaDailog;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useState, useCallback, KeyboardEvent, ChangeEvent, FocusEvent } from 'react';
2+
import { Box, TextInput, Button } from '@neo4j-ndl/react';
3+
import { importerValidation } from '../../../../utils/Utils';
4+
5+
const ImporterInput = () => {
6+
const [value, setValue] = useState<string>('');
7+
const [isValid, setIsValid] = useState<boolean>(false);
8+
const [isFocused, setIsFocused] = useState<boolean>(false);
9+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
10+
const newValue = e.target.value;
11+
setValue(newValue);
12+
setIsValid(importerValidation(newValue));
13+
};
14+
const handleBlur = () => {
15+
setIsFocused(false);
16+
setIsValid(importerValidation(value));
17+
};
18+
const handleFocus = () => {
19+
setIsFocused(true);
20+
};
21+
const handleSubmit = useCallback(() => {
22+
if (importerValidation(value)) {
23+
window.open(value, '_blank');
24+
}
25+
}, [value]);
26+
const handleCancel = () => {
27+
setValue('');
28+
setIsValid(false);
29+
};
30+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
31+
if (e.code === 'Enter' && isValid) {
32+
handleSubmit();
33+
}
34+
};
35+
const isEmpty = value.trim() === '';
36+
return (
37+
<Box>
38+
<div className='w-full inline-block mb-2'>
39+
<TextInput
40+
htmlAttributes={{
41+
onBlur: handleBlur,
42+
onFocus: handleFocus,
43+
onKeyDown: handleKeyDown,
44+
placeholder: 'Enter URL to import...',
45+
'aria-label': 'Importer URL Input',
46+
}}
47+
value={value}
48+
label='Import Link'
49+
isFluid
50+
isRequired
51+
onChange={handleChange}
52+
errorText={value && !isValid && isFocused ? 'Please enter a valid URL' : ''}
53+
/>
54+
</div>
55+
<div className='w-full flex justify-end gap-2'>
56+
<Button onClick={handleCancel} isDisabled={isEmpty} size='medium'>
57+
Cancel
58+
</Button>
59+
<Button onClick={handleSubmit} isDisabled={!isValid} size='medium'>
60+
Apply
61+
</Button>
62+
</div>
63+
</Box>
64+
);
65+
};
66+
67+
export default ImporterInput;

0 commit comments

Comments
 (0)