@cccsaurora/howler-ui 2.17.0-dev.501 → 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.
Files changed (117) hide show
  1. package/api/index.d.ts +2 -0
  2. package/api/index.js +4 -2
  3. package/api/search/case.d.ts +4 -0
  4. package/api/search/case.js +8 -0
  5. package/api/search/index.d.ts +2 -1
  6. package/api/search/index.js +2 -1
  7. package/api/v2/case/index.d.ts +6 -0
  8. package/api/v2/case/index.js +18 -0
  9. package/api/v2/index.d.ts +4 -0
  10. package/api/v2/index.js +6 -0
  11. package/api/v2/search/index.d.ts +4 -0
  12. package/api/v2/search/index.js +16 -0
  13. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  14. package/components/app/App.js +14 -0
  15. package/components/app/providers/FavouritesProvider.js +2 -2
  16. package/components/elements/{hit/HitDetails.d.ts → ObjectDetails.d.ts} +2 -1
  17. package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +14 -14
  18. package/components/elements/PluginTypography.d.ts +2 -1
  19. package/components/elements/PluginTypography.js +3 -2
  20. package/components/elements/UserList.d.ts +1 -0
  21. package/components/elements/UserList.js +2 -2
  22. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  23. package/components/elements/display/HowlerCard.js +1 -1
  24. package/components/elements/hit/HitBanner.js +19 -31
  25. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  26. package/components/elements/view/ViewTitle.js +1 -1
  27. package/components/hooks/useHitSelection.js +1 -35
  28. package/components/hooks/useMyPreferences.js +10 -1
  29. package/components/hooks/useMySitemap.js +3 -1
  30. package/components/hooks/useMyTheme.js +9 -2
  31. package/components/routes/action/view/ActionSearch.js +1 -1
  32. package/components/routes/action/view/Integrations.js +1 -9
  33. package/components/routes/advanced/QueryBuilder.js +1 -1
  34. package/components/routes/analytics/AnalyticSearch.js +1 -1
  35. package/components/routes/cases/CaseCard.d.ts +8 -0
  36. package/components/routes/cases/CaseCard.js +34 -0
  37. package/components/routes/cases/CaseViewer.d.ts +2 -0
  38. package/components/routes/cases/CaseViewer.js +38 -0
  39. package/components/routes/cases/Cases.d.ts +2 -0
  40. package/components/routes/cases/Cases.js +101 -0
  41. package/components/routes/cases/constants.d.ts +5 -0
  42. package/components/routes/cases/constants.js +5 -0
  43. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  44. package/components/routes/cases/detail/AlertPanel.js +29 -0
  45. package/components/routes/cases/detail/CaseAggregate.d.ts +10 -0
  46. package/components/routes/cases/detail/CaseAggregate.js +30 -0
  47. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  48. package/components/routes/cases/detail/CaseDashboard.js +49 -0
  49. package/components/routes/cases/detail/CaseSidebar.d.ts +6 -0
  50. package/components/routes/cases/detail/CaseSidebar.js +35 -0
  51. package/components/routes/cases/detail/CaseTask.d.ts +9 -0
  52. package/components/routes/cases/detail/CaseTask.js +38 -0
  53. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  54. package/components/routes/cases/detail/ItemPage.js +93 -0
  55. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  56. package/components/routes/cases/detail/RelatedCasePanel.js +28 -0
  57. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  58. package/components/routes/cases/detail/TaskPanel.js +20 -0
  59. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +12 -0
  60. package/components/routes/cases/detail/sidebar/CaseFolder.js +114 -0
  61. package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
  62. package/components/routes/help/ApiDocumentation.js +1 -1
  63. package/components/routes/help/HitDocumentation.js +1 -3
  64. package/components/routes/hits/search/HitContextMenu.js +4 -27
  65. package/components/routes/hits/search/HitContextMenu.test.js +0 -140
  66. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  67. package/components/routes/hits/search/InformationPane.js +6 -29
  68. package/components/routes/hits/search/SearchPane.js +3 -5
  69. package/components/routes/hits/search/ViewLink.js +1 -1
  70. package/components/routes/hits/search/grid/EnhancedCell.js +1 -1
  71. package/components/routes/hits/view/HitViewer.js +3 -4
  72. package/components/routes/home/ViewCard.js +1 -1
  73. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  74. package/components/routes/observables/ObservableViewer.js +27 -0
  75. package/locales/en/translation.json +413 -397
  76. package/locales/fr/translation.json +420 -406
  77. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  78. package/models/entities/generated/Case.d.ts +27 -0
  79. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  80. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  81. package/models/entities/generated/EmailParent.d.ts +19 -0
  82. package/models/entities/generated/Enrichments.d.ts +7 -0
  83. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  84. package/models/entities/generated/Howler.d.ts +0 -4
  85. package/models/entities/generated/HttpResponse.d.ts +11 -0
  86. package/models/entities/generated/Item.d.ts +9 -0
  87. package/models/entities/generated/Observable.d.ts +84 -0
  88. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  89. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  90. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  91. package/models/entities/generated/ObservableFile.d.ts +36 -0
  92. package/models/entities/generated/ObservableHowler.d.ts +44 -0
  93. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  94. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  95. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  96. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  97. package/models/entities/generated/ObservableSource.d.ts +23 -0
  98. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  99. package/models/entities/generated/ObservableTls.d.ts +12 -0
  100. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  101. package/models/entities/generated/Rule.d.ts +2 -10
  102. package/models/entities/generated/Task.d.ts +10 -0
  103. package/models/entities/generated/Threat.d.ts +2 -2
  104. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  105. package/package.json +11 -2
  106. package/plugins/clue/components/ClueTypography.js +2 -2
  107. package/plugins/clue/utils.d.ts +2 -1
  108. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  109. package/components/elements/display/icons/BundleButton.js +0 -32
  110. package/components/routes/action/view/markdown/integrations.en.md.js +0 -1
  111. package/components/routes/action/view/markdown/integrations.fr.md.js +0 -1
  112. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  113. package/components/routes/help/BundleDocumentation.js +0 -12
  114. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  115. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  116. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  117. package/components/routes/hits/search/BundleParentMenu.js +0 -32
