aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-08-30 02:12:23 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-08-30 02:36:21 +0200
commit4746d5e671a50fb900ff8c9252c26cca72278dc0 (patch)
treec8152cbd2435d64f07b0669b16ac11dddc277c9b
parentrefactor(frontend): containment arrow size (diff)
downloadrefinery-4746d5e671a50fb900ff8c9252c26cca72278dc0.tar.gz
refinery-4746d5e671a50fb900ff8c9252c26cca72278dc0.tar.zst
refinery-4746d5e671a50fb900ff8c9252c26cca72278dc0.zip
feat(frontend): projection dialog
-rw-r--r--subprojects/frontend/src/graph/DotGraphVisualizer.tsx7
-rw-r--r--subprojects/frontend/src/graph/GraphArea.tsx46
-rw-r--r--subprojects/frontend/src/graph/GraphStore.ts146
-rw-r--r--subprojects/frontend/src/graph/RelationName.tsx72
-rw-r--r--subprojects/frontend/src/graph/VisibilityDialog.tsx285
-rw-r--r--subprojects/frontend/src/graph/VisibilityPanel.tsx85
-rw-r--r--subprojects/frontend/src/graph/dotSource.ts36
7 files changed, 637 insertions, 40 deletions
diff --git a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
index 291314ec..41fd7225 100644
--- a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
+++ b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
@@ -11,9 +11,9 @@ import { reaction, type IReactionDisposer } from 'mobx';
11import { observer } from 'mobx-react-lite'; 11import { observer } from 'mobx-react-lite';
12import { useCallback, useRef } from 'react'; 12import { useCallback, useRef } from 'react';
13 13
14import { useRootStore } from '../RootStoreProvider';
15import getLogger from '../utils/getLogger'; 14import getLogger from '../utils/getLogger';
16 15
16import type GraphStore from './GraphStore';
17import GraphTheme from './GraphTheme'; 17import GraphTheme from './GraphTheme';
18import { FitZoomCallback } from './ZoomCanvas'; 18import { FitZoomCallback } from './ZoomCanvas';
19import dotSource from './dotSource'; 19import dotSource from './dotSource';
@@ -26,17 +26,16 @@ function ptToPx(pt: number): number {
26} 26}
27 27
28function DotGraphVisualizer({ 28function DotGraphVisualizer({
29 graph,
29 fitZoom, 30 fitZoom,
30 transitionTime, 31 transitionTime,
31}: { 32}: {
33 graph: GraphStore;
32 fitZoom?: FitZoomCallback; 34 fitZoom?: FitZoomCallback;
33 transitionTime?: number; 35 transitionTime?: number;
34}): JSX.Element { 36}): JSX.Element {
35 const transitionTimeOrDefault = 37 const transitionTimeOrDefault =
36 transitionTime ?? DotGraphVisualizer.defaultProps.transitionTime; 38 transitionTime ?? DotGraphVisualizer.defaultProps.transitionTime;
37
38 const { editorStore } = useRootStore();
39 const graph = editorStore?.graph;
40 const disposerRef = useRef<IReactionDisposer | undefined>(); 39 const disposerRef = useRef<IReactionDisposer | undefined>();
41 const graphvizRef = useRef< 40 const graphvizRef = useRef<
42 Graphviz<BaseType, unknown, null, undefined> | undefined 41 Graphviz<BaseType, unknown, null, undefined> | undefined
diff --git a/subprojects/frontend/src/graph/GraphArea.tsx b/subprojects/frontend/src/graph/GraphArea.tsx
index a1a741f3..f8f40d22 100644
--- a/subprojects/frontend/src/graph/GraphArea.tsx
+++ b/subprojects/frontend/src/graph/GraphArea.tsx
@@ -4,13 +4,51 @@
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 { useTheme } from '@mui/material/styles';
9import { observer } from 'mobx-react-lite';
10import { useResizeDetector } from 'react-resize-detector';
11
12import Loading from '../Loading';
13import { useRootStore } from '../RootStoreProvider';
14
7import DotGraphVisualizer from './DotGraphVisualizer'; 15import DotGraphVisualizer from './DotGraphVisualizer';
16import VisibilityPanel from './VisibilityPanel';
8import ZoomCanvas from './ZoomCanvas'; 17import ZoomCanvas from './ZoomCanvas';
9 18
10export default function GraphArea(): JSX.Element { 19function GraphArea(): JSX.Element {
20 const { editorStore } = useRootStore();
21 const { breakpoints } = useTheme();
22 const { ref, width, height } = useResizeDetector({
23 refreshMode: 'debounce',
24 });
25
26 if (editorStore === undefined) {
27 return <Loading />;
28 }
29
30 const { graph } = editorStore;
31 const breakpoint = breakpoints.values.sm;
32 const dialog =
33 width === undefined ||
34 height === undefined ||
35 width < breakpoint ||
36 height < breakpoint;
37
11 return ( 38 return (
12 <ZoomCanvas> 39 <Box
13 {(fitZoom) => <DotGraphVisualizer fitZoom={fitZoom} />} 40 width="100%"
14 </ZoomCanvas> 41 height="100%"
42 overflow="hidden"
43 position="relative"
44 ref={ref}
45 >
46 <ZoomCanvas>
47 {(fitZoom) => <DotGraphVisualizer graph={graph} fitZoom={fitZoom} />}
48 </ZoomCanvas>
49 <VisibilityPanel graph={graph} dialog={dialog} />
50 </Box>
15 ); 51 );
16} 52}
53
54export default observer(GraphArea);
diff --git a/subprojects/frontend/src/graph/GraphStore.ts b/subprojects/frontend/src/graph/GraphStore.ts
index b59bfb7d..f81b4db4 100644
--- a/subprojects/frontend/src/graph/GraphStore.ts
+++ b/subprojects/frontend/src/graph/GraphStore.ts
@@ -6,10 +6,48 @@
6 6
7import { makeAutoObservable, observable } from 'mobx'; 7import { makeAutoObservable, observable } from 'mobx';
8 8
9import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; 9import type {
10 RelationMetadata,
11 SemanticsSuccessResult,
12} from '../xtext/xtextServiceResults';
10 13
11export type Visibility = 'all' | 'must' | 'none'; 14export type Visibility = 'all' | 'must' | 'none';
12 15
16export function getDefaultVisibility(
17 metadata: RelationMetadata | undefined,
18): Visibility {
19 if (metadata === undefined || metadata.arity <= 0 || metadata.arity > 2) {
20 return 'none';
21 }
22 const { detail } = metadata;
23 switch (detail.type) {
24 case 'class':
25 case 'reference':
26 case 'opposite':
27 return 'all';
28 case 'predicate':
29 return detail.error ? 'must' : 'none';
30 default:
31 return 'none';
32 }
33}
34
35export function isVisibilityAllowed(
36 metadata: RelationMetadata | undefined,
37 visibility: Visibility,
38): boolean {
39 if (metadata === undefined || metadata.arity <= 0 || metadata.arity > 2) {
40 return visibility === 'none';
41 }
42 const { detail } = metadata;
43 if (detail.type === 'predicate' && detail.error) {
44 // We can't display may matches of error predicates,
45 // because they have none by definition.
46 return visibility !== 'all';
47 }
48 return true;
49}
50
13export default class GraphStore { 51export default class GraphStore {
14 semantics: SemanticsSuccessResult = { 52 semantics: SemanticsSuccessResult = {
15 nodes: [], 53 nodes: [],
@@ -17,35 +55,111 @@ export default class GraphStore {
17 partialInterpretation: {}, 55 partialInterpretation: {},
18 }; 56 };
19 57
58 relationMetadata = new Map<string, RelationMetadata>();
59
20 visibility = new Map<string, Visibility>(); 60 visibility = new Map<string, Visibility>();
21 61
62 abbreviate = true;
63
22 constructor() { 64 constructor() {
23 makeAutoObservable(this, { 65 makeAutoObservable(this, {
24 semantics: observable.ref, 66 semantics: observable.ref,
25 }); 67 });
26 } 68 }
27 69
28 getVisiblity(relation: string): Visibility { 70 getVisibility(relation: string): Visibility {
29 return this.visibility.get(relation) ?? 'none'; 71 const visibilityOverride = this.visibility.get(relation);
72 if (visibilityOverride !== undefined) {
73 return visibilityOverride;
74 }
75 return this.getDefaultVisibility(relation);
76 }
77
78 getDefaultVisibility(relation: string): Visibility {
79 const metadata = this.relationMetadata.get(relation);
80 return getDefaultVisibility(metadata);
81 }
82
83 isVisibilityAllowed(relation: string, visibility: Visibility): boolean {
84 const metadata = this.relationMetadata.get(relation);
85 return isVisibilityAllowed(metadata, visibility);
86 }
87
88 setVisibility(relation: string, visibility: Visibility): void {
89 const metadata = this.relationMetadata.get(relation);
90 if (metadata === undefined || !isVisibilityAllowed(metadata, visibility)) {
91 return;
92 }
93 const defaultVisiblity = getDefaultVisibility(metadata);
94 if (defaultVisiblity === visibility) {
95 this.visibility.delete(relation);
96 } else {
97 this.visibility.set(relation, visibility);
98 }
99 }
100
101 cycleVisibility(relation: string): void {
102 const metadata = this.relationMetadata.get(relation);
103 if (metadata === undefined) {
104 return;
105 }
106 switch (this.getVisibility(relation)) {
107 case 'none':
108 if (isVisibilityAllowed(metadata, 'must')) {
109 this.setVisibility(relation, 'must');
110 }
111 break;
112 case 'must':
113 {
114 const next = isVisibilityAllowed(metadata, 'all') ? 'all' : 'none';
115 this.setVisibility(relation, next);
116 }
117 break;
118 default:
119 this.setVisibility(relation, 'none');
120 break;
121 }
122 }
123
124 hideAll(): void {
125 this.relationMetadata.forEach((metadata, name) => {
126 if (getDefaultVisibility(metadata) === 'none') {
127 this.visibility.delete(name);
128 } else {
129 this.visibility.set(name, 'none');
130 }
131 });
132 }
133
134 resetFilter(): void {
135 this.visibility.clear();
136 }
137
138 getName({ name, simpleName }: { name: string; simpleName: string }): string {
139 return this.abbreviate ? simpleName : name;
140 }
141
142 toggleAbbrevaite(): void {
143 this.abbreviate = !this.abbreviate;
30 } 144 }
31 145
32 setSemantics(semantics: SemanticsSuccessResult) { 146 setSemantics(semantics: SemanticsSuccessResult) {
33 this.semantics = semantics; 147 this.semantics = semantics;
34 this.visibility.clear(); 148 this.relationMetadata.clear();
35 const names = new Set<string>(); 149 this.semantics.relations.forEach((metadata) => {
36 this.semantics.relations.forEach(({ name, detail }) => { 150 this.relationMetadata.set(metadata.name, metadata);
37 names.add(name);
38 if (!this.visibility.has(name)) {
39 const newVisibility = detail.type === 'builtin' ? 'none' : 'all';
40 this.visibility.set(name, newVisibility);
41 }
42 }); 151 });
43 const oldNames = new Set<string>(); 152 const toRemove = new Set<string>();
44 this.visibility.forEach((_, key) => oldNames.add(key)); 153 this.visibility.forEach((value, key) => {
45 oldNames.forEach((key) => { 154 if (
46 if (!names.has(key)) { 155 !this.isVisibilityAllowed(key, value) ||
47 this.visibility.delete(key); 156 this.getDefaultVisibility(key) === value
157 ) {
158 toRemove.add(key);
48 } 159 }
49 }); 160 });
161 toRemove.forEach((key) => {
162 this.visibility.delete(key);
163 });
50 } 164 }
51} 165}
diff --git a/subprojects/frontend/src/graph/RelationName.tsx b/subprojects/frontend/src/graph/RelationName.tsx
new file mode 100644
index 00000000..ec26fb21
--- /dev/null
+++ b/subprojects/frontend/src/graph/RelationName.tsx
@@ -0,0 +1,72 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import { styled } from '@mui/material/styles';
8import { observer } from 'mobx-react-lite';
9
10import { RelationMetadata } from '../xtext/xtextServiceResults';
11
12const Error = styled('span', {
13 name: 'RelationName-Error',
14})(({ theme }) => ({
15 color: theme.palette.error.main,
16}));
17
18const Qualifier = styled('span', {
19 name: 'RelationName-Qualifier',
20})(({ theme }) => ({
21 color: theme.palette.text.secondary,
22}));
23
24const FormattedName = observer(function FormattedName({
25 name,
26 metadata,
27}: {
28 name: string;
29 metadata: RelationMetadata;
30}): React.ReactNode {
31 const { detail } = metadata;
32 if (detail.type === 'class' && detail.abstractClass) {
33 return <i>{name}</i>;
34 }
35 if (detail.type === 'reference' && detail.containment) {
36 return <b>{name}</b>;
37 }
38 if (detail.type === 'predicate' && detail.error) {
39 return <Error>{name}</Error>;
40 }
41 return name;
42});
43
44function RelationName({
45 metadata,
46 abbreviate,
47}: {
48 metadata: RelationMetadata;
49 abbreviate?: boolean;
50}): JSX.Element {
51 const { name, simpleName } = metadata;
52 if (abbreviate ?? RelationName.defaultProps.abbreviate) {
53 return <FormattedName name={simpleName} metadata={metadata} />;
54 }
55 if (name.endsWith(simpleName)) {
56 return (
57 <>
58 <Qualifier>
59 {name.substring(0, name.length - simpleName.length)}
60 </Qualifier>
61 <FormattedName name={simpleName} metadata={metadata} />
62 </>
63 );
64 }
65 return <FormattedName name={name} metadata={metadata} />;
66}
67
68RelationName.defaultProps = {
69 abbreviate: false,
70};
71
72export default observer(RelationName);
diff --git a/subprojects/frontend/src/graph/VisibilityDialog.tsx b/subprojects/frontend/src/graph/VisibilityDialog.tsx
new file mode 100644
index 00000000..b28ba31a
--- /dev/null
+++ b/subprojects/frontend/src/graph/VisibilityDialog.tsx
@@ -0,0 +1,285 @@
1/*
2 * SPDX-FileCopyrightText: 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 FilterListIcon from '@mui/icons-material/FilterList';
9import LabelIcon from '@mui/icons-material/Label';
10import LabelOutlinedIcon from '@mui/icons-material/LabelOutlined';
11import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied';
12import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
13import Button from '@mui/material/Button';
14import Checkbox from '@mui/material/Checkbox';
15import FormControlLabel from '@mui/material/FormControlLabel';
16import IconButton from '@mui/material/IconButton';
17import Switch from '@mui/material/Switch';
18import { styled } from '@mui/material/styles';
19import { observer } from 'mobx-react-lite';
20
21import type GraphStore from './GraphStore';
22import { isVisibilityAllowed } from './GraphStore';
23import RelationName from './RelationName';
24
25const VisibilityDialogRoot = styled('div', {
26 name: 'VisibilityDialog-Root',
27 shouldForwardProp: (propName) => propName !== 'dialog',
28})<{ dialog: boolean }>(({ theme, dialog }) => {
29 const overlayOpacity = dialog ? 0.16 : 0.09;
30 return {
31 maxHeight: '100%',
32 maxWidth: '100%',
33 overflow: 'hidden',
34 display: 'flex',
35 padding: theme.spacing(2),
36 flexDirection: 'column',
37 '.VisibilityDialog-switch': {
38 display: 'flex',
39 flexDirection: 'row',
40 paddingLeft: theme.spacing(1),
41 marginBottom: theme.spacing(1),
42 '.MuiFormControlLabel-root': {
43 flexGrow: 1,
44 },
45 '.MuiIconButton-root': {
46 flexGrow: 0,
47 flexShrink: 0,
48 marginLeft: theme.spacing(2),
49 },
50 },
51 '.VisibilityDialog-scroll': {
52 display: 'flex',
53 flexDirection: 'column',
54 height: 'auto',
55 overflowX: 'hidden',
56 overflowY: 'auto',
57 '& table': {
58 // We use flexbox instead of `display: table` to get proper text-overflow
59 // behavior for overly long relation names.
60 display: 'flex',
61 flexDirection: 'column',
62 },
63 '& thead, & tbody': {
64 display: 'flex',
65 flexDirection: 'column',
66 },
67 '& thead': {
68 position: 'sticky',
69 top: 0,
70 zIndex: 999,
71 backgroundColor: theme.palette.background.paper,
72 ...(theme.palette.mode === 'dark'
73 ? {
74 // In dark mode, MUI Paper gets a lighter overlay.
75 backgroundImage: `linear-gradient(
76 rgba(255, 255, 255, ${overlayOpacity}),
77 rgba(255, 255, 255, ${overlayOpacity})
78 )`,
79 }
80 : {}),
81 '& tr': {
82 height: '44px',
83 },
84 },
85 '& tr': {
86 display: 'flex',
87 flexDirection: 'row',
88 maxWidth: '100%',
89 },
90 '& tbody tr': {
91 transition: theme.transitions.create('background', {
92 duration: theme.transitions.duration.shortest,
93 }),
94 '&:hover': {
95 background: theme.palette.action.hover,
96 '@media (hover: none)': {
97 background: 'transparent',
98 },
99 },
100 },
101 '& th, & td': {
102 display: 'flex',
103 flexDirection: 'row',
104 alignItems: 'center',
105 justifyContent: 'center',
106 // Set width in advance, since we can't rely on `display: table-cell`.
107 width: '44px',
108 },
109 '& th:nth-of-type(3), & td:nth-of-type(3)': {
110 justifyContent: 'start',
111 paddingLeft: theme.spacing(1),
112 paddingRight: theme.spacing(2),
113 // Only let the last column grow or shrink.
114 flexGrow: 1,
115 flexShrink: 1,
116 // Compute the maximum available space in advance to let the text overflow.
117 maxWidth: 'calc(100% - 88px)',
118 width: 'min-content',
119 },
120 '& td:nth-of-type(3)': {
121 cursor: 'pointer',
122 userSelect: 'none',
123 WebkitTapHighlightColor: 'transparent',
124 },
125
126 '& thead th, .VisibilityDialog-custom tr:last-child td': {
127 borderBottom: `1px solid ${theme.palette.divider}`,
128 },
129 },
130 // Hack to apply `text-overflow`.
131 '.VisibilityDialog-nowrap': {
132 maxWidth: '100%',
133 overflow: 'hidden',
134 wordWrap: 'nowrap',
135 textOverflow: 'ellipsis',
136 },
137 '.VisibilityDialog-buttons': {
138 marginTop: theme.spacing(2),
139 display: 'flex',
140 flexDirection: 'row',
141 justifyContent: 'flex-end',
142 },
143 '.VisibilityDialog-empty': {
144 display: 'flex',
145 flexDirection: 'column',
146 alignItems: 'center',
147 color: theme.palette.text.secondary,
148 },
149 '.VisibilityDialog-emptyIcon': {
150 fontSize: '6rem',
151 marginBottom: theme.spacing(1),
152 },
153 };
154});
155
156function VisibilityDialog({
157 graph,
158 close,
159 dialog,
160}: {
161 graph: GraphStore;
162 close: () => void;
163 dialog?: boolean;
164}): JSX.Element {
165 const builtinRows: JSX.Element[] = [];
166 const rows: JSX.Element[] = [];
167 graph.relationMetadata.forEach((metadata, name) => {
168 if (!isVisibilityAllowed(metadata, 'must')) {
169 return;
170 }
171 const visibility = graph.getVisibility(name);
172 const row = (
173 <tr key={metadata.name}>
174 <td>
175 <Checkbox
176 checked={visibility !== 'none'}
177 aria-label={`Show true and error values of ${metadata.simpleName}`}
178 onClick={() =>
179 graph.setVisibility(name, visibility === 'none' ? 'must' : 'none')
180 }
181 />
182 </td>
183 <td>
184 <Checkbox
185 checked={visibility === 'all'}
186 disabled={!isVisibilityAllowed(metadata, 'all')}
187 aria-label={`Show all values of ${metadata.simpleName}`}
188 onClick={() =>
189 graph.setVisibility(name, visibility === 'all' ? 'must' : 'all')
190 }
191 />
192 </td>
193 <td onClick={() => graph.cycleVisibility(name)}>
194 <div className="VisibilityDialog-nowrap">
195 <RelationName metadata={metadata} abbreviate={graph.abbreviate} />
196 </div>
197 </td>
198 </tr>
199 );
200 if (name.startsWith('builtin::')) {
201 builtinRows.push(row);
202 } else {
203 rows.push(row);
204 }
205 });
206
207 const hasRows = rows.length > 0 || builtinRows.length > 0;
208
209 return (
210 <VisibilityDialogRoot
211 dialog={dialog ?? VisibilityDialog.defaultProps.dialog}
212 >
213 <div className="VisibilityDialog-switch">
214 <FormControlLabel
215 control={
216 <Switch
217 checked={!graph.abbreviate}
218 onClick={() => graph.toggleAbbrevaite()}
219 />
220 }
221 label="Fully qualified names"
222 />
223 {dialog && (
224 <IconButton aria-label="Close" onClick={close}>
225 <CloseIcon />
226 </IconButton>
227 )}
228 </div>
229 <div className="VisibilityDialog-scroll">
230 {hasRows ? (
231 <table cellSpacing={0}>
232 <thead>
233 <tr>
234 <th>
235 <LabelIcon />
236 </th>
237 <th>
238 <LabelOutlinedIcon />
239 </th>
240 <th>Symbol</th>
241 </tr>
242 </thead>
243 <tbody className="VisibilityDialog-custom">{...rows}</tbody>
244 <tbody className="VisibilityDialog-builtin">{...builtinRows}</tbody>
245 </table>
246 ) : (
247 <div className="VisibilityDialog-empty">
248 <SentimentVeryDissatisfiedIcon
249 className="VisibilityDialog-emptyIcon"
250 fontSize="inherit"
251 />
252 <div>Partial model is empty</div>
253 </div>
254 )}
255 </div>
256 <div className="VisibilityDialog-buttons">
257 <Button
258 color="inherit"
259 onClick={() => graph.hideAll()}
260 startIcon={<VisibilityOffIcon />}
261 >
262 Hide all
263 </Button>
264 <Button
265 color="inherit"
266 onClick={() => graph.resetFilter()}
267 startIcon={<FilterListIcon />}
268 >
269 Reset filter
270 </Button>
271 {!dialog && (
272 <Button color="inherit" onClick={close}>
273 Close
274 </Button>
275 )}
276 </div>
277 </VisibilityDialogRoot>
278 );
279}
280
281VisibilityDialog.defaultProps = {
282 dialog: false,
283};
284
285export default observer(VisibilityDialog);
diff --git a/subprojects/frontend/src/graph/VisibilityPanel.tsx b/subprojects/frontend/src/graph/VisibilityPanel.tsx
new file mode 100644
index 00000000..c951dee2
--- /dev/null
+++ b/subprojects/frontend/src/graph/VisibilityPanel.tsx
@@ -0,0 +1,85 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
8import TuneIcon from '@mui/icons-material/Tune';
9import Badge from '@mui/material/Badge';
10import Dialog from '@mui/material/Dialog';
11import IconButton from '@mui/material/IconButton';
12import Paper from '@mui/material/Paper';
13import Slide from '@mui/material/Slide';
14import { styled } from '@mui/material/styles';
15import { observer } from 'mobx-react-lite';
16import { useCallback, useId, useState } from 'react';
17
18import type GraphStore from './GraphStore';
19import VisibilityDialog from './VisibilityDialog';
20
21const VisibilityPanelRoot = styled('div', {
22 name: 'VisibilityPanel-Root',
23})(({ theme }) => ({
24 position: 'absolute',
25 padding: theme.spacing(1),
26 top: 0,
27 left: 0,
28 maxHeight: '100%',
29 maxWidth: '100%',
30 overflow: 'hidden',
31 display: 'flex',
32 flexDirection: 'column',
33 alignItems: 'start',
34 '.VisibilityPanel-drawer': {
35 overflow: 'hidden',
36 display: 'flex',
37 maxWidth: '100%',
38 margin: theme.spacing(1),
39 },
40}));
41
42function VisibilityPanel({
43 graph,
44 dialog,
45}: {
46 graph: GraphStore;
47 dialog: boolean;
48}): JSX.Element {
49 const id = useId();
50 const [showFilter, setShowFilter] = useState(false);
51 const close = useCallback(() => setShowFilter(false), []);
52
53 return (
54 <VisibilityPanelRoot>
55 <IconButton
56 role="switch"
57 aria-checked={showFilter}
58 aria-controls={dialog ? undefined : id}
59 aria-label="Show filter panel"
60 onClick={() => setShowFilter(!showFilter)}
61 >
62 <Badge
63 color="primary"
64 variant="dot"
65 invisible={graph.visibility.size === 0}
66 >
67 {showFilter && !dialog ? <ChevronLeftIcon /> : <TuneIcon />}
68 </Badge>
69 </IconButton>
70 {dialog ? (
71 <Dialog open={showFilter} onClose={close} maxWidth="xl">
72 <VisibilityDialog graph={graph} close={close} dialog />
73 </Dialog>
74 ) : (
75 <Slide direction="right" in={showFilter} id={id}>
76 <Paper className="VisibilityPanel-drawer" elevation={4}>
77 <VisibilityDialog graph={graph} close={close} />
78 </Paper>
79 </Slide>
80 )}
81 </VisibilityPanelRoot>
82 );
83}
84
85export default observer(VisibilityPanel);
diff --git a/subprojects/frontend/src/graph/dotSource.ts b/subprojects/frontend/src/graph/dotSource.ts
index 2d6b57de..701453f4 100644
--- a/subprojects/frontend/src/graph/dotSource.ts
+++ b/subprojects/frontend/src/graph/dotSource.ts
@@ -15,25 +15,28 @@ const EDGE_WEIGHT = 1;
15const CONTAINMENT_WEIGHT = 5; 15const CONTAINMENT_WEIGHT = 5;
16const UNKNOWN_WEIGHT_FACTOR = 0.5; 16const UNKNOWN_WEIGHT_FACTOR = 0.5;
17 17
18function nodeName({ simpleName, kind }: NodeMetadata): string { 18function nodeName(graph: GraphStore, metadata: NodeMetadata): string {
19 switch (kind) { 19 const name = graph.getName(metadata);
20 switch (metadata.kind) {
20 case 'INDIVIDUAL': 21 case 'INDIVIDUAL':
21 return `<b>${simpleName}</b>`; 22 return `<b>${name}</b>`;
22 case 'NEW': 23 case 'NEW':
23 return `<i>${simpleName}</i>`; 24 return `<i>${name}</i>`;
24 default: 25 default:
25 return simpleName; 26 return name;
26 } 27 }
27} 28}
28 29
29function relationName({ simpleName, detail }: RelationMetadata): string { 30function relationName(graph: GraphStore, metadata: RelationMetadata): string {
31 const name = graph.getName(metadata);
32 const { detail } = metadata;
30 if (detail.type === 'class' && detail.abstractClass) { 33 if (detail.type === 'class' && detail.abstractClass) {
31 return `<i>${simpleName}</i>`; 34 return `<i>${name}</i>`;
32 } 35 }
33 if (detail.type === 'reference' && detail.containment) { 36 if (detail.type === 'reference' && detail.containment) {
34 return `<b>${simpleName}</b>`; 37 return `<b>${name}</b>`;
35 } 38 }
36 return simpleName; 39 return name;
37} 40}
38 41
39interface NodeData { 42interface NodeData {
@@ -57,7 +60,7 @@ function computeNodeData(graph: GraphStore): NodeData[] {
57 if (relation.arity !== 1) { 60 if (relation.arity !== 1) {
58 return; 61 return;
59 } 62 }
60 const visibility = graph.getVisiblity(relation.name); 63 const visibility = graph.getVisibility(relation.name);
61 if (visibility === 'none') { 64 if (visibility === 'none') {
62 return; 65 return;
63 } 66 }
@@ -112,7 +115,7 @@ function createNodes(graph: GraphStore, lines: string[]): void {
112 const classes = [ 115 const classes = [
113 `node-${node.kind} node-exists-${data.exists} node-equalsSelf-${data.equalsSelf}`, 116 `node-${node.kind} node-exists-${data.exists} node-equalsSelf-${data.equalsSelf}`,
114 ].join(' '); 117 ].join(' ');
115 const name = nodeName(node); 118 const name = nodeName(graph, node);
116 const border = node.kind === 'INDIVIDUAL' ? 2 : 1; 119 const border = node.kind === 'INDIVIDUAL' ? 2 : 1;
117 lines.push(`n${i} [id="${node.name}", class="${classes}", label=< 120 lines.push(`n${i} [id="${node.name}", class="${classes}", label=<
118 <table border="${border}" cellborder="0" cellspacing="0" style="rounded" bgcolor="white"> 121 <table border="${border}" cellborder="0" cellspacing="0" style="rounded" bgcolor="white">
@@ -128,7 +131,7 @@ function createNodes(graph: GraphStore, lines: string[]): void {
128 <td width="1.5"></td> 131 <td width="1.5"></td>
129 <td align="left" href="#${value}" id="${node.name},${ 132 <td align="left" href="#${value}" id="${node.name},${
130 relation.name 133 relation.name
131 },label">${relationName(relation)}</td> 134 },label">${relationName(graph, relation)}</td>
132 </tr>`, 135 </tr>`,
133 ); 136 );
134 }); 137 });
@@ -205,14 +208,15 @@ function createRelationEdges(
205 let constraint: 'true' | 'false' = 'true'; 208 let constraint: 'true' | 'false' = 'true';
206 let weight = EDGE_WEIGHT; 209 let weight = EDGE_WEIGHT;
207 let penwidth = 1; 210 let penwidth = 1;
208 let label = `"${relation.simpleName}"`; 211 const name = graph.getName(relation);
212 let label = `"${name}"`;
209 if (detail.type === 'reference' && detail.containment) { 213 if (detail.type === 'reference' && detail.containment) {
210 weight = CONTAINMENT_WEIGHT; 214 weight = CONTAINMENT_WEIGHT;
211 label = `<<b>${relation.simpleName}</b>>`; 215 label = `<<b>${name}</b>>`;
212 penwidth = 2; 216 penwidth = 2;
213 } else if ( 217 } else if (
214 detail.type === 'opposite' && 218 detail.type === 'opposite' &&
215 graph.getVisiblity(detail.opposite) !== 'none' 219 graph.getVisibility(detail.opposite) !== 'none'
216 ) { 220 ) {
217 constraint = 'false'; 221 constraint = 'false';
218 weight = 0; 222 weight = 0;
@@ -284,7 +288,7 @@ function createEdges(graph: GraphStore, lines: string[]): void {
284 if (relation.arity !== 2) { 288 if (relation.arity !== 2) {
285 return; 289 return;
286 } 290 }
287 const visibility = graph.getVisiblity(relation.name); 291 const visibility = graph.getVisibility(relation.name);
288 if (visibility !== 'none') { 292 if (visibility !== 'none') {
289 createRelationEdges(graph, relation, visibility === 'all', lines); 293 createRelationEdges(graph, relation, visibility === 'all', lines);
290 } 294 }