Skip to content

Commit 9a968d9

Browse files
committed
Add drag and drop for landscape editor
1 parent 91d893e commit 9a968d9

10 files changed

Lines changed: 1348 additions & 23 deletions

File tree

src/frontend/App.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ function App() {
4444
setError(null);
4545
};
4646

47+
const handleError = (error: string) => {
48+
setError(error);
49+
setSuccess(null);
50+
// Auto-dismiss error after 5 seconds
51+
setTimeout(() => setError(null), 5000);
52+
};
53+
4754
return (
4855
<div className="min-h-screen text-primary">
4956
<div className="container mx-auto px-4 py-8 max-w-6xl">
@@ -56,8 +63,12 @@ function App() {
5663
</div>
5764
)}
5865

59-
{/* Error message - inline at top of content */}
60-
{error && <div className="mb-6 p-4 bg-danger-light text-danger rounded-lg border border-danger">{error}</div>}
66+
{/* Error toast notification in bottom right corner */}
67+
{error && (
68+
<div className="fixed bottom-8 right-4 z-40 p-4 bg-danger-light text-danger rounded-lg border border-danger shadow-lg max-w-sm transition-all duration-300 ease-in-out">
69+
{error}
70+
</div>
71+
)}
6172

6273
<header className="mb-8 text-center">
6374
<h1 className="text-5xl md:text-6xl font-bold text-primary mb-4">Trace Generator</h1>
@@ -78,7 +89,7 @@ function App() {
7889
</p>
7990
<LandscapeGenerationForm
8091
onLandscapeGenerated={handleLandscapeGenerated}
81-
onError={setError}
92+
onError={handleError}
8293
resetButtonRef={landscapeFormResetRef}
8394
/>
8495
</div>
@@ -90,10 +101,14 @@ function App() {
90101
<div className="material-card p-6 md:p-8">
91102
<h2 className="text-3xl font-bold text-primary mb-3">Landscape Editor</h2>
92103
<p className="text-muted mb-6">
93-
Edit the landscape structure. You can add, rename, or delete entities (applications, packages, classes,
94-
and methods).
104+
Edit the landscape structure. You can add, rename, or delete entities. Drag and drop packages, classes,
105+
and functions.
95106
</p>
96-
<LandscapeEditor landscape={landscape} onLandscapeUpdated={handleLandscapeUpdated} onError={setError} />
107+
<LandscapeEditor
108+
landscape={landscape}
109+
onLandscapeUpdated={handleLandscapeUpdated}
110+
onError={handleError}
111+
/>
97112
</div>
98113
</section>
99114
)}
@@ -108,10 +123,7 @@ function App() {
108123
<h2 className="text-3xl font-bold text-primary mb-3">Step 2: Generate Traces</h2>
109124
<p className="text-muted mb-6">Generate random traces based on the current landscape.</p>
110125
<TraceGenerationForm
111-
onError={(err) => {
112-
setError(err);
113-
setSuccess(null);
114-
}}
126+
onError={handleError}
115127
onSuccess={(msg) => {
116128
setSuccess(msg);
117129
setError(null);

src/frontend/components/LandscapeEditor.tsx

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,320 @@ export function LandscapeEditor({ landscape, onLandscapeUpdated, onError }: Land
541541
}
542542
};
543543

544+
const movePackage = (
545+
sourceAppIdx: number,
546+
sourcePackageName: string,
547+
targetAppIdx: number,
548+
targetPackageName: string | null
549+
) => {
550+
const updated = [...localLandscape];
551+
const sourceApp = updated[sourceAppIdx];
552+
const targetApp = updated[targetAppIdx];
553+
554+
if (!sourceApp || !targetApp) {
555+
onError('Invalid app index');
556+
return;
557+
}
558+
559+
let sourcePackage: CleanedPackage | null = null;
560+
let sourceParent: CleanedPackage | null = null;
561+
let sourceParentArray: CleanedPackage[] | null = null;
562+
563+
// Find source package and its parent
564+
const findSource = (pkg: CleanedPackage, parent: CleanedPackage | null, parentArray: CleanedPackage[]): boolean => {
565+
if (pkg.name === sourcePackageName) {
566+
sourcePackage = pkg;
567+
sourceParent = parent;
568+
sourceParentArray = parentArray;
569+
return true;
570+
}
571+
for (let i = 0; i < pkg.subpackages.length; i++) {
572+
if (findSource(pkg.subpackages[i], pkg, pkg.subpackages)) {
573+
return true;
574+
}
575+
}
576+
return false;
577+
};
578+
579+
for (let i = 0; i < sourceApp.rootPackages.length; i++) {
580+
if (findSource(sourceApp.rootPackages[i], null, sourceApp.rootPackages)) {
581+
break;
582+
}
583+
}
584+
585+
if (!sourcePackage || !sourceParentArray) {
586+
onError(`Source package "${sourcePackageName}" not found`);
587+
return;
588+
}
589+
590+
// Store references after null check for proper type narrowing
591+
const packageToMove = sourcePackage;
592+
const parentArray: CleanedPackage[] = sourceParentArray;
593+
594+
// Prevent moving package into itself or its descendants (only if same app)
595+
if (sourceAppIdx === targetAppIdx && targetPackageName) {
596+
const targetPkg = findPackage(targetApp, targetPackageName);
597+
if (targetPkg) {
598+
// Check if targetPkg is the source package itself
599+
if (targetPkg === packageToMove) {
600+
onError('Cannot move package into itself');
601+
return;
602+
}
603+
// Check if targetPkg is a descendant (subpackage) of the source package
604+
// We need to traverse the source package's subpackages to see if targetPkg is found
605+
const isDescendantOfSource = (pkg: CleanedPackage): boolean => {
606+
if (pkg === targetPkg) return true;
607+
return pkg.subpackages.some(isDescendantOfSource);
608+
};
609+
if (isDescendantOfSource(packageToMove)) {
610+
onError('Cannot move package into its own subpackage');
611+
return;
612+
}
613+
}
614+
}
615+
616+
// Remove from source
617+
const sourceIndex = parentArray.findIndex((p: CleanedPackage) => p.name === sourcePackageName);
618+
if (sourceIndex === -1) {
619+
onError(`Source package "${sourcePackageName}" not found in parent`);
620+
return;
621+
}
622+
parentArray.splice(sourceIndex, 1);
623+
624+
// Add to target
625+
if (targetPackageName === null) {
626+
// Move to root of target app
627+
targetApp.rootPackages.push(packageToMove);
628+
} else {
629+
const targetPkg = findPackage(targetApp, targetPackageName);
630+
if (!targetPkg) {
631+
onError(`Target package "${targetPackageName}" not found`);
632+
return;
633+
}
634+
targetPkg.subpackages.push(packageToMove);
635+
}
636+
637+
// Update the tree structure for source app
638+
const updateSourceTree = (p: CleanedPackage): CleanedPackage => {
639+
if (sourceParent && p.name === sourceParent.name) {
640+
return { ...p, subpackages: [...p.subpackages] };
641+
}
642+
return { ...p, subpackages: p.subpackages.map(updateSourceTree) };
643+
};
644+
645+
updated[sourceAppIdx] = {
646+
...sourceApp,
647+
rootPackages: sourceApp.rootPackages.map(updateSourceTree),
648+
};
649+
650+
// Update the tree structure for target app
651+
if (targetPackageName) {
652+
const updateTargetTree = (p: CleanedPackage): CleanedPackage => {
653+
if (p.name === targetPackageName) {
654+
return { ...p, subpackages: [...p.subpackages] };
655+
}
656+
return { ...p, subpackages: p.subpackages.map(updateTargetTree) };
657+
};
658+
updated[targetAppIdx] = {
659+
...targetApp,
660+
rootPackages: targetApp.rootPackages.map(updateTargetTree),
661+
};
662+
}
663+
664+
updateLocalLandscape(updated);
665+
};
666+
667+
const moveClass = (sourceAppIdx: number, className: string, targetAppIdx: number, targetPackageName: string) => {
668+
const updated = [...localLandscape];
669+
const sourceApp = updated[sourceAppIdx];
670+
const targetApp = updated[targetAppIdx];
671+
672+
if (!sourceApp || !targetApp) {
673+
onError('Invalid app index');
674+
return;
675+
}
676+
677+
const sourceClass = findClass(sourceApp, className);
678+
if (!sourceClass) {
679+
onError(`Class "${className}" not found`);
680+
return;
681+
}
682+
683+
const targetPkg = findPackage(targetApp, targetPackageName);
684+
if (!targetPkg) {
685+
onError(`Target package "${targetPackageName}" not found`);
686+
return;
687+
}
688+
689+
// Update parentAppName if moving to different app
690+
const updatedClass =
691+
sourceAppIdx !== targetAppIdx ? { ...sourceClass, parentAppName: targetApp.name } : sourceClass;
692+
693+
// Remove from source package and add to target package
694+
const removeClass = (p: CleanedPackage): CleanedPackage => ({
695+
...p,
696+
classes: p.classes.filter((c) => c.identifier !== className),
697+
subpackages: p.subpackages.map(removeClass),
698+
});
699+
700+
// Update target app tree structure - add class to target package
701+
const updateTargetTree = (p: CleanedPackage): CleanedPackage => {
702+
if (p.name === targetPackageName) {
703+
return { ...p, classes: [...p.classes, updatedClass] };
704+
}
705+
return { ...p, subpackages: p.subpackages.map(updateTargetTree) };
706+
};
707+
708+
if (sourceAppIdx === targetAppIdx) {
709+
// Same app: combine both operations
710+
const combinedUpdate = (p: CleanedPackage): CleanedPackage => {
711+
// First remove the class
712+
let updated = {
713+
...p,
714+
classes: p.classes.filter((c) => c.identifier !== className),
715+
subpackages: p.subpackages.map(combinedUpdate),
716+
};
717+
// Then add to target if this is the target package
718+
if (p.name === targetPackageName) {
719+
updated = { ...updated, classes: [...updated.classes, updatedClass] };
720+
}
721+
return updated;
722+
};
723+
724+
updated[sourceAppIdx] = {
725+
...sourceApp,
726+
rootPackages: sourceApp.rootPackages.map(combinedUpdate),
727+
classes: sourceApp.classes.filter((c) => c.identifier !== className),
728+
};
729+
} else {
730+
// Different apps: update separately
731+
updated[sourceAppIdx] = {
732+
...sourceApp,
733+
rootPackages: sourceApp.rootPackages.map(removeClass),
734+
classes: sourceApp.classes.filter((c) => c.identifier !== className),
735+
};
736+
737+
updated[targetAppIdx] = {
738+
...targetApp,
739+
rootPackages: targetApp.rootPackages.map(updateTargetTree),
740+
classes: [...targetApp.classes, updatedClass],
741+
};
742+
}
743+
744+
updateLocalLandscape(updated);
745+
};
746+
747+
const moveMethod = (
748+
sourceAppIdx: number,
749+
className: string,
750+
methodName: string,
751+
targetAppIdx: number,
752+
targetClassName: string
753+
) => {
754+
const updated = [...localLandscape];
755+
const sourceApp = updated[sourceAppIdx];
756+
const targetApp = updated[targetAppIdx];
757+
758+
if (!sourceApp || !targetApp) {
759+
onError('Invalid app index');
760+
return;
761+
}
762+
763+
const sourceClass = findClass(sourceApp, className);
764+
const targetClass = findClass(targetApp, targetClassName);
765+
766+
if (!sourceClass) {
767+
onError(`Source class "${className}" not found`);
768+
return;
769+
}
770+
if (!targetClass) {
771+
onError(`Target class "${targetClassName}" not found`);
772+
return;
773+
}
774+
775+
const method = sourceClass.methods.find((m) => m.identifier === methodName);
776+
if (!method) {
777+
onError(`Method "${methodName}" not found in class "${className}"`);
778+
return;
779+
}
780+
781+
// Remove from source class
782+
const removeMethod = (p: CleanedPackage): CleanedPackage => ({
783+
...p,
784+
classes: p.classes.map((c) =>
785+
c.identifier === className
786+
? {
787+
...c,
788+
methods: c.methods.filter((m) => m.identifier !== methodName),
789+
}
790+
: c
791+
),
792+
subpackages: p.subpackages.map(removeMethod),
793+
});
794+
795+
// Add to target class
796+
const addMethod = (p: CleanedPackage): CleanedPackage => ({
797+
...p,
798+
classes: p.classes.map((c) =>
799+
c.identifier === targetClassName
800+
? {
801+
...c,
802+
methods: [...c.methods, method],
803+
}
804+
: c
805+
),
806+
subpackages: p.subpackages.map(addMethod),
807+
});
808+
809+
if (sourceAppIdx === targetAppIdx) {
810+
// Same app: combine both operations
811+
updated[sourceAppIdx] = {
812+
...sourceApp,
813+
rootPackages: sourceApp.rootPackages.map((pkg) => {
814+
const afterRemove = removeMethod(pkg);
815+
return addMethod(afterRemove);
816+
}),
817+
classes: sourceApp.classes.map((c) => {
818+
if (c.identifier === className) {
819+
return { ...c, methods: c.methods.filter((m) => m.identifier !== methodName) };
820+
}
821+
if (c.identifier === targetClassName) {
822+
return { ...c, methods: [...c.methods, method] };
823+
}
824+
return c;
825+
}),
826+
methods: sourceApp.methods.map((m) => (m.identifier === methodName ? method : m)),
827+
};
828+
} else {
829+
// Different apps: update separately
830+
updated[sourceAppIdx] = {
831+
...sourceApp,
832+
rootPackages: sourceApp.rootPackages.map(removeMethod),
833+
classes: sourceApp.classes.map((c) => {
834+
if (c.identifier === className) {
835+
return { ...c, methods: c.methods.filter((m) => m.identifier !== methodName) };
836+
}
837+
return c;
838+
}),
839+
methods: sourceApp.methods.filter((m) => m.identifier !== methodName),
840+
};
841+
842+
updated[targetAppIdx] = {
843+
...targetApp,
844+
rootPackages: targetApp.rootPackages.map(addMethod),
845+
classes: targetApp.classes.map((c) => {
846+
if (c.identifier === targetClassName) {
847+
return { ...c, methods: [...c.methods, method] };
848+
}
849+
return c;
850+
}),
851+
methods: [...targetApp.methods, method],
852+
};
853+
}
854+
855+
updateLocalLandscape(updated);
856+
};
857+
544858
const addApp = () => {
545859
const appName = prompt('Enter app name:', 'newapp');
546860
if (appName && appName.trim() !== '') {
@@ -595,6 +909,9 @@ export function LandscapeEditor({ landscape, onLandscapeUpdated, onError }: Land
595909
deletePackage,
596910
deleteClass,
597911
deleteMethod,
912+
movePackage,
913+
moveClass,
914+
moveMethod,
598915
};
599916

600917
return (

0 commit comments

Comments
 (0)