@cccsaurora/howler-ui 2.19.0-dev.1099 → 2.19.0-dev.1148
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/app/App.js +4 -2
- package/components/app/providers/GridColumnsProvider.d.ts +52 -0
- package/components/app/providers/GridColumnsProvider.js +177 -0
- package/components/app/providers/GridColumnsProvider.test.js +255 -0
- package/components/elements/addons/buttons/CustomIconButton.test.d.ts +1 -0
- package/components/elements/addons/buttons/CustomIconButton.test.js +81 -0
- package/components/elements/addons/layout/FlexVertical.test.d.ts +1 -0
- package/components/elements/addons/layout/FlexVertical.test.js +32 -0
- package/components/elements/addons/layout/vsbox/VSBoxContent.test.d.ts +1 -0
- package/components/elements/addons/layout/vsbox/VSBoxContent.test.js +26 -0
- package/components/elements/addons/search/phrase/WordLexer.test.d.ts +1 -0
- package/components/elements/addons/search/phrase/WordLexer.test.js +55 -0
- package/components/elements/hit/HitContextMenu.test.d.ts +1 -0
- package/components/{routes/hits/search → elements/hit}/grid/ColumnHeader.d.ts +2 -1
- package/components/{routes/hits/search → elements/hit}/grid/ColumnHeader.js +3 -3
- package/components/{routes/hits/search → elements/hit}/grid/HitRow.d.ts +1 -1
- package/components/elements/hit/grid/HitTable.d.ts +12 -0
- package/components/elements/hit/grid/HitTable.js +82 -0
- package/components/elements/view/LayoutToggle.d.ts +8 -0
- package/components/elements/view/LayoutToggle.js +12 -0
- package/components/elements/view/ViewTitle.d.ts +3 -0
- package/components/elements/view/ViewTitle.js +6 -3
- package/components/hooks/useMyTheme.test.d.ts +1 -0
- package/components/hooks/useMyTheme.test.js +35 -0
- package/components/routes/advanced/eqlTokenProvider.test.d.ts +1 -0
- package/components/routes/advanced/eqlTokenProvider.test.js +34 -0
- package/components/routes/advanced/luceneTokenProvider.test.d.ts +1 -0
- package/components/routes/advanced/luceneTokenProvider.test.js +34 -0
- package/components/routes/hits/search/HitBrowser.js +2 -1
- package/components/routes/hits/search/SearchPane.js +6 -8
- package/components/routes/hits/search/ViewLink.js +3 -2
- package/components/routes/hits/search/grid/HitGrid.js +17 -85
- package/components/routes/hits/search/shared/LayoutSettings.js +25 -0
- package/components/routes/hits/search/shared/SearchActionMenu.d.ts +4 -0
- package/components/routes/hits/search/shared/SearchActionMenu.js +17 -0
- package/components/routes/home/HomeSettings.test.d.ts +1 -0
- package/components/routes/home/HomeSettings.test.js +44 -0
- package/components/routes/home/ViewCard.js +1 -1
- package/components/routes/overviews/markdownExtendedTokenProvider.test.d.ts +1 -0
- package/components/routes/overviews/markdownExtendedTokenProvider.test.js +50 -0
- package/components/routes/views/ViewComposer.js +32 -6
- package/locales/en/translation.json +6 -3
- package/locales/fr/translation.json +6 -3
- package/models/entities/generated/Column.d.ts +7 -0
- package/models/entities/generated/Host.d.ts +1 -0
- package/models/entities/generated/Settings.d.ts +4 -0
- package/package.json +2 -1
- package/rest/AxiosClient.test.d.ts +1 -0
- package/rest/AxiosClient.test.js +184 -0
- package/utils/stringUtils.d.ts +1 -0
- package/utils/stringUtils.js +7 -0
- package/utils/stringUtils.test.js +21 -1
- package/utils/utils.test.js +119 -2
- package/components/routes/hits/search/LayoutSettings.js +0 -24
- /package/components/{routes/hits/search/HitContextMenu.test.d.ts → app/providers/GridColumnsProvider.test.d.ts} +0 -0
- /package/components/{routes/hits/search → elements/hit}/HitContextMenu.d.ts +0 -0
- /package/components/{routes/hits/search → elements/hit}/HitContextMenu.js +0 -0
- /package/components/{routes/hits/search → elements/hit}/HitContextMenu.test.js +0 -0
- /package/components/{routes/hits/search → elements/hit}/grid/AddColumnModal.d.ts +0 -0
- /package/components/{routes/hits/search → elements/hit}/grid/AddColumnModal.js +0 -0
- /package/components/{routes/hits/search → elements/hit}/grid/EnhancedCell.d.ts +0 -0
- /package/components/{routes/hits/search → elements/hit}/grid/EnhancedCell.js +0 -0
- /package/components/{routes/hits/search → elements/hit}/grid/HitRow.js +0 -0
- /package/components/routes/hits/search/{BundleParentMenu.d.ts → shared/BundleParentMenu.d.ts} +0 -0
- /package/components/routes/hits/search/{BundleParentMenu.js → shared/BundleParentMenu.js} +0 -0
- /package/components/routes/hits/search/{LayoutSettings.d.ts → shared/LayoutSettings.d.ts} +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import VSBoxContent from './VSBoxContent';
|
|
6
|
+
describe('VSBoxContent', () => {
|
|
7
|
+
it('renders children inside a div with data-vsbox-content attribute', () => {
|
|
8
|
+
render(_jsx(VSBoxContent, { children: _jsx("span", { children: "hello" }) }));
|
|
9
|
+
const el = document.querySelector('[data-vsbox-content]');
|
|
10
|
+
expect(el).toBeInTheDocument();
|
|
11
|
+
expect(el).toHaveAttribute('data-vsbox-content', 'true');
|
|
12
|
+
});
|
|
13
|
+
it('renders children correctly', () => {
|
|
14
|
+
render(_jsx(VSBoxContent, { children: _jsx("span", { id: "child", children: "content" }) }));
|
|
15
|
+
expect(screen.getByTestId('child')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
it('passes additional MUI Box props through', () => {
|
|
18
|
+
const { container } = render(_jsx(VSBoxContent, { sx: { display: 'flex' }, children: _jsx("span", { children: "content" }) }));
|
|
19
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
it('renders multiple children', () => {
|
|
22
|
+
render(_jsxs(VSBoxContent, { children: [_jsx("span", { id: "a", children: "A" }), _jsx("span", { id: "b", children: "B" })] }));
|
|
23
|
+
expect(screen.getByTestId('a')).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByTestId('b')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import WordLexer from './word/WordLexer';
|
|
4
|
+
describe('WordLexer', () => {
|
|
5
|
+
const lexer = new WordLexer();
|
|
6
|
+
describe('parse', () => {
|
|
7
|
+
it('parses a single word', () => {
|
|
8
|
+
const result = lexer.parse('hello');
|
|
9
|
+
expect(result.tokens).toHaveLength(2); // word + eop
|
|
10
|
+
expect(result.tokens[0].type).toBe('word');
|
|
11
|
+
expect(result.tokens[0].value).toBe('hello');
|
|
12
|
+
});
|
|
13
|
+
it('parses two words separated by a space', () => {
|
|
14
|
+
const result = lexer.parse('hello world');
|
|
15
|
+
const nonEop = result.tokens.filter(t => t.type !== 'eop');
|
|
16
|
+
expect(nonEop.some(t => t.type === 'word' && t.value === 'hello')).toBe(true);
|
|
17
|
+
expect(nonEop.some(t => t.type === 'word' && t.value === 'world')).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
it('always ends with an eop token', () => {
|
|
20
|
+
const result = lexer.parse('foo bar');
|
|
21
|
+
const last = result.tokens[result.tokens.length - 1];
|
|
22
|
+
expect(last.type).toBe('eop');
|
|
23
|
+
expect(last.value).toBe('');
|
|
24
|
+
});
|
|
25
|
+
it('returns an eop-only token list for empty input', () => {
|
|
26
|
+
const result = lexer.parse('');
|
|
27
|
+
expect(result.tokens).toHaveLength(1);
|
|
28
|
+
expect(result.tokens[0].type).toBe('eop');
|
|
29
|
+
});
|
|
30
|
+
it('sets cursor correctly', () => {
|
|
31
|
+
const result = lexer.parse('hello', 3);
|
|
32
|
+
expect(result.cursor).toBe(3);
|
|
33
|
+
});
|
|
34
|
+
it('sets current token for cursor in the middle of a word', () => {
|
|
35
|
+
const result = lexer.parse('hello', 3);
|
|
36
|
+
expect(result.current?.type).toBe('word');
|
|
37
|
+
});
|
|
38
|
+
it('provides suggest token for current cursor position', () => {
|
|
39
|
+
const result = lexer.parse('hello world', 7);
|
|
40
|
+
expect(result.suggest).toBeDefined();
|
|
41
|
+
expect(result.suggest.token).toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
it('sets startIndex/endIndex correctly on word tokens', () => {
|
|
44
|
+
const result = lexer.parse('hi');
|
|
45
|
+
const wordToken = result.tokens.find(t => t.type === 'word');
|
|
46
|
+
expect(wordToken.startIndex).toBe(0);
|
|
47
|
+
expect(wordToken.endIndex).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
it('whitespace tokens have correct type', () => {
|
|
50
|
+
const result = lexer.parse('a b');
|
|
51
|
+
const wsToken = result.tokens.find(t => t.type === 'whitespace');
|
|
52
|
+
expect(wsToken).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type FC, type SetStateAction } from 'react';
|
|
2
2
|
declare const ColumnHeader: FC<{
|
|
3
|
-
width:
|
|
3
|
+
width: number;
|
|
4
4
|
col: string;
|
|
5
|
+
colSource?: string[];
|
|
5
6
|
setColumns: (val: SetStateAction<string[]>) => void;
|
|
6
7
|
onMouseDown: (col: string, event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
|
7
8
|
}>;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useSortable } from '@dnd-kit/sortable';
|
|
3
3
|
import { CSS } from '@dnd-kit/utilities';
|
|
4
|
-
import { DragIndicator, Remove } from '@mui/icons-material';
|
|
4
|
+
import { DragIndicator, JoinFull, Remove } from '@mui/icons-material';
|
|
5
5
|
import { Box, IconButton, Stack, TableCell, Tooltip, useTheme } from '@mui/material';
|
|
6
6
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
7
7
|
import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
|
|
8
8
|
import { useContext } from 'react';
|
|
9
|
-
const ColumnHeader = ({ col, width, setColumns, onMouseDown }) => {
|
|
9
|
+
const ColumnHeader = ({ col, width, colSource, setColumns, onMouseDown }) => {
|
|
10
10
|
const theme = useTheme();
|
|
11
11
|
const { config } = useContext(ApiConfigContext);
|
|
12
12
|
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: col });
|
|
@@ -15,7 +15,7 @@ const ColumnHeader = ({ col, width, setColumns, onMouseDown }) => {
|
|
|
15
15
|
borderRightColor: 'divider',
|
|
16
16
|
py: 0.5,
|
|
17
17
|
position: 'relative'
|
|
18
|
-
}, style: { transform: CSS.Translate.toString(transform), transition }, children: _jsxs(Stack, { className: `col-${col.replaceAll('.', '-')}`, direction: "row", spacing: 1, alignItems: "center", sx: [{ minWidth: '220px' }, !!width ? { width, maxWidth: width } : { maxWidth: '300px' }], children: [_jsx(Tooltip, { title: config.indexes.hit[col].description, children: _jsx("span", { children: col }) }), _jsx(FlexOne, {}), _jsx(IconButton, { size: "small", sx: { fontSize: '1rem' }, onClick: () => setColumns(_columns => _columns.filter(_col => _col !== col)), children: _jsx(Remove, { fontSize: "inherit" }) }), _jsx(DragIndicator, { fontSize: "small", sx: { cursor: 'grab' }, ...attributes, ...listeners }), _jsx(Box, { sx: {
|
|
18
|
+
}, style: { transform: CSS.Translate.toString(transform), transition }, children: _jsxs(Stack, { className: `col-${col.replaceAll('.', '-')}`, direction: "row", spacing: 1, alignItems: "center", sx: [{ minWidth: '220px' }, !!width ? { width, maxWidth: width } : { maxWidth: '300px' }], children: [_jsx(Tooltip, { title: config.indexes.hit[col].description, children: _jsx("span", { children: col }) }), colSource?.length > 1 && (_jsx(Tooltip, { title: colSource.join(', '), children: _jsx(JoinFull, { fontSize: "small" }) })), _jsx(FlexOne, {}), _jsx(IconButton, { size: "small", sx: { fontSize: '1rem' }, onClick: () => setColumns(_columns => _columns.filter(_col => _col !== col)), children: _jsx(Remove, { fontSize: "inherit" }) }), _jsx(DragIndicator, { fontSize: "small", sx: { cursor: 'grab' }, ...attributes, ...listeners }), _jsx(Box, { sx: {
|
|
19
19
|
position: 'absolute',
|
|
20
20
|
top: theme.spacing(0.75),
|
|
21
21
|
bottom: theme.spacing(0.75),
|
|
@@ -3,7 +3,7 @@ declare const _default: import("react").NamedExoticComponent<{
|
|
|
3
3
|
hit: Hit;
|
|
4
4
|
analyticIds: Record<string, string>;
|
|
5
5
|
columns: string[];
|
|
6
|
-
columnWidths: Record<string,
|
|
6
|
+
columnWidths: Record<string, number>;
|
|
7
7
|
collapseMainColumn: boolean;
|
|
8
8
|
onClick: (e: React.MouseEvent<HTMLDivElement>, hit: Hit) => void;
|
|
9
9
|
}>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
2
|
+
import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
|
|
3
|
+
import React, { type PropsWithChildren } from 'react';
|
|
4
|
+
declare const HitTable: ({ query, items, refreshItems, ContextMenu, contextMenuProps, onItemClick }: {
|
|
5
|
+
query: string;
|
|
6
|
+
items?: WithMetadata<Hit>[];
|
|
7
|
+
refreshItems?: (query: string, append?: boolean) => void;
|
|
8
|
+
ContextMenu?: React.FC<PropsWithChildren<object>>;
|
|
9
|
+
contextMenuProps?: object;
|
|
10
|
+
onItemClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>, hit: Hit) => void;
|
|
11
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export default HitTable;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DndContext, KeyboardSensor, PointerSensor, pointerWithin, useSensor, useSensors } from '@dnd-kit/core';
|
|
3
|
+
import { arrayMove, SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
|
|
4
|
+
import { FormatIndentDecrease, FormatIndentIncrease, Search } from '@mui/icons-material';
|
|
5
|
+
import { IconButton, Stack, Table, TableBody, TableCell, TableHead, TableRow } from '@mui/material';
|
|
6
|
+
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
7
|
+
import { GridColumnsContext } from '@cccsaurora/howler-ui/components/app/providers/GridColumnsProvider';
|
|
8
|
+
import ColumnHeader from '@cccsaurora/howler-ui/components/elements/hit/grid/ColumnHeader';
|
|
9
|
+
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
10
|
+
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
11
|
+
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
12
|
+
import HitRow from './HitRow';
|
|
13
|
+
const HitTable = ({ query, items, refreshItems, ContextMenu, contextMenuProps, onItemClick }) => {
|
|
14
|
+
const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
15
|
+
const { getMatchingAnalytic } = useMatchers();
|
|
16
|
+
const [collapseMainColumn, setCollapseMainColumn] = useMyLocalStorageItem(StorageKey.GRID_COLLAPSE_COLUMN, false);
|
|
17
|
+
const [analyticIds, setAnalyticIds] = useState({});
|
|
18
|
+
const { columns, columnWidths, columnSources, setColumnWidth, setColumns, isReady } = useContext(GridColumnsContext);
|
|
19
|
+
const resizingCol = useRef();
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
items?.forEach(hit => {
|
|
22
|
+
if (!analyticIds[hit.howler.analytic]) {
|
|
23
|
+
getMatchingAnalytic(hit).then(_analytic => {
|
|
24
|
+
if (_analytic) {
|
|
25
|
+
setAnalyticIds(_analyticIds => ({ ..._analyticIds, [hit.howler.analytic]: _analytic.analytic_id }));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
31
|
+
}, [analyticIds, items]);
|
|
32
|
+
const onMouseMove = useCallback((event) => {
|
|
33
|
+
event.stopPropagation();
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
const { col, width } = resizingCol.current;
|
|
36
|
+
const newWidth = width + event.movementX;
|
|
37
|
+
document.querySelectorAll(`.col-${col.replaceAll('.', '-')}`).forEach(el => {
|
|
38
|
+
el.style.maxWidth = newWidth + 'px';
|
|
39
|
+
el.style.width = newWidth + 'px';
|
|
40
|
+
});
|
|
41
|
+
resizingCol.current.width = newWidth;
|
|
42
|
+
}, []);
|
|
43
|
+
const onMouseUp = useCallback(() => {
|
|
44
|
+
const { col, width, element } = resizingCol.current;
|
|
45
|
+
if (isReady) {
|
|
46
|
+
setColumnWidth(col, Math.round(width));
|
|
47
|
+
}
|
|
48
|
+
element.style.width = null;
|
|
49
|
+
element.style.maxWidth = null;
|
|
50
|
+
document.querySelectorAll(`.col-${col.replaceAll('.', '-')}`).forEach(el => {
|
|
51
|
+
el.style.maxWidth = null;
|
|
52
|
+
el.style.width = null;
|
|
53
|
+
});
|
|
54
|
+
window.removeEventListener('mousemove', onMouseMove);
|
|
55
|
+
window.removeEventListener('mouseup', onMouseUp);
|
|
56
|
+
}, [onMouseMove, setColumnWidth, isReady]);
|
|
57
|
+
const onMouseDown = useCallback((col, event) => {
|
|
58
|
+
event.stopPropagation();
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
const element = event.target.parentElement;
|
|
61
|
+
const rect = element.getBoundingClientRect();
|
|
62
|
+
resizingCol.current = { col, width: rect.width, element };
|
|
63
|
+
window.addEventListener('mousemove', onMouseMove);
|
|
64
|
+
window.addEventListener('mouseup', onMouseUp);
|
|
65
|
+
}, [onMouseMove, onMouseUp]);
|
|
66
|
+
const handleDragEnd = useCallback((event) => {
|
|
67
|
+
const { active, over } = event;
|
|
68
|
+
if (over && active.id !== over.id) {
|
|
69
|
+
const oldIndex = (columns ?? []).findIndex(entry => entry === active.id);
|
|
70
|
+
const newIndex = (columns ?? []).findIndex(entry => entry === over.id);
|
|
71
|
+
if (isReady) {
|
|
72
|
+
setColumns(arrayMove(columns, oldIndex, newIndex));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}, [columns, setColumns, isReady]);
|
|
76
|
+
const tableContent = (_jsxs(_Fragment, { children: [items?.map(hit => (_jsx(HitRow, { hit: hit, analyticIds: analyticIds, columns: columns, columnWidths: columnWidths, collapseMainColumn: collapseMainColumn, onClick: onItemClick ? onItemClick : (_ev, _hit) => null }, hit.howler.id))), refreshItems && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columns.length + 2, children: _jsx(Stack, { alignItems: "center", justifyContent: "center", py: 0.5, px: 1, children: _jsx(IconButton, { onClick: () => refreshItems(query, true), children: _jsx(Search, {}) }) }) }) }))] }));
|
|
77
|
+
return (_jsxs(Table, { sx: { '& td,th': { px: 1, py: 0.25, whiteSpace: 'nowrap' } }, children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { sx: {
|
|
78
|
+
borderRight: 'thin solid',
|
|
79
|
+
borderRightColor: 'divider'
|
|
80
|
+
}, children: _jsx(IconButton, { onClick: () => setCollapseMainColumn(!collapseMainColumn), children: collapseMainColumn ? (_jsx(FormatIndentIncrease, { fontSize: "small" })) : (_jsx(FormatIndentDecrease, { fontSize: "small" })) }) }), _jsx(DndContext, { sensors: sensors, collisionDetection: pointerWithin, onDragEnd: handleDragEnd, children: _jsx(SortableContext, { items: columns, children: columns.map(col => (_jsx(ColumnHeader, { col: col, width: columnWidths[col], colSource: columnSources[col], onMouseDown: onMouseDown, setColumns: isReady ? setColumns : () => null }, col))) }) }), _jsx(TableCell, { sx: { width: '100%' } })] }) }), ContextMenu ? (_jsx(ContextMenu, { ...contextMenuProps, children: tableContent })) : (_jsx(TableBody, { children: tableContent }))] }));
|
|
81
|
+
};
|
|
82
|
+
export default HitTable;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type HowlerViewLayoutType = 'list' | 'grid' | null;
|
|
2
|
+
declare const LayoutToggle: ({ displayType, setDisplayType, size, allowNullValue }: {
|
|
3
|
+
displayType: HowlerViewLayoutType;
|
|
4
|
+
setDisplayType: (type: HowlerViewLayoutType) => void;
|
|
5
|
+
size?: "small" | "medium" | "large";
|
|
6
|
+
allowNullValue?: boolean;
|
|
7
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export default LayoutToggle;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { List, TableChart } from '@mui/icons-material';
|
|
3
|
+
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
|
|
4
|
+
const LayoutToggle = ({ displayType, setDisplayType, size, allowNullValue = false }) => {
|
|
5
|
+
return (_jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => {
|
|
6
|
+
if (!value && !allowNullValue) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
setDisplayType(value);
|
|
10
|
+
}, size: size ?? 'small', children: [_jsx(ToggleButton, { value: "list", children: _jsx(List, { fontSize: size ?? 'medium' }) }), _jsx(ToggleButton, { value: "grid", children: _jsx(TableChart, { fontSize: size ?? 'medium' }) })] }));
|
|
11
|
+
};
|
|
12
|
+
export default LayoutToggle;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ArrowDownward, ArrowUpward, Language, Lock, Person } from '@mui/icons-material';
|
|
2
|
+
import { ArrowDownward, ArrowUpward, Language, List, Lock, Person, TableChart } from '@mui/icons-material';
|
|
3
3
|
import { Chip, Stack, Tooltip, Typography } from '@mui/material';
|
|
4
4
|
import { useMemo } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { convertLuceneToDate } from '@cccsaurora/howler-ui/utils/utils';
|
|
7
|
-
export const ViewTitle = ({ title, type, query, sort, span }) => {
|
|
7
|
+
export const ViewTitle = ({ title, type, query, sort, span, settings }) => {
|
|
8
8
|
const { t } = useTranslation();
|
|
9
9
|
const spanLabel = useMemo(() => {
|
|
10
10
|
if (!span) {
|
|
@@ -21,5 +21,8 @@ export const ViewTitle = ({ title, type, query, sort, span }) => {
|
|
|
21
21
|
readonly: _jsx(Lock, { fontSize: "small" }),
|
|
22
22
|
global: _jsx(Language, { fontSize: "small" }),
|
|
23
23
|
personal: _jsx(Person, { fontSize: "small" })
|
|
24
|
-
}[type] }),
|
|
24
|
+
}[type] }), settings?.display && (_jsx(Tooltip, { title: t(`page.settings.local.hits.display_type.${settings.display}`), children: {
|
|
25
|
+
list: _jsx(List, { fontSize: "small" }),
|
|
26
|
+
grid: _jsx(TableChart, { fontSize: "small" })
|
|
27
|
+
}[settings.display] })), _jsx(Typography, { variant: "body1", children: t(title) })] }), _jsx(Typography, { variant: "caption", children: _jsx("code", { children: query }) }), (sort || span) && (_jsxs(Stack, { direction: "row", sx: { mt: 1 }, spacing: 1, children: [sort?.split(',').map(_sort => (_jsx(Chip, { size: "small", label: _sort.split(' ')[0], icon: _sort.endsWith('desc') ? _jsx(ArrowDownward, {}) : _jsx(ArrowUpward, {}) }, _sort.split(' ')[0]))), spanLabel && _jsx(Chip, { size: "small", label: spanLabel })] }))] }));
|
|
25
28
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { renderHook } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import useMyTheme from './useMyTheme';
|
|
5
|
+
describe('useMyTheme', () => {
|
|
6
|
+
it('returns a theme configuration object', () => {
|
|
7
|
+
const { result } = renderHook(() => useMyTheme());
|
|
8
|
+
expect(result.current).toBeDefined();
|
|
9
|
+
expect(typeof result.current).toBe('object');
|
|
10
|
+
});
|
|
11
|
+
it('contains a dark palette entry', () => {
|
|
12
|
+
const { result } = renderHook(() => useMyTheme());
|
|
13
|
+
expect(result.current.palette?.dark).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
it('contains a light palette entry', () => {
|
|
16
|
+
const { result } = renderHook(() => useMyTheme());
|
|
17
|
+
expect(result.current.palette?.light).toBeDefined();
|
|
18
|
+
});
|
|
19
|
+
it('returns the same reference on re-render (stable return value)', () => {
|
|
20
|
+
const { result, rerender } = renderHook(() => useMyTheme());
|
|
21
|
+
const first = result.current;
|
|
22
|
+
rerender();
|
|
23
|
+
expect(result.current).toBe(first);
|
|
24
|
+
});
|
|
25
|
+
it('dark palette includes primary and secondary colours', () => {
|
|
26
|
+
const { result } = renderHook(() => useMyTheme());
|
|
27
|
+
expect(result.current.palette.dark.primary).toHaveProperty('main');
|
|
28
|
+
expect(result.current.palette.dark.secondary).toHaveProperty('main');
|
|
29
|
+
});
|
|
30
|
+
it('light palette includes primary and secondary colours', () => {
|
|
31
|
+
const { result } = renderHook(() => useMyTheme());
|
|
32
|
+
expect(result.current.palette.light.primary).toHaveProperty('main');
|
|
33
|
+
expect(result.current.palette.light.secondary).toHaveProperty('main');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import TOKEN_PROVIDER from './eqlTokenProvider';
|
|
4
|
+
describe('EQL token provider', () => {
|
|
5
|
+
it('exports a non-null object', () => {
|
|
6
|
+
expect(TOKEN_PROVIDER).toBeDefined();
|
|
7
|
+
expect(TOKEN_PROVIDER).not.toBeNull();
|
|
8
|
+
expect(typeof TOKEN_PROVIDER).toBe('object');
|
|
9
|
+
});
|
|
10
|
+
it('has a "root" tokenizer entry', () => {
|
|
11
|
+
expect(TOKEN_PROVIDER.tokenizer).toBeDefined();
|
|
12
|
+
expect(TOKEN_PROVIDER.tokenizer.root).toBeDefined();
|
|
13
|
+
expect(Array.isArray(TOKEN_PROVIDER.tokenizer.root)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it('uses "invalid" as the defaultToken', () => {
|
|
16
|
+
expect(TOKEN_PROVIDER.defaultToken).toBe('invalid');
|
|
17
|
+
});
|
|
18
|
+
it('defines the expected EQL keywords', () => {
|
|
19
|
+
const kws = TOKEN_PROVIDER.keywords;
|
|
20
|
+
expect(kws).toContain('where');
|
|
21
|
+
expect(kws).toContain('not');
|
|
22
|
+
expect(kws).toContain('in');
|
|
23
|
+
expect(kws).toContain('head');
|
|
24
|
+
expect(kws).toContain('tail');
|
|
25
|
+
});
|
|
26
|
+
it('defines boolean operators', () => {
|
|
27
|
+
const booleans = TOKEN_PROVIDER.booleans;
|
|
28
|
+
expect(booleans).toContain('and');
|
|
29
|
+
expect(booleans).toContain('or');
|
|
30
|
+
});
|
|
31
|
+
it('has includeLF set to true', () => {
|
|
32
|
+
expect(TOKEN_PROVIDER.includeLF).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import TOKEN_PROVIDER from './luceneTokenProvider';
|
|
4
|
+
describe('Lucene token provider', () => {
|
|
5
|
+
it('exports a non-null object', () => {
|
|
6
|
+
expect(TOKEN_PROVIDER).toBeDefined();
|
|
7
|
+
expect(TOKEN_PROVIDER).not.toBeNull();
|
|
8
|
+
expect(typeof TOKEN_PROVIDER).toBe('object');
|
|
9
|
+
});
|
|
10
|
+
it('has a "root" tokenizer entry', () => {
|
|
11
|
+
expect(TOKEN_PROVIDER.tokenizer).toBeDefined();
|
|
12
|
+
expect(TOKEN_PROVIDER.tokenizer.root).toBeDefined();
|
|
13
|
+
expect(Array.isArray(TOKEN_PROVIDER.tokenizer.root)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it('defines the expected operators', () => {
|
|
16
|
+
const ops = TOKEN_PROVIDER.operators;
|
|
17
|
+
expect(ops).toContain('-');
|
|
18
|
+
expect(ops).toContain('&&');
|
|
19
|
+
expect(ops).toContain('||');
|
|
20
|
+
expect(ops).toContain(':');
|
|
21
|
+
});
|
|
22
|
+
it('defines the expected keywords (AND, OR, NOT)', () => {
|
|
23
|
+
const kws = TOKEN_PROVIDER.keywords;
|
|
24
|
+
expect(kws).toContain('AND');
|
|
25
|
+
expect(kws).toContain('OR');
|
|
26
|
+
expect(kws).toContain('NOT');
|
|
27
|
+
});
|
|
28
|
+
it('uses "default" as the defaultToken', () => {
|
|
29
|
+
expect(TOKEN_PROVIDER.defaultToken).toBe('default');
|
|
30
|
+
});
|
|
31
|
+
it('includes a string tokenizer state for quoted values', () => {
|
|
32
|
+
expect(TOKEN_PROVIDER.tokenizer).toHaveProperty('string');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronLeft, Close, ManageSearch } from '@mui/icons-material';
|
|
3
3
|
import { Box, Card, Checkbox, Collapse, Drawer, Fab, IconButton, Stack, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material';
|
|
4
|
+
import GridColumnsProvider from '@cccsaurora/howler-ui/components/app/providers/GridColumnsProvider';
|
|
4
5
|
import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
|
|
5
6
|
import HitSearchProvider, { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
|
|
6
7
|
import ParameterProvider, { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
@@ -128,6 +129,6 @@ const HitBrowser = () => {
|
|
|
128
129
|
}, children: _jsx(ChevronLeft, { sx: { transition: 'rotate 250ms', rotate: show ? '180deg' : '0deg' } }) }))] }));
|
|
129
130
|
};
|
|
130
131
|
const HitBrowserProvider = () => {
|
|
131
|
-
return (_jsx(ParameterProvider, { children: _jsx(HitSearchProvider, { children: _jsx(HitBrowser, {}) }) }));
|
|
132
|
+
return (_jsx(ParameterProvider, { children: _jsx(HitSearchProvider, { children: _jsx(GridColumnsProvider, { children: _jsx(HitBrowser, {}) }) }) }));
|
|
132
133
|
};
|
|
133
134
|
export default HitBrowserProvider;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { Box,
|
|
2
|
+
import { ErrorOutline } from '@mui/icons-material';
|
|
3
|
+
import { Box, LinearProgress, Stack, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material';
|
|
4
4
|
import { grey } from '@mui/material/colors';
|
|
5
5
|
import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
|
|
6
6
|
import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
|
|
@@ -17,21 +17,20 @@ import SearchTotal from '@cccsaurora/howler-ui/components/elements/addons/search
|
|
|
17
17
|
import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
|
|
18
18
|
import HitBanner from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
|
|
19
19
|
import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
|
|
20
|
+
import HitContextMenu from '@cccsaurora/howler-ui/components/elements/hit/HitContextMenu';
|
|
20
21
|
import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
|
|
21
22
|
import useHitSelection from '@cccsaurora/howler-ui/components/hooks/useHitSelection';
|
|
22
23
|
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
23
24
|
import React, { memo, useCallback, useEffect, useMemo } from 'react';
|
|
24
25
|
import { isMobile } from 'react-device-detect';
|
|
25
26
|
import { useTranslation } from 'react-i18next';
|
|
26
|
-
import {
|
|
27
|
+
import { useLocation, useParams } from 'react-router-dom';
|
|
27
28
|
import { useContextSelector } from 'use-context-selector';
|
|
28
29
|
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
29
|
-
import BundleParentMenu from './BundleParentMenu';
|
|
30
30
|
import { BundleScroller } from './BundleScroller';
|
|
31
|
-
import HitContextMenu from './HitContextMenu';
|
|
32
31
|
import HitQuery from './HitQuery';
|
|
33
|
-
import LayoutSettings from './LayoutSettings';
|
|
34
32
|
import QuerySettings from './QuerySettings';
|
|
33
|
+
import SearchActionMenu from './shared/SearchActionMenu';
|
|
35
34
|
const Item = memo(({ hit, onClick }) => {
|
|
36
35
|
const theme = useTheme();
|
|
37
36
|
const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
|
|
@@ -80,7 +79,6 @@ const Item = memo(({ hit, onClick }) => {
|
|
|
80
79
|
const SearchPane = () => {
|
|
81
80
|
const { t } = useTranslation();
|
|
82
81
|
const location = useLocation();
|
|
83
|
-
const navigate = useNavigate();
|
|
84
82
|
const routeParams = useParams();
|
|
85
83
|
const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
|
|
86
84
|
const setSelected = useContextSelector(ParameterContext, ctx => ctx.setSelected);
|
|
@@ -117,7 +115,7 @@ const SearchPane = () => {
|
|
|
117
115
|
], onClick: () => {
|
|
118
116
|
clearSelectedHits(bundleHit.howler.id);
|
|
119
117
|
setSelected(bundleHit.howler.id);
|
|
120
|
-
}, children: _jsx(HitBanner, { hit: bundleHit, layout: HitLayout.DENSE, useListener: true }) }) }) }) })), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5 }, variant: "body2", children: t('hit.search.prompt') }), error && (_jsx(Tooltip, { title: `${t('route.advanced.error')}: ${error}`, children: _jsx(ErrorOutline, { fontSize: "small", color: "error" }) })), _jsx(FlexOne, {}),
|
|
118
|
+
}, children: _jsx(HitBanner, { hit: bundleHit, layout: HitLayout.DENSE, useListener: true }) }) }) }) })), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5 }, variant: "body2", children: t('hit.search.prompt') }), error && (_jsx(Tooltip, { title: `${t('route.advanced.error')}: ${error}`, children: _jsx(ErrorOutline, { fontSize: "small", color: "error" }) })), _jsx(FlexOne, {}), _jsx(SearchActionMenu, { query: query })] })] }), _jsxs(VSBoxHeader, { ml: -3, mr: -3, px: 2, pb: 1, sx: { zIndex: 989 }, children: [_jsxs(Stack, { sx: { pt: 1 }, children: [_jsxs(Stack, { sx: { position: 'relative', flex: 1 }, children: [_jsx(HitQuery, { searching: searching, triggerSearch: triggerSearch }), searching && (_jsx(LinearProgress, { sx: theme => ({
|
|
121
119
|
position: 'absolute',
|
|
122
120
|
left: 0,
|
|
123
121
|
right: 0,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createElement as _createElement } from "react";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { ArrowDropDown, Delete, Edit, Language, Lock, OpenInNew, Person, Refresh, SavedSearch, SelectAll } from '@mui/icons-material';
|
|
3
|
+
import { ArrowDropDown, Delete, Edit, Language, Lock, OpenInNew, Person, Refresh, SavedSearch, SelectAll, Warning } from '@mui/icons-material';
|
|
4
4
|
import { Autocomplete, Chip, CircularProgress, IconButton, Stack, TextField, Tooltip, Typography } from '@mui/material';
|
|
5
5
|
import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
|
|
6
6
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
@@ -21,6 +21,7 @@ const ViewLink = ({ id, viewId }) => {
|
|
|
21
21
|
const removeView = useContextSelector(ParameterContext, ctx => ctx.removeView);
|
|
22
22
|
const setParamView = useContextSelector(ParameterContext, ctx => ctx.setView);
|
|
23
23
|
const search = useContextSelector(HitSearchContext, ctx => ctx.search);
|
|
24
|
+
const displayType = useContextSelector(HitSearchContext, ctx => ctx.displayType);
|
|
24
25
|
const [loading, setLoading] = useState(true);
|
|
25
26
|
const [view, setView] = useState(null);
|
|
26
27
|
useEffect(() => {
|
|
@@ -60,6 +61,6 @@ const ViewLink = ({ id, viewId }) => {
|
|
|
60
61
|
readonly: _jsx(Lock, { fontSize: "small", "aria-label": t(`route.views.manager.${view.type}`) }),
|
|
61
62
|
global: _jsx(Language, { fontSize: "small", "aria-label": t(`route.views.manager.${view.type}`) }),
|
|
62
63
|
personal: _jsx(Person, { fontSize: "small", "aria-label": t(`route.views.manager.${view.type}`) })
|
|
63
|
-
}[view.type] }), label: _jsx(Tooltip, { title: view.query, children: _jsx(Typography, { role: "link", sx: { color: 'text.primary' }, variant: "body2", component: Link, to: `/views/${view.view_id}/edit`, "aria-label": `${t(view.title)} - ${view.query ?? t('unknown')}`, children: t(view.title) }) }), onDelete: () => removeView(viewId), children: _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", children: [_jsx(Tooltip, { title: view ? t('route.views.edit') : t('route.views.create'), children: _jsx(IconButton, { "aria-label": view ? t('route.views.edit') : t('route.views.create'), size: "small", component: Link, disabled: (!view && !query) || span?.endsWith('custom'), to: viewUrl, role: "link", children: view ? _jsx(Edit, { fontSize: "small" }) : _jsx(SavedSearch, {}) }) }), _jsx(Tooltip, { title: t('view.refresh'), children: _jsx(IconButton, { size: "small", onClick: () => search(query), "aria-label": t('view.refresh'), children: _jsx(Refresh, { fontSize: "small" }) }) }), _jsx(Tooltip, { title: t('view.open'), children: _jsx(IconButton, { size: "small", component: Link, to: `/search?query=${view.query}`, "aria-label": t('view.open'), role: "link", children: _jsx(OpenInNew, { fontSize: "small" }) }) })] }) }));
|
|
64
|
+
}[view.type] }), label: _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", children: [_jsx(Tooltip, { title: view.query, children: _jsx(Typography, { role: "link", sx: { color: 'text.primary' }, variant: "body2", component: Link, to: `/views/${view.view_id}/edit`, "aria-label": `${t(view.title)} - ${view.query ?? t('unknown')}`, children: t(view.title) }) }), view.settings?.display === 'grid' && displayType !== 'grid' && (_jsx(Tooltip, { title: t('view.display.grid.inactive_warning'), children: _jsx(Warning, { fontSize: "small", color: "warning" }) }))] }), onDelete: () => removeView(viewId), children: _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", children: [_jsx(Tooltip, { title: view ? t('route.views.edit') : t('route.views.create'), children: _jsx(IconButton, { "aria-label": view ? t('route.views.edit') : t('route.views.create'), size: "small", component: Link, disabled: (!view && !query) || span?.endsWith('custom'), to: viewUrl, role: "link", children: view ? _jsx(Edit, { fontSize: "small" }) : _jsx(SavedSearch, {}) }) }), _jsx(Tooltip, { title: t('view.refresh'), children: _jsx(IconButton, { size: "small", onClick: () => search(query), "aria-label": t('view.refresh'), children: _jsx(Refresh, { fontSize: "small" }) }) }), _jsx(Tooltip, { title: t('view.open'), children: _jsx(IconButton, { size: "small", component: Link, to: `/search?query=${view.query}`, "aria-label": t('view.open'), role: "link", children: _jsx(OpenInNew, { fontSize: "small" }) }) })] }) }));
|
|
64
65
|
};
|
|
65
66
|
export default memo(ViewLink);
|