Skip to content

Commit 8a85bb8

Browse files
committed
feat: Integrate flamapy.conf
closes #33
1 parent 2ec38a4 commit 8a85bb8

File tree

12 files changed

+531
-84
lines changed

12 files changed

+531
-84
lines changed

public/flamapy/flamapy.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Flamapy {
2929
await micropip.install("flamapy/uvlparser-2.0.1-py3-none-any.whl", deps=False)
3030
await micropip.install("flamapy/afmparser-1.0.3-py3-none-any.whl", deps=False)
3131
await micropip.install("flamapy/antlr4_python3_runtime-4.13.1-py3-none-any.whl", deps=False)
32+
await micropip.install("flamapy/flamapy_configurator-2.0.1-py3-none-any.whl", deps=False)
3233
`);
3334
await pyodideInstance.runPythonAsync(await pythonFile.text());
3435
pyodideInstance.FS.mkdir("export");
@@ -122,4 +123,23 @@ class Flamapy {
122123
}
123124
}
124125
}
126+
127+
async startConfigurator() {
128+
const result = await this.pyodide.runPythonAsync(`
129+
start_configurator()`);
130+
return JSON.parse(result);
131+
}
132+
133+
async answerQuestion(answer) {
134+
this.pyodide.globals.set("answer", answer);
135+
const result = await this.pyodide.runPythonAsync(`
136+
answer_question(answer)`);
137+
return JSON.parse(result);
138+
}
139+
140+
async undoAnswer() {
141+
const result = await this.pyodide.runPythonAsync(`
142+
undo_answer()`);
143+
return JSON.parse(result);
144+
}
125145
}
7.38 KB
Binary file not shown.

public/flamapy/flamapy_ide.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
from flamapy.core.discover import DiscoverMetamodels
99
from flamapy.metamodels.fm_metamodel.transformations import GlencoeReader, AFMReader, FeatureIDEReader, JSONReader, XMLReader, UVLReader, GlencoeWriter
1010
from flamapy.metamodels.configuration_metamodel.models import Configuration
11+
from flamapy.metamodels.configurator_metamodel.transformation import FmToConfigurator
1112
from collections import defaultdict
1213

1314
fm = None
15+
configurator = None
16+
1417

1518
# Custom error listener
1619
class CustomErrorListener(ErrorListener):
@@ -211,4 +214,30 @@ def execute_configurator_operation(name: str, conf):
211214
result = operation.get_result()
212215
if type(result) is list:
213216
return [str(conf) for conf in result]
214-
return result
217+
return result
218+
219+
def start_configurator():
220+
global configurator
221+
configurator = FmToConfigurator(fm.fm_model).transform()
222+
configurator.start()
223+
224+
return json.dumps(configurator.get_current_status())
225+
226+
def answer_question(answer):
227+
valid = configurator.answer_question(answer)
228+
229+
result = dict()
230+
result['valid'] = valid
231+
if valid:
232+
if configurator.next_question():
233+
result['nextQuestion'] = configurator.get_current_status()
234+
else:
235+
result['configuration'] = configurator._get_configuration()
236+
else:
237+
result['contradiction'] = {'msg': 'The selected choice is incompatible with the model definition. Please choose another option.'}
238+
239+
return json.dumps(result)
240+
241+
def undo_answer():
242+
configurator.previous_question()
243+
return json.dumps(configurator.get_current_status())

public/webworker.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ self.onmessage = async (event) => {
3333
results = await self.flamapy.getFeatures();
3434
} else if (action === "executeActionWithConf") {
3535
results = await self.flamapy.executeActionWithConf(data);
36+
} else if (action === "startConfigurator") {
37+
results = await self.flamapy.startConfigurator();
38+
} else if (action === "answerQuestion") {
39+
results = await self.flamapy.answerQuestion(data);
40+
} else if (action === "undoAnswer") {
41+
results = await self.flamapy.undoAnswer();
3642
}
3743

3844
self.postMessage({ results, action });

src/components/Configuration.jsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
function Configuration({ configuration }) {
2+
const categorizedConfig = {
3+
selected: [],
4+
deselected: [],
5+
undecided: [],
6+
};
7+
8+
Object.entries(configuration).forEach(([feature, status]) => {
9+
if (status === true) {
10+
categorizedConfig.selected.push(feature);
11+
} else if (status === false) {
12+
categorizedConfig.deselected.push(feature);
13+
} else {
14+
categorizedConfig.undecided.push(feature);
15+
}
16+
});
17+
18+
return (
19+
<div className="bg-white w-full rounded-xl p-4 text-xl shadow-md overflow-auto">
20+
<h2 className="text-2xl font-bold text-gray-800 mb-4">
21+
Feature Configuration
22+
</h2>
23+
<div className="text-lg">
24+
<div className="mb-2">
25+
<h3 className="font-semibold text-green-600">Selected Features:</h3>
26+
<ul className="list-disc list-inside text-black">
27+
{categorizedConfig.selected.length > 0 ? (
28+
categorizedConfig.selected.map((feature, index) => (
29+
<li key={index}>{feature}</li>
30+
))
31+
) : (
32+
<p className="text-gray-500">None</p>
33+
)}
34+
</ul>
35+
</div>
36+
37+
<div className="mb-2">
38+
<h3 className="font-semibold text-red-600">Deselected Features:</h3>
39+
<ul className="list-disc list-inside text-black">
40+
{categorizedConfig.deselected.length > 0 ? (
41+
categorizedConfig.deselected.map((feature, index) => (
42+
<li key={index}>{feature}</li>
43+
))
44+
) : (
45+
<p className="text-gray-500">None</p>
46+
)}
47+
</ul>
48+
</div>
49+
50+
<div>
51+
<h3 className="font-semibold text-yellow-600">Undecided Features:</h3>
52+
<ul className="list-disc list-inside text-black">
53+
{categorizedConfig.undecided.length > 0 ? (
54+
categorizedConfig.undecided.map((feature, index) => (
55+
<li key={index}>{feature}</li>
56+
))
57+
) : (
58+
<p className="text-gray-500">None</p>
59+
)}
60+
</ul>
61+
</div>
62+
</div>
63+
</div>
64+
);
65+
}
66+
67+
export default Configuration;

src/components/CustomButton.jsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* eslint-disable react/prop-types */
2+
function CustomButton({ active = true, onClick, children }) {
3+
const baseClasses =
4+
"w-max text-white py-2 px-4 m-2 rounded shadow-lg transition-colors duration-200";
5+
const activeClasses = "bg-[#356C99] hover:bg-[#0D486C]";
6+
const disabledClasses = "bg-gray-400 cursor-not-allowed";
7+
8+
return (
9+
<button
10+
className={`${baseClasses} ${active ? activeClasses : disabledClasses}`}
11+
onClick={active ? onClick : undefined}
12+
disabled={!active}
13+
>
14+
{children}
15+
</button>
16+
);
17+
}
18+
19+
export default CustomButton;

src/components/DropdownMenu.jsx

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,63 @@
11
/* eslint-disable react/prop-types */
2-
import { useState } from "react";
2+
import { useState, useRef, useEffect } from "react";
33

4-
const DropdownMenu = ({ options, buttonLabel, executeAction }) => {
4+
const DropdownMenu = ({
5+
options,
6+
buttonLabel,
7+
executeAction,
8+
className = "w-full bg-[#356C99] text-white py-2 px-4 rounded shadow-lg flex justify-between items-center",
9+
}) => {
510
const [isOpen, setIsOpen] = useState(false);
11+
const dropdownRef = useRef(null);
612

713
const handleToggle = () => {
8-
setIsOpen(!isOpen);
14+
setIsOpen((prev) => !prev);
915
};
1016

1117
const handleAction = async (action) => {
1218
await executeAction(action);
1319
setIsOpen(false);
1420
};
1521

22+
// Close dropdown when clicking outside
23+
useEffect(() => {
24+
const handleClickOutside = (event) => {
25+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
26+
setIsOpen(false);
27+
}
28+
};
29+
30+
document.addEventListener("mousedown", handleClickOutside);
31+
return () => {
32+
document.removeEventListener("mousedown", handleClickOutside);
33+
};
34+
}, []);
35+
1636
return (
17-
<div className="relative inline-block w-max">
37+
<div ref={dropdownRef} className="relative inline-block w-max">
1838
<button
1939
onClick={handleToggle}
20-
className="w-full bg-[#356C99] text-white py-2 px-4 rounded shadow-lg flex justify-between items-center"
40+
className={className}
41+
aria-haspopup="true"
42+
aria-expanded={isOpen}
2143
>
2244
{buttonLabel}
23-
<span className="ml-2">&#9660;</span> {/* Dropdown arrow */}
45+
<span className="ml-2">&#9660;</span>
2446
</button>
47+
2548
{isOpen && (
26-
<div className="absolute w-full mt-1 bg-white border border-[#356C99] rounded-lg shadow-lg z-10">
49+
<div
50+
className="absolute w-full mt-1 bg-white border border-[#356C99] rounded-lg shadow-lg z-10"
51+
role="menu"
52+
>
2753
{options?.length ? (
2854
options.map((option) => (
2955
<div
3056
key={option.label}
3157
onClick={() => handleAction(option)}
32-
className="w-full text-left py-2 px-4 cursor-pointer hover:bg-[#0D486C] text-[#356C99] hover:text-white"
58+
tabIndex={0}
59+
className="w-full text-left py-2 px-4 cursor-pointer hover:bg-[#0D486C] text-[#356C99] hover:text-white focus:outline-none focus:bg-[#0D486C] focus:text-white"
60+
role="menuitem"
3361
>
3462
{option.label}
3563
</div>

0 commit comments

Comments
 (0)