Files
nebula.js/commands/serve/web/components/Visualize/FieldsPopover.jsx
Tobias Åström 8ec407dd4a chore: mui v6 (#1612)
* chore: mui v6

* chore: add react-is

* chore: fix connection history

* chore: skip some tests

* chore: lint

* fix: some tests

* chore: get rid of v4 theme adapt

* chore: revert bg color changes

* chore: revert bg color changes
2025-09-11 10:04:07 +02:00

198 lines
5.9 KiB
JavaScript

import React, { useContext, useState, useMemo } from 'react';
import { Popover, List, ListSubheader, ListItemButton, ListItemText, ListItemIcon, Divider } from '@mui/material';
import parse from 'autosuggest-highlight/parse';
import match from 'autosuggest-highlight/match';
import { ChevronRight, ChevronLeft } from '@nebula.js/ui/icons';
import { useTheme } from '@nebula.js/ui/theme';
import useSessionModel from '@nebula.js/nucleus/src/hooks/useSessionModel';
import useLayout from '@nebula.js/nucleus/src/hooks/useLayout';
import useLibraryList from '../../hooks/useLibraryList';
import AppContext from '../../contexts/AppContext';
import Search from './Search';
function Field({ field, onSelect, sub, parts }) {
return (
<ListItemButton onClick={() => onSelect(field.qName)} data-key={field.qName}>
<ListItemText>
{parts.map((part, ix) => (
<span
// eslint-disable-next-line react/no-array-index-key
key={ix}
style={part.highlight ? { flex: '0 1 auto', backgroundColor: '#FFC72A' } : { flex: '0 1 auto' }}
>
{part.text}
</span>
))}
</ListItemText>
{sub && <ChevronRight fontSize="small" />}
</ListItemButton>
);
}
function LibraryItem({ item, onSelect, parts }) {
return (
<ListItemButton onClick={() => onSelect(item.qInfo)} data-key={item.qInfo.qId}>
<ListItemText>
{parts.map((part, ix) => (
<span
// eslint-disable-next-line react/no-array-index-key
key={ix}
style={part.highlight ? { flex: '0 1 auto', backgroundColor: '#FFC72A' } : { flex: '0 1 auto' }}
>
{part.text}
</span>
))}
</ListItemText>
</ListItemButton>
);
}
function Aggr({ aggr, field, onSelect }) {
return (
<ListItemButton onClick={() => onSelect(aggr)} data-key={aggr}>
<ListItemText>{`${aggr}(${field})`}</ListItemText>
</ListItemButton>
);
}
function LibraryList({ app, onSelect, title = '', type = 'dimension', searchTerm = '' }) {
const [libraryItems] = useLibraryList(app, type);
const sortedLibraryItems = useMemo(
() => libraryItems.slice().sort((a, b) => a.qData.title.toLowerCase().localeCompare(b.qData.title.toLowerCase())),
[libraryItems]
);
return libraryItems.length > 0 ? (
<>
<ListSubheader component="div" style={{ backgroundColor: 'inherit' }}>
{title}
</ListSubheader>
{sortedLibraryItems.map((item) => {
const matches = match(item.qData.title, searchTerm);
const parts = parse(item.qData.title, matches);
if (searchTerm && matches.length === 0) return null;
return <LibraryItem key={item.qInfo.qId} item={item} onSelect={onSelect} parts={parts} />;
})}
</>
) : null;
}
export default function FieldsPopover({ alignTo, show, close, onSelected, type }) {
const app = useContext(AppContext);
const [selectedField, setSelectedField] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const theme = useTheme();
const [model] = useSessionModel(
{
qInfo: {
qType: 'FieldList',
qId: 'FieldList',
},
qFieldListDef: {
qShowDerivedFelds: false,
qShowHidden: false,
qShowSemantic: true,
qShowSrcTables: true,
qShowSystem: false,
},
},
app
);
const [layout] = useLayout(model);
const fields = useMemo(
() =>
(layout ? layout.qFieldList.qItems || [] : [])
.slice()
.sort((a, b) => a.qName.toLowerCase().localeCompare(b.qName.toLowerCase())),
[layout]
);
const onSelect = (s) => {
if (s && s.qId) {
onSelected(s);
close();
} else if (type === 'measure') {
setSelectedField(s);
} else {
onSelected({
field: s,
});
close();
}
};
const onAggregateSelected = (s) => {
onSelected({
field: selectedField,
aggregation: s,
});
close();
};
return (
<Popover
open={show}
onClose={close}
anchorEl={alignTo.current}
marginThreshold={16} // theme.spacing(1)
elevation={3}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
PaperProps={{
style: { minWidth: '250px', maxHeight: '300px', background: theme.palette.background.lightest },
}}
>
{!selectedField && <Search onChange={setSearchTerm} />}
{selectedField && (
<List dense component="nav">
<ListItemButton onClick={() => setSelectedField(null)}>
<ListItemIcon>
<ChevronLeft />
</ListItemIcon>
<ListItemText>Back</ListItemText>
</ListItemButton>
<Divider />
<ListSubheader component="div">Aggregation</ListSubheader>
{['sum', 'count', 'avg', 'min', 'max'].map((v) => (
<Aggr key={v} aggr={v} field={selectedField} onSelect={onAggregateSelected} />
))}
</List>
)}
{!selectedField && fields.length > 0 && (
<List dense component="nav" style={{ background: theme.palette.background.lightest }}>
<LibraryList
app={app}
onSelect={onSelect}
type={type}
title={type === 'measure' ? 'Measures' : 'Dimensions'}
searchTerm={searchTerm}
/>
<ListSubheader component="div" style={{ backgroundColor: 'inherit' }}>
Fields
</ListSubheader>
{fields.map((field) => {
const matches = match(field.qName, searchTerm);
const parts = parse(field.qName, matches);
if (searchTerm && matches.length === 0) return null;
return <Field key={field.qName} field={field} onSelect={onSelect} sub={type === 'measure'} parts={parts} />;
})}
</List>
)}
</Popover>
);
}