@cccsaurora/howler-ui 2.19.0-cases.1138 → 2.19.0-cases.1192
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.d.ts +1 -0
- package/components/app/providers/GridColumnsProvider.test.js +255 -0
- package/components/app/providers/RecordSearchProvider.js +10 -18
- 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/HitLabels.test.d.ts +1 -0
- package/components/elements/hit/HitLabels.test.js +77 -0
- package/components/elements/hit/HitLinks.d.ts +5 -4
- package/components/elements/hit/HitLinks.js +28 -10
- package/components/elements/hit/HitNotebooks.js +23 -22
- package/components/elements/hit/NotebookTooltip.d.ts +3 -0
- package/components/elements/hit/NotebookTooltip.js +8 -0
- package/components/elements/hit/PivotTooltip.d.ts +7 -0
- package/components/elements/hit/PivotTooltip.js +9 -0
- package/components/elements/hit/RelatedLinkTooltip.d.ts +6 -0
- package/components/elements/hit/RelatedLinkTooltip.js +8 -0
- package/components/elements/hit/ResolvePivotUrl.d.ts +4 -0
- package/components/elements/hit/ResolvePivotUrl.js +22 -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/RecordRow.d.ts +1 -1
- package/components/elements/hit/grid/RecordTable.d.ts +13 -0
- package/components/elements/hit/grid/RecordTable.js +84 -0
- package/components/elements/hit/related/PivotLink.d.ts +3 -0
- package/components/elements/hit/related/PivotLink.js +5 -2
- package/components/elements/hit/related/RelatedLink.d.ts +1 -0
- package/components/elements/hit/related/RelatedLink.js +18 -15
- 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/RecordBrowser.js +4 -3
- package/components/routes/hits/search/SearchPane.js +4 -5
- package/components/routes/hits/search/ViewLink.js +3 -2
- package/components/routes/hits/search/grid/RecordGrid.d.ts +3 -0
- package/components/routes/hits/search/grid/RecordGrid.js +64 -0
- 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 +11 -0
- package/components/routes/home/HomeSettings.test.d.ts +1 -0
- package/components/routes/home/HomeSettings.test.js +44 -0
- 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 +11 -3
- package/locales/fr/translation.json +11 -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 +10 -12
- 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/grid/HitGrid.d.ts +0 -3
- package/components/routes/hits/search/grid/HitGrid.js +0 -134
- /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/RecordRow.js +0 -0
- /package/components/routes/hits/search/{LayoutSettings.d.ts → shared/LayoutSettings.d.ts} +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import { setupReactRouterMock } from '@cccsaurora/howler-ui/tests/mocks';
|
|
5
|
+
import { vi } from 'vitest';
|
|
6
|
+
setupReactRouterMock();
|
|
7
|
+
import CustomIconButton from './CustomIconButton';
|
|
8
|
+
describe('CustomIconButton', () => {
|
|
9
|
+
afterAll(() => vi.resetModules());
|
|
10
|
+
describe('rendering', () => {
|
|
11
|
+
it('renders a button element', () => {
|
|
12
|
+
render(_jsx(CustomIconButton, { "aria-label": "test", children: "icon" }));
|
|
13
|
+
expect(screen.getByRole('button', { name: 'test' })).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
it('renders children inside the button', () => {
|
|
16
|
+
render(_jsx(CustomIconButton, { children: "\u2605" }));
|
|
17
|
+
expect(screen.getByText('★')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
it('renders with MUI IconButton classes', () => {
|
|
20
|
+
render(_jsx(CustomIconButton, { children: "i" }));
|
|
21
|
+
expect(screen.getByRole('button')).toHaveClass('MuiIconButton-root');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('disabled state', () => {
|
|
25
|
+
it('is disabled when disabled prop is true', () => {
|
|
26
|
+
render(_jsx(CustomIconButton, { disabled: true, children: "icon" }));
|
|
27
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
28
|
+
});
|
|
29
|
+
it('is disabled when progress is truthy', () => {
|
|
30
|
+
render(_jsx(CustomIconButton, { progress: true, children: "icon" }));
|
|
31
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
32
|
+
});
|
|
33
|
+
it('is NOT disabled when progress is set but clickableWithProgress=true', () => {
|
|
34
|
+
render(_jsx(CustomIconButton, { progress: true, clickableWithProgress: true, children: "icon" }));
|
|
35
|
+
expect(screen.getByRole('button')).not.toBeDisabled();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('progress indicator', () => {
|
|
39
|
+
it('renders a CircularProgress when progress is truthy', () => {
|
|
40
|
+
render(_jsx(CustomIconButton, { progress: true, children: "icon" }));
|
|
41
|
+
// CircularProgress renders an svg with role="progressbar"
|
|
42
|
+
expect(document.querySelector('circle')).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
it('does not render CircularProgress when progress is falsy', () => {
|
|
45
|
+
render(_jsx(CustomIconButton, { children: "icon" }));
|
|
46
|
+
expect(document.querySelector('[role="progressbar"]')).not.toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('tooltip', () => {
|
|
50
|
+
it('wraps the button in a Tooltip when tooltip prop is provided', () => {
|
|
51
|
+
render(_jsx(CustomIconButton, { tooltip: "Save", children: "icon" }));
|
|
52
|
+
// The button is wrapped in a <span> when a Tooltip is used
|
|
53
|
+
const span = screen.getByRole('button').closest('span');
|
|
54
|
+
expect(span).toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
it('does not add a surrounding span when no tooltip is given', () => {
|
|
57
|
+
render(_jsx(CustomIconButton, { children: "icon" }));
|
|
58
|
+
const button = screen.getByRole('button');
|
|
59
|
+
// Without tooltip the immediate parent should NOT be a span wrapper
|
|
60
|
+
expect(button.parentElement?.tagName).not.toBe('SPAN');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('route link', () => {
|
|
64
|
+
it('wraps the button in a Link when route prop is provided', () => {
|
|
65
|
+
render(_jsx(CustomIconButton, { route: "/hits", children: "icon" }));
|
|
66
|
+
expect(screen.getByRole('link')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
it('does not render a link when no route is given', () => {
|
|
69
|
+
render(_jsx(CustomIconButton, { children: "icon" }));
|
|
70
|
+
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('href link', () => {
|
|
74
|
+
it('wraps the button in an anchor tag when href is provided', () => {
|
|
75
|
+
render(_jsx(CustomIconButton, { href: "https://example.com", children: "icon" }));
|
|
76
|
+
const anchor = screen.getByRole('link');
|
|
77
|
+
expect(anchor).toBeInTheDocument();
|
|
78
|
+
expect(anchor).toHaveAttribute('href', 'https://example.com');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
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 FlexVertical from './FlexVertical';
|
|
6
|
+
describe('FlexVertical', () => {
|
|
7
|
+
it('renders a div with display:flex and flex-direction:column', () => {
|
|
8
|
+
const { container } = render(_jsx(FlexVertical, { children: _jsx("span", { children: "child" }) }));
|
|
9
|
+
const div = container.firstChild;
|
|
10
|
+
expect(div).toBeInTheDocument();
|
|
11
|
+
expect(div).toHaveStyle({ display: 'flex', flexDirection: 'column' });
|
|
12
|
+
});
|
|
13
|
+
it('renders children inside the flex container', () => {
|
|
14
|
+
render(_jsx(FlexVertical, { children: _jsx("span", { id: "inner", children: "hello" }) }));
|
|
15
|
+
expect(screen.getByTestId('inner')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
it('renders multiple children', () => {
|
|
18
|
+
render(_jsxs(FlexVertical, { children: [_jsx("span", { id: "a", children: "A" }), _jsx("span", { id: "b", children: "B" })] }));
|
|
19
|
+
expect(screen.getByTestId('a')).toBeInTheDocument();
|
|
20
|
+
expect(screen.getByTestId('b')).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
it('applies the default flex value of 1', () => {
|
|
23
|
+
const { container } = render(_jsx(FlexVertical, { children: _jsx("span", { children: "child" }) }));
|
|
24
|
+
const div = container.firstChild;
|
|
25
|
+
expect(div).toHaveStyle({ flex: 1 });
|
|
26
|
+
});
|
|
27
|
+
it('applies a custom flex value when provided', () => {
|
|
28
|
+
const { container } = render(_jsx(FlexVertical, { flex: 2, children: _jsx("span", { children: "child" }) }));
|
|
29
|
+
const div = container.firstChild;
|
|
30
|
+
expect(div).toHaveStyle({ flex: 2 });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import HitLabels from './HitLabels';
|
|
5
|
+
/**
|
|
6
|
+
* HitLabels.test.tsx
|
|
7
|
+
*
|
|
8
|
+
* Purpose:
|
|
9
|
+
* This test suite validates the core behavior of the HitLabels component.
|
|
10
|
+
* - Correct transformation of hit.howler.labels into UI chips
|
|
11
|
+
* - Proper rendering of all label values
|
|
12
|
+
* - Conditional display of the edit button based on readOnly mode
|
|
13
|
+
* - Safe rendering when no labels are present
|
|
14
|
+
*/
|
|
15
|
+
vi.mock('components/hooks/useMyApi', () => ({
|
|
16
|
+
default: () => ({
|
|
17
|
+
dispatchApi: vi.fn()
|
|
18
|
+
})
|
|
19
|
+
}));
|
|
20
|
+
vi.mock('components/app/providers/RecordProvider', () => ({
|
|
21
|
+
RecordContext: {}
|
|
22
|
+
}));
|
|
23
|
+
vi.mock('use-context-selector', () => ({
|
|
24
|
+
useContextSelector: () => vi.fn()
|
|
25
|
+
}));
|
|
26
|
+
vi.mock('react-i18next', () => ({
|
|
27
|
+
useTranslation: () => ({
|
|
28
|
+
t: (key) => key
|
|
29
|
+
}),
|
|
30
|
+
Trans: ({ i18nKey }) => i18nKey
|
|
31
|
+
}));
|
|
32
|
+
vi.mock('utils/constants', () => ({
|
|
33
|
+
LABEL_TYPES: {
|
|
34
|
+
security: { icon: null, color: '#ff0000' },
|
|
35
|
+
system: { icon: null, color: '#00ff00' }
|
|
36
|
+
}
|
|
37
|
+
}));
|
|
38
|
+
const baseHit = {
|
|
39
|
+
howler: {
|
|
40
|
+
id: 'hit-1',
|
|
41
|
+
labels: {
|
|
42
|
+
security: ['critical', 'high'],
|
|
43
|
+
system: ['info']
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
describe('HitLabels', () => {
|
|
48
|
+
it('renders all labels from hit.howler.labels', () => {
|
|
49
|
+
render(_jsx(HitLabels, { hit: baseHit, readOnly: true }));
|
|
50
|
+
expect(screen.getByText('critical')).toBeInTheDocument();
|
|
51
|
+
expect(screen.getByText('high')).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByText('info')).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
it('renders correct number of labels', () => {
|
|
55
|
+
render(_jsx(HitLabels, { hit: baseHit, readOnly: true }));
|
|
56
|
+
const labels = screen.getAllByText(/critical|high|info/);
|
|
57
|
+
expect(labels.length).toBe(3);
|
|
58
|
+
});
|
|
59
|
+
it('shows edit button when not readOnly', () => {
|
|
60
|
+
render(_jsx(HitLabels, { hit: baseHit, readOnly: false }));
|
|
61
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
it('hides edit button when readOnly', () => {
|
|
64
|
+
render(_jsx(HitLabels, { hit: baseHit, readOnly: true }));
|
|
65
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
it('renders without crashing when labels are empty', () => {
|
|
68
|
+
const emptyHit = {
|
|
69
|
+
howler: {
|
|
70
|
+
id: 'hit-1',
|
|
71
|
+
labels: {}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const { container } = render(_jsx(HitLabels, { hit: emptyHit, readOnly: true }));
|
|
75
|
+
expect(container).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type { FC } from 'react';
|
|
1
2
|
import type { Analytic } from '@cccsaurora/howler-ui/models/entities/generated/Analytic';
|
|
2
3
|
import type { Dossier } from '@cccsaurora/howler-ui/models/entities/generated/Dossier';
|
|
3
4
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
4
|
-
|
|
5
|
-
declare const HitLinks: FC<{
|
|
5
|
+
interface HitLinksProps {
|
|
6
6
|
hit?: Hit;
|
|
7
7
|
analytic?: Analytic;
|
|
8
|
-
dossiers
|
|
9
|
-
}
|
|
8
|
+
dossiers?: Dossier[];
|
|
9
|
+
}
|
|
10
|
+
declare const HitLinks: FC<HitLinksProps>;
|
|
10
11
|
export default HitLinks;
|
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Grid, gridClasses } from '@mui/material';
|
|
3
|
+
import { sortBy, uniqBy } from 'lodash-es';
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
3
6
|
import HitNotebooks from '@cccsaurora/howler-ui/components/elements/hit/HitNotebooks';
|
|
7
|
+
import ResolvePivotUrl from '@cccsaurora/howler-ui/components/elements/hit/ResolvePivotUrl';
|
|
4
8
|
import PivotLink from '@cccsaurora/howler-ui/components/elements/hit/related/PivotLink';
|
|
5
9
|
import RelatedLink from '@cccsaurora/howler-ui/components/elements/hit/related/RelatedLink';
|
|
6
|
-
import { sortBy, uniqBy } from 'lodash-es';
|
|
7
|
-
import { useTranslation } from 'react-i18next';
|
|
8
10
|
const HitLinks = ({ hit, analytic, dossiers = [] }) => {
|
|
9
11
|
const { i18n } = useTranslation();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
dossiers.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
const displayLinks = useMemo(() => uniqBy(hit?.howler?.links ?? [], 'href').slice(0, 3), [hit?.howler?.links]);
|
|
13
|
+
const displayPivots = useMemo(() => {
|
|
14
|
+
const flattened = dossiers.flatMap(dossier => (dossier.pivots ?? []).map(pivot => {
|
|
15
|
+
const pivotUrl = pivot.format === 'link' ? ResolvePivotUrl(pivot, hit) : undefined;
|
|
16
|
+
return {
|
|
17
|
+
pivot,
|
|
18
|
+
dossier,
|
|
19
|
+
resolvedUrl: pivotUrl || `/dossier/${dossier.dossier_id}`
|
|
20
|
+
};
|
|
21
|
+
}));
|
|
22
|
+
return sortBy(flattened, item => item.pivot.label?.[i18n.language]);
|
|
23
|
+
}, [dossiers, i18n.language, hit]);
|
|
24
|
+
const hasNotebooks = (analytic?.notebooks?.length ?? 0) > 0;
|
|
25
|
+
if (displayLinks.length === 0 && displayPivots.length === 0 && !hasNotebooks) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return (_jsxs(Grid, { container: true, spacing: 1, pr: 2, sx: { [`& .${gridClasses.item}`]: { display: 'flex' } }, children: [displayLinks
|
|
29
|
+
.filter(link => !!link.href)
|
|
30
|
+
.map(link => {
|
|
31
|
+
const safeTitle = link.title ?? link.href;
|
|
32
|
+
return (_jsx(Grid, { item: true, children: _jsx(RelatedLink, { compact: true, title: safeTitle, href: link.href, target: "_blank", rel: "noopener noreferrer" }) }, link.href));
|
|
33
|
+
}), displayPivots.map(({ pivot, dossier, resolvedUrl }) => {
|
|
34
|
+
return (_jsx(Grid, { item: true, children: _jsx(PivotLink, { pivot: pivot, hit: hit, dossier: dossier, resolvedUrl: resolvedUrl, compact: true }) }, `${dossier.dossier_id}-${pivot.value}`));
|
|
35
|
+
}), hasNotebooks && (_jsx(Grid, { item: true, children: _jsx(HitNotebooks, { analytic: analytic, hit: hit, compact: true }) }))] }));
|
|
18
36
|
};
|
|
19
37
|
export default HitLinks;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Avatar, Backdrop, Box, Button, Chip, CircularProgress, ClickAwayListener, Fade, MenuItem, Paper, Popper, Stack, TextField } from '@mui/material';
|
|
2
|
+
import { Avatar, Backdrop, Box, Button, Chip, CircularProgress, ClickAwayListener, Fade, MenuItem, Paper, Popper, Stack, TextField, Tooltip } from '@mui/material';
|
|
3
3
|
import api from '@cccsaurora/howler-ui/api';
|
|
4
4
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
5
5
|
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
6
|
+
import NotebookTooltip from '@cccsaurora/howler-ui/components/elements/hit/NotebookTooltip';
|
|
6
7
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
7
8
|
import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
|
|
8
9
|
import { memo, useCallback, useContext, useEffect, useState } from 'react';
|
|
@@ -124,26 +125,26 @@ const HitNotebooks = ({ analytic, selectedNotebook, hit, compact }) => {
|
|
|
124
125
|
MenuProps: {
|
|
125
126
|
style: { zIndex: 35001 }
|
|
126
127
|
}
|
|
127
|
-
}, defaultValue: selectedNotebook ? analytic?.notebooks.find(n => n.name === selectedNotebook).value : '', children: [_jsx(MenuItem, { disabled: true, value: "", children: _jsx("em", { children: t('hit.notebook.select') }) }), analytic?.notebooks.sort(safeStringPropertyCompare('detection')).map(e => (_jsx(MenuItem, { value: e.value, children: _jsxs(Stack, { direction: 'row', sx: { width: '100%' }, children: [e.name, e.detection && (_jsxs(_Fragment, { children: [_jsx(Box, { flex: 1 }), _jsx(Chip, { label: e.detection, size: "small" })] }))] }) }, e.value)))] }), _jsx(Button, { variant: "outlined", disabled: loadedNotebook.name === '' || envs.length === 0, color: "success", onClick: () => checkJupyhub(), children: t('hit.notebook.goTo') })] })] }) })) }), _jsx(HowlerCard, { variant: compact ? 'outlined' : 'elevation', onClick: handleToggle, sx: [
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
128
|
+
}, defaultValue: selectedNotebook ? analytic?.notebooks.find(n => n.name === selectedNotebook).value : '', children: [_jsx(MenuItem, { disabled: true, value: "", children: _jsx("em", { children: t('hit.notebook.select') }) }), analytic?.notebooks.sort(safeStringPropertyCompare('detection')).map(e => (_jsx(MenuItem, { value: e.value, children: _jsxs(Stack, { direction: 'row', sx: { width: '100%' }, children: [e.name, e.detection && (_jsxs(_Fragment, { children: [_jsx(Box, { flex: 1 }), _jsx(Chip, { label: e.detection, size: "small" })] }))] }) }, e.value)))] }), _jsx(Button, { variant: "outlined", disabled: loadedNotebook.name === '' || envs.length === 0, color: "success", onClick: () => checkJupyhub(), children: t('hit.notebook.goTo') })] })] }) })) }), _jsx(Tooltip, { title: _jsx(NotebookTooltip, {}), children: _jsx("span", { children: _jsx(HowlerCard, { variant: compact ? 'outlined' : 'elevation', onClick: handleToggle, sx: [
|
|
129
|
+
theme => ({
|
|
130
|
+
cursor: 'pointer',
|
|
131
|
+
backgroundColor: 'transparent',
|
|
132
|
+
transition: theme.transitions.create(['border-color']),
|
|
133
|
+
'&:hover': { borderColor: 'primary.main', '& a': { textDecoration: 'underline' } },
|
|
134
|
+
'& > div': {
|
|
135
|
+
height: '100%'
|
|
136
|
+
},
|
|
137
|
+
'& a': { textDecoration: 'none', color: 'text.primary' }
|
|
138
|
+
}),
|
|
139
|
+
!compact && { border: 'thin solid', borderColor: 'transparent' }
|
|
140
|
+
], children: _jsxs(Stack, { direction: "row", p: 1, spacing: 1, alignItems: "center", children: [_jsx(Avatar, { variant: "rounded", src: '/images/jupyter_notebook_file_icon.png', sx: [
|
|
141
|
+
theme => ({
|
|
142
|
+
width: theme.spacing(3),
|
|
143
|
+
height: theme.spacing(3),
|
|
144
|
+
'& img': {
|
|
145
|
+
objectFit: 'contain'
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
], children: 'jupyter_notebook_file_icon.png' }), t('hit.notebook.tooltip')] }) }) }) })] }) })));
|
|
148
149
|
};
|
|
149
150
|
export default memo(HitNotebooks);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Typography } from '@mui/material';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
const NotebookTooltip = () => {
|
|
5
|
+
const { t } = useTranslation();
|
|
6
|
+
return (_jsxs(Box, { sx: { maxWidth: 300 }, children: [_jsx(Typography, { variant: "subtitle2", children: t('notebook.title') }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: t('notebook.opens.panel') })] }));
|
|
7
|
+
};
|
|
8
|
+
export default NotebookTooltip;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Link, Typography } from '@mui/material';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
const PivotTooltip = ({ dossier, resolvedUrl }) => {
|
|
5
|
+
const dossierUrl = `/dossiers/${dossier.dossier_id}/edit?tab=leads&query=${encodeURIComponent(dossier.query)}`;
|
|
6
|
+
const { t } = useTranslation();
|
|
7
|
+
return (_jsxs(Box, { sx: { maxWidth: 300 }, children: [_jsx(Typography, { variant: "subtitle2", children: dossier.title }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: dossier.owner }), _jsx(Typography, { variant: "body2", sx: { mt: 2 }, children: dossier.leads?.[0]?.content?.slice(0, 120) ?? t('pivot.description.none') }), _jsxs(Box, { sx: { mt: 2, wordBreak: 'break-all' }, children: [_jsx(Typography, { variant: "caption", display: "block", children: t('pivot.url') }), _jsx(Link, { href: resolvedUrl, target: "_blank", rel: "noopener noreferrer", underline: "hover", children: resolvedUrl })] }), _jsx(Box, { sx: { mt: 2 }, children: _jsx(Link, { href: dossierUrl, target: "_blank", rel: "noopener noreferrer", underline: "hover", children: t('pivot.dossier.open') }) })] }));
|
|
8
|
+
};
|
|
9
|
+
export default PivotTooltip;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Link, Typography } from '@mui/material';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
const RelatedLinkTooltip = ({ title, href }) => {
|
|
5
|
+
const { t } = useTranslation();
|
|
6
|
+
return (_jsxs(Box, { sx: { maxWidth: 300 }, children: [_jsx(Typography, { variant: "subtitle2", children: title }), _jsx(Box, { sx: { mt: 1, wordBreak: 'break-all' }, children: _jsx(Link, { href: href, target: "_blank", rel: "noopener noreferrer", underline: "hover", children: href }) }), _jsx(Box, { sx: { mt: 2 }, children: _jsx(Link, { href: href, target: "_blank", rel: "noopener noreferrer", underline: "hover", children: t('hit.header.link') }) })] }));
|
|
7
|
+
};
|
|
8
|
+
export default RelatedLinkTooltip;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Dossier } from '@cccsaurora/howler-ui/models/entities/generated/Dossier';
|
|
2
|
+
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
3
|
+
declare const resolvePivotUrl: (pivot: NonNullable<Dossier["pivots"]>[number], currentHit?: Hit) => string;
|
|
4
|
+
export default resolvePivotUrl;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Handlebars from 'handlebars';
|
|
2
|
+
import { flattenDeep } from '@cccsaurora/howler-ui/utils/utils';
|
|
3
|
+
const resolvePivotUrl = (pivot, currentHit) => {
|
|
4
|
+
const flatHit = flattenDeep(currentHit ?? {});
|
|
5
|
+
const templateObject = Object.fromEntries((pivot.mappings ?? []).map(mapping => {
|
|
6
|
+
const value = mapping.field === 'custom'
|
|
7
|
+
? mapping.custom_value
|
|
8
|
+
: Array.isArray(flatHit[mapping.field])
|
|
9
|
+
? flatHit[mapping.field][0]
|
|
10
|
+
: flatHit[mapping.field];
|
|
11
|
+
return [mapping.key, value];
|
|
12
|
+
}));
|
|
13
|
+
try {
|
|
14
|
+
return Handlebars.compile(pivot.value)(templateObject);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
// eslint-disable-next-line no-console
|
|
18
|
+
console.error(`Failed to compile pivot template for value "${pivot.value}":`, e);
|
|
19
|
+
return pivot.value;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export default resolvePivotUrl;
|
|
@@ -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),
|
|
@@ -4,7 +4,7 @@ declare const _default: import("react").NamedExoticComponent<{
|
|
|
4
4
|
record: Hit | Event;
|
|
5
5
|
analyticIds: Record<string, string>;
|
|
6
6
|
columns: string[];
|
|
7
|
-
columnWidths: Record<string,
|
|
7
|
+
columnWidths: Record<string, number>;
|
|
8
8
|
collapseMainColumn: boolean;
|
|
9
9
|
onClick: (e: React.MouseEvent<HTMLDivElement>, record: Hit | Event) => void;
|
|
10
10
|
}>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Event } from '@cccsaurora/howler-ui/models/entities/generated/Event';
|
|
2
|
+
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
3
|
+
import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
|
|
4
|
+
import React, { type PropsWithChildren } from 'react';
|
|
5
|
+
declare const RecordTable: ({ query, items, refreshItems, ContextMenu, contextMenuProps, onItemClick }: {
|
|
6
|
+
query: string;
|
|
7
|
+
items?: WithMetadata<Hit | Event>[];
|
|
8
|
+
refreshItems?: (query: string, append?: boolean) => void;
|
|
9
|
+
ContextMenu?: React.FC<PropsWithChildren<object>>;
|
|
10
|
+
contextMenuProps?: object;
|
|
11
|
+
onItemClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>, record: Hit | Event) => void;
|
|
12
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export default RecordTable;
|