diff options
Diffstat (limited to 'subprojects/frontend/src/graph/VisibilityPanel.tsx')
-rw-r--r-- | subprojects/frontend/src/graph/VisibilityPanel.tsx | 303 |
1 files changed, 240 insertions, 63 deletions
diff --git a/subprojects/frontend/src/graph/VisibilityPanel.tsx b/subprojects/frontend/src/graph/VisibilityPanel.tsx index 20c4ffca..210ff5d5 100644 --- a/subprojects/frontend/src/graph/VisibilityPanel.tsx +++ b/subprojects/frontend/src/graph/VisibilityPanel.tsx | |||
@@ -1,43 +1,133 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | 6 | ||
7 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; | 7 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; |
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'; | ||
8 | import TuneIcon from '@mui/icons-material/Tune'; | 12 | import TuneIcon from '@mui/icons-material/Tune'; |
13 | import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; | ||
9 | import Badge from '@mui/material/Badge'; | 14 | import Badge from '@mui/material/Badge'; |
10 | import Dialog from '@mui/material/Dialog'; | 15 | import Button from '@mui/material/Button'; |
11 | import IconButton from '@mui/material/IconButton'; | 16 | import Checkbox from '@mui/material/Checkbox'; |
12 | import Paper from '@mui/material/Paper'; | 17 | import FormControlLabel from '@mui/material/FormControlLabel'; |
13 | import Slide from '@mui/material/Slide'; | 18 | import Switch from '@mui/material/Switch'; |
14 | import { styled } from '@mui/material/styles'; | 19 | import { styled } from '@mui/material/styles'; |
15 | import { observer } from 'mobx-react-lite'; | 20 | import { observer } from 'mobx-react-lite'; |
16 | import { useCallback, useId, useState } from 'react'; | 21 | import { useCallback } from 'react'; |
17 | 22 | ||
18 | import type GraphStore from './GraphStore'; | 23 | import type GraphStore from './GraphStore'; |
19 | import VisibilityDialog from './VisibilityDialog'; | 24 | import { isVisibilityAllowed } from './GraphStore'; |
25 | import RelationName from './RelationName'; | ||
26 | import SlideInPanel from './SlideInPanel'; | ||
20 | 27 | ||
21 | const VisibilityPanelRoot = styled('div', { | 28 | const VisibilityDialogScroll = styled('div', { |
22 | name: 'VisibilityPanel-Root', | 29 | name: 'VisibilityDialog-Scroll', |
23 | })(({ theme }) => ({ | 30 | shouldForwardProp: (propName) => propName !== 'dialog', |
24 | position: 'absolute', | 31 | })<{ dialog: boolean }>(({ theme, dialog }) => { |
25 | padding: theme.spacing(1), | 32 | const overlayOpacity = dialog ? 0.16 : 0.09; |
26 | top: 0, | 33 | return { |
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', | 34 | display: 'flex', |
37 | maxWidth: '100%', | 35 | flexDirection: 'column', |
38 | margin: theme.spacing(1), | 36 | height: 'auto', |
39 | }, | 37 | overflowX: 'hidden', |
40 | })); | 38 | overflowY: 'auto', |
39 | margin: `0 ${theme.spacing(2)}`, | ||
40 | '& table': { | ||
41 | // We use flexbox instead of `display: table` to get proper text-overflow | ||
42 | // behavior for overly long relation names. | ||
43 | display: 'flex', | ||
44 | flexDirection: 'column', | ||
45 | }, | ||
46 | '& thead, & tbody': { | ||
47 | display: 'flex', | ||
48 | flexDirection: 'column', | ||
49 | }, | ||
50 | '& thead': { | ||
51 | position: 'sticky', | ||
52 | top: 0, | ||
53 | zIndex: 999, | ||
54 | backgroundColor: theme.palette.background.paper, | ||
55 | ...(theme.palette.mode === 'dark' | ||
56 | ? { | ||
57 | // In dark mode, MUI Paper gets a lighter overlay. | ||
58 | backgroundImage: `linear-gradient( | ||
59 | rgba(255, 255, 255, ${overlayOpacity}), | ||
60 | rgba(255, 255, 255, ${overlayOpacity}) | ||
61 | )`, | ||
62 | } | ||
63 | : {}), | ||
64 | '& tr': { | ||
65 | height: '44px', | ||
66 | }, | ||
67 | }, | ||
68 | '& tr': { | ||
69 | display: 'flex', | ||
70 | flexDirection: 'row', | ||
71 | maxWidth: '100%', | ||
72 | }, | ||
73 | '& tbody tr': { | ||
74 | transition: theme.transitions.create('background', { | ||
75 | duration: theme.transitions.duration.shortest, | ||
76 | }), | ||
77 | '&:hover': { | ||
78 | background: theme.palette.action.hover, | ||
79 | '@media (hover: none)': { | ||
80 | background: 'transparent', | ||
81 | }, | ||
82 | }, | ||
83 | }, | ||
84 | '& th, & td': { | ||
85 | display: 'flex', | ||
86 | flexDirection: 'row', | ||
87 | alignItems: 'center', | ||
88 | justifyContent: 'center', | ||
89 | // Set width in advance, since we can't rely on `display: table-cell`. | ||
90 | width: '44px', | ||
91 | }, | ||
92 | '& th:nth-of-type(3), & td:nth-of-type(3)': { | ||
93 | justifyContent: 'start', | ||
94 | paddingLeft: theme.spacing(1), | ||
95 | paddingRight: theme.spacing(2), | ||
96 | // Only let the last column grow or shrink. | ||
97 | flexGrow: 1, | ||
98 | flexShrink: 1, | ||
99 | // Compute the maximum available space in advance to let the text overflow. | ||
100 | maxWidth: 'calc(100% - 88px)', | ||
101 | width: 'min-content', | ||
102 | }, | ||
103 | '& td:nth-of-type(3)': { | ||
104 | cursor: 'pointer', | ||
105 | userSelect: 'none', | ||
106 | WebkitTapHighlightColor: 'transparent', | ||
107 | }, | ||
108 | |||
109 | '& thead th, .VisibilityDialog-custom tr:last-child td': { | ||
110 | borderBottom: `1px solid ${theme.palette.divider}`, | ||
111 | }, | ||
112 | // Hack to apply `text-overflow`. | ||
113 | '.VisibilityDialog-nowrap': { | ||
114 | maxWidth: '100%', | ||
115 | overflow: 'hidden', | ||
116 | wordWrap: 'nowrap', | ||
117 | textOverflow: 'ellipsis', | ||
118 | }, | ||
119 | '.VisibilityDialog-empty': { | ||
120 | display: 'flex', | ||
121 | flexDirection: 'column', | ||
122 | alignItems: 'center', | ||
123 | color: theme.palette.text.secondary, | ||
124 | }, | ||
125 | '.VisibilityDialog-emptyIcon': { | ||
126 | fontSize: '6rem', | ||
127 | marginBottom: theme.spacing(1), | ||
128 | }, | ||
129 | }; | ||
130 | }); | ||
41 | 131 | ||
42 | function VisibilityPanel({ | 132 | function VisibilityPanel({ |
43 | graph, | 133 | graph, |
@@ -46,45 +136,132 @@ function VisibilityPanel({ | |||
46 | graph: GraphStore; | 136 | graph: GraphStore; |
47 | dialog: boolean; | 137 | dialog: boolean; |
48 | }): JSX.Element { | 138 | }): JSX.Element { |
49 | const id = useId(); | 139 | const builtinRows: JSX.Element[] = []; |
50 | const [showFilter, setShowFilter] = useState(false); | 140 | const rows: JSX.Element[] = []; |
51 | const close = useCallback(() => setShowFilter(false), []); | 141 | graph.relationMetadata.forEach((metadata, name) => { |
142 | if (!isVisibilityAllowed(metadata, 'must')) { | ||
143 | return; | ||
144 | } | ||
145 | const visibility = graph.getVisibility(name); | ||
146 | const row = ( | ||
147 | <tr key={metadata.name}> | ||
148 | <td> | ||
149 | <Checkbox | ||
150 | checked={visibility !== 'none'} | ||
151 | aria-label={`Show true and error values of ${metadata.simpleName}`} | ||
152 | onClick={() => | ||
153 | graph.setVisibility(name, visibility === 'none' ? 'must' : 'none') | ||
154 | } | ||
155 | /> | ||
156 | </td> | ||
157 | <td> | ||
158 | <Checkbox | ||
159 | checked={visibility === 'all'} | ||
160 | disabled={!isVisibilityAllowed(metadata, 'all')} | ||
161 | aria-label={`Show all values of ${metadata.simpleName}`} | ||
162 | onClick={() => | ||
163 | graph.setVisibility(name, visibility === 'all' ? 'must' : 'all') | ||
164 | } | ||
165 | /> | ||
166 | </td> | ||
167 | <td | ||
168 | onClick={() => graph.cycleVisibility(name)} | ||
169 | aria-label="Toggle visiblity" | ||
170 | > | ||
171 | <div className="VisibilityDialog-nowrap"> | ||
172 | <RelationName metadata={metadata} abbreviate={graph.abbreviate} /> | ||
173 | </div> | ||
174 | </td> | ||
175 | </tr> | ||
176 | ); | ||
177 | if (name.startsWith('builtin::')) { | ||
178 | builtinRows.push(row); | ||
179 | } else { | ||
180 | rows.push(row); | ||
181 | } | ||
182 | }); | ||
183 | |||
184 | const hasRows = rows.length > 0 || builtinRows.length > 0; | ||
185 | |||
186 | const hideBadge = graph.visibility.size === 0; | ||
187 | const icon = useCallback( | ||
188 | (show: boolean) => ( | ||
189 | <Badge color="primary" variant="dot" invisible={hideBadge}> | ||
190 | {show && !dialog ? <ChevronLeftIcon /> : <TuneIcon />} | ||
191 | </Badge> | ||
192 | ), | ||
193 | [dialog, hideBadge], | ||
194 | ); | ||
52 | 195 | ||
53 | return ( | 196 | return ( |
54 | <VisibilityPanelRoot> | 197 | <SlideInPanel |
55 | <IconButton | 198 | anchor="left" |
56 | role="switch" | 199 | dialog={dialog} |
57 | aria-checked={showFilter} | 200 | title="Customize view" |
58 | aria-controls={dialog ? undefined : id} | 201 | icon={icon} |
59 | aria-label="Show filter panel" | 202 | iconLabel="Show filter panel" |
60 | onClick={() => setShowFilter(!showFilter)} | 203 | buttons={ |
61 | > | 204 | <> |
62 | <Badge | 205 | <Button |
63 | color="primary" | 206 | color="inherit" |
64 | variant="dot" | 207 | onClick={() => graph.hideAll()} |
65 | invisible={graph.visibility.size === 0} | 208 | startIcon={<VisibilityOffIcon />} |
66 | > | 209 | > |
67 | {showFilter && !dialog ? <ChevronLeftIcon /> : <TuneIcon />} | 210 | Hide all |
68 | </Badge> | 211 | </Button> |
69 | </IconButton> | 212 | <Button |
70 | {dialog ? ( | 213 | color="inherit" |
71 | <Dialog open={showFilter} onClose={close} maxWidth="xl"> | 214 | onClick={() => graph.resetFilter()} |
72 | <VisibilityDialog graph={graph} close={close} dialog /> | 215 | startIcon={<FilterListIcon />} |
73 | </Dialog> | 216 | > |
74 | ) : ( | 217 | Reset filter |
75 | <Slide | 218 | </Button> |
76 | direction="right" | 219 | </> |
77 | in={showFilter} | 220 | } |
78 | id={id} | 221 | > |
79 | mountOnEnter | 222 | <FormControlLabel |
80 | unmountOnExit | 223 | control={ |
81 | > | 224 | <Switch |
82 | <Paper className="VisibilityPanel-drawer" elevation={4}> | 225 | checked={!graph.abbreviate} |
83 | <VisibilityDialog graph={graph} close={close} /> | 226 | onClick={() => graph.toggleAbbrevaite()} |
84 | </Paper> | 227 | /> |
85 | </Slide> | 228 | } |
86 | )} | 229 | label="Fully qualified names" |
87 | </VisibilityPanelRoot> | 230 | /> |
231 | <FormControlLabel | ||
232 | control={ | ||
233 | <Switch checked={graph.scopes} onClick={() => graph.toggleScopes()} /> | ||
234 | } | ||
235 | label="Object scopes" | ||
236 | /> | ||
237 | <VisibilityDialogScroll dialog={dialog}> | ||
238 | {hasRows ? ( | ||
239 | <table cellSpacing={0}> | ||
240 | <thead> | ||
241 | <tr> | ||
242 | <th aria-label="Show true and error values"> | ||
243 | <LabelIcon /> | ||
244 | </th> | ||
245 | <th aria-label="Show unknown values"> | ||
246 | <LabelOutlinedIcon /> | ||
247 | </th> | ||
248 | <th>Symbol</th> | ||
249 | </tr> | ||
250 | </thead> | ||
251 | <tbody className="VisibilityDialog-custom">{...rows}</tbody> | ||
252 | <tbody className="VisibilityDialog-builtin">{...builtinRows}</tbody> | ||
253 | </table> | ||
254 | ) : ( | ||
255 | <div className="VisibilityDialog-empty"> | ||
256 | <SentimentVeryDissatisfiedIcon | ||
257 | className="VisibilityDialog-emptyIcon" | ||
258 | fontSize="inherit" | ||
259 | /> | ||
260 | <div>Partial model is empty</div> | ||
261 | </div> | ||
262 | )} | ||
263 | </VisibilityDialogScroll> | ||
264 | </SlideInPanel> | ||
88 | ); | 265 | ); |
89 | } | 266 | } |
90 | 267 | ||