aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/graph/VisibilityDialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/graph/VisibilityDialog.tsx')
-rw-r--r--subprojects/frontend/src/graph/VisibilityDialog.tsx315
1 files changed, 315 insertions, 0 deletions
diff --git a/subprojects/frontend/src/graph/VisibilityDialog.tsx b/subprojects/frontend/src/graph/VisibilityDialog.tsx
new file mode 100644
index 00000000..f1fef28b
--- /dev/null
+++ b/subprojects/frontend/src/graph/VisibilityDialog.tsx
@@ -0,0 +1,315 @@
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 Typography from '@mui/material/Typography';
19import { styled } from '@mui/material/styles';
20import { observer } from 'mobx-react-lite';
21import { useId } from 'react';
22
23import type GraphStore from './GraphStore';
24import { isVisibilityAllowed } from './GraphStore';
25import RelationName from './RelationName';
26
27const VisibilityDialogRoot = styled('div', {
28 name: 'VisibilityDialog-Root',
29 shouldForwardProp: (propName) => propName !== 'dialog',
30})<{ dialog: boolean }>(({ theme, dialog }) => {
31 const overlayOpacity = dialog ? 0.16 : 0.09;
32 return {
33 maxHeight: '100%',
34 maxWidth: '100%',
35 overflow: 'hidden',
36 display: 'flex',
37 flexDirection: 'column',
38 '.VisibilityDialog-title': {
39 display: 'flex',
40 flexDirection: 'row',
41 alignItems: 'center',
42 padding: theme.spacing(1),
43 paddingLeft: theme.spacing(2),
44 borderBottom: `1px solid ${theme.palette.divider}`,
45 '& h2': {
46 flexGrow: 1,
47 },
48 '.MuiIconButton-root': {
49 flexGrow: 0,
50 flexShrink: 0,
51 marginLeft: theme.spacing(2),
52 },
53 },
54 '.MuiFormControlLabel-root': {
55 marginLeft: 0,
56 paddingTop: theme.spacing(1),
57 paddingLeft: theme.spacing(1),
58 '& + .MuiFormControlLabel-root': {
59 paddingTop: 0,
60 },
61 },
62 '.VisibilityDialog-scroll': {
63 display: 'flex',
64 flexDirection: 'column',
65 height: 'auto',
66 overflowX: 'hidden',
67 overflowY: 'auto',
68 margin: `0 ${theme.spacing(2)}`,
69 '& table': {
70 // We use flexbox instead of `display: table` to get proper text-overflow
71 // behavior for overly long relation names.
72 display: 'flex',
73 flexDirection: 'column',
74 },
75 '& thead, & tbody': {
76 display: 'flex',
77 flexDirection: 'column',
78 },
79 '& thead': {
80 position: 'sticky',
81 top: 0,
82 zIndex: 999,
83 backgroundColor: theme.palette.background.paper,
84 ...(theme.palette.mode === 'dark'
85 ? {
86 // In dark mode, MUI Paper gets a lighter overlay.
87 backgroundImage: `linear-gradient(
88 rgba(255, 255, 255, ${overlayOpacity}),
89 rgba(255, 255, 255, ${overlayOpacity})
90 )`,
91 }
92 : {}),
93 '& tr': {
94 height: '44px',
95 },
96 },
97 '& tr': {
98 display: 'flex',
99 flexDirection: 'row',
100 maxWidth: '100%',
101 },
102 '& tbody tr': {
103 transition: theme.transitions.create('background', {
104 duration: theme.transitions.duration.shortest,
105 }),
106 '&:hover': {
107 background: theme.palette.action.hover,
108 '@media (hover: none)': {
109 background: 'transparent',
110 },
111 },
112 },
113 '& th, & td': {
114 display: 'flex',
115 flexDirection: 'row',
116 alignItems: 'center',
117 justifyContent: 'center',
118 // Set width in advance, since we can't rely on `display: table-cell`.
119 width: '44px',
120 },
121 '& th:nth-of-type(3), & td:nth-of-type(3)': {
122 justifyContent: 'start',
123 paddingLeft: theme.spacing(1),
124 paddingRight: theme.spacing(2),
125 // Only let the last column grow or shrink.
126 flexGrow: 1,
127 flexShrink: 1,
128 // Compute the maximum available space in advance to let the text overflow.
129 maxWidth: 'calc(100% - 88px)',
130 width: 'min-content',
131 },
132 '& td:nth-of-type(3)': {
133 cursor: 'pointer',
134 userSelect: 'none',
135 WebkitTapHighlightColor: 'transparent',
136 },
137
138 '& thead th, .VisibilityDialog-custom tr:last-child td': {
139 borderBottom: `1px solid ${theme.palette.divider}`,
140 },
141 },
142 // Hack to apply `text-overflow`.
143 '.VisibilityDialog-nowrap': {
144 maxWidth: '100%',
145 overflow: 'hidden',
146 wordWrap: 'nowrap',
147 textOverflow: 'ellipsis',
148 },
149 '.VisibilityDialog-buttons': {
150 padding: theme.spacing(1),
151 display: 'flex',
152 flexDirection: 'row',
153 justifyContent: 'flex-end',
154 ...(dialog
155 ? {
156 marginTop: theme.spacing(1),
157 borderTop: `1px solid ${theme.palette.divider}`,
158 }
159 : {}),
160 },
161 '.VisibilityDialog-empty': {
162 display: 'flex',
163 flexDirection: 'column',
164 alignItems: 'center',
165 color: theme.palette.text.secondary,
166 },
167 '.VisibilityDialog-emptyIcon': {
168 fontSize: '6rem',
169 marginBottom: theme.spacing(1),
170 },
171 };
172});
173
174function VisibilityDialog({
175 graph,
176 close,
177 dialog,
178}: {
179 graph: GraphStore;
180 close: () => void;
181 dialog?: boolean;
182}): JSX.Element {
183 const titleId = useId();
184
185 const builtinRows: JSX.Element[] = [];
186 const rows: JSX.Element[] = [];
187 graph.relationMetadata.forEach((metadata, name) => {
188 if (!isVisibilityAllowed(metadata, 'must')) {
189 return;
190 }
191 const visibility = graph.getVisibility(name);
192 const row = (
193 <tr key={metadata.name}>
194 <td>
195 <Checkbox
196 checked={visibility !== 'none'}
197 aria-label={`Show true and error values of ${metadata.simpleName}`}
198 onClick={() =>
199 graph.setVisibility(name, visibility === 'none' ? 'must' : 'none')
200 }
201 />
202 </td>
203 <td>
204 <Checkbox
205 checked={visibility === 'all'}
206 disabled={!isVisibilityAllowed(metadata, 'all')}
207 aria-label={`Show all values of ${metadata.simpleName}`}
208 onClick={() =>
209 graph.setVisibility(name, visibility === 'all' ? 'must' : 'all')
210 }
211 />
212 </td>
213 <td onClick={() => graph.cycleVisibility(name)}>
214 <div className="VisibilityDialog-nowrap">
215 <RelationName metadata={metadata} abbreviate={graph.abbreviate} />
216 </div>
217 </td>
218 </tr>
219 );
220 if (name.startsWith('builtin::')) {
221 builtinRows.push(row);
222 } else {
223 rows.push(row);
224 }
225 });
226
227 const hasRows = rows.length > 0 || builtinRows.length > 0;
228
229 return (
230 <VisibilityDialogRoot
231 dialog={dialog ?? VisibilityDialog.defaultProps.dialog}
232 aria-labelledby={dialog ? titleId : undefined}
233 >
234 {dialog && (
235 <div className="VisibilityDialog-title">
236 <Typography variant="h6" component="h2" id={titleId}>
237 Customize view
238 </Typography>
239 <IconButton aria-label="Close" onClick={close}>
240 <CloseIcon />
241 </IconButton>
242 </div>
243 )}
244 <FormControlLabel
245 control={
246 <Switch
247 checked={!graph.abbreviate}
248 onClick={() => graph.toggleAbbrevaite()}
249 />
250 }
251 label="Fully qualified names"
252 />
253 <FormControlLabel
254 control={
255 <Switch checked={graph.scopes} onClick={() => graph.toggleScopes()} />
256 }
257 label="Object scopes"
258 />
259 <div className="VisibilityDialog-scroll">
260 {hasRows ? (
261 <table cellSpacing={0}>
262 <thead>
263 <tr>
264 <th>
265 <LabelIcon />
266 </th>
267 <th>
268 <LabelOutlinedIcon />
269 </th>
270 <th>Symbol</th>
271 </tr>
272 </thead>
273 <tbody className="VisibilityDialog-custom">{...rows}</tbody>
274 <tbody className="VisibilityDialog-builtin">{...builtinRows}</tbody>
275 </table>
276 ) : (
277 <div className="VisibilityDialog-empty">
278 <SentimentVeryDissatisfiedIcon
279 className="VisibilityDialog-emptyIcon"
280 fontSize="inherit"
281 />
282 <div>Partial model is empty</div>
283 </div>
284 )}
285 </div>
286 <div className="VisibilityDialog-buttons">
287 <Button
288 color="inherit"
289 onClick={() => graph.hideAll()}
290 startIcon={<VisibilityOffIcon />}
291 >
292 Hide all
293 </Button>
294 <Button
295 color="inherit"
296 onClick={() => graph.resetFilter()}
297 startIcon={<FilterListIcon />}
298 >
299 Reset filter
300 </Button>
301 {!dialog && (
302 <Button color="inherit" onClick={close}>
303 Close
304 </Button>
305 )}
306 </div>
307 </VisibilityDialogRoot>
308 );
309}
310
311VisibilityDialog.defaultProps = {
312 dialog: false,
313};
314
315export default observer(VisibilityDialog);