diff options
Diffstat (limited to 'subprojects/frontend/src/table')
-rw-r--r-- | subprojects/frontend/src/table/RelationGrid.tsx | 109 | ||||
-rw-r--r-- | subprojects/frontend/src/table/SymbolSelector.tsx | 65 | ||||
-rw-r--r-- | subprojects/frontend/src/table/TableArea.tsx | 24 | ||||
-rw-r--r-- | subprojects/frontend/src/table/TablePane.tsx | 22 | ||||
-rw-r--r-- | subprojects/frontend/src/table/TableToolbar.tsx | 41 | ||||
-rw-r--r-- | subprojects/frontend/src/table/ValueRenderer.tsx | 62 |
6 files changed, 323 insertions, 0 deletions
diff --git a/subprojects/frontend/src/table/RelationGrid.tsx b/subprojects/frontend/src/table/RelationGrid.tsx new file mode 100644 index 00000000..004982c9 --- /dev/null +++ b/subprojects/frontend/src/table/RelationGrid.tsx | |||
@@ -0,0 +1,109 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import Box from '@mui/material/Box'; | ||
8 | import { | ||
9 | DataGrid, | ||
10 | type GridRenderCellParams, | ||
11 | type GridColDef, | ||
12 | } from '@mui/x-data-grid'; | ||
13 | import { observer } from 'mobx-react-lite'; | ||
14 | import { useMemo } from 'react'; | ||
15 | |||
16 | import type GraphStore from '../graph/GraphStore'; | ||
17 | |||
18 | import TableToolbar from './TableToolbar'; | ||
19 | import ValueRenderer from './ValueRenderer'; | ||
20 | |||
21 | interface Row { | ||
22 | nodes: string[]; | ||
23 | value: string; | ||
24 | } | ||
25 | |||
26 | function RelationGrid({ graph }: { graph: GraphStore }): JSX.Element { | ||
27 | const { | ||
28 | selectedSymbol, | ||
29 | semantics: { nodes, partialInterpretation }, | ||
30 | } = graph; | ||
31 | const symbolName = selectedSymbol?.name; | ||
32 | const arity = selectedSymbol?.arity ?? 0; | ||
33 | |||
34 | const columns = useMemo<GridColDef<Row>[]>(() => { | ||
35 | const defs: GridColDef<Row>[] = []; | ||
36 | for (let i = 0; i < arity; i += 1) { | ||
37 | defs.push({ | ||
38 | field: `n${i}`, | ||
39 | headerName: String(i + 1), | ||
40 | valueGetter: (row) => row.row.nodes[i] ?? '', | ||
41 | flex: 1, | ||
42 | }); | ||
43 | } | ||
44 | defs.push({ | ||
45 | field: 'value', | ||
46 | headerName: 'Value', | ||
47 | flex: 1, | ||
48 | renderCell: ({ value }: GridRenderCellParams<Row, string>) => ( | ||
49 | <ValueRenderer value={value} /> | ||
50 | ), | ||
51 | }); | ||
52 | return defs; | ||
53 | }, [arity]); | ||
54 | |||
55 | const rows = useMemo<Row[]>(() => { | ||
56 | if (symbolName === undefined) { | ||
57 | return []; | ||
58 | } | ||
59 | const interpretation = partialInterpretation[symbolName] ?? []; | ||
60 | return interpretation.map((tuple) => { | ||
61 | const nodeNames: string[] = []; | ||
62 | for (let i = 0; i < arity; i += 1) { | ||
63 | const index = tuple[i]; | ||
64 | if (typeof index === 'number') { | ||
65 | const node = nodes[index]; | ||
66 | if (node !== undefined) { | ||
67 | nodeNames.push(node.name); | ||
68 | } | ||
69 | } | ||
70 | } | ||
71 | return { | ||
72 | nodes: nodeNames, | ||
73 | value: String(tuple[arity]), | ||
74 | }; | ||
75 | }); | ||
76 | }, [arity, nodes, partialInterpretation, symbolName]); | ||
77 | |||
78 | return ( | ||
79 | <Box | ||
80 | width="100%" | ||
81 | height="100%" | ||
82 | p={1} | ||
83 | sx={(theme) => ({ | ||
84 | '.MuiDataGrid-withBorderColor': { | ||
85 | borderColor: | ||
86 | theme.palette.mode === 'dark' | ||
87 | ? theme.palette.divider | ||
88 | : theme.palette.outer.border, | ||
89 | }, | ||
90 | })} | ||
91 | > | ||
92 | <DataGrid | ||
93 | slots={{ toolbar: TableToolbar }} | ||
94 | slotProps={{ | ||
95 | toolbar: { | ||
96 | graph, | ||
97 | }, | ||
98 | }} | ||
99 | density="compact" | ||
100 | rowSelection={false} | ||
101 | columns={columns} | ||
102 | rows={rows} | ||
103 | getRowId={(row) => row.nodes.join(',')} | ||
104 | /> | ||
105 | </Box> | ||
106 | ); | ||
107 | } | ||
108 | |||
109 | export default observer(RelationGrid); | ||
diff --git a/subprojects/frontend/src/table/SymbolSelector.tsx b/subprojects/frontend/src/table/SymbolSelector.tsx new file mode 100644 index 00000000..5272f8ed --- /dev/null +++ b/subprojects/frontend/src/table/SymbolSelector.tsx | |||
@@ -0,0 +1,65 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import Autocomplete from '@mui/material/Autocomplete'; | ||
8 | import Box from '@mui/material/Box'; | ||
9 | import TextField from '@mui/material/TextField'; | ||
10 | import { observer } from 'mobx-react-lite'; | ||
11 | |||
12 | import type GraphStore from '../graph/GraphStore'; | ||
13 | import RelationName from '../graph/RelationName'; | ||
14 | |||
15 | function SymbolSelector({ graph }: { graph: GraphStore }): JSX.Element { | ||
16 | const { | ||
17 | selectedSymbol, | ||
18 | semantics: { relations }, | ||
19 | } = graph; | ||
20 | |||
21 | return ( | ||
22 | <Autocomplete | ||
23 | renderInput={(params) => ( | ||
24 | <TextField | ||
25 | {...{ | ||
26 | ...params, | ||
27 | InputLabelProps: { | ||
28 | ...params.InputLabelProps, | ||
29 | // Workaround for type errors. | ||
30 | className: params.InputLabelProps.className ?? '', | ||
31 | style: params.InputLabelProps.style ?? {}, | ||
32 | }, | ||
33 | }} | ||
34 | variant="standard" | ||
35 | size="medium" | ||
36 | placeholder="Symbol" | ||
37 | /> | ||
38 | )} | ||
39 | options={relations} | ||
40 | getOptionLabel={(option) => option.name} | ||
41 | renderOption={(props, option) => ( | ||
42 | <Box component="li" {...props}> | ||
43 | <RelationName metadata={option} /> | ||
44 | </Box> | ||
45 | )} | ||
46 | value={selectedSymbol ?? null} | ||
47 | isOptionEqualToValue={(option, value) => option.name === value.name} | ||
48 | onChange={(_event, value) => graph.setSelectedSymbol(value ?? undefined)} | ||
49 | sx={(theme) => ({ | ||
50 | flexBasis: 200, | ||
51 | maxWidth: 600, | ||
52 | flexGrow: 1, | ||
53 | flexShrink: 1, | ||
54 | '.MuiInput-underline::before': { | ||
55 | borderColor: | ||
56 | theme.palette.mode === 'dark' | ||
57 | ? theme.palette.divider | ||
58 | : theme.palette.outer.border, | ||
59 | }, | ||
60 | })} | ||
61 | /> | ||
62 | ); | ||
63 | } | ||
64 | |||
65 | export default observer(SymbolSelector); | ||
diff --git a/subprojects/frontend/src/table/TableArea.tsx b/subprojects/frontend/src/table/TableArea.tsx new file mode 100644 index 00000000..cf37b96a --- /dev/null +++ b/subprojects/frontend/src/table/TableArea.tsx | |||
@@ -0,0 +1,24 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import { observer } from 'mobx-react-lite'; | ||
8 | |||
9 | import Loading from '../Loading'; | ||
10 | import { useRootStore } from '../RootStoreProvider'; | ||
11 | |||
12 | import RelationGrid from './RelationGrid'; | ||
13 | |||
14 | function TablePane(): JSX.Element { | ||
15 | const { editorStore } = useRootStore(); | ||
16 | |||
17 | if (editorStore === undefined) { | ||
18 | return <Loading />; | ||
19 | } | ||
20 | |||
21 | return <RelationGrid graph={editorStore.graph} />; | ||
22 | } | ||
23 | |||
24 | export default observer(TablePane); | ||
diff --git a/subprojects/frontend/src/table/TablePane.tsx b/subprojects/frontend/src/table/TablePane.tsx new file mode 100644 index 00000000..01442c3a --- /dev/null +++ b/subprojects/frontend/src/table/TablePane.tsx | |||
@@ -0,0 +1,22 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import Stack from '@mui/material/Stack'; | ||
8 | import { Suspense, lazy } from 'react'; | ||
9 | |||
10 | import Loading from '../Loading'; | ||
11 | |||
12 | const TableArea = lazy(() => import('./TableArea')); | ||
13 | |||
14 | export default function TablePane(): JSX.Element { | ||
15 | return ( | ||
16 | <Stack direction="column" height="100%" overflow="auto" alignItems="center"> | ||
17 | <Suspense fallback={<Loading />}> | ||
18 | <TableArea /> | ||
19 | </Suspense> | ||
20 | </Stack> | ||
21 | ); | ||
22 | } | ||
diff --git a/subprojects/frontend/src/table/TableToolbar.tsx b/subprojects/frontend/src/table/TableToolbar.tsx new file mode 100644 index 00000000..b14e73c5 --- /dev/null +++ b/subprojects/frontend/src/table/TableToolbar.tsx | |||
@@ -0,0 +1,41 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import Stack from '@mui/material/Stack'; | ||
8 | import { | ||
9 | GridToolbarColumnsButton, | ||
10 | GridToolbarContainer, | ||
11 | GridToolbarExport, | ||
12 | GridToolbarFilterButton, | ||
13 | } from '@mui/x-data-grid'; | ||
14 | |||
15 | import type GraphStore from '../graph/GraphStore'; | ||
16 | |||
17 | import SymbolSelector from './SymbolSelector'; | ||
18 | |||
19 | export default function TableToolbar({ | ||
20 | graph, | ||
21 | }: { | ||
22 | graph: GraphStore; | ||
23 | }): JSX.Element { | ||
24 | return ( | ||
25 | <GridToolbarContainer | ||
26 | sx={{ | ||
27 | display: 'flex', | ||
28 | flexDirection: 'row', | ||
29 | flexWrap: 'wrap-reverse', | ||
30 | justifyContent: 'space-between', | ||
31 | }} | ||
32 | > | ||
33 | <Stack direction="row" flexWrap="wrap"> | ||
34 | <GridToolbarColumnsButton /> | ||
35 | <GridToolbarFilterButton /> | ||
36 | <GridToolbarExport /> | ||
37 | </Stack> | ||
38 | <SymbolSelector graph={graph} /> | ||
39 | </GridToolbarContainer> | ||
40 | ); | ||
41 | } | ||
diff --git a/subprojects/frontend/src/table/ValueRenderer.tsx b/subprojects/frontend/src/table/ValueRenderer.tsx new file mode 100644 index 00000000..ac5700e4 --- /dev/null +++ b/subprojects/frontend/src/table/ValueRenderer.tsx | |||
@@ -0,0 +1,62 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import CancelIcon from '@mui/icons-material/Cancel'; | ||
8 | import LabelIcon from '@mui/icons-material/Label'; | ||
9 | import LabelOutlinedIcon from '@mui/icons-material/LabelOutlined'; | ||
10 | import { styled } from '@mui/material/styles'; | ||
11 | |||
12 | const Label = styled('div', { | ||
13 | name: 'ValueRenderer-Label', | ||
14 | shouldForwardProp: (prop) => prop !== 'value', | ||
15 | })<{ | ||
16 | value: 'TRUE' | 'UNKNOWN' | 'ERROR'; | ||
17 | }>(({ theme, value }) => ({ | ||
18 | display: 'flex', | ||
19 | alignItems: 'center', | ||
20 | ...(value === 'UNKNOWN' | ||
21 | ? { | ||
22 | color: theme.palette.text.secondary, | ||
23 | } | ||
24 | : {}), | ||
25 | ...(value === 'ERROR' | ||
26 | ? { | ||
27 | color: theme.palette.error.main, | ||
28 | } | ||
29 | : {}), | ||
30 | '& svg': { | ||
31 | marginRight: theme.spacing(0.5), | ||
32 | }, | ||
33 | })); | ||
34 | |||
35 | export default function ValueRenderer({ | ||
36 | value, | ||
37 | }: { | ||
38 | value: string | undefined; | ||
39 | }): React.ReactNode { | ||
40 | switch (value) { | ||
41 | case 'TRUE': | ||
42 | return ( | ||
43 | <Label value={value}> | ||
44 | <LabelIcon fontSize="small" /> true | ||
45 | </Label> | ||
46 | ); | ||
47 | case 'UNKNOWN': | ||
48 | return ( | ||
49 | <Label value={value}> | ||
50 | <LabelOutlinedIcon fontSize="small" /> unknown | ||
51 | </Label> | ||
52 | ); | ||
53 | case 'ERROR': | ||
54 | return ( | ||
55 | <Label value={value}> | ||
56 | <CancelIcon fontSize="small" /> error | ||
57 | </Label> | ||
58 | ); | ||
59 | default: | ||
60 | return value; | ||
61 | } | ||
62 | } | ||