diff options
Diffstat (limited to 'subprojects/frontend/src/graph/VisibilityDialog.tsx')
-rw-r--r-- | subprojects/frontend/src/graph/VisibilityDialog.tsx | 285 |
1 files changed, 285 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..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 | |||
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 { styled } from '@mui/material/styles'; | ||
19 | import { observer } from 'mobx-react-lite'; | ||
20 | |||
21 | import type GraphStore from './GraphStore'; | ||
22 | import { isVisibilityAllowed } from './GraphStore'; | ||
23 | import RelationName from './RelationName'; | ||
24 | |||
25 | const 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 | |||
156 | function 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 | |||
281 | VisibilityDialog.defaultProps = { | ||
282 | dialog: false, | ||
283 | }; | ||
284 | |||
285 | export default observer(VisibilityDialog); | ||