@cccsaurora/howler-ui 2.18.0-dev.704 → 2.18.0-dev.710
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/facet/hit.d.ts +1 -3
- package/api/search/facet/index.d.ts +3 -1
- package/api/search/index.d.ts +2 -1
- package/api/search/index.js +2 -1
- package/api/v2/case/index.d.ts +8 -0
- package/api/v2/case/index.js +20 -0
- package/api/v2/case/items.d.ts +6 -0
- package/api/v2/case/items.js +18 -0
- package/api/v2/index.d.ts +4 -0
- package/api/v2/index.js +6 -0
- package/api/v2/search/facet.d.ts +3 -0
- package/api/v2/search/facet.js +12 -0
- package/api/v2/search/index.d.ts +5 -0
- package/api/v2/search/index.js +24 -0
- package/commons/components/leftnav/LeftNavDrawer.js +1 -1
- package/components/app/App.js +39 -7
- package/components/app/hooks/useMatchers.js +2 -2
- package/components/app/hooks/useMatchers.test.js +22 -22
- package/components/app/hooks/useTitle.js +3 -3
- package/components/app/providers/FavouritesProvider.js +2 -2
- package/components/app/providers/ModalProvider.d.ts +1 -0
- package/components/app/providers/ParameterProvider.d.ts +9 -2
- package/components/app/providers/ParameterProvider.js +165 -240
- package/components/app/providers/ParameterProvider.test.js +307 -14
- package/components/app/providers/RecordProvider.d.ts +23 -0
- package/components/app/providers/{HitProvider.js → RecordProvider.js} +41 -41
- package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
- package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
- package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +51 -70
- package/components/elements/ContextMenu.d.ts +56 -0
- package/components/elements/ContextMenu.js +109 -0
- package/components/elements/ContextMenu.test.js +215 -0
- package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
- package/components/elements/ObjectDetails.d.ts +6 -0
- package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
- package/components/elements/PluginTypography.d.ts +2 -1
- package/components/elements/PluginTypography.js +3 -2
- package/components/elements/UserList.d.ts +5 -2
- package/components/elements/UserList.js +14 -5
- package/components/elements/addons/search/phrase/Phrase.js +1 -1
- package/components/elements/case/CaseCard.d.ts +12 -0
- package/components/elements/case/CaseCard.js +42 -0
- package/components/elements/case/CasePreview.d.ts +6 -0
- package/components/elements/case/CasePreview.js +17 -0
- package/components/elements/case/StatusIcon.d.ts +5 -0
- package/components/elements/case/StatusIcon.js +13 -0
- package/components/elements/display/ChipPopper.d.ts +1 -1
- package/components/elements/display/HowlerCard.js +1 -1
- package/components/elements/display/Modal.js +2 -0
- package/components/elements/hit/HitActions.js +4 -4
- package/components/elements/hit/HitBanner.js +28 -48
- package/components/elements/hit/HitCard.js +5 -5
- package/components/elements/hit/HitLabels.js +2 -2
- package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
- package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
- package/components/elements/hit/HitSummary.d.ts +2 -1
- package/components/elements/hit/HitSummary.js +6 -5
- package/components/elements/hit/aggregate/HitGraph.js +8 -8
- package/components/elements/hit/elements/AnalyticLink.d.ts +8 -0
- package/components/elements/hit/elements/AnalyticLink.js +22 -0
- package/components/elements/hit/outlines/DefaultOutline.js +1 -1
- package/components/elements/hit/related/RelatedRecords.js +63 -0
- package/components/elements/observable/ObservableCard.d.ts +6 -0
- package/components/elements/observable/ObservableCard.js +22 -0
- package/components/elements/observable/ObservablePreview.d.ts +6 -0
- package/components/elements/observable/ObservablePreview.js +12 -0
- package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
- package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
- package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
- package/components/elements/record/RecordContextMenu.js +247 -0
- package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
- package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +94 -39
- package/components/elements/record/RecordRelated.d.ts +7 -0
- package/components/elements/record/RecordRelated.js +34 -0
- package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
- package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
- package/components/elements/view/ViewTitle.d.ts +1 -0
- package/components/elements/view/ViewTitle.js +9 -2
- package/components/hooks/useHitActions.d.ts +1 -1
- package/components/hooks/useHitActions.js +4 -4
- package/components/hooks/useMyPreferences.js +10 -1
- package/components/hooks/useMySearch.js +2 -2
- package/components/hooks/useMySitemap.js +4 -1
- package/components/hooks/useMyTheme.js +9 -2
- package/components/hooks/useParamState.test.js +3 -4
- package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
- package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
- package/components/hooks/useRelatedRecords.d.ts +13 -0
- package/components/hooks/useRelatedRecords.js +32 -0
- package/components/routes/action/edit/ActionEditor.js +2 -2
- package/components/routes/action/view/ActionSearch.js +1 -1
- package/components/routes/advanced/QueryBuilder.js +1 -1
- package/components/routes/advanced/QueryEditor.js +3 -3
- package/components/routes/advanced/historyCompletionProvider.js +3 -3
- package/components/routes/analytics/AnalyticDetails.js +2 -2
- package/components/routes/analytics/AnalyticSearch.js +1 -1
- package/components/routes/cases/CaseViewer.d.ts +2 -0
- package/components/routes/cases/CaseViewer.js +22 -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 +33 -0
- package/components/routes/cases/detail/CaseAssets.d.ts +11 -0
- package/components/routes/cases/detail/CaseAssets.js +104 -0
- package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseAssets.test.js +167 -0
- package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
- package/components/routes/cases/detail/CaseDashboard.js +54 -0
- package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
- package/components/routes/cases/detail/CaseDetails.js +61 -0
- package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
- package/components/routes/cases/detail/CaseOverview.js +43 -0
- package/components/routes/cases/detail/CaseSidebar.d.ts +8 -0
- package/components/routes/cases/detail/CaseSidebar.js +50 -0
- package/components/routes/cases/detail/CaseTask.d.ts +11 -0
- package/components/routes/cases/detail/CaseTask.js +57 -0
- package/components/routes/cases/detail/CaseTimeline.d.ts +12 -0
- package/components/routes/cases/detail/CaseTimeline.js +106 -0
- package/components/routes/cases/detail/CaseTimeline.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseTimeline.test.js +227 -0
- package/components/routes/cases/detail/ItemPage.d.ts +6 -0
- package/components/routes/cases/detail/ItemPage.js +99 -0
- package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
- package/components/routes/cases/detail/RelatedCasePanel.js +31 -0
- package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
- package/components/routes/cases/detail/TaskPanel.js +52 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +12 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.js +19 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.js +30 -0
- package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
- package/components/routes/cases/detail/assets/Asset.js +12 -0
- package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
- package/components/routes/cases/detail/assets/Asset.test.js +72 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +14 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.js +133 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +34 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +105 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +351 -0
- package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
- package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
- package/components/routes/cases/detail/sidebar/utils.js +25 -0
- package/components/routes/cases/hooks/useCase.d.ts +13 -0
- package/components/routes/cases/hooks/useCase.js +51 -0
- package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
- package/components/routes/cases/modals/AddToCaseModal.js +62 -0
- package/components/routes/cases/modals/RenameItemModal.d.ts +9 -0
- package/components/routes/cases/modals/RenameItemModal.js +48 -0
- package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
- package/components/routes/cases/modals/ResolveModal.js +62 -0
- package/components/routes/dossiers/DossierEditor.js +2 -2
- package/components/routes/dossiers/DossierEditor.test.js +1 -1
- package/components/routes/help/ApiDocumentation.js +1 -1
- package/components/routes/help/HitBannerDocumentation.js +1 -0
- package/components/routes/help/HitDocumentation.js +1 -3
- package/components/routes/hits/search/InformationPane.d.ts +1 -0
- package/components/routes/hits/search/InformationPane.js +47 -60
- package/components/routes/hits/search/LayoutSettings.js +3 -3
- package/components/routes/hits/search/QuerySettings.js +2 -1
- package/components/routes/hits/search/QuerySettings.test.js +14 -9
- package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
- package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
- package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
- package/components/routes/hits/search/SearchPane.js +26 -49
- package/components/routes/hits/search/ViewLink.js +3 -3
- package/components/routes/hits/search/ViewLink.test.js +8 -8
- package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
- package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
- package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
- package/components/routes/hits/search/grid/HitGrid.js +20 -18
- package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
- package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
- package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
- package/components/routes/hits/search/shared/IndexPicker.js +20 -0
- package/components/routes/hits/view/HitViewer.js +12 -13
- package/components/routes/home/ViewCard.js +47 -41
- package/components/routes/observables/ObservableViewer.d.ts +7 -0
- package/components/routes/observables/ObservableViewer.js +27 -0
- package/components/routes/overviews/OverviewViewer.js +2 -2
- package/components/routes/views/ViewComposer.js +46 -19
- package/locales/en/translation.json +87 -3
- package/locales/fr/translation.json +85 -3
- package/models/WithMetadata.d.ts +2 -1
- package/models/entities/generated/AttachmentsFile.d.ts +12 -0
- package/models/entities/generated/Case.d.ts +28 -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/Hit.d.ts +1 -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 +85 -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 +43 -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/models/entities/generated/View.d.ts +1 -0
- package/package.json +18 -1
- package/plugins/clue/components/ClueTypography.js +2 -2
- package/plugins/clue/utils.d.ts +2 -1
- package/tests/server-handlers.js +6 -1
- package/tests/utils.d.ts +4 -0
- package/tests/utils.js +20 -0
- package/utils/constants.d.ts +3 -3
- package/utils/hitFunctions.d.ts +2 -1
- package/utils/hitFunctions.js +4 -4
- package/utils/typeUtils.d.ts +7 -0
- package/utils/typeUtils.js +27 -0
- package/utils/viewUtils.js +3 -0
- package/components/app/providers/HitProvider.d.ts +0 -22
- package/components/elements/display/icons/BundleButton.d.ts +0 -6
- package/components/elements/display/icons/BundleButton.js +0 -32
- package/components/elements/hit/HitRelated.d.ts +0 -6
- package/components/elements/hit/HitRelated.js +0 -7
- 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
- package/components/routes/hits/search/BundleScroller.d.ts +0 -2
- package/components/routes/hits/search/BundleScroller.js +0 -6
- package/components/routes/hits/search/HitContextMenu.js +0 -227
- /package/components/app/providers/{HitSearchProvider.test.d.ts → RecordSearchProvider.test.d.ts} +0 -0
- /package/components/{routes/hits/search/HitContextMenu.test.d.ts → elements/ContextMenu.test.d.ts} +0 -0
- /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
- /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
- /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { KeyboardArrowUp } from '@mui/icons-material';
|
|
3
3
|
import { Box, Collapse, IconButton, lighten, Stack, TableCell, TableRow, Typography, useTheme } from '@mui/material';
|
|
4
|
-
import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
|
|
5
4
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
5
|
+
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
6
6
|
import Assigned from '@cccsaurora/howler-ui/components/elements/hit/elements/Assigned';
|
|
7
7
|
import EscalationChip from '@cccsaurora/howler-ui/components/elements/hit/elements/EscalationChip';
|
|
8
8
|
import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
|
|
9
9
|
import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
|
|
10
|
+
import ObservableCard from '@cccsaurora/howler-ui/components/elements/observable/ObservableCard';
|
|
10
11
|
import { get } from 'lodash-es';
|
|
11
12
|
import { memo, useState } from 'react';
|
|
12
13
|
import { useTranslation } from 'react-i18next';
|
|
13
14
|
import { Link } from 'react-router-dom';
|
|
14
15
|
import { useContextSelector } from 'use-context-selector';
|
|
16
|
+
import { isHit } from '@cccsaurora/howler-ui/utils/typeUtils';
|
|
15
17
|
import EnhancedCell from './EnhancedCell';
|
|
16
|
-
const
|
|
18
|
+
const RecordRow = ({ record, analyticIds, columns, columnWidths, collapseMainColumn, onClick }) => {
|
|
17
19
|
const theme = useTheme();
|
|
18
20
|
const { t } = useTranslation();
|
|
19
|
-
const selectedHits = useContextSelector(
|
|
21
|
+
const selectedHits = useContextSelector(RecordContext, ctx => ctx.selectedRecords);
|
|
20
22
|
const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
|
|
21
23
|
const [expandRow, setExpandRow] = useState(false);
|
|
22
|
-
return (_jsxs(_Fragment, { children: [_jsxs(TableRow, { id:
|
|
24
|
+
return (_jsxs(_Fragment, { children: [_jsxs(TableRow, { id: record.howler.id, onClick: ev => onClick(ev, record), sx: [
|
|
23
25
|
{
|
|
24
26
|
transition: theme.transitions.create('background-color'),
|
|
25
27
|
'&:hover': {
|
|
@@ -27,10 +29,10 @@ const HitRow = ({ hit, analyticIds, columns, columnWidths, collapseMainColumn, o
|
|
|
27
29
|
backgroundColor: theme.palette.background.paper
|
|
28
30
|
}
|
|
29
31
|
},
|
|
30
|
-
selectedHits.some(_hit => _hit.howler.id ===
|
|
32
|
+
selectedHits.some(_hit => _hit.howler.id === record.howler.id) && {
|
|
31
33
|
backgroundColor: lighten(theme.palette.background.paper, 0.15)
|
|
32
34
|
},
|
|
33
|
-
selected ===
|
|
35
|
+
selected === record.howler.id && {
|
|
34
36
|
backgroundColor: lighten(theme.palette.background.paper, 0.25)
|
|
35
37
|
}
|
|
36
38
|
], children: [_jsx(TableCell, { sx: {
|
|
@@ -45,6 +47,6 @@ const HitRow = ({ hit, analyticIds, columns, columnWidths, collapseMainColumn, o
|
|
|
45
47
|
e.preventDefault();
|
|
46
48
|
e.stopPropagation();
|
|
47
49
|
setExpandRow(_expanded => !_expanded);
|
|
48
|
-
}, children: _jsx(KeyboardArrowUp, {}) }), _jsx(Collapse, { in: !collapseMainColumn, orientation: "horizontal", unmountOnExit: true, children: _jsxs(Stack, { direction: "row", spacing: 1, flexWrap: "nowrap", children: [_jsx(EscalationChip, { hit:
|
|
50
|
+
}, children: _jsx(KeyboardArrowUp, {}) }), _jsx(Collapse, { in: !collapseMainColumn, orientation: "horizontal", unmountOnExit: true, children: _jsxs(Stack, { direction: "row", spacing: 1, flexWrap: "nowrap", children: [isHit(record) && _jsx(EscalationChip, { hit: record, layout: HitLayout.DENSE, hideLabel: true }), _jsxs(Typography, { sx: { textWrap: 'nowrap', whiteSpace: 'nowrap', fontSize: 'inherit' }, children: [analyticIds[record.howler.analytic] ? (_jsx(Link, { to: `/analytics/${analyticIds[record.howler.analytic]}`, onClick: e => e.stopPropagation(), children: record.howler.analytic })) : (record.howler.analytic), record.howler.detection && ': ', record.howler.detection] }), isHit(record) && record.howler.assignment !== 'unassigned' && (_jsx(Assigned, { hit: record, layout: HitLayout.DENSE, hideLabel: true }))] }) })] }) }), columns.map(col => (_jsx(EnhancedCell, { record: record, className: `col-${col.replaceAll('.', '-')}`, value: get(record, col) ?? t('none'), sx: columnWidths[col] ? { width: columnWidths[col] } : { width: '220px', maxWidth: '300px' }, field: col }, col)))] }, record.howler.id), _jsx(TableRow, { onClick: ev => onClick(ev, record), children: _jsx(TableCell, { colSpan: columns.length + 2, style: { paddingBottom: 0, paddingTop: 0 }, children: _jsx(Collapse, { in: expandRow, unmountOnExit: true, children: _jsx(Box, { width: "100%", maxWidth: "1200px", margin: 1, children: isHit(record) ? (_jsx(HitCard, { id: record.howler.id, layout: HitLayout.NORMAL })) : (_jsx(ObservableCard, { id: record.howler.id, observable: record })) }) }) }) })] }));
|
|
49
51
|
};
|
|
50
|
-
export default memo(
|
|
52
|
+
export default memo(RecordRow);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { FilterList } from '@mui/icons-material';
|
|
3
|
+
import { Autocomplete, TextField } from '@mui/material';
|
|
4
|
+
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
5
|
+
import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
|
|
6
|
+
import { memo } from 'react';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import { useContextSelector } from 'use-context-selector';
|
|
9
|
+
const FILTER_OPTIONS = [
|
|
10
|
+
{ label: 'hit.search.index.hit', value: 'hit' },
|
|
11
|
+
{ label: 'hit.search.index.observable', value: 'observable' }
|
|
12
|
+
];
|
|
13
|
+
const IndexPicker = () => {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const indexes = useContextSelector(ParameterContext, ctx => ctx.indexes);
|
|
16
|
+
const setIndexes = useContextSelector(ParameterContext, ctx => ctx.setIndexes);
|
|
17
|
+
const selectedOptions = FILTER_OPTIONS.filter(opt => indexes.includes(opt.value));
|
|
18
|
+
return (_jsx(ChipPopper, { icon: _jsx(FilterList, { fontSize: "small" }), label: selectedOptions.map(opt => t(opt.label)).join(', '), minWidth: "225px", slotProps: { chip: { size: 'small' } }, children: _jsx(Autocomplete, { size: "small", multiple: true, options: FILTER_OPTIONS, value: selectedOptions, onChange: (_ev, values) => values.length > 0 && setIndexes(values.map(val => val.value)), isOptionEqualToValue: (opt, val) => opt.value === val.value, getOptionLabel: opt => t(opt.label), renderInput: params => _jsx(TextField, { ...params }) }) }));
|
|
19
|
+
};
|
|
20
|
+
export default memo(IndexPicker);
|
|
@@ -4,23 +4,22 @@ import { Code, Comment, DataObject, History, LinkSharp, QueryStats, ViewAgenda }
|
|
|
4
4
|
import { Badge, Box, CardContent, Collapse, IconButton, Skeleton, Stack, Tab, Tabs, Tooltip, useMediaQuery, useTheme } from '@mui/material';
|
|
5
5
|
import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
|
|
6
6
|
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
7
|
-
import {
|
|
7
|
+
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
8
8
|
import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
|
|
9
9
|
import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
|
|
10
|
-
import BundleButton from '@cccsaurora/howler-ui/components/elements/display/icons/BundleButton';
|
|
11
10
|
import SocketBadge from '@cccsaurora/howler-ui/components/elements/display/icons/SocketBadge';
|
|
12
11
|
import JSONViewer from '@cccsaurora/howler-ui/components/elements/display/json/JSONViewer';
|
|
13
12
|
import HitActions from '@cccsaurora/howler-ui/components/elements/hit/HitActions';
|
|
14
13
|
import HitBanner from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
|
|
15
|
-
import HitComments from '@cccsaurora/howler-ui/components/elements/hit/HitComments';
|
|
16
|
-
import HitDetails from '@cccsaurora/howler-ui/components/elements/hit/HitDetails';
|
|
17
14
|
import HitLabels from '@cccsaurora/howler-ui/components/elements/hit/HitLabels';
|
|
18
15
|
import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
|
|
19
16
|
import HitLinks from '@cccsaurora/howler-ui/components/elements/hit/HitLinks';
|
|
20
17
|
import HitOutline from '@cccsaurora/howler-ui/components/elements/hit/HitOutline';
|
|
21
18
|
import HitOverview from '@cccsaurora/howler-ui/components/elements/hit/HitOverview';
|
|
22
|
-
import
|
|
23
|
-
import
|
|
19
|
+
import ObjectDetails from '@cccsaurora/howler-ui/components/elements/ObjectDetails';
|
|
20
|
+
import RecordComments from '@cccsaurora/howler-ui/components/elements/record/RecordComments';
|
|
21
|
+
import RecordRelated from '@cccsaurora/howler-ui/components/elements/record/RecordRelated';
|
|
22
|
+
import RecordWorklog from '@cccsaurora/howler-ui/components/elements/record/RecordWorklog';
|
|
24
23
|
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
25
24
|
import useMyUserList from '@cccsaurora/howler-ui/components/hooks/useMyUserList';
|
|
26
25
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
@@ -44,8 +43,8 @@ const HitViewer = () => {
|
|
|
44
43
|
const isUnderLg = useMediaQuery(theme.breakpoints.down('lg'));
|
|
45
44
|
const [orientation, setOrientation] = useMyLocalStorageItem(StorageKey.VIEWER_ORIENTATION, Orientation.VERTICAL);
|
|
46
45
|
const { getMatchingOverview, getMatchingDossiers, getMatchingAnalytic } = useMatchers();
|
|
47
|
-
const getHit = useContextSelector(
|
|
48
|
-
const hit = useContextSelector(
|
|
46
|
+
const getHit = useContextSelector(RecordContext, ctx => ctx.getRecord);
|
|
47
|
+
const hit = useContextSelector(RecordContext, ctx => ctx.records[params.id]);
|
|
49
48
|
const [userIds, setUserIds] = useState(new Set());
|
|
50
49
|
const users = useMyUserList(userIds);
|
|
51
50
|
const [tab, setTab] = useState('details');
|
|
@@ -94,12 +93,12 @@ const HitViewer = () => {
|
|
|
94
93
|
}
|
|
95
94
|
return {
|
|
96
95
|
overview: () => _jsx(HitOverview, { hit: hit }),
|
|
97
|
-
details: () => _jsx(
|
|
98
|
-
hit_comments: () => _jsx(
|
|
96
|
+
details: () => _jsx(ObjectDetails, { obj: hit }),
|
|
97
|
+
hit_comments: () => _jsx(RecordComments, { record: hit, users: users }),
|
|
99
98
|
hit_raw: () => _jsx(JSONViewer, { data: hit }),
|
|
100
99
|
hit_data: () => _jsx(JSONViewer, { data: hit?.howler?.data?.map(entry => tryParse(entry)), collapse: false }),
|
|
101
|
-
hit_worklog: () => _jsx(
|
|
102
|
-
hit_related: () => _jsx(
|
|
100
|
+
hit_worklog: () => _jsx(RecordWorklog, { record: hit, users: users }),
|
|
101
|
+
hit_related: () => _jsx(RecordRelated, { record: hit }),
|
|
103
102
|
...Object.fromEntries(hit?.howler.dossier?.map((lead, index) => ['lead:' + index, () => _jsx(LeadRenderer, { lead: lead, hit: hit })]) ?? []),
|
|
104
103
|
...Object.fromEntries(dossiers.flatMap((_dossier, dossierIndex) => (_dossier.leads ?? []).map((_lead, leadIndex) => [
|
|
105
104
|
`external-lead:${dossierIndex}:${leadIndex}`,
|
|
@@ -132,7 +131,7 @@ const HitViewer = () => {
|
|
|
132
131
|
position: 'absolute',
|
|
133
132
|
top: theme.spacing(2),
|
|
134
133
|
right: theme.spacing(-6)
|
|
135
|
-
}, children: [_jsx(Tooltip, { title: t('hit.panel.view.layout'), children: _jsx(IconButton, { onClick: onOrientationChange, children: _jsx(ViewAgenda, { sx: { transition: 'rotate 250ms', rotate: orientation === 'vertical' ? '90deg' : '0deg' } }) }) }), _jsx(SocketBadge, { size: "medium" }), analytic && (_jsx(Tooltip, { title: t('
|
|
134
|
+
}, children: [_jsx(Tooltip, { title: t('hit.panel.view.layout'), children: _jsx(IconButton, { onClick: onOrientationChange, children: _jsx(ViewAgenda, { sx: { transition: 'rotate 250ms', rotate: orientation === 'vertical' ? '90deg' : '0deg' } }) }) }), _jsx(SocketBadge, { size: "medium" }), analytic && (_jsx(Tooltip, { title: t('analytic.open'), children: _jsx(IconButton, { onClick: () => navigate(`/analytics/${analytic.analytic_id}`), children: _jsx(QueryStats, {}) }) }))] }))] }), _jsx(HowlerCard, { sx: [orientation === 'horizontal' && { height: '0px' }], children: _jsx(CardContent, { sx: { padding: 1, position: 'relative' }, children: _jsx(HitActions, { hit: hit, orientation: "vertical" }) }) }), _jsx(Box, { sx: { gridColumn: '1 / span 2', mb: 1 }, children: _jsxs(Tabs, { value: tab === 'overview' && !hasOverview ? 'details' : tab, sx: { display: 'flex', flexDirection: 'row', pr: 2, alignItems: 'center' }, children: [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
|
|
136
135
|
// eslint-disable-next-line react/no-array-index-key
|
|
137
136
|
, { 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
|
|
138
137
|
// eslint-disable-next-line react/no-array-index-key
|
|
@@ -3,74 +3,80 @@ import { OpenInNew } from '@mui/icons-material';
|
|
|
3
3
|
import { Card, CardContent, IconButton, Skeleton, Stack, Typography } from '@mui/material';
|
|
4
4
|
import api from '@cccsaurora/howler-ui/api';
|
|
5
5
|
import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
|
|
6
|
-
import { useHitContextSelector } from '@cccsaurora/howler-ui/components/app/providers/
|
|
6
|
+
import { useHitContextSelector as useRecordContextSelector } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
7
7
|
import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
|
|
8
8
|
import HitBanner from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
|
|
9
9
|
import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
|
|
10
|
+
import ObservableCard from '@cccsaurora/howler-ui/components/elements/observable/ObservableCard';
|
|
11
|
+
import RecordContextMenu from '@cccsaurora/howler-ui/components/elements/record/RecordContextMenu';
|
|
10
12
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
11
|
-
import HitContextMenu from '@cccsaurora/howler-ui/components/routes/hits/search/HitContextMenu';
|
|
12
13
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
13
14
|
import { useTranslation } from 'react-i18next';
|
|
14
15
|
import { Link, useNavigate } from 'react-router-dom';
|
|
15
16
|
import { useContextSelector } from 'use-context-selector';
|
|
17
|
+
import { isObservable } from '@cccsaurora/howler-ui/utils/typeUtils';
|
|
16
18
|
import { buildViewUrl } from '@cccsaurora/howler-ui/utils/viewUtils';
|
|
17
|
-
// Custom hook to select
|
|
18
|
-
const
|
|
19
|
-
const
|
|
19
|
+
// Custom hook to select records by IDs with proper memoization
|
|
20
|
+
const useSelectRecordsByIds = (recordIds) => {
|
|
21
|
+
const recordIdsRef = useRef(recordIds);
|
|
20
22
|
const prevResultRef = useRef([]);
|
|
21
|
-
const
|
|
22
|
-
// Keep ref up to date with latest
|
|
23
|
-
|
|
23
|
+
const prevRecordIdsRef = useRef([]);
|
|
24
|
+
// Keep ref up to date with latest recordIds
|
|
25
|
+
recordIdsRef.current = recordIds;
|
|
24
26
|
const selector = useCallback(ctx => {
|
|
25
|
-
const
|
|
26
|
-
// Fast path: if
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
27
|
+
const currentRecordIds = recordIdsRef.current;
|
|
28
|
+
// Fast path: if recordIds array didn't change, check if record objects changed
|
|
29
|
+
if (prevRecordIdsRef.current.length === currentRecordIds.length &&
|
|
30
|
+
currentRecordIds.every((id, i) => id === prevRecordIdsRef.current[i])) {
|
|
31
|
+
// RecordIds unchanged - check if any record objects changed by reference
|
|
32
|
+
const anyRecordChanged = currentRecordIds.some((id, i) => ctx.records[id] !== prevResultRef.current[i]);
|
|
33
|
+
if (!anyRecordChanged) {
|
|
32
34
|
return prevResultRef.current;
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
// Something changed - rebuild the array
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
prevResultRef.current =
|
|
39
|
-
return
|
|
38
|
+
const currentRecords = currentRecordIds.map(id => ctx.records[id]).filter(Boolean);
|
|
39
|
+
prevRecordIdsRef.current = currentRecordIds;
|
|
40
|
+
prevResultRef.current = currentRecords;
|
|
41
|
+
return currentRecords;
|
|
40
42
|
}, []); // Empty deps - selector never changes
|
|
41
|
-
return
|
|
43
|
+
return useRecordContextSelector(selector);
|
|
42
44
|
};
|
|
43
45
|
// Utility functions
|
|
44
46
|
const normalize = (val) => (val == null ? '' : String(val));
|
|
45
47
|
// Have to normalize the fields as websockets and api return null and undefined respectively. This causes false positives when comparing signatures if not normalized to a consistent value. We also stringify non-primitive values to ensure changes are detected.
|
|
46
|
-
const
|
|
47
|
-
if (!
|
|
48
|
+
const createRecordSignature = (record) => {
|
|
49
|
+
if (!record) {
|
|
48
50
|
return '';
|
|
49
|
-
|
|
51
|
+
}
|
|
52
|
+
if (isObservable(record)) {
|
|
53
|
+
return record.howler?.id;
|
|
54
|
+
}
|
|
55
|
+
return `${record.howler?.id}:${normalize(record.howler?.status)}:${normalize(record.howler?.assignment)}:${normalize(record.howler?.assessment)}`;
|
|
50
56
|
};
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
57
|
+
const createSignatureFromRecords = (records) => {
|
|
58
|
+
if (records.length === 0)
|
|
53
59
|
return '';
|
|
54
|
-
return
|
|
60
|
+
return records.map(createRecordSignature).join('|');
|
|
55
61
|
};
|
|
56
62
|
const DEBOUNCE_TIME = 1000; // 1 second debounce for signature changes
|
|
57
63
|
const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
|
|
58
64
|
const navigate = useNavigate();
|
|
59
65
|
const { t } = useTranslation();
|
|
60
66
|
const { dispatchApi } = useMyApi();
|
|
61
|
-
const [
|
|
67
|
+
const [recordIds, setRecordIds] = useState([]);
|
|
62
68
|
const [loading, setLoading] = useState(false);
|
|
63
69
|
const debounceTimerRef = useRef(null);
|
|
64
70
|
const isRefreshing = useRef(false);
|
|
65
71
|
const lastSignature = useRef('');
|
|
66
72
|
const view = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
|
|
67
73
|
const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
|
|
68
|
-
const
|
|
74
|
+
const loadRecords = useRecordContextSelector(ctx => ctx.loadRecords);
|
|
69
75
|
// Subscribe to hits from HitProvider cache based on current hitIds in the view
|
|
70
76
|
// Uses memoized selector to avoid unnecessary re-renders on unrelated hit updates
|
|
71
|
-
const
|
|
77
|
+
const records = useSelectRecordsByIds(recordIds);
|
|
72
78
|
// Create a stable signature that only changes when relevant fields change
|
|
73
|
-
const
|
|
79
|
+
const recordsSignature = useMemo(() => createSignatureFromRecords(records), [records]);
|
|
74
80
|
const refreshView = useCallback(async () => {
|
|
75
81
|
if (!view?.query || isRefreshing.current) {
|
|
76
82
|
onRefreshComplete?.();
|
|
@@ -78,21 +84,21 @@ const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
|
|
|
78
84
|
}
|
|
79
85
|
isRefreshing.current = true;
|
|
80
86
|
try {
|
|
81
|
-
const res = await dispatchApi(api.search.
|
|
87
|
+
const res = await dispatchApi(api.v2.search.post(view.indexes, {
|
|
82
88
|
query: view.query,
|
|
83
89
|
rows: limit,
|
|
84
90
|
metadata: ['analytic']
|
|
85
91
|
}));
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
lastSignature.current =
|
|
92
|
+
const fetchedRecords = res.items ?? [];
|
|
93
|
+
loadRecords(fetchedRecords);
|
|
94
|
+
setRecordIds(fetchedRecords.map(r => r.howler.id));
|
|
95
|
+
lastSignature.current = createSignatureFromRecords(fetchedRecords);
|
|
90
96
|
}
|
|
91
97
|
finally {
|
|
92
98
|
isRefreshing.current = false;
|
|
93
99
|
onRefreshComplete?.();
|
|
94
100
|
}
|
|
95
|
-
}, [dispatchApi, limit, view?.query,
|
|
101
|
+
}, [dispatchApi, limit, view?.query, view?.indexes, loadRecords, onRefreshComplete]);
|
|
96
102
|
const debouncedRefresh = useCallback(() => {
|
|
97
103
|
if (debounceTimerRef.current) {
|
|
98
104
|
clearTimeout(debounceTimerRef.current);
|
|
@@ -125,16 +131,16 @@ const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
|
|
|
125
131
|
}, [view?.query, limit, refreshView]);
|
|
126
132
|
// Monitor hits currently in the view for changes that might affect query results
|
|
127
133
|
useEffect(() => {
|
|
128
|
-
if (!
|
|
129
|
-
lastSignature.current =
|
|
134
|
+
if (!recordsSignature || recordIds.length === 0 || !lastSignature.current) {
|
|
135
|
+
lastSignature.current = recordsSignature;
|
|
130
136
|
return;
|
|
131
137
|
}
|
|
132
138
|
// Check if signature actually changed
|
|
133
|
-
if (lastSignature.current ===
|
|
139
|
+
if (lastSignature.current === recordsSignature) {
|
|
134
140
|
return;
|
|
135
141
|
}
|
|
136
142
|
debouncedRefresh();
|
|
137
|
-
}, [
|
|
143
|
+
}, [recordsSignature, recordIds, debouncedRefresh]);
|
|
138
144
|
useEffect(() => {
|
|
139
145
|
return () => {
|
|
140
146
|
if (debounceTimerRef.current) {
|
|
@@ -151,6 +157,6 @@ const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
|
|
|
151
157
|
}
|
|
152
158
|
return selectedElement.id;
|
|
153
159
|
}, []);
|
|
154
|
-
return (_jsx(Card, { variant: "outlined", sx: { height: '100%' }, children: _jsxs(Stack, { spacing: 1, sx: { p: 1, minHeight: 100 }, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h6", children: t(view?.title) || _jsx(Skeleton, { variant: "text", height: "2em", width: "100px" }) }), _jsx(IconButton, { size: "small", component: Link, disabled: !view, to: view ? buildViewUrl(view) : '', onClick: () => onClick(view.query), children: _jsx(OpenInNew, { fontSize: "small" }) })] }), loading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { height: 150, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 160, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 140, width: "100%", variant: "rounded" })] })) :
|
|
160
|
+
return (_jsx(Card, { variant: "outlined", sx: { height: '100%' }, children: _jsxs(Stack, { spacing: 1, sx: { p: 1, minHeight: 100 }, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h6", children: t(view?.title) || _jsx(Skeleton, { variant: "text", height: "2em", width: "100px" }) }), _jsx(IconButton, { size: "small", component: Link, disabled: !view, to: view ? buildViewUrl(view) : '', onClick: () => onClick(view.query), children: _jsx(OpenInNew, { fontSize: "small" }) })] }), loading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { height: 150, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 160, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 140, width: "100%", variant: "rounded" })] })) : records.length > 0 ? (_jsx(RecordContextMenu, { getSelectedId: getSelectedId, children: records.map((r) => (_jsx(Card, { id: r.howler.id, variant: "outlined", sx: { cursor: 'pointer' }, onClick: () => navigate(`/hits/${r.howler.id}`), children: _jsx(CardContent, { children: r.__index == 'hit' ? (_jsx(HitBanner, { layout: HitLayout.DENSE, hit: r })) : (_jsx(ObservableCard, { observable: r })) }) }, r.howler.id))) })) : (_jsx(AppListEmpty, {}))] }) }));
|
|
155
161
|
};
|
|
156
162
|
export default ViewCard;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Skeleton } from '@mui/material';
|
|
3
|
+
import api from '@cccsaurora/howler-ui/api';
|
|
4
|
+
import ObjectDetails from '@cccsaurora/howler-ui/components/elements/ObjectDetails';
|
|
5
|
+
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
6
|
+
import { useEffect, useState } from 'react';
|
|
7
|
+
const ObservableViewer = ({ observable: provided, observableId }) => {
|
|
8
|
+
const { dispatchApi } = useMyApi();
|
|
9
|
+
const [observable, setObservable] = useState(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (provided) {
|
|
12
|
+
setObservable(provided);
|
|
13
|
+
}
|
|
14
|
+
}, [provided]);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (observableId) {
|
|
17
|
+
dispatchApi(api.v2.search.post('observable', { query: `howler.id:${observableId}`, rows: 1 }), {
|
|
18
|
+
throwError: false
|
|
19
|
+
}).then(res => setObservable(res.items[0]));
|
|
20
|
+
}
|
|
21
|
+
}, [dispatchApi, observableId]);
|
|
22
|
+
if (!observable) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
return _jsx(Box, { p: 1, children: observable ? _jsx(ObjectDetails, { obj: observable }) : _jsx(Skeleton, { height: 120 }) });
|
|
26
|
+
};
|
|
27
|
+
export default ObservableViewer;
|
|
@@ -15,7 +15,7 @@ import useMyTheme from '@cccsaurora/howler-ui/components/hooks/useMyTheme';
|
|
|
15
15
|
import { useSearchParams } from 'react-router-dom';
|
|
16
16
|
import hitsData from '@cccsaurora/howler-ui/utils/hit.json';
|
|
17
17
|
import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
18
|
-
import
|
|
18
|
+
import MarkdownEditor from '../../elements/MarkdownEditor';
|
|
19
19
|
import { useStartingTemplate } from './startingTemplate';
|
|
20
20
|
const OverviewViewer = () => {
|
|
21
21
|
const theme = useTheme();
|
|
@@ -188,7 +188,7 @@ const OverviewViewer = () => {
|
|
|
188
188
|
right: `calc(50% + 7px - ${x}px)`,
|
|
189
189
|
mr: -2.4,
|
|
190
190
|
pr: 1.5
|
|
191
|
-
}, children: _jsx(
|
|
191
|
+
}, children: _jsx(MarkdownEditor, { height: "100%", content: content, setContent: setContent }) }) }), _jsx(Box, { sx: {
|
|
192
192
|
position: 'absolute',
|
|
193
193
|
top: 0,
|
|
194
194
|
bottom: 0,
|
|
@@ -4,10 +4,11 @@ import { useTranslation } from 'react-i18next';
|
|
|
4
4
|
import { HelpOutline, Save } from '@mui/icons-material';
|
|
5
5
|
import { Alert, Checkbox, CircularProgress, LinearProgress, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from '@mui/material';
|
|
6
6
|
import api from '@cccsaurora/howler-ui/api';
|
|
7
|
+
import {} from '@cccsaurora/howler-ui/api/search';
|
|
7
8
|
import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
|
|
8
9
|
import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
|
|
9
|
-
import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
|
|
10
10
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
11
|
+
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
11
12
|
import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
|
|
12
13
|
import CustomButton from '@cccsaurora/howler-ui/components/elements/addons/buttons/CustomButton';
|
|
13
14
|
import FlexPort from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexPort';
|
|
@@ -17,6 +18,7 @@ import VSBoxHeader from '@cccsaurora/howler-ui/components/elements/addons/layout
|
|
|
17
18
|
import SearchTotal from '@cccsaurora/howler-ui/components/elements/addons/search/SearchTotal';
|
|
18
19
|
import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
|
|
19
20
|
import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
|
|
21
|
+
import ObservableCard from '@cccsaurora/howler-ui/components/elements/observable/ObservableCard';
|
|
20
22
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
21
23
|
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
22
24
|
import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
|
|
@@ -26,8 +28,9 @@ import { DEFAULT_QUERY, StorageKey } from '@cccsaurora/howler-ui/utils/constants
|
|
|
26
28
|
import { convertDateToLucene } from '@cccsaurora/howler-ui/utils/utils';
|
|
27
29
|
import { buildViewUrl } from '@cccsaurora/howler-ui/utils/viewUtils';
|
|
28
30
|
import ErrorBoundary from '../ErrorBoundary';
|
|
29
|
-
import
|
|
31
|
+
import RecordQuery from '../hits/search/RecordQuery';
|
|
30
32
|
import HitSort from '../hits/search/shared/HitSort';
|
|
33
|
+
import IndexPicker from '../hits/search/shared/IndexPicker';
|
|
31
34
|
import SearchSpan from '../hits/search/shared/SearchSpan';
|
|
32
35
|
const ViewComposer = () => {
|
|
33
36
|
const { t } = useTranslation();
|
|
@@ -38,8 +41,10 @@ const ViewComposer = () => {
|
|
|
38
41
|
const addView = useContextSelector(ViewContext, ctx => ctx.addView);
|
|
39
42
|
const editView = useContextSelector(ViewContext, ctx => ctx.editView);
|
|
40
43
|
const getCurrentViews = useContextSelector(ViewContext, ctx => ctx.getCurrentViews);
|
|
44
|
+
const indexes = useContextSelector(ParameterContext, ctx => ctx.indexes);
|
|
45
|
+
const setIndexes = useContextSelector(ParameterContext, ctx => ctx.setIndexes);
|
|
41
46
|
const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
|
|
42
|
-
const
|
|
47
|
+
const loadRecords = useContextSelector(RecordContext, ctx => ctx.loadRecords);
|
|
43
48
|
// view state
|
|
44
49
|
const [title, setTitle] = useState('');
|
|
45
50
|
const [type, setType] = useState('global');
|
|
@@ -56,14 +61,17 @@ const ViewComposer = () => {
|
|
|
56
61
|
const [searching, setSearching] = useState(false);
|
|
57
62
|
const [error, setError] = useState(null);
|
|
58
63
|
const [response, setResponse] = useState();
|
|
64
|
+
const [isLoadingView, setIsLoadingView] = useState(!!routeParams.id);
|
|
59
65
|
const onSave = useCallback(async () => {
|
|
60
66
|
setLoading(true);
|
|
61
67
|
try {
|
|
68
|
+
const normalizedIndexes = indexes?.length > 0 ? indexes : ['hit'];
|
|
62
69
|
if (!routeParams.id) {
|
|
63
70
|
const newView = await addView({
|
|
64
71
|
title,
|
|
65
72
|
type,
|
|
66
73
|
query,
|
|
74
|
+
indexes: normalizedIndexes,
|
|
67
75
|
sort: sort || null,
|
|
68
76
|
span: span || null,
|
|
69
77
|
settings: {
|
|
@@ -77,6 +85,7 @@ const ViewComposer = () => {
|
|
|
77
85
|
title,
|
|
78
86
|
type,
|
|
79
87
|
query,
|
|
88
|
+
indexes: normalizedIndexes,
|
|
80
89
|
sort,
|
|
81
90
|
span,
|
|
82
91
|
settings: { advance_on_triage: advanceOnTriage }
|
|
@@ -101,23 +110,24 @@ const ViewComposer = () => {
|
|
|
101
110
|
sort,
|
|
102
111
|
span,
|
|
103
112
|
advanceOnTriage,
|
|
113
|
+
indexes,
|
|
104
114
|
navigate,
|
|
105
115
|
editView,
|
|
106
116
|
showErrorMessage
|
|
107
117
|
]);
|
|
108
|
-
const
|
|
109
|
-
setQuery(_query);
|
|
118
|
+
const performSearch = useCallback(async (searchQuery, searchIndexes, searchSort, searchSpan) => {
|
|
110
119
|
setSearching(true);
|
|
111
120
|
setError(null);
|
|
112
121
|
try {
|
|
113
|
-
const
|
|
122
|
+
const normalizedIndexes = searchIndexes?.length > 0 ? searchIndexes : ['hit'];
|
|
123
|
+
const _response = await dispatchApi(api.v2.search.post(normalizedIndexes, {
|
|
114
124
|
rows: pageCount,
|
|
115
|
-
query:
|
|
116
|
-
sort,
|
|
117
|
-
filters:
|
|
125
|
+
query: searchQuery,
|
|
126
|
+
sort: searchSort,
|
|
127
|
+
filters: searchSpan ? [`event.created:${convertDateToLucene(searchSpan)}`] : [],
|
|
118
128
|
metadata: ['template', 'analytic']
|
|
119
129
|
}), { showError: false, throwError: true });
|
|
120
|
-
|
|
130
|
+
loadRecords(_response.items);
|
|
121
131
|
setResponse(_response);
|
|
122
132
|
}
|
|
123
133
|
catch (e) {
|
|
@@ -126,18 +136,25 @@ const ViewComposer = () => {
|
|
|
126
136
|
finally {
|
|
127
137
|
setSearching(false);
|
|
128
138
|
}
|
|
129
|
-
}, [dispatchApi,
|
|
139
|
+
}, [dispatchApi, loadRecords, pageCount]);
|
|
140
|
+
const search = useCallback(async (_query) => {
|
|
141
|
+
setQuery(_query);
|
|
142
|
+
await performSearch(_query, indexes, sort, span);
|
|
143
|
+
}, [performSearch, indexes, sort, span, setQuery]);
|
|
130
144
|
useEffect(() => {
|
|
131
|
-
search
|
|
145
|
+
// Only run initial search if we're NOT editing an existing view
|
|
146
|
+
if (!routeParams.id) {
|
|
147
|
+
search(query || DEFAULT_QUERY);
|
|
148
|
+
}
|
|
132
149
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
133
|
-
}, []);
|
|
150
|
+
}, [routeParams.id]);
|
|
134
151
|
// We only run this when ancillary properties (i.e. filters, sorting) change
|
|
135
152
|
useEffect(() => {
|
|
136
|
-
if (query) {
|
|
153
|
+
if (query && !isLoadingView) {
|
|
137
154
|
search(query);
|
|
138
155
|
}
|
|
139
156
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
140
|
-
}, [sort, span]);
|
|
157
|
+
}, [sort, span, indexes, isLoadingView]);
|
|
141
158
|
useEffect(() => {
|
|
142
159
|
if (!routeParams.id) {
|
|
143
160
|
return;
|
|
@@ -153,13 +170,23 @@ const ViewComposer = () => {
|
|
|
153
170
|
}
|
|
154
171
|
setTitle(viewToEdit.title);
|
|
155
172
|
setAdvanceOnTriage(viewToEdit.settings?.advance_on_triage ?? false);
|
|
156
|
-
|
|
173
|
+
const loadedQuery = viewToEdit.query || DEFAULT_QUERY;
|
|
174
|
+
const loadedIndexes = viewToEdit.indexes || indexes;
|
|
175
|
+
const loadedSort = viewToEdit.sort || sort;
|
|
176
|
+
const loadedSpan = viewToEdit.span || span;
|
|
177
|
+
setQuery(loadedQuery);
|
|
178
|
+
if (viewToEdit.indexes) {
|
|
179
|
+
setIndexes(loadedIndexes);
|
|
180
|
+
}
|
|
157
181
|
if (viewToEdit.sort) {
|
|
158
|
-
setSort(
|
|
182
|
+
setSort(loadedSort);
|
|
159
183
|
}
|
|
160
184
|
if (viewToEdit.span) {
|
|
161
|
-
setSpan(
|
|
185
|
+
setSpan(loadedSpan);
|
|
162
186
|
}
|
|
187
|
+
// Perform search with the loaded values to avoid using stale state
|
|
188
|
+
await performSearch(loadedQuery, loadedIndexes, loadedSort, loadedSpan);
|
|
189
|
+
setIsLoadingView(false);
|
|
163
190
|
})();
|
|
164
191
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
165
192
|
}, [routeParams.id]);
|
|
@@ -172,6 +199,6 @@ const ViewComposer = () => {
|
|
|
172
199
|
fontSize: '0.9em',
|
|
173
200
|
fontStyle: 'italic',
|
|
174
201
|
mb: 0.5
|
|
175
|
-
}), variant: "body2", children: t('hit.search.prompt') }), _jsx(
|
|
202
|
+
}), variant: "body2", children: t('hit.search.prompt') }), _jsx(RecordQuery, { triggerSearch: search, searching: searching, onChange: (_query, isDirty) => setIsSearchDirty(isDirty) }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(IndexPicker, {}), _jsx(HitSort, {}), _jsx(SearchSpan, { omitCustom: true }), _jsx("div", { style: { flex: 1 } }), _jsxs(Stack, { spacing: 1, direction: "row", alignItems: "center", sx: { flex: '0 !important', minWidth: '300px' }, children: [_jsx(Typography, { component: "span", children: t('view.settings.advance_on_triage') }), _jsx(Tooltip, { title: t('view.settings.advance_on_triage.description'), children: _jsx(HelpOutline, { sx: { fontSize: '16px' } }) }), _jsx(Checkbox, { size: "small", checked: advanceOnTriage, onChange: (_event, checked) => setAdvanceOnTriage(checked) })] })] }), response?.total ? (_jsx(SearchTotal, { total: response.total, pageLength: response.items.length, offset: response.offset, sx: theme => ({ color: theme.palette.text.secondary, fontSize: '0.9em', fontStyle: 'italic' }) })) : null, _jsx(LinearProgress, { sx: [!searching && { opacity: 0 }] })] }) }), _jsx(VSBoxContent, { children: _jsxs(Stack, { spacing: 1, children: [!response?.total && _jsx(AppListEmpty, {}), response?.items.map(record => record.__index === 'hit' ? (_jsx(HitCard, { id: record.howler.id, layout: HitLayout.DENSE }, record.howler.id)) : (_jsx(ObservableCard, { observable: record }, record.howler.id)))] }) })] }) }) }) }));
|
|
176
203
|
};
|
|
177
204
|
export default ViewComposer;
|