aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/graph/VisibilityPanel.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/graph/VisibilityPanel.tsx')
-rw-r--r--subprojects/frontend/src/graph/VisibilityPanel.tsx303
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
7import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; 7import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
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';
8import TuneIcon from '@mui/icons-material/Tune'; 12import TuneIcon from '@mui/icons-material/Tune';
13import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
9import Badge from '@mui/material/Badge'; 14import Badge from '@mui/material/Badge';
10import Dialog from '@mui/material/Dialog'; 15import Button from '@mui/material/Button';
11import IconButton from '@mui/material/IconButton'; 16import Checkbox from '@mui/material/Checkbox';
12import Paper from '@mui/material/Paper'; 17import FormControlLabel from '@mui/material/FormControlLabel';
13import Slide from '@mui/material/Slide'; 18import Switch from '@mui/material/Switch';
14import { styled } from '@mui/material/styles'; 19import { styled } from '@mui/material/styles';
15import { observer } from 'mobx-react-lite'; 20import { observer } from 'mobx-react-lite';
16import { useCallback, useId, useState } from 'react'; 21import { useCallback } from 'react';
17 22
18import type GraphStore from './GraphStore'; 23import type GraphStore from './GraphStore';
19import VisibilityDialog from './VisibilityDialog'; 24import { isVisibilityAllowed } from './GraphStore';
25import RelationName from './RelationName';
26import SlideInPanel from './SlideInPanel';
20 27
21const VisibilityPanelRoot = styled('div', { 28const 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
42function VisibilityPanel({ 132function 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