@cccsaurora/howler-ui 2.17.0-dev.502 → 2.17.0-dev.508
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/api/index.d.ts +2 -0
- package/api/index.js +4 -2
- package/api/search/case.d.ts +4 -0
- package/api/search/case.js +8 -0
- package/api/search/index.d.ts +2 -1
- package/api/search/index.js +2 -1
- package/api/v2/case/index.d.ts +6 -0
- package/api/v2/case/index.js +18 -0
- package/api/v2/index.d.ts +4 -0
- package/api/v2/index.js +6 -0
- package/api/v2/search/index.d.ts +4 -0
- package/api/v2/search/index.js +16 -0
- package/commons/components/leftnav/LeftNavDrawer.js +1 -1
- package/components/app/App.js +14 -0
- package/components/app/providers/FavouritesProvider.js +2 -2
- package/components/elements/{hit/HitDetails.d.ts → ObjectDetails.d.ts} +2 -1
- package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +14 -14
- package/components/elements/PluginTypography.d.ts +2 -1
- package/components/elements/PluginTypography.js +3 -2
- package/components/elements/UserList.d.ts +1 -0
- package/components/elements/UserList.js +2 -2
- package/components/elements/addons/search/phrase/Phrase.js +1 -1
- package/components/elements/display/HowlerCard.js +1 -1
- package/components/elements/hit/HitBanner.js +19 -31
- package/components/elements/hit/outlines/DefaultOutline.js +1 -1
- package/components/elements/view/ViewTitle.js +1 -1
- package/components/hooks/useHitSelection.js +1 -35
- package/components/hooks/useMyPreferences.js +10 -1
- package/components/hooks/useMySitemap.js +3 -1
- package/components/hooks/useMyTheme.js +9 -2
- package/components/routes/action/view/ActionSearch.js +1 -1
- package/components/routes/action/view/Integrations.js +1 -9
- package/components/routes/advanced/QueryBuilder.js +1 -1
- package/components/routes/analytics/AnalyticSearch.js +1 -1
- package/components/routes/cases/CaseCard.d.ts +8 -0
- package/components/routes/cases/CaseCard.js +34 -0
- package/components/routes/cases/CaseViewer.d.ts +2 -0
- package/components/routes/cases/CaseViewer.js +38 -0
- package/components/routes/cases/Cases.d.ts +2 -0
- package/components/routes/cases/Cases.js +101 -0
- package/components/routes/cases/constants.d.ts +5 -0
- package/components/routes/cases/constants.js +5 -0
- package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
- package/components/routes/cases/detail/AlertPanel.js +29 -0
- package/components/routes/cases/detail/CaseAggregate.d.ts +10 -0
- package/components/routes/cases/detail/CaseAggregate.js +30 -0
- package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
- package/components/routes/cases/detail/CaseDashboard.js +49 -0
- package/components/routes/cases/detail/CaseSidebar.d.ts +6 -0
- package/components/routes/cases/detail/CaseSidebar.js +35 -0
- package/components/routes/cases/detail/CaseTask.d.ts +9 -0
- package/components/routes/cases/detail/CaseTask.js +38 -0
- package/components/routes/cases/detail/ItemPage.d.ts +6 -0
- package/components/routes/cases/detail/ItemPage.js +93 -0
- package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
- package/components/routes/cases/detail/RelatedCasePanel.js +28 -0
- package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
- package/components/routes/cases/detail/TaskPanel.js +20 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +12 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.js +114 -0
- package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
- package/components/routes/help/ApiDocumentation.js +1 -1
- package/components/routes/help/HitDocumentation.js +1 -3
- package/components/routes/hits/search/HitContextMenu.js +4 -27
- package/components/routes/hits/search/HitContextMenu.test.js +0 -140
- package/components/routes/hits/search/InformationPane.d.ts +1 -0
- package/components/routes/hits/search/InformationPane.js +6 -29
- package/components/routes/hits/search/SearchPane.js +3 -5
- package/components/routes/hits/search/ViewLink.js +1 -1
- package/components/routes/hits/search/grid/EnhancedCell.js +1 -1
- package/components/routes/hits/view/HitViewer.js +3 -4
- package/components/routes/home/ViewCard.js +1 -1
- package/components/routes/observables/ObservableViewer.d.ts +7 -0
- package/components/routes/observables/ObservableViewer.js +27 -0
- package/locales/en/translation.json +413 -397
- package/locales/fr/translation.json +420 -406
- package/models/entities/generated/AttachmentsFile.d.ts +12 -0
- package/models/entities/generated/Case.d.ts +27 -0
- package/models/entities/generated/DestinationOriginal.d.ts +19 -0
- package/models/entities/generated/EmailAttachment.d.ts +8 -0
- package/models/entities/generated/EmailParent.d.ts +19 -0
- package/models/entities/generated/Enrichments.d.ts +7 -0
- package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
- package/models/entities/generated/Howler.d.ts +0 -4
- package/models/entities/generated/HttpResponse.d.ts +11 -0
- package/models/entities/generated/Item.d.ts +9 -0
- package/models/entities/generated/Observable.d.ts +84 -0
- package/models/entities/generated/ObservableCloud.d.ts +20 -0
- package/models/entities/generated/ObservableDestination.d.ts +23 -0
- package/models/entities/generated/ObservableEmail.d.ts +30 -0
- package/models/entities/generated/ObservableFile.d.ts +36 -0
- package/models/entities/generated/ObservableHowler.d.ts +44 -0
- package/models/entities/generated/ObservableHttp.d.ts +11 -0
- package/models/entities/generated/ObservableObserver.d.ts +21 -0
- package/models/entities/generated/ObservableOrganization.d.ts +7 -0
- package/models/entities/generated/ObservableProcess.d.ts +34 -0
- package/models/entities/generated/ObservableSource.d.ts +23 -0
- package/models/entities/generated/ObservableThreat.d.ts +21 -0
- package/models/entities/generated/ObservableTls.d.ts +12 -0
- package/models/entities/generated/ObserverIngress.d.ts +9 -0
- package/models/entities/generated/Rule.d.ts +2 -10
- package/models/entities/generated/Task.d.ts +10 -0
- package/models/entities/generated/Threat.d.ts +2 -2
- package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
- package/package.json +11 -2
- package/plugins/clue/components/ClueTypography.js +2 -2
- package/plugins/clue/utils.d.ts +2 -1
- package/components/elements/display/icons/BundleButton.d.ts +0 -6
- package/components/elements/display/icons/BundleButton.js +0 -32
- package/components/routes/action/view/markdown/integrations.en.md.js +0 -1
- package/components/routes/action/view/markdown/integrations.fr.md.js +0 -1
- package/components/routes/help/BundleDocumentation.d.ts +0 -3
- package/components/routes/help/BundleDocumentation.js +0 -12
- package/components/routes/help/markdown/en/bundles.md.js +0 -1
- package/components/routes/help/markdown/fr/bundles.md.js +0 -1
- package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
- package/components/routes/hits/search/BundleParentMenu.js +0 -32
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Divider, Pagination, Stack, Typography, useTheme } from '@mui/material';
|
|
3
|
+
import { chunk, uniq } from 'lodash-es';
|
|
4
|
+
import { useMemo, useState } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { Link } from 'react-router-dom';
|
|
7
|
+
import CaseCard from '../CaseCard';
|
|
8
|
+
const RelatedCasePanel = ({ case: _case }) => {
|
|
9
|
+
const { t } = useTranslation();
|
|
10
|
+
const theme = useTheme();
|
|
11
|
+
const [casePage, setCasePage] = useState(1);
|
|
12
|
+
const casePages = useMemo(() => chunk(uniq((_case?.items ?? []).filter(item => item.type === 'case')), 5), [_case?.items]);
|
|
13
|
+
return (_jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", children: [_jsx(Typography, { flex: 1, variant: "h4", children: t('page.cases.dashboard.alerts') }), _jsx(Pagination, { count: casePages.length, page: casePage, onChange: (_, page) => setCasePage(page) })] }), _jsx(Divider, {}), casePages[casePage - 1]?.map(item => (_jsxs(Box, { position: "relative", children: [_jsx(CaseCard, { caseId: item.id }), _jsx(Box, { component: Link, to: item.path, sx: {
|
|
14
|
+
position: 'absolute',
|
|
15
|
+
top: 0,
|
|
16
|
+
left: 0,
|
|
17
|
+
width: '100%',
|
|
18
|
+
height: '100%',
|
|
19
|
+
cursor: 'pointer',
|
|
20
|
+
zIndex: 100,
|
|
21
|
+
borderRadius: '4px',
|
|
22
|
+
'&:hover': {
|
|
23
|
+
background: theme.palette.divider,
|
|
24
|
+
border: `thin solid ${theme.palette.primary.light}`
|
|
25
|
+
}
|
|
26
|
+
} })] }, item.id)))] }));
|
|
27
|
+
};
|
|
28
|
+
export default RelatedCasePanel;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Divider, Stack, Typography } from '@mui/material';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import CaseTask from './CaseTask';
|
|
5
|
+
const TaskPanel = ({ case: _case, updateCase }) => {
|
|
6
|
+
const { t } = useTranslation();
|
|
7
|
+
// TODO: Implement adding tasks, checking tasks off, etc.
|
|
8
|
+
return (_jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { flex: 1, variant: "h4", children: t('page.cases.dashboard.tasks') }), _jsx(Divider, {}), _case.tasks.map(task => (_jsx(CaseTask, { task: task, paths: _case.items.map(item => item.path), onEdit: newTask => updateCase({
|
|
9
|
+
tasks: _case.tasks.map(_task => {
|
|
10
|
+
if (_task.id !== task.id) {
|
|
11
|
+
return _task;
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
..._task,
|
|
15
|
+
...newTask
|
|
16
|
+
};
|
|
17
|
+
})
|
|
18
|
+
}), onDelete: () => updateCase({ tasks: _case.tasks.filter(_task => _task.id !== task.id) }) }, task.id)))] }));
|
|
19
|
+
};
|
|
20
|
+
export default TaskPanel;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
|
|
2
|
+
import { type FC } from 'react';
|
|
3
|
+
import type { Tree } from './types';
|
|
4
|
+
declare const CaseFolder: FC<{
|
|
5
|
+
case: Case;
|
|
6
|
+
folder?: Tree;
|
|
7
|
+
name?: string;
|
|
8
|
+
step?: number;
|
|
9
|
+
rootCaseId?: string;
|
|
10
|
+
pathPrefix?: string;
|
|
11
|
+
}>;
|
|
12
|
+
export default CaseFolder;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Article, BookRounded, ChevronRight, Folder as FolderIcon } from '@mui/icons-material';
|
|
3
|
+
import { Skeleton, Stack, Typography, useTheme } from '@mui/material';
|
|
4
|
+
import api from '@cccsaurora/howler-ui/api';
|
|
5
|
+
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
6
|
+
import { get, last, omit, set } from 'lodash-es';
|
|
7
|
+
import { useMemo, useState } from 'react';
|
|
8
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
9
|
+
const buildTree = (items = []) => {
|
|
10
|
+
// Root tree node stores direct children in `leaves` and nested folders as object keys.
|
|
11
|
+
const tree = { leaves: [] };
|
|
12
|
+
items.forEach(item => {
|
|
13
|
+
// Ignore items that cannot be placed in the folder structure.
|
|
14
|
+
if (!item?.path) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
// Split path into folder segments + item name, then remove the item name.
|
|
18
|
+
const parts = item.path.split('/');
|
|
19
|
+
parts.pop();
|
|
20
|
+
if (parts.length > 0) {
|
|
21
|
+
// Use dot notation so lodash `get/set` can address nested folder objects.
|
|
22
|
+
const key = parts.join('.');
|
|
23
|
+
const size = get(tree, key)?.leaves?.length || 0;
|
|
24
|
+
// Append this item to the folder's `leaves` array.
|
|
25
|
+
set(tree, `${key}.leaves.${size}`, item);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Items without parent folders are top-level leaves.
|
|
29
|
+
tree.leaves.push(item);
|
|
30
|
+
});
|
|
31
|
+
return tree;
|
|
32
|
+
};
|
|
33
|
+
const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPrefix = '' }) => {
|
|
34
|
+
const theme = useTheme();
|
|
35
|
+
const location = useLocation();
|
|
36
|
+
const { dispatchApi } = useMyApi();
|
|
37
|
+
const [open, setOpen] = useState(true);
|
|
38
|
+
const [openCases, setOpenCases] = useState({});
|
|
39
|
+
const [loadingCases, setLoadingCases] = useState({});
|
|
40
|
+
const [nestedCases, setNestedCases] = useState({});
|
|
41
|
+
const tree = useMemo(() => folder || buildTree(_case?.items), [folder, _case?.items]);
|
|
42
|
+
const currentRootCaseId = rootCaseId || _case?.case_id;
|
|
43
|
+
const toggleCase = (item, itemKey) => {
|
|
44
|
+
// Use the fully-qualified path key when available so nested case toggles are unique.
|
|
45
|
+
const resolvedItemKey = itemKey || item.path || item.id;
|
|
46
|
+
if (!resolvedItemKey) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Toggle expand/collapse state for this case node.
|
|
50
|
+
const shouldOpen = !openCases[resolvedItemKey];
|
|
51
|
+
setOpenCases(current => ({ ...current, [resolvedItemKey]: shouldOpen }));
|
|
52
|
+
// Only fetch when opening, with a valid case id, and when no fetch/data is already in-flight/cached.
|
|
53
|
+
if (!shouldOpen || !item.id || nestedCases[resolvedItemKey] || loadingCases[resolvedItemKey]) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
setLoadingCases(current => ({ ...current, [resolvedItemKey]: true }));
|
|
57
|
+
// Lazy-load the nested case content and cache it by the same unique key.
|
|
58
|
+
dispatchApi(api.v2.case.get(item.id), { throwError: false })
|
|
59
|
+
.then(caseResponse => {
|
|
60
|
+
if (!caseResponse) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
setNestedCases(current => ({ ...current, [resolvedItemKey]: caseResponse }));
|
|
64
|
+
})
|
|
65
|
+
.finally(() => {
|
|
66
|
+
setLoadingCases(current => ({ ...current, [resolvedItemKey]: false }));
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
return (_jsxs(Stack, { sx: { overflow: 'visible' }, children: [name && (_jsxs(Stack, { direction: "row", pl: step * 1.5, py: 0.25, sx: {
|
|
70
|
+
cursor: 'pointer',
|
|
71
|
+
transition: theme.transitions.create('background', { duration: 50 }),
|
|
72
|
+
background: 'transparent',
|
|
73
|
+
'&:hover': {
|
|
74
|
+
background: theme.palette.grey[800]
|
|
75
|
+
}
|
|
76
|
+
}, onClick: () => setOpen(_open => !_open), children: [_jsx(ChevronRight, { fontSize: "small", color: "disabled", sx: [
|
|
77
|
+
{ transition: theme.transitions.create('transform', { duration: 100 }), transform: 'rotate(0deg)' },
|
|
78
|
+
open && { transform: 'rotate(90deg)' }
|
|
79
|
+
] }), _jsx(FolderIcon, { fontSize: "small", color: "disabled" }), _jsx(Typography, { variant: "caption", color: "textSecondary", sx: { userSelect: 'none', pl: 0.5, textWrap: 'nowrap' }, children: name })] })), open && (_jsxs(_Fragment, { children: [Object.entries(omit(tree, 'leaves')).map(([path, subfolder]) => (_jsx(CaseFolder, { name: path, case: _case, folder: subfolder, step: step + 1, rootCaseId: currentRootCaseId, pathPrefix: pathPrefix }, `${_case?.case_id}-${path}`))), tree.leaves?.map(leaf => {
|
|
80
|
+
const itemType = leaf.type?.toLowerCase();
|
|
81
|
+
const isCase = itemType === 'case';
|
|
82
|
+
const fullRelativePath = [pathPrefix, leaf.path].filter(Boolean).join('/');
|
|
83
|
+
const itemKey = fullRelativePath || leaf.id;
|
|
84
|
+
const isCaseOpen = !!(itemKey && openCases[itemKey]);
|
|
85
|
+
const isCaseLoading = !!(itemKey && loadingCases[itemKey]);
|
|
86
|
+
const nestedCase = itemKey ? nestedCases[itemKey] : null;
|
|
87
|
+
const itemPath = fullRelativePath
|
|
88
|
+
? `/cases/${currentRootCaseId}/${fullRelativePath}`
|
|
89
|
+
: `/cases/${currentRootCaseId}`;
|
|
90
|
+
return (_jsxs(Stack, { children: [_jsxs(Stack, { direction: "row", pl: step * 1.5 + 1, py: 0.25, sx: [
|
|
91
|
+
{
|
|
92
|
+
cursor: 'pointer',
|
|
93
|
+
overflow: 'visible',
|
|
94
|
+
color: `${theme.palette.text.secondary} !important`,
|
|
95
|
+
textDecoration: 'none',
|
|
96
|
+
transition: theme.transitions.create('background', { duration: 100 }),
|
|
97
|
+
background: 'transparent',
|
|
98
|
+
'&:hover': {
|
|
99
|
+
background: theme.palette.grey[800]
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
decodeURIComponent(location.pathname) === itemPath && {
|
|
103
|
+
background: theme.palette.grey[800]
|
|
104
|
+
}
|
|
105
|
+
], onClick: () => isCase && toggleCase(leaf, itemKey), component: Link, to: itemPath, children: [_jsx(ChevronRight, { fontSize: "small", sx: [
|
|
106
|
+
!isCase && { opacity: 0 },
|
|
107
|
+
isCase && {
|
|
108
|
+
transition: theme.transitions.create('transform', { duration: 100 }),
|
|
109
|
+
transform: isCaseOpen ? 'rotate(90deg)' : 'rotate(0deg)'
|
|
110
|
+
}
|
|
111
|
+
] }), isCase ? _jsx(BookRounded, { fontSize: "small" }) : _jsx(Article, { fontSize: "small" }), _jsx(Typography, { variant: "caption", color: "textSecondary", sx: { userSelect: 'none', pl: 0.5, textWrap: 'nowrap' }, children: last(leaf.path?.split('/') || []) })] }), isCase && isCaseOpen && isCaseLoading && (_jsx(Stack, { pl: step * 1.5 + 4, py: 0.25, children: _jsx(Skeleton, { width: 140, height: 16 }) })), isCase && isCaseOpen && nestedCase && (_jsx(CaseFolder, { case: nestedCase, step: step + 1, rootCaseId: currentRootCaseId, pathPrefix: fullRelativePath }))] }, `${_case?.case_id}-${leaf.id}-${leaf.path}`));
|
|
112
|
+
})] }))] }));
|
|
113
|
+
};
|
|
114
|
+
export default CaseFolder;
|
|
@@ -49,7 +49,7 @@ const ApiDocumentation = () => {
|
|
|
49
49
|
.replace(/(\S+)\s+=>\s+(.+)/g, '\n`$1`: $2\n')
|
|
50
50
|
.replace(/(Data Block:\n)([\s\S]+)(Result Example:)/, (__, p1, p2, p3) => `${p1}\`\`\`\n${p2.trim()}\n\`\`\`\n${p3}`)
|
|
51
51
|
.replace(/(Result Example:\n)([\s\S]+)$/, (__, p1, p2) => `${p1}\`\`\`\n${p2.trim()}\n\`\`\``) }));
|
|
52
|
-
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { style: { marginBottom: '1rem' }, sx: [isLg && { '& > td': { borderBottom: 0 } }], children: [_jsx(TableCell, { children: _jsxs(Stack, { direction: "column", spacing: 1, alignItems: "start", children: [_jsx(Typography, { children: endpoint.name }), _jsx("code", { children: endpoint.path }), _jsxs(Stack, { direction: "row", spacing: 1, children: [endpoint.complete ? (_jsx(Chip, {
|
|
52
|
+
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { style: { marginBottom: '1rem' }, sx: [isLg && { '& > td': { borderBottom: 0 } }], children: [_jsx(TableCell, { children: _jsxs(Stack, { direction: "column", spacing: 1, alignItems: "start", children: [_jsx(Typography, { children: endpoint.name }), _jsx("code", { children: endpoint.path }), _jsxs(Stack, { direction: "row", spacing: 1, children: [endpoint.complete ? (_jsx(Chip, { label: "Stable", color: "success" })) : (_jsx(Chip, { label: "Unstable", color: "error" })), endpoint.protected ? (_jsx(Chip, { label: "Protected", color: "warning" })) : (_jsx(Chip, { label: "Unprotected" }))] }), _jsx(Stack, { spacing: 1, direction: "row", children: endpoint.methods.map(m => (_jsx(Chip, { size: "small", label: m }, m))) }), endpoint.ui_only && _jsx(Chip, { label: "UI Only" })] }) }), _jsx(TableCell, { children: _jsx(Stack, { spacing: 1, direction: "row", children: endpoint.required_type.map(type => (_jsx(Chip, { size: "small", label: type, color: user.roles?.includes(type) ? 'success' : 'default' }, type))) }) }), _jsx(TableCell, { children: _jsx(Stack, { spacing: 1, direction: "row", children: endpoint.required_priv.map((p) => (_jsx(Chip, { size: "small", label: t(APIKEY_LABELS[p]) }, p))) }) }), !isLg && _jsx(TableCell, { children: documentationCell })] }), isLg && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 3, sx: { '& pre': { whiteSpace: 'pre-wrap' } }, children: documentationCell }) }))] }, endpoint.id));
|
|
53
53
|
}) })] }) })] }) }));
|
|
54
54
|
};
|
|
55
55
|
export default ApiDocumentation;
|
|
@@ -5,7 +5,6 @@ import { useScrollRestoration } from '@cccsaurora/howler-ui/components/hooks/use
|
|
|
5
5
|
import { useCallback, useState } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { useSearchParams } from 'react-router-dom';
|
|
8
|
-
import BundleDocumentation from './BundleDocumentation';
|
|
9
8
|
import HelpTabs from './components/HelpTabs';
|
|
10
9
|
import HitBannerDocumentation from './HitBannerDocumentation';
|
|
11
10
|
import HitLabelsDocumentation from './HitLabelsDocumentation';
|
|
@@ -23,8 +22,7 @@ const HitDocumentation = () => {
|
|
|
23
22
|
searchParams.set('tab', _tab);
|
|
24
23
|
setSearchParams(new URLSearchParams(searchParams));
|
|
25
24
|
}, [searchParams, setSearchParams]);
|
|
26
|
-
return (_jsx(PageCenter, { margin: 4, width: "100%", maxWidth: "1750px", textAlign: "left", children: _jsxs(Stack, { sx: { flexDirection: useHorizontal ? 'column' : 'row', '& h1': { mt: 0 } }, children: [_jsxs(HelpTabs, { value: tab, children: [_jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.schema.title') }), value: "schema", onClick: () => onChange('schema') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.banner.title') }), value: "header", onClick: () => onChange('header') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.
|
|
27
|
-
bundle: () => _jsx(BundleDocumentation, {}),
|
|
25
|
+
return (_jsx(PageCenter, { margin: 4, width: "100%", maxWidth: "1750px", textAlign: "left", children: _jsxs(Stack, { sx: { flexDirection: useHorizontal ? 'column' : 'row', '& h1': { mt: 0 } }, children: [_jsxs(HelpTabs, { value: tab, children: [_jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.schema.title') }), value: "schema", onClick: () => onChange('schema') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.banner.title') }), value: "header", onClick: () => onChange('header') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.links.title') }), value: "links", onClick: () => onChange('links') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.labels.title') }), value: "labels", onClick: () => onChange('labels') })] }), _jsx(Box, { children: {
|
|
28
26
|
header: () => _jsx(HitBannerDocumentation, {}),
|
|
29
27
|
links: () => _jsx(HitLinksDocumentation, {}),
|
|
30
28
|
labels: () => _jsx(HitLabelsDocumentation, {}),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { Assignment, Edit, HowToVote, KeyboardArrowRight, OpenInNew, QueryStats, RemoveCircleOutline, SettingsSuggest, Terminal } from '@mui/icons-material';
|
|
3
3
|
import { Box, Divider, Fade, ListItemIcon, ListItemText, Menu, MenuItem, MenuList, Paper } from '@mui/material';
|
|
4
4
|
import api from '@cccsaurora/howler-ui/api';
|
|
5
5
|
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
@@ -170,9 +170,10 @@ const HitContextMenu = ({ children, getSelectedId, Component = Box }) => {
|
|
|
170
170
|
sx: {
|
|
171
171
|
...transformProps,
|
|
172
172
|
overflow: 'visible !important'
|
|
173
|
-
}
|
|
173
|
+
},
|
|
174
|
+
elevation: 2
|
|
174
175
|
}
|
|
175
|
-
}, MenuListProps: { dense: true, sx: { minWidth: '250px' } }, anchorOrigin: { vertical: 'top', horizontal: 'left' }, onClick: () => setAnchorEl(null), children: [_jsxs(MenuItem, { component: Link, to: `/hits/${hit?.howler.id}`, disabled: !hit, children: [_jsx(ListItemIcon, { children: _jsx(OpenInNew, {}) }), _jsx(ListItemText, { children: t('hit.panel.open') })] }), _jsxs(MenuItem, { component: Link, to: `/analytics/${analytic?.analytic_id}`, disabled: !analytic, children: [_jsx(ListItemIcon, { children: _jsx(QueryStats, {}) }), _jsx(ListItemText, { children: t('hit.panel.analytic.open') })] }), _jsx(Divider, {}), entries.map(([type, items]) => (_jsxs(MenuItem, { id: `${type}-menu-item`, sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, [type]: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, [type]: null })), disabled: rowStatus[type] === false, children: [_jsx(ListItemIcon, { children: ICON_MAP[type] ?? _jsx(Terminal, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t(`hit.details.actions.${type}`) }), rowStatus[type] !== false && (_jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } })), _jsx(Fade, { in: !!show[type], unmountOnExit: true, children: _jsx(Paper, { id: `${type}-submenu`, sx: calculateSubMenuStyles(show[type]), elevation:
|
|
176
|
+
}, MenuListProps: { dense: true, sx: { minWidth: '250px' } }, anchorOrigin: { vertical: 'top', horizontal: 'left' }, onClick: () => setAnchorEl(null), children: [_jsxs(MenuItem, { component: Link, to: `/hits/${hit?.howler.id}`, disabled: !hit, children: [_jsx(ListItemIcon, { children: _jsx(OpenInNew, {}) }), _jsx(ListItemText, { children: t('hit.panel.open') })] }), _jsxs(MenuItem, { component: Link, to: `/analytics/${analytic?.analytic_id}`, disabled: !analytic, children: [_jsx(ListItemIcon, { children: _jsx(QueryStats, {}) }), _jsx(ListItemText, { children: t('hit.panel.analytic.open') })] }), _jsx(Divider, {}), entries.map(([type, items]) => (_jsxs(MenuItem, { id: `${type}-menu-item`, sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, [type]: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, [type]: null })), disabled: rowStatus[type] === false, children: [_jsx(ListItemIcon, { children: ICON_MAP[type] ?? _jsx(Terminal, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t(`hit.details.actions.${type}`) }), rowStatus[type] !== false && (_jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } })), _jsx(Fade, { in: !!show[type], unmountOnExit: true, children: _jsx(Paper, { id: `${type}-submenu`, sx: calculateSubMenuStyles(show[type]), elevation: 2, children: _jsx(MenuList, { sx: { p: 0, borderTopLeftRadius: 0 }, dense: true, role: "group", children: items.map(a => (_jsx(MenuItem, { value: a.name, onClick: a.actionFunction, children: a.i18nKey ? t(a.i18nKey) : capitalize(a.name) }, a.name))) }) }) })] }, type))), _jsxs(MenuItem, { id: "actions-menu-item", sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, actions: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, actions: null })), disabled: actions.length < 1, children: [_jsx(ListItemIcon, { children: _jsx(SettingsSuggest, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t('route.actions.change') }), actions.length > 0 && _jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } }), _jsx(Fade, { in: !!show.actions, unmountOnExit: true, children: _jsx(Paper, { id: "actions-submenu", sx: calculateSubMenuStyles(show.actions), elevation: 2, children: _jsx(MenuList, { sx: { p: 0 }, dense: true, role: "group", children: actions.map(action => (_jsx(MenuItem, { onClick: () => executeAction(action.action_id, `howler.id:${hit?.howler.id}`), children: _jsx(ListItemText, { children: action.name }) }, action.action_id))) }) }) })] }), !isEmpty(template?.keys ?? []) && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(MenuItem, { id: "excludes-menu-item", sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, excludes: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, excludes: null })), children: [_jsx(ListItemIcon, { children: _jsx(RemoveCircleOutline, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t('hit.panel.exclude') }), _jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } }), _jsx(Fade, { in: !!show.excludes, unmountOnExit: true, children: _jsx(Paper, { id: "excludes-submenu", sx: calculateSubMenuStyles(show.excludes), elevation: 2, children: _jsx(MenuList, { sx: { p: 0 }, dense: true, role: "group", children: template?.keys.map(key => {
|
|
176
177
|
// Build exclusion query based on current query and field value
|
|
177
178
|
let newQuery = '';
|
|
178
179
|
if (query !== DEFAULT_QUERY) {
|
|
@@ -198,30 +199,6 @@ const HitContextMenu = ({ children, getSelectedId, Component = Box }) => {
|
|
|
198
199
|
newQuery += `-${key}:"${sanitizeLuceneQuery(value.toString())}"`;
|
|
199
200
|
}
|
|
200
201
|
return (_jsx(MenuItem, { onClick: () => setQuery(newQuery), children: _jsx(ListItemText, { children: key }) }, key));
|
|
201
|
-
}) }) }) })] }), _jsxs(MenuItem, { id: "includes-menu-item", sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, includes: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, includes: null })), children: [_jsx(ListItemIcon, { children: _jsx(AddCircleOutline, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t('hit.panel.include') }), _jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } }), _jsx(Fade, { in: !!show.includes, unmountOnExit: true, children: _jsx(Paper, { id: "includes-submenu", sx: calculateSubMenuStyles(show.includes), elevation: 8, children: _jsx(MenuList, { sx: { p: 0 }, dense: true, role: "group", children: template?.keys.map(key => {
|
|
202
|
-
// Build inclusion query based on current query and field
|
|
203
|
-
// If default, we include default query
|
|
204
|
-
let newQuery = `(${query}) AND `;
|
|
205
|
-
const value = get(hit, key);
|
|
206
|
-
if (!value) {
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
else if (Array.isArray(value)) {
|
|
210
|
-
// Handle array values by including all items
|
|
211
|
-
const sanitizedValues = value
|
|
212
|
-
.map(toString)
|
|
213
|
-
.filter(val => !!val)
|
|
214
|
-
.map(val => `"${sanitizeLuceneQuery(val)}"`);
|
|
215
|
-
if (sanitizedValues.length < 1) {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
newQuery += `${key}:(${sanitizedValues.join(' OR ')})`;
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
// Handle single value
|
|
222
|
-
newQuery += `${key}:"${sanitizeLuceneQuery(value.toString())}"`;
|
|
223
|
-
}
|
|
224
|
-
return (_jsx(MenuItem, { onClick: () => setQuery(newQuery), children: _jsx(ListItemText, { children: key }) }, key));
|
|
225
202
|
}) }) }) })] })] }))] })] }));
|
|
226
203
|
};
|
|
227
204
|
export default HitContextMenu;
|
|
@@ -623,132 +623,6 @@ describe('HitContextMenu', () => {
|
|
|
623
623
|
});
|
|
624
624
|
});
|
|
625
625
|
});
|
|
626
|
-
describe('Inclusion Filter Functionality', () => {
|
|
627
|
-
beforeEach(() => {
|
|
628
|
-
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
629
|
-
keys: ['howler.detection', 'event.id']
|
|
630
|
-
}));
|
|
631
|
-
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
632
|
-
});
|
|
633
|
-
it('should render inclusion submenu with template keys', async () => {
|
|
634
|
-
act(() => {
|
|
635
|
-
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
636
|
-
fireEvent.contextMenu(contextMenuWrapper);
|
|
637
|
-
});
|
|
638
|
-
await waitFor(() => {
|
|
639
|
-
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
640
|
-
});
|
|
641
|
-
act(() => {
|
|
642
|
-
const includesMenuItem = screen.getByText('Include By');
|
|
643
|
-
fireEvent.mouseEnter(includesMenuItem);
|
|
644
|
-
});
|
|
645
|
-
await waitFor(() => {
|
|
646
|
-
const submenu = screen.getByTestId('includes-submenu');
|
|
647
|
-
expect(submenu).toBeInTheDocument();
|
|
648
|
-
expect(submenu.textContent).toContain('howler.detection');
|
|
649
|
-
expect(submenu.textContent).toContain('event.id');
|
|
650
|
-
});
|
|
651
|
-
});
|
|
652
|
-
it('should generate inclusion query for single value', async () => {
|
|
653
|
-
act(() => {
|
|
654
|
-
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
655
|
-
fireEvent.contextMenu(contextMenuWrapper);
|
|
656
|
-
});
|
|
657
|
-
await waitFor(() => {
|
|
658
|
-
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
659
|
-
});
|
|
660
|
-
act(() => {
|
|
661
|
-
const includesMenuItem = screen.getByText('Include By');
|
|
662
|
-
fireEvent.mouseEnter(includesMenuItem);
|
|
663
|
-
});
|
|
664
|
-
await waitFor(() => {
|
|
665
|
-
expect(screen.getByTestId('includes-submenu')).toBeInTheDocument();
|
|
666
|
-
});
|
|
667
|
-
await act(async () => {
|
|
668
|
-
const detectionKey = screen.getByText('howler.detection');
|
|
669
|
-
await user.click(detectionKey);
|
|
670
|
-
});
|
|
671
|
-
await waitFor(() => {
|
|
672
|
-
expect(mockParameterContext.setQuery).toHaveBeenCalledWith('(howler.status:open) AND howler.detection:"Test Detection"');
|
|
673
|
-
});
|
|
674
|
-
});
|
|
675
|
-
it('should generate inclusion query for array values', async () => {
|
|
676
|
-
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
677
|
-
keys: ['howler.outline.indicators']
|
|
678
|
-
}));
|
|
679
|
-
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
680
|
-
act(() => {
|
|
681
|
-
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
682
|
-
fireEvent.contextMenu(contextMenuWrapper);
|
|
683
|
-
});
|
|
684
|
-
await waitFor(() => {
|
|
685
|
-
const includesMenuItem = screen.getByText('Include By');
|
|
686
|
-
fireEvent.mouseEnter(includesMenuItem);
|
|
687
|
-
});
|
|
688
|
-
await waitFor(() => {
|
|
689
|
-
expect(screen.getByTestId('includes-submenu')).toBeInTheDocument();
|
|
690
|
-
});
|
|
691
|
-
await act(async () => {
|
|
692
|
-
const tagsKey = screen.getByText('howler.outline.indicators');
|
|
693
|
-
await user.click(tagsKey);
|
|
694
|
-
});
|
|
695
|
-
await waitFor(() => {
|
|
696
|
-
expect(mockParameterContext.setQuery).toHaveBeenCalledWith('(howler.status:open) AND howler.outline.indicators:("a" OR "b" OR "c")');
|
|
697
|
-
});
|
|
698
|
-
});
|
|
699
|
-
it('should preserve existing query when adding inclusion', async () => {
|
|
700
|
-
mockParameterContext.query = 'howler.status:open';
|
|
701
|
-
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
702
|
-
fireEvent.contextMenu(contextMenuWrapper);
|
|
703
|
-
await waitFor(() => {
|
|
704
|
-
const includesMenuItem = screen.getByText('Include By');
|
|
705
|
-
fireEvent.mouseEnter(includesMenuItem);
|
|
706
|
-
});
|
|
707
|
-
await waitFor(() => {
|
|
708
|
-
expect(screen.getByTestId('includes-submenu')).toBeInTheDocument();
|
|
709
|
-
});
|
|
710
|
-
await act(async () => {
|
|
711
|
-
const detectionKey = screen.getByText('howler.detection');
|
|
712
|
-
await user.click(detectionKey);
|
|
713
|
-
});
|
|
714
|
-
await waitFor(() => {
|
|
715
|
-
expect(mockParameterContext.setQuery).toHaveBeenCalledWith('(howler.status:open) AND howler.detection:"Test Detection"');
|
|
716
|
-
});
|
|
717
|
-
});
|
|
718
|
-
it('should not render inclusion menu when template has no keys', async () => {
|
|
719
|
-
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
720
|
-
keys: []
|
|
721
|
-
}));
|
|
722
|
-
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
723
|
-
act(() => {
|
|
724
|
-
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
725
|
-
fireEvent.contextMenu(contextMenuWrapper);
|
|
726
|
-
});
|
|
727
|
-
await waitFor(() => {
|
|
728
|
-
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
729
|
-
});
|
|
730
|
-
expect(screen.queryByText('Include By')).toBeNull();
|
|
731
|
-
});
|
|
732
|
-
it('should skip null field values in inclusion menu', async () => {
|
|
733
|
-
act(() => {
|
|
734
|
-
mockHitContext.hits['test-hit-1'].event = {};
|
|
735
|
-
});
|
|
736
|
-
act(() => {
|
|
737
|
-
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
738
|
-
fireEvent.contextMenu(contextMenuWrapper);
|
|
739
|
-
});
|
|
740
|
-
await waitFor(() => {
|
|
741
|
-
const includesMenuItem = screen.getByText('Include By');
|
|
742
|
-
fireEvent.mouseEnter(includesMenuItem);
|
|
743
|
-
});
|
|
744
|
-
await waitFor(() => {
|
|
745
|
-
const submenu = screen.getByTestId('includes-submenu');
|
|
746
|
-
expect(submenu).toBeInTheDocument();
|
|
747
|
-
expect(submenu.textContent).toContain('howler.detection');
|
|
748
|
-
expect(submenu.textContent).not.toContain('event.id');
|
|
749
|
-
});
|
|
750
|
-
});
|
|
751
|
-
});
|
|
752
626
|
describe('Multiple Hit Selection', () => {
|
|
753
627
|
it('should use selectedHits when current hit is included', async () => {
|
|
754
628
|
act(() => {
|
|
@@ -847,20 +721,6 @@ describe('HitContextMenu', () => {
|
|
|
847
721
|
expect(screen.queryByText('Exclude By')).toBeNull();
|
|
848
722
|
});
|
|
849
723
|
});
|
|
850
|
-
it('should not render inclusion menu when template is null', async () => {
|
|
851
|
-
mockGetMatchingTemplate.mockResolvedValue(null);
|
|
852
|
-
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
853
|
-
act(() => {
|
|
854
|
-
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
855
|
-
fireEvent.contextMenu(contextMenuWrapper);
|
|
856
|
-
});
|
|
857
|
-
await waitFor(() => {
|
|
858
|
-
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
859
|
-
});
|
|
860
|
-
await waitFor(() => {
|
|
861
|
-
expect(screen.queryByText('Include By')).toBeNull();
|
|
862
|
-
});
|
|
863
|
-
});
|
|
864
724
|
it('should handle API failure gracefully', async () => {
|
|
865
725
|
mockDispatchApi.mockResolvedValue(null);
|
|
866
726
|
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
@@ -12,13 +12,10 @@ import VSBox from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox
|
|
|
12
12
|
import VSBoxContent from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBoxContent';
|
|
13
13
|
import VSBoxHeader from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBoxHeader';
|
|
14
14
|
import Phrase from '@cccsaurora/howler-ui/components/elements/addons/search/phrase/Phrase';
|
|
15
|
-
import BundleButton from '@cccsaurora/howler-ui/components/elements/display/icons/BundleButton';
|
|
16
15
|
import SocketBadge from '@cccsaurora/howler-ui/components/elements/display/icons/SocketBadge';
|
|
17
16
|
import JSONViewer from '@cccsaurora/howler-ui/components/elements/display/json/JSONViewer';
|
|
18
17
|
import HitActions from '@cccsaurora/howler-ui/components/elements/hit/HitActions';
|
|
19
|
-
import HitBanner from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
|
|
20
18
|
import HitComments from '@cccsaurora/howler-ui/components/elements/hit/HitComments';
|
|
21
|
-
import HitDetails from '@cccsaurora/howler-ui/components/elements/hit/HitDetails';
|
|
22
19
|
import HitLabels from '@cccsaurora/howler-ui/components/elements/hit/HitLabels';
|
|
23
20
|
import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
|
|
24
21
|
import HitNotebooks from '@cccsaurora/howler-ui/components/elements/hit/HitNotebooks';
|
|
@@ -29,6 +26,7 @@ import HitSummary from '@cccsaurora/howler-ui/components/elements/hit/HitSummary
|
|
|
29
26
|
import HitWorklog from '@cccsaurora/howler-ui/components/elements/hit/HitWorklog';
|
|
30
27
|
import PivotLink from '@cccsaurora/howler-ui/components/elements/hit/related/PivotLink';
|
|
31
28
|
import RelatedLink from '@cccsaurora/howler-ui/components/elements/hit/related/RelatedLink';
|
|
29
|
+
import ObjectDetails from '@cccsaurora/howler-ui/components/elements/ObjectDetails';
|
|
32
30
|
import useMyUserList from '@cccsaurora/howler-ui/components/hooks/useMyUserList';
|
|
33
31
|
import ErrorBoundary from '@cccsaurora/howler-ui/components/routes/ErrorBoundary';
|
|
34
32
|
import { uniqBy } from 'lodash-es';
|
|
@@ -42,13 +40,13 @@ import { getUserList } from '@cccsaurora/howler-ui/utils/hitFunctions';
|
|
|
42
40
|
import { validateRegex } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
43
41
|
import { tryParse } from '@cccsaurora/howler-ui/utils/utils';
|
|
44
42
|
import LeadRenderer from '../view/LeadRenderer';
|
|
45
|
-
const InformationPane = ({ onClose }) => {
|
|
43
|
+
const InformationPane = ({ onClose, selected: _selected }) => {
|
|
46
44
|
const { t, i18n } = useTranslation();
|
|
47
45
|
const theme = useTheme();
|
|
48
46
|
const location = useLocation();
|
|
49
47
|
const { emit, isOpen } = useContext(SocketContext);
|
|
50
48
|
const { getMatchingOverview, getMatchingDossiers, getMatchingAnalytic } = useMatchers();
|
|
51
|
-
const selected = useContextSelector(ParameterContext, ctx => ctx
|
|
49
|
+
const selected = useContextSelector(ParameterContext, ctx => ctx?.selected) ?? _selected;
|
|
52
50
|
const pluginStore = usePluginStore();
|
|
53
51
|
const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
|
|
54
52
|
const [userIds, setUserIds] = useState(new Set());
|
|
@@ -97,11 +95,6 @@ const InformationPane = ({ onClose }) => {
|
|
|
97
95
|
useEffect(() => {
|
|
98
96
|
getMatchingOverview(hit).then(_overview => setHasOverview(!!_overview));
|
|
99
97
|
}, [getMatchingOverview, hit]);
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
if (tab === 'hit_aggregate' && !hit?.howler.is_bundle) {
|
|
102
|
-
setTab('overview');
|
|
103
|
-
}
|
|
104
|
-
}, [hit?.howler.is_bundle, tab]);
|
|
105
98
|
useEffect(() => {
|
|
106
99
|
if (selected && isOpen()) {
|
|
107
100
|
emit({
|
|
@@ -125,28 +118,13 @@ const InformationPane = ({ onClose }) => {
|
|
|
125
118
|
}
|
|
126
119
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
127
120
|
}, [hasOverview]);
|
|
128
|
-
/**
|
|
129
|
-
* What to show as the header? If loading a skeleton, then it depends on bundle or not. Bundles don't
|
|
130
|
-
* show anything while normal hits do
|
|
131
|
-
*/
|
|
132
|
-
const header = useMemo(() => {
|
|
133
|
-
if (loading && !hit?.howler?.is_bundle) {
|
|
134
|
-
return _jsx(Skeleton, { variant: "rounded", height: 152 });
|
|
135
|
-
}
|
|
136
|
-
else if (!!hit && !hit.howler.is_bundle) {
|
|
137
|
-
return _jsx(HitBanner, { layout: HitLayout.DENSE, hit: hit });
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
}, [hit, loading]);
|
|
143
121
|
const tabContent = useMemo(() => {
|
|
144
122
|
if (!tab) {
|
|
145
123
|
return;
|
|
146
124
|
}
|
|
147
125
|
return {
|
|
148
126
|
overview: () => _jsx(HitOverview, { hit: hit }),
|
|
149
|
-
details: () => _jsx(
|
|
127
|
+
details: () => _jsx(ObjectDetails, { obj: hit }),
|
|
150
128
|
hit_comments: () => _jsx(HitComments, { hit: hit, users: users }),
|
|
151
129
|
hit_raw: () => _jsx(JSONViewer, { data: !loading && hit, hideSearch: true, filter: filter }),
|
|
152
130
|
hit_data: () => (_jsx(JSONViewer, { data: !loading && hit?.howler?.data?.map(entry => tryParse(entry)), collapse: false, hideSearch: true, filter: filter })),
|
|
@@ -164,8 +142,7 @@ const InformationPane = ({ onClose }) => {
|
|
|
164
142
|
}[tab]?.();
|
|
165
143
|
}, [dossiers, filter, hit, loading, tab, users]);
|
|
166
144
|
const hasError = useMemo(() => !validateRegex(filter), [filter]);
|
|
167
|
-
return (_jsxs(VSBox, { top: 10, sx: { height: '100%', flex: 1 }, children: [_jsxs(Stack, { direction: "column", flex: 1, sx: { overflowY: 'auto', flexGrow: 1 }, position: "relative", spacing: 1, ml: 2, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, flexShrink: 0, pr: 2,
|
|
168
|
-
!hit.howler.is_bundle &&
|
|
145
|
+
return (_jsxs(VSBox, { top: 10, sx: { height: '100%', flex: 1 }, children: [_jsxs(Stack, { direction: "column", flex: 1, sx: { overflowY: 'auto', flexGrow: 1 }, position: "relative", spacing: 1, ml: 2, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, flexShrink: 0, pr: 2, children: [_jsx(FlexOne, {}), onClose && !location.pathname.startsWith('/bundles') && (_jsx(TuiIconButton, { size: "small", onClick: onClose, tooltip: t('hit.panel.details.exit'), children: _jsx(Clear, {}) })), _jsx(SocketBadge, { size: "small" }), analytic && (_jsx(TuiIconButton, { size: "small", tooltip: t('hit.panel.analytic.open'), disabled: !analytic || loading, route: `/analytics/${analytic.analytic_id}`, children: _jsx(QueryStats, {}) })), !!hit && (_jsx(TuiIconButton, { tooltip: t('hit.panel.open'), href: `/hits/${selected}`, disabled: !hit || loading, size: "small", target: "_blank", children: _jsx(OpenInNew, {}) }))] }), !!hit &&
|
|
169
146
|
(!loading ? (_jsxs(_Fragment, { children: [_jsx(HitOutline, { hit: hit, layout: HitLayout.DENSE }), _jsx(HitLabels, { hit: hit })] })) : (_jsx(Skeleton, { height: 124 }))), (hit?.howler?.links?.length > 0 ||
|
|
170
147
|
analytic?.notebooks?.length > 0 ||
|
|
171
148
|
dossiers.filter(_dossier => _dossier.pivots?.length > 0).length > 0) && (_jsxs(Stack, { direction: "row", spacing: 1, pr: 2, children: [analytic?.notebooks?.length > 0 && _jsx(HitNotebooks, { analytic: analytic, hit: hit }), hit?.howler?.links?.length > 0 &&
|
|
@@ -202,7 +179,7 @@ const InformationPane = ({ onClose }) => {
|
|
|
202
179
|
right: theme.spacing(-0.5)
|
|
203
180
|
},
|
|
204
181
|
'& > svg': { zIndex: 2 }
|
|
205
|
-
}, badgeContent: hit?.howler.comment?.length ?? 0, children: _jsx(Comment, {}) }) }), value: "hit_comments", onClick: () => setTab('hit_comments') }),
|
|
182
|
+
}, badgeContent: hit?.howler.comment?.length ?? 0, children: _jsx(Comment, {}) }) }), value: "hit_comments", onClick: () => setTab('hit_comments') }), hasOverview && (_jsx(Tab, { label: t('hit.viewer.overview'), value: "overview", onClick: () => setTab('overview') })), _jsx(Tab, { label: t('hit.viewer.details'), value: "details", onClick: () => setTab('details') }), hit?.howler.dossier?.map((lead, index) => (_jsx(Tab
|
|
206
183
|
// eslint-disable-next-line react/no-array-index-key
|
|
207
184
|
, { label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [lead.icon && _jsx(Icon, { icon: lead.icon }), _jsx("span", { children: i18n.language === 'en' ? lead.label.en : lead.label.fr })] }), value: 'lead:' + index, onClick: () => setTab('lead:' + index) }, 'lead:' + index))), dossiers.flatMap((_dossier, dossierIndex) => (_dossier.leads ?? []).map((_lead, leadIndex) => (_jsx(Tab
|
|
208
185
|
// eslint-disable-next-line react/no-array-index-key
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { ErrorOutline, List, SavedSearch, TableChart, Terminal } from '@mui/icons-material';
|
|
3
3
|
import { Box, IconButton, LinearProgress, Stack, ToggleButton, ToggleButtonGroup, 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';
|
|
@@ -23,10 +23,9 @@ import useMyLocalStorage, { useMyLocalStorageItem } from '@cccsaurora/howler-ui/
|
|
|
23
23
|
import React, { memo, useCallback, useEffect, useMemo } from 'react';
|
|
24
24
|
import { isMobile } from 'react-device-detect';
|
|
25
25
|
import { useTranslation } from 'react-i18next';
|
|
26
|
-
import { Link, useLocation,
|
|
26
|
+
import { Link, useLocation, useParams } from 'react-router-dom';
|
|
27
27
|
import { useContextSelector } from 'use-context-selector';
|
|
28
28
|
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
29
|
-
import BundleParentMenu from './BundleParentMenu';
|
|
30
29
|
import { BundleScroller } from './BundleScroller';
|
|
31
30
|
import HitContextMenu from './HitContextMenu';
|
|
32
31
|
import HitQuery from './HitQuery';
|
|
@@ -79,7 +78,6 @@ const Item = memo(({ hit, onClick }) => {
|
|
|
79
78
|
const SearchPane = () => {
|
|
80
79
|
const { t } = useTranslation();
|
|
81
80
|
const location = useLocation();
|
|
82
|
-
const navigate = useNavigate();
|
|
83
81
|
const routeParams = useParams();
|
|
84
82
|
const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
|
|
85
83
|
const setSelected = useContextSelector(ParameterContext, ctx => ctx.setSelected);
|
|
@@ -118,7 +116,7 @@ const SearchPane = () => {
|
|
|
118
116
|
], onClick: () => {
|
|
119
117
|
clearSelectedHits(bundleHit.howler.id);
|
|
120
118
|
setSelected(bundleHit.howler.id);
|
|
121
|
-
}, 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, {}),
|
|
119
|
+
}, 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(Tooltip, { title: t('route.views.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/views/create?query=${query}`, children: _jsx(SavedSearch, {}) }) }), _jsx(Tooltip, { title: t('route.actions.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/action/execute?query=${query}`, children: _jsx(Terminal, {}) }) }), _jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => setDisplayType(value), size: "small", children: [_jsx(ToggleButton, { value: "list", children: _jsx(List, {}) }), _jsx(ToggleButton, { value: "grid", children: _jsx(TableChart, {}) })] })] })] }), _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 => ({
|
|
122
120
|
position: 'absolute',
|
|
123
121
|
left: 0,
|
|
124
122
|
right: 0,
|