aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src')
-rw-r--r--subprojects/frontend/src/ModelWorkArea.tsx198
-rw-r--r--subprojects/frontend/src/WorkArea.tsx12
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts85
-rw-r--r--subprojects/frontend/src/editor/GenerateButton.tsx20
-rw-r--r--subprojects/frontend/src/editor/GeneratedModelStore.ts50
-rw-r--r--subprojects/frontend/src/graph/GraphArea.tsx12
-rw-r--r--subprojects/frontend/src/graph/GraphPane.tsx10
-rw-r--r--subprojects/frontend/src/index.tsx102
-rw-r--r--subprojects/frontend/src/table/RelationGrid.tsx109
-rw-r--r--subprojects/frontend/src/table/TableArea.tsx105
-rw-r--r--subprojects/frontend/src/table/TablePane.tsx9
-rw-r--r--subprojects/frontend/src/xtext/ModelGenerationService.ts46
-rw-r--r--subprojects/frontend/src/xtext/UpdateService.ts39
-rw-r--r--subprojects/frontend/src/xtext/XtextClient.ts21
-rw-r--r--subprojects/frontend/src/xtext/xtextMessages.ts1
-rw-r--r--subprojects/frontend/src/xtext/xtextServiceResults.ts24
16 files changed, 679 insertions, 164 deletions
diff --git a/subprojects/frontend/src/ModelWorkArea.tsx b/subprojects/frontend/src/ModelWorkArea.tsx
new file mode 100644
index 00000000..3aba31e3
--- /dev/null
+++ b/subprojects/frontend/src/ModelWorkArea.tsx
@@ -0,0 +1,198 @@
1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import CloseIcon from '@mui/icons-material/Close';
8import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied';
9import CircularProgress from '@mui/material/CircularProgress';
10import IconButton from '@mui/material/IconButton';
11import Stack from '@mui/material/Stack';
12import Tab from '@mui/material/Tab';
13import Tabs from '@mui/material/Tabs';
14import { styled } from '@mui/material/styles';
15import { observer } from 'mobx-react-lite';
16
17import DirectionalSplitPane from './DirectionalSplitPane';
18import Loading from './Loading';
19import { useRootStore } from './RootStoreProvider';
20import type GeneratedModelStore from './editor/GeneratedModelStore';
21import GraphPane from './graph/GraphPane';
22import type GraphStore from './graph/GraphStore';
23import TablePane from './table/TablePane';
24import type ThemeStore from './theme/ThemeStore';
25
26const SplitGraphPane = observer(function SplitGraphPane({
27 graph,
28 themeStore,
29}: {
30 graph: GraphStore;
31 themeStore: ThemeStore;
32}): JSX.Element {
33 return (
34 <DirectionalSplitPane
35 primary={<GraphPane graph={graph} />}
36 secondary={<TablePane graph={graph} />}
37 primaryOnly={!themeStore.showTable}
38 secondaryOnly={!themeStore.showGraph}
39 />
40 );
41});
42
43const GenerationStatus = styled('div', {
44 name: 'ModelWorkArea-GenerationStatus',
45 shouldForwardProp: (prop) => prop !== 'error',
46})<{ error: boolean }>(({ error, theme }) => ({
47 color: error ? theme.palette.error.main : theme.palette.text.primary,
48 ...(error
49 ? {
50 fontWeight: theme.typography.fontWeightBold ?? 600,
51 }
52 : {}),
53}));
54
55const GeneratedModelPane = observer(function GeneratedModelPane({
56 generatedModel,
57 themeStore,
58}: {
59 generatedModel: GeneratedModelStore;
60 themeStore: ThemeStore;
61}): JSX.Element {
62 const { message, error, graph } = generatedModel;
63
64 if (graph !== undefined) {
65 return <SplitGraphPane graph={graph} themeStore={themeStore} />;
66 }
67
68 return (
69 <Stack
70 direction="column"
71 alignItems="center"
72 justifyContent="center"
73 height="100%"
74 width="100%"
75 overflow="hidden"
76 my={2}
77 >
78 <Stack
79 direction="column"
80 alignItems="center"
81 flexGrow={1}
82 flexShrink={1}
83 flexBasis={0}
84 sx={(theme) => ({
85 maxHeight: '6rem',
86 height: 'calc(100% - 8rem)',
87 marginBottom: theme.spacing(1),
88 padding: error ? 0 : theme.spacing(1),
89 color: theme.palette.text.secondary,
90 '.MuiCircularProgress-root, .MuiCircularProgress-svg, .MuiSvgIcon-root':
91 {
92 height: '100% !important',
93 width: '100% !important',
94 },
95 })}
96 >
97 {error ? (
98 <SentimentVeryDissatisfiedIcon
99 className="VisibilityDialog-emptyIcon"
100 fontSize="inherit"
101 />
102 ) : (
103 <CircularProgress color="inherit" />
104 )}
105 </Stack>
106 <GenerationStatus error={error}>{message}</GenerationStatus>
107 </Stack>
108 );
109});
110
111function ModelWorkArea(): JSX.Element {
112 const { editorStore, themeStore } = useRootStore();
113
114 if (editorStore === undefined) {
115 return <Loading />;
116 }
117
118 const { graph, generatedModels, selectedGeneratedModel } = editorStore;
119
120 const generatedModelNames: string[] = [];
121 const generatedModelTabs: JSX.Element[] = [];
122 generatedModels.forEach((value, key) => {
123 generatedModelNames.push(key);
124 /* eslint-disable react/no-array-index-key -- Key is a string here, not the array index. */
125 generatedModelTabs.push(
126 <Tab
127 label={value.title}
128 key={key}
129 onAuxClick={(event) => {
130 if (event.button === 1) {
131 editorStore.deleteGeneratedModel(key);
132 event.preventDefault();
133 event.stopPropagation();
134 }
135 }}
136 />,
137 );
138 /* eslint-enable react/no-array-index-key */
139 });
140 const generatedModel =
141 selectedGeneratedModel === undefined
142 ? undefined
143 : generatedModels.get(selectedGeneratedModel);
144 const selectedIndex =
145 selectedGeneratedModel === undefined
146 ? 0
147 : generatedModelNames.indexOf(selectedGeneratedModel) + 1;
148
149 return (
150 <Stack direction="column" height="100%" width="100%" overflow="hidden">
151 <Stack
152 direction="row"
153 sx={(theme) => ({
154 display: generatedModelNames.length === 0 ? 'none' : 'flex',
155 alignItems: 'center',
156 borderBottom: `1px solid ${theme.palette.outer.border}`,
157 })}
158 >
159 <Tabs
160 value={selectedIndex}
161 onChange={(_event, value) => {
162 if (value === 0) {
163 editorStore.selectGeneratedModel(undefined);
164 } else if (typeof value === 'number') {
165 editorStore.selectGeneratedModel(generatedModelNames[value - 1]);
166 }
167 }}
168 sx={{ flexGrow: 1 }}
169 >
170 <Tab label="Initial model" />
171 {generatedModelTabs}
172 </Tabs>
173 <IconButton
174 aria-label="Close generated model"
175 onClick={() =>
176 editorStore.deleteGeneratedModel(selectedGeneratedModel)
177 }
178 sx={{
179 display: selectedIndex === 0 ? 'none' : 'flex',
180 mx: 1,
181 }}
182 >
183 <CloseIcon fontSize="small" />
184 </IconButton>
185 </Stack>
186 {generatedModel === undefined ? (
187 <SplitGraphPane graph={graph} themeStore={themeStore} />
188 ) : (
189 <GeneratedModelPane
190 generatedModel={generatedModel}
191 themeStore={themeStore}
192 />
193 )}
194 </Stack>
195 );
196}
197
198export default observer(ModelWorkArea);
diff --git a/subprojects/frontend/src/WorkArea.tsx b/subprojects/frontend/src/WorkArea.tsx
index adb29a50..a1fbf7dc 100644
--- a/subprojects/frontend/src/WorkArea.tsx
+++ b/subprojects/frontend/src/WorkArea.tsx
@@ -7,10 +7,9 @@
7import { observer } from 'mobx-react-lite'; 7import { observer } from 'mobx-react-lite';
8 8
9import DirectionalSplitPane from './DirectionalSplitPane'; 9import DirectionalSplitPane from './DirectionalSplitPane';
10import ModelWorkArea from './ModelWorkArea';
10import { useRootStore } from './RootStoreProvider'; 11import { useRootStore } from './RootStoreProvider';
11import EditorPane from './editor/EditorPane'; 12import EditorPane from './editor/EditorPane';
12import GraphPane from './graph/GraphPane';
13import TablePane from './table/TablePane';
14 13
15export default observer(function WorkArea(): JSX.Element { 14export default observer(function WorkArea(): JSX.Element {
16 const { themeStore } = useRootStore(); 15 const { themeStore } = useRootStore();
@@ -18,14 +17,7 @@ export default observer(function WorkArea(): JSX.Element {
18 return ( 17 return (
19 <DirectionalSplitPane 18 <DirectionalSplitPane
20 primary={<EditorPane />} 19 primary={<EditorPane />}
21 secondary={ 20 secondary={<ModelWorkArea />}
22 <DirectionalSplitPane
23 primary={<GraphPane />}
24 secondary={<TablePane />}
25 primaryOnly={!themeStore.showTable}
26 secondaryOnly={!themeStore.showGraph}
27 />
28 }
29 primaryOnly={!themeStore.showGraph && !themeStore.showTable} 21 primaryOnly={!themeStore.showGraph && !themeStore.showTable}
30 secondaryOnly={!themeStore.showCode} 22 secondaryOnly={!themeStore.showCode}
31 /> 23 />
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts
index b5989ad1..f9a9a7da 100644
--- a/subprojects/frontend/src/editor/EditorStore.ts
+++ b/subprojects/frontend/src/editor/EditorStore.ts
@@ -32,6 +32,7 @@ import type XtextClient from '../xtext/XtextClient';
32import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; 32import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults';
33 33
34import EditorErrors from './EditorErrors'; 34import EditorErrors from './EditorErrors';
35import GeneratedModelStore from './GeneratedModelStore';
35import LintPanelStore from './LintPanelStore'; 36import LintPanelStore from './LintPanelStore';
36import SearchPanelStore from './SearchPanelStore'; 37import SearchPanelStore from './SearchPanelStore';
37import createEditorState from './createEditorState'; 38import createEditorState from './createEditorState';
@@ -69,6 +70,10 @@ export default class EditorStore {
69 70
70 graph: GraphStore; 71 graph: GraphStore;
71 72
73 generatedModels = new Map<string, GeneratedModelStore>();
74
75 selectedGeneratedModel: string | undefined;
76
72 constructor(initialValue: string, pwaStore: PWAStore) { 77 constructor(initialValue: string, pwaStore: PWAStore) {
73 this.id = nanoid(); 78 this.id = nanoid();
74 this.state = createEditorState(initialValue, this); 79 this.state = createEditorState(initialValue, this);
@@ -307,4 +312,84 @@ export default class EditorStore {
307 this.delayedErrors.dispose(); 312 this.delayedErrors.dispose();
308 this.disposed = true; 313 this.disposed = true;
309 } 314 }
315
316 startModelGeneration(): void {
317 this.client
318 ?.startModelGeneration()
319 ?.catch((error) => log.error('Could not start model generation', error));
320 }
321
322 addGeneratedModel(uuid: string): void {
323 this.generatedModels.set(uuid, new GeneratedModelStore());
324 this.selectGeneratedModel(uuid);
325 }
326
327 cancelModelGeneration(): void {
328 this.client
329 ?.cancelModelGeneration()
330 ?.catch((error) => log.error('Could not start model generation', error));
331 }
332
333 selectGeneratedModel(uuid: string | undefined): void {
334 if (uuid === undefined) {
335 this.selectedGeneratedModel = uuid;
336 return;
337 }
338 if (this.generatedModels.has(uuid)) {
339 this.selectedGeneratedModel = uuid;
340 return;
341 }
342 this.selectedGeneratedModel = undefined;
343 }
344
345 deleteGeneratedModel(uuid: string | undefined): void {
346 if (uuid === undefined) {
347 return;
348 }
349 if (this.selectedGeneratedModel === uuid) {
350 let previous: string | undefined;
351 let found: string | undefined;
352 this.generatedModels.forEach((_value, key) => {
353 if (key === uuid) {
354 found = previous;
355 }
356 previous = key;
357 });
358 this.selectGeneratedModel(found);
359 }
360 const generatedModel = this.generatedModels.get(uuid);
361 if (generatedModel !== undefined && generatedModel.running) {
362 this.cancelModelGeneration();
363 }
364 this.generatedModels.delete(uuid);
365 }
366
367 modelGenerationCancelled(): void {
368 this.generatedModels.forEach((value) =>
369 value.setError('Model generation cancelled'),
370 );
371 }
372
373 setGeneratedModelMessage(uuid: string, message: string): void {
374 this.generatedModels.get(uuid)?.setMessage(message);
375 }
376
377 setGeneratedModelError(uuid: string, message: string): void {
378 this.generatedModels.get(uuid)?.setError(message);
379 }
380
381 setGeneratedModelSemantics(
382 uuid: string,
383 semantics: SemanticsSuccessResult,
384 ): void {
385 this.generatedModels.get(uuid)?.setSemantics(semantics);
386 }
387
388 get generating(): boolean {
389 let generating = false;
390 this.generatedModels.forEach((value) => {
391 generating = generating || value.running;
392 });
393 return generating;
394 }
310} 395}
diff --git a/subprojects/frontend/src/editor/GenerateButton.tsx b/subprojects/frontend/src/editor/GenerateButton.tsx
index 5bac0464..b8dcd531 100644
--- a/subprojects/frontend/src/editor/GenerateButton.tsx
+++ b/subprojects/frontend/src/editor/GenerateButton.tsx
@@ -5,6 +5,7 @@
5 */ 5 */
6 6
7import CancelIcon from '@mui/icons-material/Cancel'; 7import CancelIcon from '@mui/icons-material/Cancel';
8import CloseIcon from '@mui/icons-material/Close';
8import PlayArrowIcon from '@mui/icons-material/PlayArrow'; 9import PlayArrowIcon from '@mui/icons-material/PlayArrow';
9import { observer } from 'mobx-react-lite'; 10import { observer } from 'mobx-react-lite';
10 11
@@ -28,8 +29,10 @@ const GenerateButton = observer(function GenerateButton({
28 ); 29 );
29 } 30 }
30 31
31 const { analyzing, errorCount, warningCount, semanticsError } = 32 const {
32 editorStore.delayedErrors; 33 delayedErrors: { analyzing, errorCount, warningCount, semanticsError },
34 generating,
35 } = editorStore;
33 36
34 if (analyzing) { 37 if (analyzing) {
35 return ( 38 return (
@@ -39,6 +42,18 @@ const GenerateButton = observer(function GenerateButton({
39 ); 42 );
40 } 43 }
41 44
45 if (generating) {
46 return (
47 <AnimatedButton
48 color="inherit"
49 onClick={() => editorStore.cancelModelGeneration()}
50 startIcon={<CloseIcon />}
51 >
52 Cancel
53 </AnimatedButton>
54 );
55 }
56
42 if (semanticsError !== undefined && editorStore.opened) { 57 if (semanticsError !== undefined && editorStore.opened) {
43 return ( 58 return (
44 <AnimatedButton 59 <AnimatedButton
@@ -83,6 +98,7 @@ const GenerateButton = observer(function GenerateButton({
83 disabled={!editorStore.opened} 98 disabled={!editorStore.opened}
84 color={warningCount > 0 ? 'warning' : 'primary'} 99 color={warningCount > 0 ? 'warning' : 'primary'}
85 startIcon={<PlayArrowIcon />} 100 startIcon={<PlayArrowIcon />}
101 onClick={() => editorStore.startModelGeneration()}
86 > 102 >
87 {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} 103 {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`}
88 </AnimatedButton> 104 </AnimatedButton>
diff --git a/subprojects/frontend/src/editor/GeneratedModelStore.ts b/subprojects/frontend/src/editor/GeneratedModelStore.ts
new file mode 100644
index 00000000..d0181eed
--- /dev/null
+++ b/subprojects/frontend/src/editor/GeneratedModelStore.ts
@@ -0,0 +1,50 @@
1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import { makeAutoObservable } from 'mobx';
8
9import GraphStore from '../graph/GraphStore';
10import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults';
11
12export default class GeneratedModelStore {
13 title: string;
14
15 message = 'Waiting for server';
16
17 error = false;
18
19 graph: GraphStore | undefined;
20
21 constructor() {
22 const time = new Date().toLocaleTimeString(undefined, { hour12: false });
23 this.title = `Generated at ${time}`;
24 makeAutoObservable(this);
25 }
26
27 get running(): boolean {
28 return !this.error && this.graph === undefined;
29 }
30
31 setMessage(message: string): void {
32 if (this.running) {
33 this.message = message;
34 }
35 }
36
37 setError(message: string): void {
38 if (this.running) {
39 this.error = true;
40 this.message = message;
41 }
42 }
43
44 setSemantics(semantics: SemanticsSuccessResult): void {
45 if (this.running) {
46 this.graph = new GraphStore();
47 this.graph.setSemantics(semantics);
48 }
49 }
50}
diff --git a/subprojects/frontend/src/graph/GraphArea.tsx b/subprojects/frontend/src/graph/GraphArea.tsx
index f8f40d22..d5801b9a 100644
--- a/subprojects/frontend/src/graph/GraphArea.tsx
+++ b/subprojects/frontend/src/graph/GraphArea.tsx
@@ -9,25 +9,17 @@ import { useTheme } from '@mui/material/styles';
9import { observer } from 'mobx-react-lite'; 9import { observer } from 'mobx-react-lite';
10import { useResizeDetector } from 'react-resize-detector'; 10import { useResizeDetector } from 'react-resize-detector';
11 11
12import Loading from '../Loading';
13import { useRootStore } from '../RootStoreProvider';
14
15import DotGraphVisualizer from './DotGraphVisualizer'; 12import DotGraphVisualizer from './DotGraphVisualizer';
13import type GraphStore from './GraphStore';
16import VisibilityPanel from './VisibilityPanel'; 14import VisibilityPanel from './VisibilityPanel';
17import ZoomCanvas from './ZoomCanvas'; 15import ZoomCanvas from './ZoomCanvas';
18 16
19function GraphArea(): JSX.Element { 17function GraphArea({ graph }: { graph: GraphStore }): JSX.Element {
20 const { editorStore } = useRootStore();
21 const { breakpoints } = useTheme(); 18 const { breakpoints } = useTheme();
22 const { ref, width, height } = useResizeDetector({ 19 const { ref, width, height } = useResizeDetector({
23 refreshMode: 'debounce', 20 refreshMode: 'debounce',
24 }); 21 });
25 22
26 if (editorStore === undefined) {
27 return <Loading />;
28 }
29
30 const { graph } = editorStore;
31 const breakpoint = breakpoints.values.sm; 23 const breakpoint = breakpoints.values.sm;
32 const dialog = 24 const dialog =
33 width === undefined || 25 width === undefined ||
diff --git a/subprojects/frontend/src/graph/GraphPane.tsx b/subprojects/frontend/src/graph/GraphPane.tsx
index c2ef8927..67dbfcbd 100644
--- a/subprojects/frontend/src/graph/GraphPane.tsx
+++ b/subprojects/frontend/src/graph/GraphPane.tsx
@@ -9,9 +9,15 @@ import { Suspense, lazy } from 'react';
9 9
10import Loading from '../Loading'; 10import Loading from '../Loading';
11 11
12import type GraphStore from './GraphStore';
13
12const GraphArea = lazy(() => import('./GraphArea')); 14const GraphArea = lazy(() => import('./GraphArea'));
13 15
14export default function GraphPane(): JSX.Element { 16export default function GraphPane({
17 graph,
18}: {
19 graph: GraphStore;
20}): JSX.Element {
15 return ( 21 return (
16 <Stack 22 <Stack
17 direction="column" 23 direction="column"
@@ -21,7 +27,7 @@ export default function GraphPane(): JSX.Element {
21 justifyContent="center" 27 justifyContent="center"
22 > 28 >
23 <Suspense fallback={<Loading />}> 29 <Suspense fallback={<Loading />}>
24 <GraphArea /> 30 <GraphArea graph={graph} />
25 </Suspense> 31 </Suspense>
26 </Stack> 32 </Stack>
27 ); 33 );
diff --git a/subprojects/frontend/src/index.tsx b/subprojects/frontend/src/index.tsx
index e8a22e82..4b251a23 100644
--- a/subprojects/frontend/src/index.tsx
+++ b/subprojects/frontend/src/index.tsx
@@ -16,35 +16,101 @@ import RootStore from './RootStore';
16(window as unknown as { fixViteIssue: unknown }).fixViteIssue = styled; 16(window as unknown as { fixViteIssue: unknown }).fixViteIssue = styled;
17 17
18const initialValue = `% Metamodel 18const initialValue = `% Metamodel
19class Person { 19
20 contains Post[] posts opposite author 20abstract class CompositeElement {
21 Person[] friend opposite friend 21 contains Region[] regions
22}
23
24class Region {
25 contains Vertex[] vertices opposite region
26}
27
28abstract class Vertex {
29 container Region region opposite vertices
30 contains Transition[] outgoingTransition opposite source
31 Transition[] incomingTransition opposite target
22} 32}
23 33
24class Post { 34class Transition {
25 container Person author opposite posts 35 container Vertex source opposite outgoingTransition
26 Post replyTo 36 Vertex target opposite incomingTransition
27} 37}
28 38
39abstract class Pseudostate extends Vertex.
40
41abstract class RegularState extends Vertex.
42
43class Entry extends Pseudostate.
44
45class Exit extends Pseudostate.
46
47class Choice extends Pseudostate.
48
49class FinalState extends RegularState.
50
51class State extends RegularState, CompositeElement.
52
53class Statechart extends CompositeElement.
54
29% Constraints 55% Constraints
30error replyToNotFriend(Post x, Post y) <->
31 replyTo(x, y),
32 author(x, xAuthor),
33 author(y, yAuthor),
34 xAuthor != yAuthor,
35 !friend(xAuthor, yAuthor).
36 56
37error replyToCycle(Post x) <-> replyTo+(x, x). 57%% Entry
58
59pred entryInRegion(Region r, Entry e) <->
60 vertices(r, e).
61
62error noEntryInRegion(Region r) <->
63 !entryInRegion(r, _).
64
65error multipleEntryInRegion(Region r) <->
66 entryInRegion(r, e1),
67 entryInRegion(r, e2),
68 e1 != e2.
69
70error incomingToEntry(Transition t, Entry e) <->
71 target(t, e).
72
73error noOutgoingTransitionFromEntry(Entry e) <->
74 !source(_, e).
75
76error multipleTransitionFromEntry(Entry e, Transition t1, Transition t2) <->
77 outgoingTransition(e, t1),
78 outgoingTransition(e, t2),
79 t1 != t2.
80
81%% Exit
82
83error outgoingFromExit(Transition t, Exit e) <->
84 source(t, e).
85
86%% Final
87
88error outgoingFromFinal(Transition t, FinalState e) <->
89 source(t, e).
90
91%% State vs Region
92
93pred stateInRegion(Region r, State s) <->
94 vertices(r, s).
95
96error noStateInRegion(Region r) <->
97 !stateInRegion(r, _).
98
99%% Choice
100
101error choiceHasNoOutgoing(Choice c) <->
102 !source(_, c).
103
104error choiceHasNoIncoming(Choice c) <->
105 !target(_, c).
38 106
39% Instance model 107% Instance model
40friend(a, b).
41author(p1, a).
42author(p2, b).
43 108
44!author(Post::new, a). 109Statechart(sct).
45 110
46% Scope 111% Scope
47scope Post = 10..15, Person += 0. 112
113scope node = 20..30, Region = 2..*, Choice = 1..*, Statechart += 0.
48`; 114`;
49 115
50configure({ 116configure({
diff --git a/subprojects/frontend/src/table/RelationGrid.tsx b/subprojects/frontend/src/table/RelationGrid.tsx
deleted file mode 100644
index 004982c9..00000000
--- a/subprojects/frontend/src/table/RelationGrid.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import Box from '@mui/material/Box';
8import {
9 DataGrid,
10 type GridRenderCellParams,
11 type GridColDef,
12} from '@mui/x-data-grid';
13import { observer } from 'mobx-react-lite';
14import { useMemo } from 'react';
15
16import type GraphStore from '../graph/GraphStore';
17
18import TableToolbar from './TableToolbar';
19import ValueRenderer from './ValueRenderer';
20
21interface Row {
22 nodes: string[];
23 value: string;
24}
25
26function RelationGrid({ graph }: { graph: GraphStore }): JSX.Element {
27 const {
28 selectedSymbol,
29 semantics: { nodes, partialInterpretation },
30 } = graph;
31 const symbolName = selectedSymbol?.name;
32 const arity = selectedSymbol?.arity ?? 0;
33
34 const columns = useMemo<GridColDef<Row>[]>(() => {
35 const defs: GridColDef<Row>[] = [];
36 for (let i = 0; i < arity; i += 1) {
37 defs.push({
38 field: `n${i}`,
39 headerName: String(i + 1),
40 valueGetter: (row) => row.row.nodes[i] ?? '',
41 flex: 1,
42 });
43 }
44 defs.push({
45 field: 'value',
46 headerName: 'Value',
47 flex: 1,
48 renderCell: ({ value }: GridRenderCellParams<Row, string>) => (
49 <ValueRenderer value={value} />
50 ),
51 });
52 return defs;
53 }, [arity]);
54
55 const rows = useMemo<Row[]>(() => {
56 if (symbolName === undefined) {
57 return [];
58 }
59 const interpretation = partialInterpretation[symbolName] ?? [];
60 return interpretation.map((tuple) => {
61 const nodeNames: string[] = [];
62 for (let i = 0; i < arity; i += 1) {
63 const index = tuple[i];
64 if (typeof index === 'number') {
65 const node = nodes[index];
66 if (node !== undefined) {
67 nodeNames.push(node.name);
68 }
69 }
70 }
71 return {
72 nodes: nodeNames,
73 value: String(tuple[arity]),
74 };
75 });
76 }, [arity, nodes, partialInterpretation, symbolName]);
77
78 return (
79 <Box
80 width="100%"
81 height="100%"
82 p={1}
83 sx={(theme) => ({
84 '.MuiDataGrid-withBorderColor': {
85 borderColor:
86 theme.palette.mode === 'dark'
87 ? theme.palette.divider
88 : theme.palette.outer.border,
89 },
90 })}
91 >
92 <DataGrid
93 slots={{ toolbar: TableToolbar }}
94 slotProps={{
95 toolbar: {
96 graph,
97 },
98 }}
99 density="compact"
100 rowSelection={false}
101 columns={columns}
102 rows={rows}
103 getRowId={(row) => row.nodes.join(',')}
104 />
105 </Box>
106 );
107}
108
109export default observer(RelationGrid);
diff --git a/subprojects/frontend/src/table/TableArea.tsx b/subprojects/frontend/src/table/TableArea.tsx
index cf37b96a..166b8adf 100644
--- a/subprojects/frontend/src/table/TableArea.tsx
+++ b/subprojects/frontend/src/table/TableArea.tsx
@@ -4,21 +4,106 @@
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
6 6
7import Box from '@mui/material/Box';
8import {
9 DataGrid,
10 type GridRenderCellParams,
11 type GridColDef,
12} from '@mui/x-data-grid';
7import { observer } from 'mobx-react-lite'; 13import { observer } from 'mobx-react-lite';
14import { useMemo } from 'react';
8 15
9import Loading from '../Loading'; 16import type GraphStore from '../graph/GraphStore';
10import { useRootStore } from '../RootStoreProvider';
11 17
12import RelationGrid from './RelationGrid'; 18import TableToolbar from './TableToolbar';
19import ValueRenderer from './ValueRenderer';
13 20
14function TablePane(): JSX.Element { 21interface Row {
15 const { editorStore } = useRootStore(); 22 nodes: string[];
23 value: string;
24}
25
26function TableArea({ graph }: { graph: GraphStore }): JSX.Element {
27 const {
28 selectedSymbol,
29 semantics: { nodes, partialInterpretation },
30 } = graph;
31 const symbolName = selectedSymbol?.name;
32 const arity = selectedSymbol?.arity ?? 0;
33
34 const columns = useMemo<GridColDef<Row>[]>(() => {
35 const defs: GridColDef<Row>[] = [];
36 for (let i = 0; i < arity; i += 1) {
37 defs.push({
38 field: `n${i}`,
39 headerName: String(i + 1),
40 valueGetter: (row) => row.row.nodes[i] ?? '',
41 flex: 1,
42 });
43 }
44 defs.push({
45 field: 'value',
46 headerName: 'Value',
47 flex: 1,
48 renderCell: ({ value }: GridRenderCellParams<Row, string>) => (
49 <ValueRenderer value={value} />
50 ),
51 });
52 return defs;
53 }, [arity]);
16 54
17 if (editorStore === undefined) { 55 const rows = useMemo<Row[]>(() => {
18 return <Loading />; 56 if (symbolName === undefined) {
19 } 57 return [];
58 }
59 const interpretation = partialInterpretation[symbolName] ?? [];
60 return interpretation.map((tuple) => {
61 const nodeNames: string[] = [];
62 for (let i = 0; i < arity; i += 1) {
63 const index = tuple[i];
64 if (typeof index === 'number') {
65 const node = nodes[index];
66 if (node !== undefined) {
67 nodeNames.push(node.name);
68 }
69 }
70 }
71 return {
72 nodes: nodeNames,
73 value: String(tuple[arity]),
74 };
75 });
76 }, [arity, nodes, partialInterpretation, symbolName]);
20 77
21 return <RelationGrid graph={editorStore.graph} />; 78 return (
79 <Box
80 width="100%"
81 height="100%"
82 p={1}
83 sx={(theme) => ({
84 '.MuiDataGrid-withBorderColor': {
85 borderColor:
86 theme.palette.mode === 'dark'
87 ? theme.palette.divider
88 : theme.palette.outer.border,
89 },
90 })}
91 >
92 <DataGrid
93 slots={{ toolbar: TableToolbar }}
94 slotProps={{
95 toolbar: {
96 graph,
97 },
98 }}
99 density="compact"
100 rowSelection={false}
101 columns={columns}
102 rows={rows}
103 getRowId={(row) => row.nodes.join(',')}
104 />
105 </Box>
106 );
22} 107}
23 108
24export default observer(TablePane); 109export default observer(TableArea);
diff --git a/subprojects/frontend/src/table/TablePane.tsx b/subprojects/frontend/src/table/TablePane.tsx
index 01442c3a..8b640217 100644
--- a/subprojects/frontend/src/table/TablePane.tsx
+++ b/subprojects/frontend/src/table/TablePane.tsx
@@ -8,14 +8,19 @@ import Stack from '@mui/material/Stack';
8import { Suspense, lazy } from 'react'; 8import { Suspense, lazy } from 'react';
9 9
10import Loading from '../Loading'; 10import Loading from '../Loading';
11import type GraphStore from '../graph/GraphStore';
11 12
12const TableArea = lazy(() => import('./TableArea')); 13const TableArea = lazy(() => import('./TableArea'));
13 14
14export default function TablePane(): JSX.Element { 15export default function TablePane({
16 graph,
17}: {
18 graph: GraphStore;
19}): JSX.Element {
15 return ( 20 return (
16 <Stack direction="column" height="100%" overflow="auto" alignItems="center"> 21 <Stack direction="column" height="100%" overflow="auto" alignItems="center">
17 <Suspense fallback={<Loading />}> 22 <Suspense fallback={<Loading />}>
18 <TableArea /> 23 <TableArea graph={graph} />
19 </Suspense> 24 </Suspense>
20 </Stack> 25 </Stack>
21 ); 26 );
diff --git a/subprojects/frontend/src/xtext/ModelGenerationService.ts b/subprojects/frontend/src/xtext/ModelGenerationService.ts
new file mode 100644
index 00000000..1e9f837a
--- /dev/null
+++ b/subprojects/frontend/src/xtext/ModelGenerationService.ts
@@ -0,0 +1,46 @@
1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import type EditorStore from '../editor/EditorStore';
8
9import type UpdateService from './UpdateService';
10import { ModelGenerationResult } from './xtextServiceResults';
11
12export default class ModelGenerationService {
13 constructor(
14 private readonly store: EditorStore,
15 private readonly updateService: UpdateService,
16 ) {}
17
18 onPush(push: unknown): void {
19 const result = ModelGenerationResult.parse(push);
20 if ('status' in result) {
21 this.store.setGeneratedModelMessage(result.uuid, result.status);
22 } else if ('error' in result) {
23 this.store.setGeneratedModelError(result.uuid, result.error);
24 } else {
25 this.store.setGeneratedModelSemantics(result.uuid, result);
26 }
27 }
28
29 onDisconnect(): void {
30 this.store.modelGenerationCancelled();
31 }
32
33 async start(): Promise<void> {
34 const result = await this.updateService.startModelGeneration();
35 if (!result.cancelled) {
36 this.store.addGeneratedModel(result.data.uuid);
37 }
38 }
39
40 async cancel(): Promise<void> {
41 const result = await this.updateService.cancelModelGeneration();
42 if (!result.cancelled) {
43 this.store.modelGenerationCancelled();
44 }
45 }
46}
diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts
index 1ac722e1..d1246d5e 100644
--- a/subprojects/frontend/src/xtext/UpdateService.ts
+++ b/subprojects/frontend/src/xtext/UpdateService.ts
@@ -22,6 +22,7 @@ import {
22 FormattingResult, 22 FormattingResult,
23 isConflictResult, 23 isConflictResult,
24 OccurrencesResult, 24 OccurrencesResult,
25 ModelGenerationStartedResult,
25} from './xtextServiceResults'; 26} from './xtextServiceResults';
26 27
27const UPDATE_TIMEOUT_MS = 500; 28const UPDATE_TIMEOUT_MS = 500;
@@ -341,4 +342,42 @@ export default class UpdateService {
341 } 342 }
342 return { cancelled: false, data: parsedOccurrencesResult }; 343 return { cancelled: false, data: parsedOccurrencesResult };
343 } 344 }
345
346 async startModelGeneration(): Promise<
347 CancellableResult<ModelGenerationStartedResult>
348 > {
349 try {
350 await this.updateOrThrow();
351 } catch (error) {
352 if (error instanceof CancelledError || error instanceof TimeoutError) {
353 return { cancelled: true };
354 }
355 throw error;
356 }
357 log.debug('Starting model generation');
358 const data = await this.webSocketClient.send({
359 resource: this.resourceName,
360 serviceType: 'modelGeneration',
361 requiredStateId: this.xtextStateId,
362 start: true,
363 });
364 if (isConflictResult(data)) {
365 return { cancelled: true };
366 }
367 const parsedResult = ModelGenerationStartedResult.parse(data);
368 return { cancelled: false, data: parsedResult };
369 }
370
371 async cancelModelGeneration(): Promise<CancellableResult<unknown>> {
372 log.debug('Cancelling model generation');
373 const data = await this.webSocketClient.send({
374 resource: this.resourceName,
375 serviceType: 'modelGeneration',
376 cancel: true,
377 });
378 if (isConflictResult(data)) {
379 return { cancelled: true };
380 }
381 return { cancelled: false, data };
382 }
344} 383}
diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts
index 87778084..77980d35 100644
--- a/subprojects/frontend/src/xtext/XtextClient.ts
+++ b/subprojects/frontend/src/xtext/XtextClient.ts
@@ -16,6 +16,7 @@ import getLogger from '../utils/getLogger';
16 16
17import ContentAssistService from './ContentAssistService'; 17import ContentAssistService from './ContentAssistService';
18import HighlightingService from './HighlightingService'; 18import HighlightingService from './HighlightingService';
19import ModelGenerationService from './ModelGenerationService';
19import OccurrencesService from './OccurrencesService'; 20import OccurrencesService from './OccurrencesService';
20import SemanticsService from './SemanticsService'; 21import SemanticsService from './SemanticsService';
21import UpdateService from './UpdateService'; 22import UpdateService from './UpdateService';
@@ -40,6 +41,8 @@ export default class XtextClient {
40 41
41 private readonly semanticsService: SemanticsService; 42 private readonly semanticsService: SemanticsService;
42 43
44 private readonly modelGenerationService: ModelGenerationService;
45
43 constructor( 46 constructor(
44 private readonly store: EditorStore, 47 private readonly store: EditorStore,
45 private readonly pwaStore: PWAStore, 48 private readonly pwaStore: PWAStore,
@@ -58,6 +61,10 @@ export default class XtextClient {
58 this.validationService = new ValidationService(store, this.updateService); 61 this.validationService = new ValidationService(store, this.updateService);
59 this.occurrencesService = new OccurrencesService(store, this.updateService); 62 this.occurrencesService = new OccurrencesService(store, this.updateService);
60 this.semanticsService = new SemanticsService(store, this.validationService); 63 this.semanticsService = new SemanticsService(store, this.validationService);
64 this.modelGenerationService = new ModelGenerationService(
65 store,
66 this.updateService,
67 );
61 } 68 }
62 69
63 start(): void { 70 start(): void {
@@ -75,6 +82,7 @@ export default class XtextClient {
75 this.highlightingService.onDisconnect(); 82 this.highlightingService.onDisconnect();
76 this.validationService.onDisconnect(); 83 this.validationService.onDisconnect();
77 this.occurrencesService.onDisconnect(); 84 this.occurrencesService.onDisconnect();
85 this.modelGenerationService.onDisconnect();
78 } 86 }
79 87
80 onTransaction(transaction: Transaction): void { 88 onTransaction(transaction: Transaction): void {
@@ -101,7 +109,7 @@ export default class XtextClient {
101 ); 109 );
102 return; 110 return;
103 } 111 }
104 if (stateId !== xtextStateId) { 112 if (stateId !== xtextStateId && service !== 'modelGeneration') {
105 log.error( 113 log.error(
106 'Unexpected xtext state id: expected:', 114 'Unexpected xtext state id: expected:',
107 xtextStateId, 115 xtextStateId,
@@ -122,6 +130,9 @@ export default class XtextClient {
122 case 'semantics': 130 case 'semantics':
123 this.semanticsService.onPush(push); 131 this.semanticsService.onPush(push);
124 return; 132 return;
133 case 'modelGeneration':
134 this.modelGenerationService.onPush(push);
135 return;
125 default: 136 default:
126 throw new Error('Unknown service'); 137 throw new Error('Unknown service');
127 } 138 }
@@ -131,6 +142,14 @@ export default class XtextClient {
131 return this.contentAssistService.contentAssist(context); 142 return this.contentAssistService.contentAssist(context);
132 } 143 }
133 144
145 startModelGeneration(): Promise<void> {
146 return this.modelGenerationService.start();
147 }
148
149 cancelModelGeneration(): Promise<void> {
150 return this.modelGenerationService.cancel();
151 }
152
134 formatText(): void { 153 formatText(): void {
135 this.updateService.formatText().catch((e) => { 154 this.updateService.formatText().catch((e) => {
136 log.error('Error while formatting text', e); 155 log.error('Error while formatting text', e);
diff --git a/subprojects/frontend/src/xtext/xtextMessages.ts b/subprojects/frontend/src/xtext/xtextMessages.ts
index 971720e1..15831c5a 100644
--- a/subprojects/frontend/src/xtext/xtextMessages.ts
+++ b/subprojects/frontend/src/xtext/xtextMessages.ts
@@ -38,6 +38,7 @@ export const XtextWebPushService = z.enum([
38 'highlight', 38 'highlight',
39 'validate', 39 'validate',
40 'semantics', 40 'semantics',
41 'modelGeneration',
41]); 42]);
42 43
43export type XtextWebPushService = z.infer<typeof XtextWebPushService>; 44export type XtextWebPushService = z.infer<typeof XtextWebPushService>;
diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts
index caf2cf0b..e473bd48 100644
--- a/subprojects/frontend/src/xtext/xtextServiceResults.ts
+++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts
@@ -126,6 +126,14 @@ export const FormattingResult = DocumentStateResult.extend({
126 126
127export type FormattingResult = z.infer<typeof FormattingResult>; 127export type FormattingResult = z.infer<typeof FormattingResult>;
128 128
129export const ModelGenerationStartedResult = z.object({
130 uuid: z.string().nonempty(),
131});
132
133export type ModelGenerationStartedResult = z.infer<
134 typeof ModelGenerationStartedResult
135>;
136
129export const NodeMetadata = z.object({ 137export const NodeMetadata = z.object({
130 name: z.string(), 138 name: z.string(),
131 simpleName: z.string(), 139 simpleName: z.string(),
@@ -171,3 +179,19 @@ export const SemanticsResult = z.union([
171]); 179]);
172 180
173export type SemanticsResult = z.infer<typeof SemanticsResult>; 181export type SemanticsResult = z.infer<typeof SemanticsResult>;
182
183export const ModelGenerationResult = z.union([
184 z.object({
185 uuid: z.string().nonempty(),
186 status: z.string(),
187 }),
188 z.object({
189 uuid: z.string().nonempty(),
190 error: z.string(),
191 }),
192 SemanticsSuccessResult.extend({
193 uuid: z.string().nonempty(),
194 }),
195]);
196
197export type ModelGenerationResult = z.infer<typeof ModelGenerationResult>;