aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/editor
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/editor')
-rw-r--r--subprojects/frontend/src/editor/AnalysisErrorNotification.tsx74
-rw-r--r--subprojects/frontend/src/editor/AnimatedButton.tsx11
-rw-r--r--subprojects/frontend/src/editor/DiagnosticValue.ts1
-rw-r--r--subprojects/frontend/src/editor/EditorButtons.tsx6
-rw-r--r--subprojects/frontend/src/editor/EditorErrors.tsx93
-rw-r--r--subprojects/frontend/src/editor/EditorPane.tsx4
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts132
-rw-r--r--subprojects/frontend/src/editor/EditorTheme.ts15
-rw-r--r--subprojects/frontend/src/editor/GenerateButton.tsx68
-rw-r--r--subprojects/frontend/src/editor/GeneratedModelStore.ts50
10 files changed, 410 insertions, 44 deletions
diff --git a/subprojects/frontend/src/editor/AnalysisErrorNotification.tsx b/subprojects/frontend/src/editor/AnalysisErrorNotification.tsx
new file mode 100644
index 00000000..591a3600
--- /dev/null
+++ b/subprojects/frontend/src/editor/AnalysisErrorNotification.tsx
@@ -0,0 +1,74 @@
1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import { reaction } from 'mobx';
8import { type SnackbarKey, useSnackbar } from 'notistack';
9import { useEffect, useState } from 'react';
10
11import type EditorStore from './EditorStore';
12
13function MessageObserver({
14 editorStore,
15}: {
16 editorStore: EditorStore;
17}): React.ReactNode {
18 const [message, setMessage] = useState(
19 editorStore.delayedErrors.semanticsError ?? '',
20 );
21 // Instead of making this component an `observer`,
22 // we only update the message is one is present to make sure that the
23 // disappear animation has a chance to complete.
24 useEffect(
25 () =>
26 reaction(
27 () => editorStore.delayedErrors.semanticsError,
28 (newMessage) => {
29 if (newMessage !== undefined) {
30 setMessage(newMessage);
31 }
32 },
33 { fireImmediately: false },
34 ),
35 [editorStore],
36 );
37 return message;
38}
39
40export default function AnalysisErrorNotification({
41 editorStore,
42}: {
43 editorStore: EditorStore;
44}): null {
45 const { enqueueSnackbar, closeSnackbar } = useSnackbar();
46 useEffect(() => {
47 let key: SnackbarKey | undefined;
48 const disposer = reaction(
49 () => editorStore.delayedErrors.semanticsError !== undefined,
50 (hasError) => {
51 if (hasError) {
52 if (key === undefined) {
53 key = enqueueSnackbar({
54 message: <MessageObserver editorStore={editorStore} />,
55 variant: 'error',
56 persist: true,
57 });
58 }
59 } else if (key !== undefined) {
60 closeSnackbar(key);
61 key = undefined;
62 }
63 },
64 { fireImmediately: true },
65 );
66 return () => {
67 disposer();
68 if (key !== undefined) {
69 closeSnackbar(key);
70 }
71 };
72 }, [editorStore, enqueueSnackbar, closeSnackbar]);
73 return null;
74}
diff --git a/subprojects/frontend/src/editor/AnimatedButton.tsx b/subprojects/frontend/src/editor/AnimatedButton.tsx
index dbbda618..606aabea 100644
--- a/subprojects/frontend/src/editor/AnimatedButton.tsx
+++ b/subprojects/frontend/src/editor/AnimatedButton.tsx
@@ -45,10 +45,10 @@ export default function AnimatedButton({
45 children, 45 children,
46}: { 46}: {
47 'aria-label'?: string; 47 'aria-label'?: string;
48 onClick?: () => void; 48 onClick?: React.MouseEventHandler<HTMLElement>;
49 color: 'error' | 'warning' | 'primary' | 'inherit'; 49 color: 'error' | 'warning' | 'primary' | 'inherit';
50 disabled?: boolean; 50 disabled?: boolean;
51 startIcon: JSX.Element; 51 startIcon?: JSX.Element;
52 sx?: SxProps<Theme> | undefined; 52 sx?: SxProps<Theme> | undefined;
53 children?: ReactNode; 53 children?: ReactNode;
54}): JSX.Element { 54}): JSX.Element {
@@ -79,7 +79,11 @@ export default function AnimatedButton({
79 className="rounded shaded" 79 className="rounded shaded"
80 disabled={disabled ?? false} 80 disabled={disabled ?? false}
81 startIcon={startIcon} 81 startIcon={startIcon}
82 width={width === undefined ? 'auto' : `calc(${width} + 50px)`} 82 width={
83 width === undefined
84 ? 'auto'
85 : `calc(${width} + ${startIcon === undefined ? 28 : 50}px)`
86 }
83 > 87 >
84 <Box 88 <Box
85 display="flex" 89 display="flex"
@@ -100,6 +104,7 @@ AnimatedButton.defaultProps = {
100 'aria-label': undefined, 104 'aria-label': undefined,
101 onClick: undefined, 105 onClick: undefined,
102 disabled: false, 106 disabled: false,
107 startIcon: undefined,
103 sx: undefined, 108 sx: undefined,
104 children: undefined, 109 children: undefined,
105}; 110};
diff --git a/subprojects/frontend/src/editor/DiagnosticValue.ts b/subprojects/frontend/src/editor/DiagnosticValue.ts
index 20478262..410a46b7 100644
--- a/subprojects/frontend/src/editor/DiagnosticValue.ts
+++ b/subprojects/frontend/src/editor/DiagnosticValue.ts
@@ -14,6 +14,7 @@ export default class DiagnosticValue extends RangeValue {
14 error: new DiagnosticValue('error'), 14 error: new DiagnosticValue('error'),
15 warning: new DiagnosticValue('warning'), 15 warning: new DiagnosticValue('warning'),
16 info: new DiagnosticValue('info'), 16 info: new DiagnosticValue('info'),
17 hint: new DiagnosticValue('hint'),
17 }; 18 };
18 19
19 private constructor(public readonly severity: Severity) { 20 private constructor(public readonly severity: Severity) {
diff --git a/subprojects/frontend/src/editor/EditorButtons.tsx b/subprojects/frontend/src/editor/EditorButtons.tsx
index 9b187e5c..ca51f975 100644
--- a/subprojects/frontend/src/editor/EditorButtons.tsx
+++ b/subprojects/frontend/src/editor/EditorButtons.tsx
@@ -5,8 +5,8 @@
5 */ 5 */
6 6
7import type { Diagnostic } from '@codemirror/lint'; 7import type { Diagnostic } from '@codemirror/lint';
8import CancelIcon from '@mui/icons-material/Cancel';
8import CheckIcon from '@mui/icons-material/Check'; 9import CheckIcon from '@mui/icons-material/Check';
9import ErrorIcon from '@mui/icons-material/Error';
10import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; 10import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
11import FormatPaint from '@mui/icons-material/FormatPaint'; 11import FormatPaint from '@mui/icons-material/FormatPaint';
12import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; 12import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
@@ -28,7 +28,7 @@ import type EditorStore from './EditorStore';
28function getLintIcon(severity: Diagnostic['severity'] | undefined) { 28function getLintIcon(severity: Diagnostic['severity'] | undefined) {
29 switch (severity) { 29 switch (severity) {
30 case 'error': 30 case 'error':
31 return <ErrorIcon fontSize="small" />; 31 return <CancelIcon fontSize="small" />;
32 case 'warning': 32 case 'warning':
33 return <WarningIcon fontSize="small" />; 33 return <WarningIcon fontSize="small" />;
34 case 'info': 34 case 'info':
@@ -95,7 +95,7 @@ export default observer(function EditorButtons({
95 })} 95 })}
96 value="show-lint-panel" 96 value="show-lint-panel"
97 > 97 >
98 {getLintIcon(editorStore?.highestDiagnosticLevel)} 98 {getLintIcon(editorStore?.delayedErrors?.highestDiagnosticLevel)}
99 </ToggleButton> 99 </ToggleButton>
100 </ToggleButtonGroup> 100 </ToggleButtonGroup>
101 <IconButton 101 <IconButton
diff --git a/subprojects/frontend/src/editor/EditorErrors.tsx b/subprojects/frontend/src/editor/EditorErrors.tsx
new file mode 100644
index 00000000..40becf7e
--- /dev/null
+++ b/subprojects/frontend/src/editor/EditorErrors.tsx
@@ -0,0 +1,93 @@
1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import { Diagnostic } from '@codemirror/lint';
8import { type IReactionDisposer, makeAutoObservable, reaction } from 'mobx';
9
10import type EditorStore from './EditorStore';
11
12const HYSTERESIS_TIME_MS = 250;
13
14export interface State {
15 analyzing: boolean;
16 errorCount: number;
17 warningCount: number;
18 infoCount: number;
19 semanticsError: string | undefined;
20}
21
22export default class EditorErrors implements State {
23 private readonly disposer: IReactionDisposer;
24
25 private timer: number | undefined;
26
27 analyzing = false;
28
29 errorCount = 0;
30
31 warningCount = 0;
32
33 infoCount = 0;
34
35 semanticsError: string | undefined;
36
37 constructor(private readonly store: EditorStore) {
38 this.updateImmediately(this.getNextState());
39 makeAutoObservable<EditorErrors, 'disposer' | 'timer'>(this, {
40 disposer: false,
41 timer: false,
42 });
43 this.disposer = reaction(
44 () => this.getNextState(),
45 (nextState) => {
46 if (this.timer !== undefined) {
47 clearTimeout(this.timer);
48 this.timer = undefined;
49 }
50 if (nextState.analyzing) {
51 this.timer = setTimeout(
52 () => this.updateImmediately(nextState),
53 HYSTERESIS_TIME_MS,
54 );
55 } else {
56 this.updateImmediately(nextState);
57 }
58 },
59 { fireImmediately: true },
60 );
61 }
62
63 get highestDiagnosticLevel(): Diagnostic['severity'] | undefined {
64 if (this.errorCount > 0) {
65 return 'error';
66 }
67 if (this.warningCount > 0) {
68 return 'warning';
69 }
70 if (this.infoCount > 0) {
71 return 'info';
72 }
73 return undefined;
74 }
75
76 private getNextState(): State {
77 return {
78 analyzing: this.store.analyzing,
79 errorCount: this.store.errorCount,
80 warningCount: this.store.warningCount,
81 infoCount: this.store.infoCount,
82 semanticsError: this.store.semanticsError,
83 };
84 }
85
86 private updateImmediately(nextState: State) {
87 Object.assign(this, nextState);
88 }
89
90 dispose() {
91 this.disposer();
92 }
93}
diff --git a/subprojects/frontend/src/editor/EditorPane.tsx b/subprojects/frontend/src/editor/EditorPane.tsx
index 87f408fe..1125a0ec 100644
--- a/subprojects/frontend/src/editor/EditorPane.tsx
+++ b/subprojects/frontend/src/editor/EditorPane.tsx
@@ -13,6 +13,7 @@ import { useState } from 'react';
13 13
14import { useRootStore } from '../RootStoreProvider'; 14import { useRootStore } from '../RootStoreProvider';
15 15
16import AnalysisErrorNotification from './AnalysisErrorNotification';
16import ConnectionStatusNotification from './ConnectionStatusNotification'; 17import ConnectionStatusNotification from './ConnectionStatusNotification';
17import EditorArea from './EditorArea'; 18import EditorArea from './EditorArea';
18import EditorButtons from './EditorButtons'; 19import EditorButtons from './EditorButtons';
@@ -39,7 +40,7 @@ export default observer(function EditorPane(): JSX.Element {
39 const { editorStore } = useRootStore(); 40 const { editorStore } = useRootStore();
40 41
41 return ( 42 return (
42 <Stack direction="column" flexGrow={1} flexShrink={1} overflow="auto"> 43 <Stack direction="column" height="100%" overflow="auto">
43 <Toolbar variant="dense"> 44 <Toolbar variant="dense">
44 <EditorButtons editorStore={editorStore} /> 45 <EditorButtons editorStore={editorStore} />
45 </Toolbar> 46 </Toolbar>
@@ -48,6 +49,7 @@ export default observer(function EditorPane(): JSX.Element {
48 <EditorLoading /> 49 <EditorLoading />
49 ) : ( 50 ) : (
50 <> 51 <>
52 <AnalysisErrorNotification editorStore={editorStore} />
51 <ConnectionStatusNotification editorStore={editorStore} /> 53 <ConnectionStatusNotification editorStore={editorStore} />
52 <SearchPanelPortal editorStore={editorStore} /> 54 <SearchPanelPortal editorStore={editorStore} />
53 <EditorArea editorStore={editorStore} /> 55 <EditorArea editorStore={editorStore} />
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts
index b98f085e..9508858d 100644
--- a/subprojects/frontend/src/editor/EditorStore.ts
+++ b/subprojects/frontend/src/editor/EditorStore.ts
@@ -26,9 +26,13 @@ import { makeAutoObservable, observable, runInAction } from 'mobx';
26import { nanoid } from 'nanoid'; 26import { nanoid } from 'nanoid';
27 27
28import type PWAStore from '../PWAStore'; 28import type PWAStore from '../PWAStore';
29import GraphStore from '../graph/GraphStore';
29import getLogger from '../utils/getLogger'; 30import getLogger from '../utils/getLogger';
30import type XtextClient from '../xtext/XtextClient'; 31import type XtextClient from '../xtext/XtextClient';
32import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults';
31 33
34import EditorErrors from './EditorErrors';
35import GeneratedModelStore from './GeneratedModelStore';
32import LintPanelStore from './LintPanelStore'; 36import LintPanelStore from './LintPanelStore';
33import SearchPanelStore from './SearchPanelStore'; 37import SearchPanelStore from './SearchPanelStore';
34import createEditorState from './createEditorState'; 38import createEditorState from './createEditorState';
@@ -54,13 +58,26 @@ export default class EditorStore {
54 58
55 readonly lintPanel: LintPanelStore; 59 readonly lintPanel: LintPanelStore;
56 60
61 readonly delayedErrors: EditorErrors;
62
57 showLineNumbers = false; 63 showLineNumbers = false;
58 64
59 disposed = false; 65 disposed = false;
60 66
67 analyzing = false;
68
69 semanticsError: string | undefined;
70
71 graph: GraphStore;
72
73 generatedModels = new Map<string, GeneratedModelStore>();
74
75 selectedGeneratedModel: string | undefined;
76
61 constructor(initialValue: string, pwaStore: PWAStore) { 77 constructor(initialValue: string, pwaStore: PWAStore) {
62 this.id = nanoid(); 78 this.id = nanoid();
63 this.state = createEditorState(initialValue, this); 79 this.state = createEditorState(initialValue, this);
80 this.delayedErrors = new EditorErrors(this);
64 this.searchPanel = new SearchPanelStore(this); 81 this.searchPanel = new SearchPanelStore(this);
65 this.lintPanel = new LintPanelStore(this); 82 this.lintPanel = new LintPanelStore(this);
66 (async () => { 83 (async () => {
@@ -75,6 +92,7 @@ export default class EditorStore {
75 })().catch((error) => { 92 })().catch((error) => {
76 log.error('Failed to load XtextClient', error); 93 log.error('Failed to load XtextClient', error);
77 }); 94 });
95 this.graph = new GraphStore();
78 makeAutoObservable<EditorStore, 'client'>(this, { 96 makeAutoObservable<EditorStore, 'client'>(this, {
79 id: false, 97 id: false,
80 state: observable.ref, 98 state: observable.ref,
@@ -213,19 +231,6 @@ export default class EditorStore {
213 this.doCommand(nextDiagnostic); 231 this.doCommand(nextDiagnostic);
214 } 232 }
215 233
216 get highestDiagnosticLevel(): Diagnostic['severity'] | undefined {
217 if (this.errorCount > 0) {
218 return 'error';
219 }
220 if (this.warningCount > 0) {
221 return 'warning';
222 }
223 if (this.infoCount > 0) {
224 return 'info';
225 }
226 return undefined;
227 }
228
229 updateSemanticHighlighting(ranges: IHighlightRange[]): void { 234 updateSemanticHighlighting(ranges: IHighlightRange[]): void {
230 this.dispatch(setSemanticHighlighting(ranges)); 235 this.dispatch(setSemanticHighlighting(ranges));
231 } 236 }
@@ -282,8 +287,109 @@ export default class EditorStore {
282 return true; 287 return true;
283 } 288 }
284 289
290 analysisStarted() {
291 this.analyzing = true;
292 }
293
294 analysisCompleted(semanticAnalysisSkipped = false) {
295 this.analyzing = false;
296 if (semanticAnalysisSkipped) {
297 this.semanticsError = undefined;
298 }
299 }
300
301 setSemanticsError(semanticsError: string) {
302 this.semanticsError = semanticsError;
303 }
304
305 setSemantics(semantics: SemanticsSuccessResult) {
306 this.semanticsError = undefined;
307 this.graph.setSemantics(semantics);
308 }
309
285 dispose(): void { 310 dispose(): void {
286 this.client?.dispose(); 311 this.client?.dispose();
312 this.delayedErrors.dispose();
287 this.disposed = true; 313 this.disposed = true;
288 } 314 }
315
316 startModelGeneration(randomSeed?: number): void {
317 this.client
318 ?.startModelGeneration(randomSeed)
319 ?.catch((error) => log.error('Could not start model generation', error));
320 }
321
322 addGeneratedModel(uuid: string, randomSeed: number): void {
323 this.generatedModels.set(uuid, new GeneratedModelStore(randomSeed));
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 }
289} 395}
diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts
index e057ce18..055b62e2 100644
--- a/subprojects/frontend/src/editor/EditorTheme.ts
+++ b/subprojects/frontend/src/editor/EditorTheme.ts
@@ -4,15 +4,13 @@
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
6 6
7import errorSVG from '@material-icons/svg/svg/error/baseline.svg?raw'; 7import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw';
8import expandMoreSVG from '@material-icons/svg/svg/expand_more/baseline.svg?raw'; 8import expandMoreSVG from '@material-icons/svg/svg/expand_more/baseline.svg?raw';
9import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw'; 9import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw';
10import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; 10import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw';
11import { alpha, styled, type CSSObject } from '@mui/material/styles'; 11import { alpha, styled, type CSSObject } from '@mui/material/styles';
12 12
13function svgURL(svg: string): string { 13import svgURL from '../utils/svgURL';
14 return `url('data:image/svg+xml;utf8,${svg}')`;
15}
16 14
17export default styled('div', { 15export default styled('div', {
18 name: 'EditorTheme', 16 name: 'EditorTheme',
@@ -56,15 +54,16 @@ export default styled('div', {
56 '.cm-activeLineGutter': { 54 '.cm-activeLineGutter': {
57 background: 'transparent', 55 background: 'transparent',
58 }, 56 },
59 '.cm-cursor, .cm-cursor-primary': { 57 '.cm-cursor, .cm-dropCursor, .cm-cursor-primary': {
60 borderLeft: `2px solid ${theme.palette.info.main}`, 58 borderLeft: `2px solid ${theme.palette.info.main}`,
59 marginLeft: -1,
61 }, 60 },
62 '.cm-selectionBackground': { 61 '.cm-selectionBackground': {
63 background: theme.palette.highlight.selection, 62 background: theme.palette.highlight.selection,
64 }, 63 },
65 '.cm-focused': { 64 '.cm-focused': {
66 outline: 'none', 65 outline: 'none',
67 '.cm-selectionBackground': { 66 '& > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
68 background: theme.palette.highlight.selection, 67 background: theme.palette.highlight.selection,
69 }, 68 },
70 }, 69 },
@@ -106,7 +105,7 @@ export default styled('div', {
106 color: theme.palette.text.primary, 105 color: theme.palette.text.primary,
107 }, 106 },
108 }, 107 },
109 '.tok-problem-abstract, .tok-problem-new': { 108 '.tok-problem-abstract': {
110 fontStyle: 'italic', 109 fontStyle: 'italic',
111 }, 110 },
112 '.tok-problem-containment': { 111 '.tok-problem-containment': {
@@ -331,7 +330,7 @@ export default styled('div', {
331 '.cm-lintRange-active': { 330 '.cm-lintRange-active': {
332 background: theme.palette.highlight.activeLintRange, 331 background: theme.palette.highlight.activeLintRange,
333 }, 332 },
334 ...lintSeverityStyle('error', errorSVG, 120), 333 ...lintSeverityStyle('error', cancelSVG, 120),
335 ...lintSeverityStyle('warning', warningSVG, 110), 334 ...lintSeverityStyle('warning', warningSVG, 110),
336 ...lintSeverityStyle('info', infoSVG, 100), 335 ...lintSeverityStyle('info', infoSVG, 100),
337 }; 336 };
diff --git a/subprojects/frontend/src/editor/GenerateButton.tsx b/subprojects/frontend/src/editor/GenerateButton.tsx
index 3837ef8e..b6b1655a 100644
--- a/subprojects/frontend/src/editor/GenerateButton.tsx
+++ b/subprojects/frontend/src/editor/GenerateButton.tsx
@@ -4,10 +4,9 @@
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
6 6
7import DangerousOutlinedIcon from '@mui/icons-material/DangerousOutlined'; 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 Button from '@mui/material/Button';
10import type { SxProps, Theme } from '@mui/material/styles';
11import { observer } from 'mobx-react-lite'; 10import { observer } from 'mobx-react-lite';
12 11
13import AnimatedButton from './AnimatedButton'; 12import AnimatedButton from './AnimatedButton';
@@ -18,26 +17,59 @@ const GENERATE_LABEL = 'Generate';
18const GenerateButton = observer(function GenerateButton({ 17const GenerateButton = observer(function GenerateButton({
19 editorStore, 18 editorStore,
20 hideWarnings, 19 hideWarnings,
21 sx,
22}: { 20}: {
23 editorStore: EditorStore | undefined; 21 editorStore: EditorStore | undefined;
24 hideWarnings?: boolean | undefined; 22 hideWarnings?: boolean | undefined;
25 sx?: SxProps<Theme> | undefined;
26}): JSX.Element { 23}): JSX.Element {
27 if (editorStore === undefined) { 24 if (editorStore === undefined) {
28 return ( 25 return (
29 <Button 26 <AnimatedButton color="inherit" disabled>
27 Loading&hellip;
28 </AnimatedButton>
29 );
30 }
31
32 const {
33 delayedErrors: { analyzing, errorCount, warningCount, semanticsError },
34 generating,
35 } = editorStore;
36
37 if (analyzing) {
38 return (
39 <AnimatedButton color="inherit" disabled>
40 Analyzing&hellip;
41 </AnimatedButton>
42 );
43 }
44
45 if (generating) {
46 return (
47 <AnimatedButton
30 color="inherit" 48 color="inherit"
31 className="rounded shaded" 49 onClick={() => editorStore.cancelModelGeneration()}
32 disabled 50 startIcon={<CloseIcon />}
33 {...(sx === undefined ? {} : { sx })}
34 > 51 >
35 Loading&hellip; 52 Cancel
36 </Button> 53 </AnimatedButton>
37 ); 54 );
38 } 55 }
39 56
40 const { errorCount, warningCount } = editorStore; 57 if (semanticsError !== undefined && editorStore.opened) {
58 return (
59 <AnimatedButton
60 color="error"
61 disabled
62 startIcon={<CancelIcon />}
63 sx={(theme) => ({
64 '&.Mui-disabled': {
65 color: `${theme.palette.error.main} !important`,
66 },
67 })}
68 >
69 Analysis error
70 </AnimatedButton>
71 );
72 }
41 73
42 const diagnostics: string[] = []; 74 const diagnostics: string[] = [];
43 if (errorCount > 0) { 75 if (errorCount > 0) {
@@ -54,8 +86,7 @@ const GenerateButton = observer(function GenerateButton({
54 aria-label={`Select next diagnostic out of ${summary}`} 86 aria-label={`Select next diagnostic out of ${summary}`}
55 onClick={() => editorStore.nextDiagnostic()} 87 onClick={() => editorStore.nextDiagnostic()}
56 color="error" 88 color="error"
57 startIcon={<DangerousOutlinedIcon />} 89 startIcon={<CancelIcon />}
58 {...(sx === undefined ? {} : { sx })}
59 > 90 >
60 {summary} 91 {summary}
61 </AnimatedButton> 92 </AnimatedButton>
@@ -67,7 +98,13 @@ const GenerateButton = observer(function GenerateButton({
67 disabled={!editorStore.opened} 98 disabled={!editorStore.opened}
68 color={warningCount > 0 ? 'warning' : 'primary'} 99 color={warningCount > 0 ? 'warning' : 'primary'}
69 startIcon={<PlayArrowIcon />} 100 startIcon={<PlayArrowIcon />}
70 {...(sx === undefined ? {} : { sx })} 101 onClick={(event) => {
102 if (event.shiftKey) {
103 editorStore.startModelGeneration(1);
104 } else {
105 editorStore.startModelGeneration();
106 }
107 }}
71 > 108 >
72 {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} 109 {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`}
73 </AnimatedButton> 110 </AnimatedButton>
@@ -76,7 +113,6 @@ const GenerateButton = observer(function GenerateButton({
76 113
77GenerateButton.defaultProps = { 114GenerateButton.defaultProps = {
78 hideWarnings: false, 115 hideWarnings: false,
79 sx: undefined,
80}; 116};
81 117
82export default GenerateButton; 118export default GenerateButton;
diff --git a/subprojects/frontend/src/editor/GeneratedModelStore.ts b/subprojects/frontend/src/editor/GeneratedModelStore.ts
new file mode 100644
index 00000000..5088d603
--- /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(randomSeed: number) {
22 const time = new Date().toLocaleTimeString(undefined, { hour12: false });
23 this.title = `Generated at ${time} (${randomSeed})`;
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}