@@ -0,0 +1,6 @@
1
+ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
+ import { type FC } from 'react';
3
+ declare const RelatedCasePanel: FC<{
4
+ case: Case;
5
+ }>;
6
+ export default RelatedCasePanel;
@@ -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,7 @@
1
+ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
+ import type { FC } from 'react';
3
+ declare const TaskPanel: FC<{
4
+ case: Case;
5
+ updateCase: (_case: Partial<Case>) => Promise<void>;
6
+ }>;
7
+ export default TaskPanel;
@@ -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;
@@ -0,0 +1,3 @@
1
+ import type { Item } from 'models/entities/generated/Item';
2
+
3
+ export type Tree = { leaves?: Item[]; [folder: string]: Tree | Item[] };
@@ -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, { size: "small", label: "Stable", color: "success" })) : (_jsx(Chip, { size: "small", label: "Unstable", color: "error" })), endpoint.protected ? (_jsx(Chip, { size: "small", label: "Protected", color: "warning" })) : (_jsx(Chip, { size: "small", 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, { size: "small", 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));
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.bundle.title') }), value: "bundle", onClick: () => onChange('bundle') }), _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: {
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 { AddCircleOutline, Assignment, Edit, HowToVote, KeyboardArrowRight, OpenInNew, QueryStats, RemoveCircleOutline, SettingsSuggest, Terminal } from '@mui/icons-material';
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: 8, 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: 8, 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: 8, children: _jsx(MenuList, { sx: { p: 0 }, dense: true, role: "group", children: template?.keys.map(key => {
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" }) }) }));
@@ -1,5 +1,6 @@
1
1
  import type { FC } from 'react';
2
2
  declare const InformationPane: FC<{
3
+ selected?: string;
3
4
  onClose?: () => void;
4
5
  }>;
5
6
  export default InformationPane;
@@ -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.selected);
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(HitDetails, { hit: hit }),
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, sx: [hit?.howler?.is_bundle && { position: 'absolute', top: 1, right: 0, zIndex: 1100 }], 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?.howler.bundles?.length > 0 && _jsx(BundleButton, { ids: hit.howler.bundles, disabled: loading }), !!hit && !hit.howler.is_bundle && (_jsx(TuiIconButton, { tooltip: t('hit.panel.open'), href: `/hits/${selected}`, disabled: !hit || loading, size: "small", target: "_blank", children: _jsx(OpenInNew, {}) }))] }), _jsx(Box, { pr: 2, children: header }), !!hit &&
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') }), hit?.howler?.is_bundle && (_jsx(Tab, { label: t('hit.viewer.aggregate'), value: "hit_aggregate", onClick: () => setTab('hit_aggregate') })), 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
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 { Close, ErrorOutline, List, SavedSearch, TableChart, Terminal } from '@mui/icons-material';
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, useNavigate, useParams } from 'react-router-dom';
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, {}), bundleHit?.howler.bundles.length > 0 && _jsx(BundleParentMenu, { bundle: bundleHit }), bundleHit && (_jsx(Tooltip, { title: t('hit.bundle.close'), children: _jsx(IconButton, { size: "small", onClick: () => navigate('/search'), children: _jsx(Close, {}) }) })), _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 => ({
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,