diff options
Diffstat (limited to 'subprojects/frontend')
-rw-r--r-- | subprojects/frontend/src/ModelWorkArea.tsx | 198 | ||||
-rw-r--r-- | subprojects/frontend/src/WorkArea.tsx | 12 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorStore.ts | 85 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/GenerateButton.tsx | 20 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/GeneratedModelStore.ts | 50 | ||||
-rw-r--r-- | subprojects/frontend/src/graph/GraphArea.tsx | 12 | ||||
-rw-r--r-- | subprojects/frontend/src/graph/GraphPane.tsx | 10 | ||||
-rw-r--r-- | subprojects/frontend/src/index.tsx | 102 | ||||
-rw-r--r-- | subprojects/frontend/src/table/RelationGrid.tsx | 109 | ||||
-rw-r--r-- | subprojects/frontend/src/table/TableArea.tsx | 105 | ||||
-rw-r--r-- | subprojects/frontend/src/table/TablePane.tsx | 9 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/ModelGenerationService.ts | 46 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/UpdateService.ts | 39 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/XtextClient.ts | 21 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/xtextMessages.ts | 1 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/xtextServiceResults.ts | 24 |
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 | |||
7 | import CloseIcon from '@mui/icons-material/Close'; | ||
8 | import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied'; | ||
9 | import CircularProgress from '@mui/material/CircularProgress'; | ||
10 | import IconButton from '@mui/material/IconButton'; | ||
11 | import Stack from '@mui/material/Stack'; | ||
12 | import Tab from '@mui/material/Tab'; | ||
13 | import Tabs from '@mui/material/Tabs'; | ||
14 | import { styled } from '@mui/material/styles'; | ||
15 | import { observer } from 'mobx-react-lite'; | ||
16 | |||
17 | import DirectionalSplitPane from './DirectionalSplitPane'; | ||
18 | import Loading from './Loading'; | ||
19 | import { useRootStore } from './RootStoreProvider'; | ||
20 | import type GeneratedModelStore from './editor/GeneratedModelStore'; | ||
21 | import GraphPane from './graph/GraphPane'; | ||
22 | import type GraphStore from './graph/GraphStore'; | ||
23 | import TablePane from './table/TablePane'; | ||
24 | import type ThemeStore from './theme/ThemeStore'; | ||
25 | |||
26 | const 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 | |||
43 | const 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 | |||
55 | const 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 | |||
111 | function 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 | |||
198 | export 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 @@ | |||
7 | import { observer } from 'mobx-react-lite'; | 7 | import { observer } from 'mobx-react-lite'; |
8 | 8 | ||
9 | import DirectionalSplitPane from './DirectionalSplitPane'; | 9 | import DirectionalSplitPane from './DirectionalSplitPane'; |
10 | import ModelWorkArea from './ModelWorkArea'; | ||
10 | import { useRootStore } from './RootStoreProvider'; | 11 | import { useRootStore } from './RootStoreProvider'; |
11 | import EditorPane from './editor/EditorPane'; | 12 | import EditorPane from './editor/EditorPane'; |
12 | import GraphPane from './graph/GraphPane'; | ||
13 | import TablePane from './table/TablePane'; | ||
14 | 13 | ||
15 | export default observer(function WorkArea(): JSX.Element { | 14 | export 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'; | |||
32 | import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; | 32 | import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; |
33 | 33 | ||
34 | import EditorErrors from './EditorErrors'; | 34 | import EditorErrors from './EditorErrors'; |
35 | import GeneratedModelStore from './GeneratedModelStore'; | ||
35 | import LintPanelStore from './LintPanelStore'; | 36 | import LintPanelStore from './LintPanelStore'; |
36 | import SearchPanelStore from './SearchPanelStore'; | 37 | import SearchPanelStore from './SearchPanelStore'; |
37 | import createEditorState from './createEditorState'; | 38 | import 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 | ||
7 | import CancelIcon from '@mui/icons-material/Cancel'; | 7 | import CancelIcon from '@mui/icons-material/Cancel'; |
8 | import CloseIcon from '@mui/icons-material/Close'; | ||
8 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; | 9 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; |
9 | import { observer } from 'mobx-react-lite'; | 10 | import { 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 | |||
7 | import { makeAutoObservable } from 'mobx'; | ||
8 | |||
9 | import GraphStore from '../graph/GraphStore'; | ||
10 | import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; | ||
11 | |||
12 | export 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'; | |||
9 | import { observer } from 'mobx-react-lite'; | 9 | import { observer } from 'mobx-react-lite'; |
10 | import { useResizeDetector } from 'react-resize-detector'; | 10 | import { useResizeDetector } from 'react-resize-detector'; |
11 | 11 | ||
12 | import Loading from '../Loading'; | ||
13 | import { useRootStore } from '../RootStoreProvider'; | ||
14 | |||
15 | import DotGraphVisualizer from './DotGraphVisualizer'; | 12 | import DotGraphVisualizer from './DotGraphVisualizer'; |
13 | import type GraphStore from './GraphStore'; | ||
16 | import VisibilityPanel from './VisibilityPanel'; | 14 | import VisibilityPanel from './VisibilityPanel'; |
17 | import ZoomCanvas from './ZoomCanvas'; | 15 | import ZoomCanvas from './ZoomCanvas'; |
18 | 16 | ||
19 | function GraphArea(): JSX.Element { | 17 | function 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 | ||
10 | import Loading from '../Loading'; | 10 | import Loading from '../Loading'; |
11 | 11 | ||
12 | import type GraphStore from './GraphStore'; | ||
13 | |||
12 | const GraphArea = lazy(() => import('./GraphArea')); | 14 | const GraphArea = lazy(() => import('./GraphArea')); |
13 | 15 | ||
14 | export default function GraphPane(): JSX.Element { | 16 | export 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 | ||
18 | const initialValue = `% Metamodel | 18 | const initialValue = `% Metamodel |
19 | class Person { | 19 | |
20 | contains Post[] posts opposite author | 20 | abstract class CompositeElement { |
21 | Person[] friend opposite friend | 21 | contains Region[] regions |
22 | } | ||
23 | |||
24 | class Region { | ||
25 | contains Vertex[] vertices opposite region | ||
26 | } | ||
27 | |||
28 | abstract class Vertex { | ||
29 | container Region region opposite vertices | ||
30 | contains Transition[] outgoingTransition opposite source | ||
31 | Transition[] incomingTransition opposite target | ||
22 | } | 32 | } |
23 | 33 | ||
24 | class Post { | 34 | class 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 | ||
39 | abstract class Pseudostate extends Vertex. | ||
40 | |||
41 | abstract class RegularState extends Vertex. | ||
42 | |||
43 | class Entry extends Pseudostate. | ||
44 | |||
45 | class Exit extends Pseudostate. | ||
46 | |||
47 | class Choice extends Pseudostate. | ||
48 | |||
49 | class FinalState extends RegularState. | ||
50 | |||
51 | class State extends RegularState, CompositeElement. | ||
52 | |||
53 | class Statechart extends CompositeElement. | ||
54 | |||
29 | % Constraints | 55 | % Constraints |
30 | error 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 | ||
37 | error replyToCycle(Post x) <-> replyTo+(x, x). | 57 | %% Entry |
58 | |||
59 | pred entryInRegion(Region r, Entry e) <-> | ||
60 | vertices(r, e). | ||
61 | |||
62 | error noEntryInRegion(Region r) <-> | ||
63 | !entryInRegion(r, _). | ||
64 | |||
65 | error multipleEntryInRegion(Region r) <-> | ||
66 | entryInRegion(r, e1), | ||
67 | entryInRegion(r, e2), | ||
68 | e1 != e2. | ||
69 | |||
70 | error incomingToEntry(Transition t, Entry e) <-> | ||
71 | target(t, e). | ||
72 | |||
73 | error noOutgoingTransitionFromEntry(Entry e) <-> | ||
74 | !source(_, e). | ||
75 | |||
76 | error multipleTransitionFromEntry(Entry e, Transition t1, Transition t2) <-> | ||
77 | outgoingTransition(e, t1), | ||
78 | outgoingTransition(e, t2), | ||
79 | t1 != t2. | ||
80 | |||
81 | %% Exit | ||
82 | |||
83 | error outgoingFromExit(Transition t, Exit e) <-> | ||
84 | source(t, e). | ||
85 | |||
86 | %% Final | ||
87 | |||
88 | error outgoingFromFinal(Transition t, FinalState e) <-> | ||
89 | source(t, e). | ||
90 | |||
91 | %% State vs Region | ||
92 | |||
93 | pred stateInRegion(Region r, State s) <-> | ||
94 | vertices(r, s). | ||
95 | |||
96 | error noStateInRegion(Region r) <-> | ||
97 | !stateInRegion(r, _). | ||
98 | |||
99 | %% Choice | ||
100 | |||
101 | error choiceHasNoOutgoing(Choice c) <-> | ||
102 | !source(_, c). | ||
103 | |||
104 | error choiceHasNoIncoming(Choice c) <-> | ||
105 | !target(_, c). | ||
38 | 106 | ||
39 | % Instance model | 107 | % Instance model |
40 | friend(a, b). | ||
41 | author(p1, a). | ||
42 | author(p2, b). | ||
43 | 108 | ||
44 | !author(Post::new, a). | 109 | Statechart(sct). |
45 | 110 | ||
46 | % Scope | 111 | % Scope |
47 | scope Post = 10..15, Person += 0. | 112 | |
113 | scope node = 20..30, Region = 2..*, Choice = 1..*, Statechart += 0. | ||
48 | `; | 114 | `; |
49 | 115 | ||
50 | configure({ | 116 | configure({ |
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 | |||
7 | import Box from '@mui/material/Box'; | ||
8 | import { | ||
9 | DataGrid, | ||
10 | type GridRenderCellParams, | ||
11 | type GridColDef, | ||
12 | } from '@mui/x-data-grid'; | ||
13 | import { observer } from 'mobx-react-lite'; | ||
14 | import { useMemo } from 'react'; | ||
15 | |||
16 | import type GraphStore from '../graph/GraphStore'; | ||
17 | |||
18 | import TableToolbar from './TableToolbar'; | ||
19 | import ValueRenderer from './ValueRenderer'; | ||
20 | |||
21 | interface Row { | ||
22 | nodes: string[]; | ||
23 | value: string; | ||
24 | } | ||
25 | |||
26 | function 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 | |||
109 | export 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 | ||
7 | import Box from '@mui/material/Box'; | ||
8 | import { | ||
9 | DataGrid, | ||
10 | type GridRenderCellParams, | ||
11 | type GridColDef, | ||
12 | } from '@mui/x-data-grid'; | ||
7 | import { observer } from 'mobx-react-lite'; | 13 | import { observer } from 'mobx-react-lite'; |
14 | import { useMemo } from 'react'; | ||
8 | 15 | ||
9 | import Loading from '../Loading'; | 16 | import type GraphStore from '../graph/GraphStore'; |
10 | import { useRootStore } from '../RootStoreProvider'; | ||
11 | 17 | ||
12 | import RelationGrid from './RelationGrid'; | 18 | import TableToolbar from './TableToolbar'; |
19 | import ValueRenderer from './ValueRenderer'; | ||
13 | 20 | ||
14 | function TablePane(): JSX.Element { | 21 | interface Row { |
15 | const { editorStore } = useRootStore(); | 22 | nodes: string[]; |
23 | value: string; | ||
24 | } | ||
25 | |||
26 | function 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 | ||
24 | export default observer(TablePane); | 109 | export 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'; | |||
8 | import { Suspense, lazy } from 'react'; | 8 | import { Suspense, lazy } from 'react'; |
9 | 9 | ||
10 | import Loading from '../Loading'; | 10 | import Loading from '../Loading'; |
11 | import type GraphStore from '../graph/GraphStore'; | ||
11 | 12 | ||
12 | const TableArea = lazy(() => import('./TableArea')); | 13 | const TableArea = lazy(() => import('./TableArea')); |
13 | 14 | ||
14 | export default function TablePane(): JSX.Element { | 15 | export 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 | |||
7 | import type EditorStore from '../editor/EditorStore'; | ||
8 | |||
9 | import type UpdateService from './UpdateService'; | ||
10 | import { ModelGenerationResult } from './xtextServiceResults'; | ||
11 | |||
12 | export 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 | ||
27 | const UPDATE_TIMEOUT_MS = 500; | 28 | const 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 | ||
17 | import ContentAssistService from './ContentAssistService'; | 17 | import ContentAssistService from './ContentAssistService'; |
18 | import HighlightingService from './HighlightingService'; | 18 | import HighlightingService from './HighlightingService'; |
19 | import ModelGenerationService from './ModelGenerationService'; | ||
19 | import OccurrencesService from './OccurrencesService'; | 20 | import OccurrencesService from './OccurrencesService'; |
20 | import SemanticsService from './SemanticsService'; | 21 | import SemanticsService from './SemanticsService'; |
21 | import UpdateService from './UpdateService'; | 22 | import 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 | ||
43 | export type XtextWebPushService = z.infer<typeof XtextWebPushService>; | 44 | export 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 | ||
127 | export type FormattingResult = z.infer<typeof FormattingResult>; | 127 | export type FormattingResult = z.infer<typeof FormattingResult>; |
128 | 128 | ||
129 | export const ModelGenerationStartedResult = z.object({ | ||
130 | uuid: z.string().nonempty(), | ||
131 | }); | ||
132 | |||
133 | export type ModelGenerationStartedResult = z.infer< | ||
134 | typeof ModelGenerationStartedResult | ||
135 | >; | ||
136 | |||
129 | export const NodeMetadata = z.object({ | 137 | export 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 | ||
173 | export type SemanticsResult = z.infer<typeof SemanticsResult>; | 181 | export type SemanticsResult = z.infer<typeof SemanticsResult>; |
182 | |||
183 | export 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 | |||
197 | export type ModelGenerationResult = z.infer<typeof ModelGenerationResult>; | ||