diff options
Diffstat (limited to 'subprojects/frontend/src/graph/VisibilityDialog.tsx')
-rw-r--r-- | subprojects/frontend/src/graph/VisibilityDialog.tsx | 315 |
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 | |||
7 | import CloseIcon from '@mui/icons-material/Close'; | ||
8 | import FilterListIcon from '@mui/icons-material/FilterList'; | ||
9 | import LabelIcon from '@mui/icons-material/Label'; | ||
10 | import LabelOutlinedIcon from '@mui/icons-material/LabelOutlined'; | ||
11 | import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied'; | ||
12 | import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; | ||
13 | import Button from '@mui/material/Button'; | ||
14 | import Checkbox from '@mui/material/Checkbox'; | ||
15 | import FormControlLabel from '@mui/material/FormControlLabel'; | ||
16 | import IconButton from '@mui/material/IconButton'; | ||
17 | import Switch from '@mui/material/Switch'; | ||
18 | import Typography from '@mui/material/Typography'; | ||
19 | import { styled } from '@mui/material/styles'; | ||
20 | import { observer } from 'mobx-react-lite'; | ||
21 | import { useId } from 'react'; | ||
22 | |||
23 | import type GraphStore from './GraphStore'; | ||
24 | import { isVisibilityAllowed } from './GraphStore'; | ||
25 | import RelationName from './RelationName'; | ||
26 | |||
27 | const 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 | |||
174 | function 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 | |||
311 | VisibilityDialog.defaultProps = { | ||
312 | dialog: false, | ||
313 | }; | ||
314 | |||
315 | export default observer(VisibilityDialog); | ||