@cccsaurora/howler-ui 2.18.0 → 2.19.0-cases.1040
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/action/index.d.ts +5 -4
- package/api/action/index.js +8 -8
- package/api/analytic/index.d.ts +3 -2
- package/api/analytic/index.js +4 -4
- package/api/analytic/owner.d.ts +2 -1
- package/api/analytic/owner.js +2 -2
- package/api/analytic/rules.d.ts +2 -1
- package/api/analytic/rules.js +2 -2
- package/api/dossier/index.d.ts +4 -3
- package/api/dossier/index.js +6 -6
- package/api/hit/index.d.ts +3 -2
- package/api/hit/index.js +4 -4
- package/api/hit/labels.d.ts +3 -2
- package/api/hit/labels.js +4 -4
- package/api/hit/overwrite.d.ts +2 -1
- package/api/hit/overwrite.js +2 -2
- package/api/hit/transition.d.ts +2 -1
- package/api/hit/transition.js +2 -2
- package/api/index.d.ts +9 -4
- package/api/index.js +18 -10
- package/api/overview/index.d.ts +4 -3
- package/api/overview/index.js +6 -6
- 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/search/overview.d.ts +2 -2
- package/api/socket/index.d.ts +3 -0
- package/api/socket/index.js +6 -0
- package/api/socket/viewers.d.ts +2 -0
- package/api/socket/viewers.js +8 -0
- package/api/socket/viewers.test.js +44 -0
- package/api/template/index.d.ts +4 -3
- package/api/template/index.js +6 -6
- package/api/user/index.d.ts +2 -1
- package/api/user/index.js +2 -2
- package/api/v2/case/index.d.ts +9 -0
- package/api/v2/case/index.js +21 -0
- package/api/v2/case/items.d.ts +6 -0
- package/api/v2/case/items.js +18 -0
- package/api/v2/case/rules.d.ts +6 -0
- package/api/v2/case/rules.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/api/view/index.d.ts +4 -3
- package/api/view/index.js +6 -6
- package/commons/components/leftnav/LeftNavDrawer.js +1 -1
- package/components/app/App.d.ts +1 -0
- package/components/app/App.js +64 -13
- package/components/app/hooks/useMatchers.d.ts +1 -1
- package/components/app/hooks/useMatchers.js +23 -11
- package/components/app/hooks/useMatchers.test.js +22 -22
- package/components/app/hooks/useTitle.js +5 -5
- package/components/app/providers/FavouritesProvider.js +2 -6
- package/components/app/providers/ModalProvider.d.ts +2 -0
- package/components/app/providers/ModalProvider.js +5 -1
- package/components/app/providers/ParameterProvider.d.ts +9 -2
- package/components/app/providers/ParameterProvider.js +165 -240
- package/components/app/providers/ParameterProvider.test.js +346 -94
- package/components/app/providers/RecordProvider.d.ts +23 -0
- package/components/app/providers/{HitProvider.js → RecordProvider.js} +42 -42
- 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} +52 -71
- package/components/app/providers/SearchResponseProvider.d.ts +21 -0
- package/components/app/providers/SearchResponseProvider.js +81 -0
- package/components/app/providers/SearchResponseProvider.test.d.ts +1 -0
- package/components/app/providers/SearchResponseProvider.test.js +406 -0
- package/components/app/providers/SocketProvider.d.ts +11 -2
- package/components/app/providers/SocketProvider.js +18 -5
- package/components/app/providers/UserListProvider.js +28 -8
- package/components/app/providers/ViewProvider.test.js +4 -1
- package/components/elements/ContextMenu.d.ts +56 -0
- package/components/elements/ContextMenu.js +109 -0
- package/components/elements/ContextMenu.test.d.ts +1 -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 +18 -8
- package/components/elements/addons/buttons/CustomButton.test.d.ts +1 -0
- package/components/elements/addons/buttons/CustomButton.test.js +106 -0
- package/components/elements/addons/layout/FlexOne.test.d.ts +1 -0
- package/components/elements/addons/layout/FlexOne.test.js +30 -0
- package/components/elements/addons/lists/TuiListBase.js +4 -0
- package/components/elements/addons/lists/TuiListElement.js +6 -3
- package/components/elements/addons/lists/index.d.ts +1 -0
- package/components/elements/addons/search/SearchPagination.d.ts +2 -1
- package/components/elements/addons/search/SearchPagination.js +4 -4
- package/components/elements/addons/search/SearchPagination.test.d.ts +1 -0
- package/components/elements/addons/search/SearchPagination.test.js +80 -0
- package/components/elements/addons/search/SearchTotal.test.d.ts +1 -0
- package/components/elements/addons/search/SearchTotal.test.js +47 -0
- 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/ChipPopper.js +5 -5
- package/components/elements/display/DocumentationButton.test.d.ts +1 -0
- package/components/elements/display/DocumentationButton.test.js +84 -0
- package/components/elements/display/DynamicTabs.test.d.ts +1 -0
- package/components/elements/display/DynamicTabs.test.js +93 -0
- package/components/elements/display/HowlerAvatar.js +2 -1
- package/components/elements/display/HowlerCard.js +1 -1
- package/components/elements/display/HowlerCard.test.d.ts +1 -0
- package/components/elements/display/HowlerCard.test.js +51 -0
- package/components/elements/display/Image.test.d.ts +1 -0
- package/components/elements/display/Image.test.js +79 -0
- package/components/elements/display/ItemManager.d.ts +2 -2
- package/components/elements/display/ItemManager.js +1 -1
- package/components/elements/display/Markdown.js +9 -2
- package/components/elements/display/Modal.js +2 -0
- package/components/elements/display/QueryResultText.test.d.ts +1 -0
- package/components/elements/display/QueryResultText.test.js +48 -0
- package/components/elements/display/TextDivider.test.d.ts +1 -0
- package/components/elements/display/TextDivider.test.js +43 -0
- package/components/elements/display/TypingIndicator.test.d.ts +1 -0
- package/components/elements/display/TypingIndicator.test.js +33 -0
- package/components/elements/display/modals/ConfirmDeleteModal.d.ts +4 -0
- package/components/elements/display/modals/ConfirmDeleteModal.js +4 -2
- package/components/elements/hit/HitActions.js +4 -4
- package/components/elements/hit/HitBanner.d.ts +1 -0
- package/components/elements/hit/HitBanner.js +34 -51
- package/components/elements/hit/HitCard.d.ts +2 -0
- package/components/elements/hit/HitCard.js +7 -7
- package/components/elements/hit/HitLabels.js +2 -2
- package/components/elements/hit/HitOutline.d.ts +1 -0
- package/components/elements/hit/HitOutline.js +3 -3
- 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 +9 -7
- package/components/elements/hit/aggregate/HitGraph.js +8 -8
- package/components/elements/hit/elements/AnalyticLink.d.ts +9 -0
- package/components/elements/hit/elements/AnalyticLink.js +22 -0
- package/components/elements/hit/elements/Assigned.d.ts +2 -0
- package/components/elements/hit/elements/Assigned.js +21 -16
- package/components/elements/hit/elements/Assigned.test.d.ts +1 -0
- package/components/elements/hit/elements/Assigned.test.js +65 -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 +268 -0
- package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
- package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +190 -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/useMyApi.d.ts +1 -1
- package/components/hooks/useMyPreferences.js +12 -2
- package/components/hooks/useMySearch.js +2 -2
- package/components/hooks/useMySitemap.js +4 -1
- package/components/hooks/useMyTheme.js +9 -2
- 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/403.d.ts +3 -0
- package/components/routes/403.js +10 -0
- package/components/routes/403.test.d.ts +1 -0
- package/components/routes/403.test.js +27 -0
- package/components/routes/404.test.d.ts +1 -0
- package/components/routes/404.test.js +26 -0
- package/components/routes/ErrorBoundary.test.d.ts +1 -0
- package/components/routes/ErrorBoundary.test.js +43 -0
- package/components/routes/ErrorOccured.test.d.ts +1 -0
- package/components/routes/ErrorOccured.test.js +35 -0
- package/components/routes/Logout.test.d.ts +1 -0
- package/components/routes/Logout.test.js +59 -0
- package/components/routes/action/edit/ActionEditor.js +3 -3
- package/components/routes/action/useMyActionFunctions.js +9 -3
- package/components/routes/action/view/ActionDetails.js +15 -2
- package/components/routes/action/view/ActionSearch.js +32 -20
- package/components/routes/action/view/markdown/integrations.en.md.js +1 -1
- package/components/routes/action/view/markdown/integrations.fr.md.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 +12 -8
- package/components/routes/analytics/AnalyticSearch.js +7 -7
- package/components/routes/cases/CaseViewer.d.ts +2 -0
- package/components/routes/cases/CaseViewer.js +44 -0
- package/components/routes/cases/CaseViewer.test.d.ts +1 -0
- package/components/routes/cases/CaseViewer.test.js +133 -0
- package/components/routes/cases/Cases.d.ts +2 -0
- package/components/routes/cases/Cases.js +147 -0
- package/components/routes/cases/constants.d.ts +6 -0
- package/components/routes/cases/constants.js +6 -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 +66 -0
- package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
- package/components/routes/cases/detail/CaseDetails.js +70 -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/CaseRules.d.ts +7 -0
- package/components/routes/cases/detail/CaseRules.js +57 -0
- package/components/routes/cases/detail/CaseRules.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseRules.test.js +221 -0
- package/components/routes/cases/detail/CaseSidebar.d.ts +8 -0
- package/components/routes/cases/detail/CaseSidebar.js +107 -0
- package/components/routes/cases/detail/CaseSidebar.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseSidebar.test.js +266 -0
- package/components/routes/cases/detail/CaseTask.d.ts +11 -0
- package/components/routes/cases/detail/CaseTask.js +66 -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 +320 -0
- package/components/routes/cases/detail/CreateRuleDialog.d.ts +9 -0
- package/components/routes/cases/detail/CreateRuleDialog.js +163 -0
- package/components/routes/cases/detail/CreateRuleDialog.test.d.ts +1 -0
- package/components/routes/cases/detail/CreateRuleDialog.test.js +259 -0
- package/components/routes/cases/detail/ItemPage.d.ts +6 -0
- package/components/routes/cases/detail/ItemPage.js +95 -0
- package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
- package/components/routes/cases/detail/RelatedCasePanel.js +34 -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 +11 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.js +24 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.js +26 -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 +20 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.js +83 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.test.js +295 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +34 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +103 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +363 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +25 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.js +88 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.test.js +206 -0
- package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +5 -0
- package/components/routes/cases/detail/sidebar/RootDropZone.js +33 -0
- package/components/routes/cases/detail/sidebar/types.d.ts +9 -0
- package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
- package/components/routes/cases/detail/sidebar/utils.js +29 -0
- package/components/routes/cases/detail/sidebar/utils.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/utils.test.js +82 -0
- package/components/routes/cases/hooks/useCase.d.ts +13 -0
- package/components/routes/cases/hooks/useCase.js +69 -0
- package/components/routes/cases/hooks/useCase.test.d.ts +1 -0
- package/components/routes/cases/hooks/useCase.test.js +141 -0
- package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
- package/components/routes/cases/modals/AddToCaseModal.js +59 -0
- package/components/routes/cases/modals/AddToCaseModal.test.d.ts +1 -0
- package/components/routes/cases/modals/AddToCaseModal.test.js +313 -0
- package/components/routes/cases/modals/CaseRecordRow.d.ts +9 -0
- package/components/routes/cases/modals/CaseRecordRow.js +15 -0
- package/components/routes/cases/modals/CreateCaseModal.d.ts +7 -0
- package/components/routes/cases/modals/CreateCaseModal.js +55 -0
- package/components/routes/cases/modals/CreateCaseModal.test.d.ts +1 -0
- package/components/routes/cases/modals/CreateCaseModal.test.js +358 -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 +116 -0
- package/components/routes/cases/modals/ResolveModal.test.d.ts +1 -0
- package/components/routes/cases/modals/ResolveModal.test.js +454 -0
- package/components/routes/cases/modals/hooks.d.ts +7 -0
- package/components/routes/cases/modals/hooks.js +44 -0
- package/components/routes/cases/modals/types.d.ts +5 -0
- package/components/routes/cases/search/CaseAssigneeFilter.d.ts +6 -0
- package/components/routes/cases/search/CaseAssigneeFilter.js +33 -0
- package/components/routes/cases/search/CaseAssigneeFilter.test.d.ts +1 -0
- package/components/routes/cases/search/CaseAssigneeFilter.test.js +127 -0
- package/components/routes/cases/search/CaseDateFilter.d.ts +13 -0
- package/components/routes/cases/search/CaseDateFilter.js +26 -0
- package/components/routes/cases/search/CaseDateFilter.test.d.ts +1 -0
- package/components/routes/cases/search/CaseDateFilter.test.js +115 -0
- package/components/routes/cases/search/CaseStatusFilter.d.ts +6 -0
- package/components/routes/cases/search/CaseStatusFilter.js +13 -0
- package/components/routes/cases/search/CaseStatusFilter.test.d.ts +1 -0
- package/components/routes/cases/search/CaseStatusFilter.test.js +86 -0
- package/components/routes/dossiers/DossierEditor.js +2 -2
- package/components/routes/dossiers/DossierEditor.test.js +1 -1
- package/components/routes/dossiers/Dossiers.js +25 -19
- package/components/routes/help/ActionIntroductionDocumentation.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/help/markdown/en/client.md.js +1 -1
- package/components/routes/help/markdown/en/retention.md.js +1 -1
- package/components/routes/help/markdown/fr/client.md.js +1 -1
- package/components/routes/help/markdown/fr/retention.md.js +1 -1
- package/components/routes/hits/search/InformationPane.d.ts +1 -0
- package/components/routes/hits/search/InformationPane.js +50 -63
- 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/CustomSpan.js +7 -7
- package/components/routes/hits/search/shared/CustomSpan.test.d.ts +1 -0
- package/components/routes/hits/search/shared/CustomSpan.test.js +170 -0
- 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/AddNewCard.js +1 -1
- package/components/routes/home/AddNewCard.test.d.ts +1 -0
- package/components/routes/home/AddNewCard.test.js +86 -0
- package/components/routes/home/ViewCard.js +47 -41
- package/components/routes/home/ViewRefresh.test.d.ts +1 -0
- package/components/routes/home/ViewRefresh.test.js +79 -0
- package/components/routes/observables/ObservableViewer.d.ts +7 -0
- package/components/routes/observables/ObservableViewer.js +27 -0
- package/components/routes/overviews/OverviewCard.test.d.ts +1 -0
- package/components/routes/overviews/OverviewCard.test.js +108 -0
- package/components/routes/overviews/OverviewViewer.js +18 -10
- package/components/routes/overviews/Overviews.js +25 -19
- package/components/routes/overviews/template/en.md.js +1 -1
- package/components/routes/overviews/template/fr.md.js +1 -1
- package/components/routes/settings/LocalSection.js +2 -1
- package/components/routes/settings/SettingsSection.test.d.ts +1 -0
- package/components/routes/settings/SettingsSection.test.js +19 -0
- package/components/routes/templates/TemplateCard.d.ts +3 -1
- package/components/routes/templates/TemplateCard.js +8 -4
- package/components/routes/templates/TemplateCard.test.d.ts +1 -0
- package/components/routes/templates/TemplateCard.test.js +115 -0
- package/components/routes/templates/TemplateViewer.js +62 -16
- package/components/routes/templates/Templates.js +35 -13
- package/components/routes/views/ViewComposer.js +46 -19
- package/components/routes/views/Views.js +21 -14
- package/locales/en/translation.json +146 -8
- package/locales/fr/translation.json +144 -8
- package/models/WithMetadata.d.ts +2 -1
- package/models/entities/generated/ApiType.d.ts +1 -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 +2 -0
- package/models/entities/generated/Howler.d.ts +1 -5
- 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 +42 -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 +6 -9
- 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/UserUser.d.ts +2 -0
- package/models/entities/generated/View.d.ts +1 -0
- package/models/socket/CaseUpdate.d.ts +5 -0
- package/models/socket/ViewersUpdate.d.ts +4 -0
- package/package.json +147 -132
- package/plugins/clue/components/ClueTypography.js +2 -2
- package/plugins/clue/utils.d.ts +2 -1
- package/rest/FetchClient.test.d.ts +1 -0
- package/rest/FetchClient.test.js +81 -0
- package/setupTests.js +5 -0
- package/tests/mocks.d.ts +11 -1
- package/tests/mocks.js +12 -7
- package/tests/server-handlers.js +6 -1
- package/tests/utils.d.ts +4 -0
- package/tests/utils.js +20 -0
- package/utils/Throttler.test.d.ts +1 -0
- package/utils/Throttler.test.js +135 -0
- package/utils/actionUtils.test.d.ts +1 -0
- package/utils/actionUtils.test.js +140 -0
- package/utils/constants.d.ts +5 -3
- package/utils/constants.js +7 -0
- package/utils/hitFunctions.d.ts +2 -1
- package/utils/hitFunctions.js +4 -4
- package/utils/hitFunctions.test.d.ts +1 -0
- package/utils/hitFunctions.test.js +52 -0
- package/utils/localStorage.test.d.ts +1 -0
- package/utils/localStorage.test.js +96 -0
- package/utils/menuUtils.js +1 -1
- package/utils/menuUtils.test.d.ts +1 -0
- package/utils/menuUtils.test.js +198 -0
- package/utils/sessionStorage.test.d.ts +1 -0
- package/utils/sessionStorage.test.js +81 -0
- package/utils/socketUtils.d.ts +14 -0
- package/utils/socketUtils.js +17 -1
- package/utils/socketUtils.test.d.ts +1 -0
- package/utils/socketUtils.test.js +59 -0
- package/utils/stringUtils.test.d.ts +1 -0
- package/utils/stringUtils.test.js +159 -0
- package/utils/typeUtils.d.ts +7 -0
- package/utils/typeUtils.js +27 -0
- package/utils/utils.test.d.ts +1 -0
- package/utils/utils.test.js +292 -0
- package/utils/viewUtils.js +3 -0
- package/utils/viewUtils.test.d.ts +1 -0
- package/utils/viewUtils.test.js +45 -0
- package/utils/xsrf.test.d.ts +1 -0
- package/utils/xsrf.test.js +41 -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 -229
- /package/{components/app/providers/HitSearchProvider.test.d.ts → api/socket/viewers.test.d.ts} +0 -0
- /package/components/{routes/hits/search/HitContextMenu.test.d.ts → app/providers/RecordSearchProvider.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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import userEvent, {} from '@testing-library/user-event';
|
|
5
|
+
import { UserListContext } from '@cccsaurora/howler-ui/components/app/providers/UserListProvider';
|
|
6
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
7
|
+
import { I18nextProvider } from 'react-i18next';
|
|
8
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import CaseAssigneeFilter from './CaseAssigneeFilter';
|
|
10
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Hoisted mocks
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
vi.mock('commons/components/app/hooks/useAppUser', () => ({
|
|
15
|
+
useAppUser: () => ({ user: { username: 'alice', name: 'Alice Smith' } })
|
|
16
|
+
}));
|
|
17
|
+
// Stub HowlerAvatar to avoid avatar API calls
|
|
18
|
+
vi.mock('components/elements/display/HowlerAvatar', () => ({
|
|
19
|
+
default: ({ userId }) => _jsx("div", { id: `avatar-${userId}`, children: userId })
|
|
20
|
+
}));
|
|
21
|
+
// Stub UserList to a simple multi-select so we can test the onChange wire-up
|
|
22
|
+
// without pulling in the full component and its popover.
|
|
23
|
+
vi.mock('components/elements/UserList', () => ({
|
|
24
|
+
default: ({ userIds, onChange, multiple }) => (_jsxs("div", { id: "user-list", children: [_jsx("button", { id: "user-list-add", onClick: () => onChange([...userIds, 'bob']), children: "Add bob" }), multiple &&
|
|
25
|
+
userIds.map(id => (_jsxs("button", { id: `remove-${id}`, onClick: () => onChange(userIds.filter(u => u !== id)), children: ["Remove ", id] }, id)))] }))
|
|
26
|
+
}));
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const mockSearchUsers = vi.fn();
|
|
31
|
+
const USERS = {
|
|
32
|
+
alice: { username: 'alice', name: 'Alice Smith' },
|
|
33
|
+
bob: { username: 'bob', name: 'Bob Jones' }
|
|
34
|
+
};
|
|
35
|
+
const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(UserListContext.Provider, { value: { users: USERS, searchUsers: mockSearchUsers, fetchUsers: vi.fn() }, children: children }) }));
|
|
36
|
+
const renderFilter = (assigneeFilter, onChange = vi.fn()) => render(_jsx(CaseAssigneeFilter, { assigneeFilter: assigneeFilter, onChange: onChange }), { wrapper: Wrapper });
|
|
37
|
+
const openPopper = async (user, labelText) => {
|
|
38
|
+
const chip = screen.getByText(labelText).closest('.MuiChip-root');
|
|
39
|
+
await user.click(chip);
|
|
40
|
+
await waitFor(() => {
|
|
41
|
+
expect(screen.getByTestId('user-list')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Tests
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
describe('CaseAssigneeFilter', () => {
|
|
48
|
+
let user;
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
user = userEvent.setup();
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
});
|
|
53
|
+
describe('searchUsers on mount', () => {
|
|
54
|
+
it('calls searchUsers with "uname:*" on mount', () => {
|
|
55
|
+
renderFilter([]);
|
|
56
|
+
expect(mockSearchUsers).toHaveBeenCalledWith('uname:*');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('label', () => {
|
|
60
|
+
it('shows the default "Assignee" label when no assignees are selected', () => {
|
|
61
|
+
renderFilter([]);
|
|
62
|
+
expect(screen.getByText(i18n.t('route.cases.filter.assignee'))).toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
it('shows the user display name when one assignee is selected', () => {
|
|
65
|
+
renderFilter(['alice']);
|
|
66
|
+
expect(screen.getByText('Alice Smith')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
it('shows count + "Assignees" when multiple assignees are selected', () => {
|
|
69
|
+
renderFilter(['alice', 'bob']);
|
|
70
|
+
expect(screen.getByText(`2 ${i18n.t('route.cases.filter.assignees')}`)).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
it('falls back to username when user is not in the user map', () => {
|
|
73
|
+
renderFilter(['unknown-user']);
|
|
74
|
+
const chipLabel = document.querySelector('.MuiChip-label');
|
|
75
|
+
expect(chipLabel?.textContent).toContain('unknown-user');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('chip color', () => {
|
|
79
|
+
it('uses default color when no assignees are selected', () => {
|
|
80
|
+
renderFilter([]);
|
|
81
|
+
const chip = screen.getByText(i18n.t('route.cases.filter.assignee')).closest('.MuiChip-root');
|
|
82
|
+
expect(chip).not.toHaveClass('MuiChip-colorPrimary');
|
|
83
|
+
});
|
|
84
|
+
it('uses primary color when assignees are selected', () => {
|
|
85
|
+
renderFilter(['alice']);
|
|
86
|
+
const chip = screen.getByText('Alice Smith').closest('.MuiChip-root');
|
|
87
|
+
expect(chip).toHaveClass('MuiChip-colorPrimary');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('"Myself" checkbox', () => {
|
|
91
|
+
it('is unchecked when the current user is not in the filter', async () => {
|
|
92
|
+
renderFilter([]);
|
|
93
|
+
await openPopper(user, i18n.t('route.cases.filter.assignee'));
|
|
94
|
+
const checkbox = screen.getByRole('checkbox', { name: i18n.t('route.cases.filter.myself') });
|
|
95
|
+
expect(checkbox).not.toBeChecked();
|
|
96
|
+
});
|
|
97
|
+
it('is checked when the current user is already in the filter', async () => {
|
|
98
|
+
renderFilter(['alice']);
|
|
99
|
+
await openPopper(user, 'Alice Smith');
|
|
100
|
+
const checkbox = screen.getByRole('checkbox', { name: i18n.t('route.cases.filter.myself') });
|
|
101
|
+
expect(checkbox).toBeChecked();
|
|
102
|
+
});
|
|
103
|
+
it('adds the current user to the filter when the checkbox is checked', async () => {
|
|
104
|
+
const onChange = vi.fn();
|
|
105
|
+
renderFilter([], onChange);
|
|
106
|
+
await openPopper(user, i18n.t('route.cases.filter.assignee'));
|
|
107
|
+
await user.click(screen.getByRole('checkbox', { name: i18n.t('route.cases.filter.myself') }));
|
|
108
|
+
expect(onChange).toHaveBeenCalledWith(['alice']);
|
|
109
|
+
});
|
|
110
|
+
it('removes the current user from the filter when the checkbox is unchecked', async () => {
|
|
111
|
+
const onChange = vi.fn();
|
|
112
|
+
renderFilter(['alice', 'bob'], onChange);
|
|
113
|
+
await openPopper(user, `2 ${i18n.t('route.cases.filter.assignees')}`);
|
|
114
|
+
await user.click(screen.getByRole('checkbox', { name: i18n.t('route.cases.filter.myself') }));
|
|
115
|
+
expect(onChange).toHaveBeenCalledWith(['bob']);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('UserList onChange passthrough', () => {
|
|
119
|
+
it('passes onChange directly to UserList', async () => {
|
|
120
|
+
const onChange = vi.fn();
|
|
121
|
+
renderFilter(['alice'], onChange);
|
|
122
|
+
await openPopper(user, 'Alice Smith');
|
|
123
|
+
await user.click(screen.getByTestId('user-list-add'));
|
|
124
|
+
expect(onChange).toHaveBeenCalledWith(['alice', 'bob']);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Dayjs } from 'dayjs';
|
|
2
|
+
import type { FC } from 'react';
|
|
3
|
+
import { DATE_RANGES } from '@cccsaurora/howler-ui/utils/constants';
|
|
4
|
+
export type DateRangeOption = (typeof DATE_RANGES)[number];
|
|
5
|
+
declare const CaseDateFilter: FC<{
|
|
6
|
+
dateRange: DateRangeOption;
|
|
7
|
+
onChange: (v: DateRangeOption) => void;
|
|
8
|
+
customStart: Dayjs;
|
|
9
|
+
customEnd: Dayjs;
|
|
10
|
+
onCustomStartChange: (v: Dayjs) => void;
|
|
11
|
+
onCustomEndChange: (v: Dayjs) => void;
|
|
12
|
+
}>;
|
|
13
|
+
export default CaseDateFilter;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AvTimer } from '@mui/icons-material';
|
|
3
|
+
import { Autocomplete, Stack, TextField, Typography } from '@mui/material';
|
|
4
|
+
import { LocalizationProvider } from '@mui/x-date-pickers';
|
|
5
|
+
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
6
|
+
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
|
|
7
|
+
import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { DATE_RANGES } from '@cccsaurora/howler-ui/utils/constants';
|
|
10
|
+
const CaseDateFilter = ({ dateRange, onChange, customStart, customEnd, onCustomStartChange, onCustomEndChange }) => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
return (_jsx(ChipPopper, { icon: _jsx(AvTimer, { fontSize: "small" }), label: _jsx(Typography, { variant: "body2", children: dateRange === 'date.range.all'
|
|
13
|
+
? t('route.cases.filter.date')
|
|
14
|
+
: dateRange === 'date.range.custom'
|
|
15
|
+
? `${customStart.format('YYYY-MM-DD')} ${t('to')} ${customEnd.format('YYYY-MM-DD')}`
|
|
16
|
+
: t(dateRange) }), minWidth: "225px", slotProps: { chip: { size: 'small', color: dateRange !== 'date.range.all' ? 'primary' : 'default' } }, children: _jsxs(Stack, { spacing: 1, children: [_jsx(Autocomplete, { size: "small", value: dateRange, options: [...DATE_RANGES], getOptionLabel: o => t(o), disableClearable: true, onChange: (_, nv) => onChange(nv), renderInput: params => _jsx(TextField, { ...params, label: t('route.cases.filter.date') }) }), dateRange === 'date.range.custom' && (_jsx(LocalizationProvider, { dateAdapter: AdapterDayjs, children: _jsxs(Stack, { direction: "row", spacing: 1, useFlexGap: true, flexWrap: "wrap", children: [_jsx(DateTimePicker, { sx: { minWidth: '175px', flexGrow: 1 }, slotProps: { textField: { size: 'small' } }, label: t('date.select.start'), value: customStart, maxDate: customEnd, onChange: nv => {
|
|
17
|
+
if (nv) {
|
|
18
|
+
onCustomStartChange(nv);
|
|
19
|
+
}
|
|
20
|
+
}, ampm: false, disableFuture: true }), _jsx(DateTimePicker, { sx: { minWidth: '175px', flexGrow: 1 }, slotProps: { textField: { size: 'small' } }, label: t('date.select.end'), value: customEnd, minDate: customStart, onChange: nv => {
|
|
21
|
+
if (nv) {
|
|
22
|
+
onCustomEndChange(nv);
|
|
23
|
+
}
|
|
24
|
+
}, ampm: false, disableFuture: true })] }) }))] }) }));
|
|
25
|
+
};
|
|
26
|
+
export default CaseDateFilter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import userEvent, {} from '@testing-library/user-event';
|
|
5
|
+
import dayjs from 'dayjs';
|
|
6
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
7
|
+
import { I18nextProvider } from 'react-i18next';
|
|
8
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import CaseDateFilter from './CaseDateFilter';
|
|
10
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Stub the date pickers so jsdom doesn't choke on them
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
vi.mock('@mui/x-date-pickers/DateTimePicker', () => ({
|
|
15
|
+
DateTimePicker: ({ label, onChange }) => (_jsx("button", { id: `picker-${label}`, onClick: () => onChange(dayjs('2025-06-01')), children: label }))
|
|
16
|
+
}));
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Wrapper / render helper
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const FIXED_START = dayjs('2025-01-01');
|
|
21
|
+
const FIXED_END = dayjs('2025-03-01');
|
|
22
|
+
const Wrapper = ({ children }) => _jsx(I18nextProvider, { i18n: i18n, children: children });
|
|
23
|
+
const renderFilter = ({ dateRange = 'date.range.all', onChange = vi.fn(), onCustomStartChange = vi.fn(), onCustomEndChange = vi.fn() } = {}) => render(_jsx(CaseDateFilter, { dateRange: dateRange, onChange: onChange, customStart: FIXED_START, customEnd: FIXED_END, onCustomStartChange: onCustomStartChange, onCustomEndChange: onCustomEndChange }), { wrapper: Wrapper });
|
|
24
|
+
const openPopper = async (user) => {
|
|
25
|
+
// The chip label varies; find the chip by the AvTimer icon's sibling
|
|
26
|
+
const chip = document.querySelector('.MuiChip-root');
|
|
27
|
+
await user.click(chip);
|
|
28
|
+
await waitFor(() => {
|
|
29
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Tests
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
describe('CaseDateFilter', () => {
|
|
36
|
+
let user;
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
user = userEvent.setup();
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
describe('label', () => {
|
|
42
|
+
it('shows "Date Range" label when date.range.all is selected', () => {
|
|
43
|
+
renderFilter({ dateRange: 'date.range.all' });
|
|
44
|
+
expect(screen.getByText(i18n.t('route.cases.filter.date'))).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
it('shows the translated range label for a non-all preset', () => {
|
|
47
|
+
renderFilter({ dateRange: 'date.range.1.week' });
|
|
48
|
+
expect(screen.getByText(i18n.t('date.range.1.week'))).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
it('shows formatted start/end dates for the custom range', () => {
|
|
51
|
+
renderFilter({ dateRange: 'date.range.custom' });
|
|
52
|
+
const expected = `${FIXED_START.format('YYYY-MM-DD')} ${i18n.t('to')} ${FIXED_END.format('YYYY-MM-DD')}`;
|
|
53
|
+
expect(screen.getByText(expected)).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('chip color', () => {
|
|
57
|
+
it('uses default color for date.range.all', () => {
|
|
58
|
+
renderFilter({ dateRange: 'date.range.all' });
|
|
59
|
+
const chip = document.querySelector('.MuiChip-root');
|
|
60
|
+
expect(chip).not.toHaveClass('MuiChip-colorPrimary');
|
|
61
|
+
});
|
|
62
|
+
it('uses primary color for any non-all range', () => {
|
|
63
|
+
renderFilter({ dateRange: 'date.range.1.day' });
|
|
64
|
+
const chip = document.querySelector('.MuiChip-root');
|
|
65
|
+
expect(chip).toHaveClass('MuiChip-colorPrimary');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('popper content', () => {
|
|
69
|
+
it('does not show the autocomplete before the chip is clicked', () => {
|
|
70
|
+
renderFilter();
|
|
71
|
+
expect(screen.queryByRole('combobox')).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
it('shows the range autocomplete after the chip is clicked', async () => {
|
|
74
|
+
renderFilter();
|
|
75
|
+
await openPopper(user);
|
|
76
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
it('does not show date pickers when a preset (non-custom) range is active', async () => {
|
|
79
|
+
renderFilter({ dateRange: 'date.range.1.day' });
|
|
80
|
+
await openPopper(user);
|
|
81
|
+
expect(screen.queryByTestId(`picker-${i18n.t('date.select.start')}`)).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
it('shows start and end date pickers when custom range is active', async () => {
|
|
84
|
+
renderFilter({ dateRange: 'date.range.custom' });
|
|
85
|
+
await openPopper(user);
|
|
86
|
+
expect(screen.getByTestId(`picker-${i18n.t('date.select.start')}`)).toBeInTheDocument();
|
|
87
|
+
expect(screen.getByTestId(`picker-${i18n.t('date.select.end')}`)).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('interactions', () => {
|
|
91
|
+
it('calls onChange when a range option is selected from the autocomplete', async () => {
|
|
92
|
+
const onChange = vi.fn();
|
|
93
|
+
renderFilter({ dateRange: 'date.range.all', onChange });
|
|
94
|
+
await openPopper(user);
|
|
95
|
+
await user.click(screen.getByRole('combobox'));
|
|
96
|
+
const option = await screen.findByRole('option', { name: i18n.t('date.range.1.week') });
|
|
97
|
+
await user.click(option);
|
|
98
|
+
expect(onChange).toHaveBeenCalledWith('date.range.1.week');
|
|
99
|
+
});
|
|
100
|
+
it('calls onCustomStartChange when the start date picker fires', async () => {
|
|
101
|
+
const onCustomStartChange = vi.fn();
|
|
102
|
+
renderFilter({ dateRange: 'date.range.custom', onCustomStartChange });
|
|
103
|
+
await openPopper(user);
|
|
104
|
+
await user.click(screen.getByTestId(`picker-${i18n.t('date.select.start')}`));
|
|
105
|
+
expect(onCustomStartChange).toHaveBeenCalledWith(dayjs('2025-06-01'));
|
|
106
|
+
});
|
|
107
|
+
it('calls onCustomEndChange when the end date picker fires', async () => {
|
|
108
|
+
const onCustomEndChange = vi.fn();
|
|
109
|
+
renderFilter({ dateRange: 'date.range.custom', onCustomEndChange });
|
|
110
|
+
await openPopper(user);
|
|
111
|
+
await user.click(screen.getByTestId(`picker-${i18n.t('date.select.end')}`));
|
|
112
|
+
expect(onCustomEndChange).toHaveBeenCalledWith(dayjs('2025-06-01'));
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { FilterList } from '@mui/icons-material';
|
|
3
|
+
import { ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
|
|
4
|
+
import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { CASE_STATUSES } from '../constants';
|
|
7
|
+
const CaseStatusFilter = ({ statusFilter, onChange }) => {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
return (_jsx(ChipPopper, { icon: _jsx(FilterList, { fontSize: "small" }), label: _jsx(Typography, { variant: "body2", children: statusFilter.length === 0
|
|
10
|
+
? t('route.cases.filter.status')
|
|
11
|
+
: statusFilter.map(s => t(`page.cases.status.${s}`)).join(', ') }), minWidth: "200px", slotProps: { chip: { size: 'small', color: statusFilter.length > 0 ? 'primary' : 'default' } }, children: _jsx(ToggleButtonGroup, { value: statusFilter, onChange: (_, nv) => onChange(nv), size: "small", orientation: "vertical", sx: { width: '100%' }, children: CASE_STATUSES.map(s => (_jsx(ToggleButton, { value: s, sx: { justifyContent: 'flex-start' }, children: t(`page.cases.status.${s}`) }, s))) }) }));
|
|
12
|
+
};
|
|
13
|
+
export default CaseStatusFilter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import userEvent, {} from '@testing-library/user-event';
|
|
5
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
6
|
+
import { I18nextProvider } from 'react-i18next';
|
|
7
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import CaseStatusFilter from './CaseStatusFilter';
|
|
9
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
const Wrapper = ({ children }) => _jsx(I18nextProvider, { i18n: i18n, children: children });
|
|
11
|
+
const renderFilter = (statusFilter, onChange = vi.fn()) => render(_jsx(CaseStatusFilter, { statusFilter: statusFilter, onChange: onChange }), { wrapper: Wrapper });
|
|
12
|
+
const openPopper = async (user) => {
|
|
13
|
+
const chip = screen.getByText(i18n.t('route.cases.filter.status')).closest('.MuiChip-root');
|
|
14
|
+
await user.click(chip);
|
|
15
|
+
await waitFor(() => {
|
|
16
|
+
expect(screen.getByRole('group')).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
describe('CaseStatusFilter', () => {
|
|
20
|
+
let user;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
user = userEvent.setup();
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
describe('label', () => {
|
|
26
|
+
it('shows the default "Status" label when no statuses are selected', () => {
|
|
27
|
+
renderFilter([]);
|
|
28
|
+
expect(screen.getByText(i18n.t('route.cases.filter.status'))).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
it('shows a comma-separated list of status labels when statuses are selected', () => {
|
|
31
|
+
renderFilter(['open', 'on-hold']);
|
|
32
|
+
const expected = [i18n.t('page.cases.status.open'), i18n.t('page.cases.status.on-hold')].join(', ');
|
|
33
|
+
expect(screen.getByText(expected)).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
it('shows a single status label when one status is selected', () => {
|
|
36
|
+
renderFilter(['resolved']);
|
|
37
|
+
expect(screen.getByText(i18n.t('page.cases.status.resolved'))).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('chip color', () => {
|
|
41
|
+
it('uses default color when no statuses are selected', () => {
|
|
42
|
+
renderFilter([]);
|
|
43
|
+
const chip = screen.getByText(i18n.t('route.cases.filter.status')).closest('.MuiChip-root');
|
|
44
|
+
expect(chip).not.toHaveClass('MuiChip-colorPrimary');
|
|
45
|
+
});
|
|
46
|
+
it('uses primary color when statuses are selected', () => {
|
|
47
|
+
renderFilter(['open']);
|
|
48
|
+
const chip = screen.getByText(i18n.t('page.cases.status.open')).closest('.MuiChip-root');
|
|
49
|
+
expect(chip).toHaveClass('MuiChip-colorPrimary');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('popper content', () => {
|
|
53
|
+
it('does not show toggle buttons before the chip is clicked', () => {
|
|
54
|
+
renderFilter([]);
|
|
55
|
+
expect(screen.queryByRole('group')).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
it('shows all four status toggle buttons when the chip is clicked', async () => {
|
|
58
|
+
renderFilter([]);
|
|
59
|
+
await openPopper(user);
|
|
60
|
+
expect(screen.getByText(i18n.t('page.cases.status.open'))).toBeInTheDocument();
|
|
61
|
+
expect(screen.getByText(i18n.t('page.cases.status.in-progress'))).toBeInTheDocument();
|
|
62
|
+
expect(screen.getByText(i18n.t('page.cases.status.on-hold'))).toBeInTheDocument();
|
|
63
|
+
expect(screen.getByText(i18n.t('page.cases.status.resolved'))).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('interactions', () => {
|
|
67
|
+
it('calls onChange when a toggle button is clicked', async () => {
|
|
68
|
+
const onChange = vi.fn();
|
|
69
|
+
renderFilter([], onChange);
|
|
70
|
+
await openPopper(user);
|
|
71
|
+
await user.click(screen.getByText(i18n.t('page.cases.status.open')));
|
|
72
|
+
expect(onChange).toHaveBeenCalledWith(['open']);
|
|
73
|
+
});
|
|
74
|
+
it('calls onChange with empty array when the only selected status is deselected', async () => {
|
|
75
|
+
const onChange = vi.fn();
|
|
76
|
+
renderFilter(['open'], onChange);
|
|
77
|
+
const chip = screen.getByText(i18n.t('page.cases.status.open')).closest('.MuiChip-root');
|
|
78
|
+
await user.click(chip);
|
|
79
|
+
await waitFor(() => {
|
|
80
|
+
expect(screen.getByRole('group')).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
await user.click(screen.getAllByText(i18n.t('page.cases.status.open'))[1]);
|
|
83
|
+
expect(onChange).toHaveBeenCalledWith([]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
13
13
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
14
14
|
import { useContextSelector } from 'use-context-selector';
|
|
15
15
|
import QueryResultText from '../../elements/display/QueryResultText';
|
|
16
|
-
import
|
|
16
|
+
import RecordQuery from '../hits/search/RecordQuery';
|
|
17
17
|
import LeadForm from './LeadForm';
|
|
18
18
|
import PivotForm from './PivotForm';
|
|
19
19
|
const DossierEditor = () => {
|
|
@@ -188,6 +188,6 @@ const DossierEditor = () => {
|
|
|
188
188
|
fontSize: '0.9em',
|
|
189
189
|
fontStyle: 'italic',
|
|
190
190
|
mb: 0.5
|
|
191
|
-
}), variant: "body2", children: t('hit.search.prompt') }), _jsx(
|
|
191
|
+
}), variant: "body2", children: t('hit.search.prompt') }), _jsx(RecordQuery, { disabled: !dossier || loading, onChange: (_val, isDirty) => setSearchDirty(isDirty), triggerSearch: query => setDossier(_dossier => ({ ..._dossier, query })) }), searchTotal >= 0 && _jsx(QueryResultText, { count: searchTotal, query: dossier.query })] }) }), _jsxs(Tabs, { value: tab, onChange: (_ev, value) => setTab(value), children: [_jsx(Tab, { label: t('route.dossiers.manager.tabs.leads'), value: "leads" }), _jsx(Tab, { label: t('route.dossiers.manager.tabs.pivots'), value: "pivots" })] }), tab === 'leads' && _jsx(LeadForm, { dossier: dossier, setDossier: setDossier, loading: loading }), tab === 'pivots' && _jsx(PivotForm, { dossier: dossier, setDossier: setDossier, loading: loading })] })] }) }));
|
|
192
192
|
};
|
|
193
193
|
export default memo(DossierEditor);
|
|
@@ -61,7 +61,7 @@ vi.mock('commons/components/pages/PageCenter', () => ({
|
|
|
61
61
|
vi.mock('../../elements/display/QueryResultText', () => ({
|
|
62
62
|
default: ({ count, query }) => (_jsxs("div", { id: "query-result-text", children: [count, " ", ' results for ', " ", query] }))
|
|
63
63
|
}));
|
|
64
|
-
vi.mock('../hits/search/
|
|
64
|
+
vi.mock('../hits/search/RecordQuery', () => ({
|
|
65
65
|
default: ({ onChange, triggerSearch, disabled }) => {
|
|
66
66
|
return (_jsxs("div", { id: "hit-query", children: [_jsx("input", { id: "query-input", disabled: disabled, onChange: e => {
|
|
67
67
|
onChange?.(e.target.value, false);
|
|
@@ -2,6 +2,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Topic } from '@mui/icons-material';
|
|
3
3
|
import { Typography } from '@mui/material';
|
|
4
4
|
import api from '@cccsaurora/howler-ui/api';
|
|
5
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
6
|
+
import SearchResponseProvider, { SearchResponseContext } from '@cccsaurora/howler-ui/components/app/providers/SearchResponseProvider';
|
|
5
7
|
import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
|
|
6
8
|
import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
|
|
7
9
|
import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
|
|
@@ -18,12 +20,13 @@ const DossiersBase = () => {
|
|
|
18
20
|
const navigate = useNavigate();
|
|
19
21
|
const { dispatchApi } = useMyApi();
|
|
20
22
|
const { showSuccessMessage } = useMySnackbar();
|
|
23
|
+
const { withConfirmDeleteModal } = useContext(ModalContext);
|
|
21
24
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
22
25
|
const { load } = useContext(TuiListMethodContext);
|
|
23
26
|
const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
|
|
27
|
+
const { response, request, remove, getSearchRequestData } = useContext(SearchResponseContext);
|
|
24
28
|
const [phrase, setPhrase] = useState('');
|
|
25
29
|
const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
|
|
26
|
-
const [response, setResponse] = useState(null);
|
|
27
30
|
const [hasError, setHasError] = useState(false);
|
|
28
31
|
const [loading, setLoading] = useState(false);
|
|
29
32
|
const onSearch = useCallback(async () => {
|
|
@@ -40,11 +43,11 @@ const DossiersBase = () => {
|
|
|
40
43
|
// Check for the actual search query
|
|
41
44
|
const query = phrase ? `*:*${phrase}*` : '*:*';
|
|
42
45
|
// Ensure the overview should be visible and/or matches the type we are filtering for
|
|
43
|
-
|
|
46
|
+
await request(api.search.dossier.post, {
|
|
44
47
|
query,
|
|
45
48
|
rows: pageCount,
|
|
46
49
|
offset
|
|
47
|
-
})
|
|
50
|
+
});
|
|
48
51
|
}
|
|
49
52
|
catch (e) {
|
|
50
53
|
setHasError(true);
|
|
@@ -52,7 +55,7 @@ const DossiersBase = () => {
|
|
|
52
55
|
finally {
|
|
53
56
|
setLoading(false);
|
|
54
57
|
}
|
|
55
|
-
}, [phrase, setSearchParams, searchParams,
|
|
58
|
+
}, [phrase, setSearchParams, searchParams, request, pageCount, offset]);
|
|
56
59
|
// Load the items into list when response changes.
|
|
57
60
|
// This hook should only trigger when the 'response' changes.
|
|
58
61
|
useEffect(() => {
|
|
@@ -68,24 +71,27 @@ const DossiersBase = () => {
|
|
|
68
71
|
}, [response, load]);
|
|
69
72
|
const onPageChange = useCallback((_offset) => {
|
|
70
73
|
if (_offset !== offset) {
|
|
71
|
-
|
|
74
|
+
const modifiedRequest = getSearchRequestData({ offset: _offset });
|
|
75
|
+
searchParams.set('offset', modifiedRequest.offset.toString());
|
|
72
76
|
setSearchParams(searchParams, { replace: true });
|
|
73
|
-
setOffset(
|
|
77
|
+
setOffset(modifiedRequest.offset);
|
|
74
78
|
}
|
|
75
|
-
}, [offset, searchParams, setSearchParams]);
|
|
76
|
-
const onDelete = useCallback(
|
|
79
|
+
}, [offset, searchParams, setSearchParams, getSearchRequestData]);
|
|
80
|
+
const onDelete = useCallback((e, id) => {
|
|
77
81
|
e.preventDefault();
|
|
78
82
|
e.stopPropagation();
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
withConfirmDeleteModal(async () => {
|
|
84
|
+
try {
|
|
85
|
+
await dispatchApi(api.dossier.del(id), { throwError: true, showError: true });
|
|
86
|
+
remove(id);
|
|
87
|
+
showSuccessMessage(t('route.dossiers.manager.delete.success'));
|
|
88
|
+
}
|
|
89
|
+
catch (_err) {
|
|
90
|
+
// eslint-disable-next-line no-console
|
|
91
|
+
console.warn(_err);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}, [dispatchApi, remove, withConfirmDeleteModal, showSuccessMessage, t]);
|
|
89
95
|
useEffect(() => {
|
|
90
96
|
onSearch();
|
|
91
97
|
if (!searchParams.has('offset')) {
|
|
@@ -111,6 +117,6 @@ const DossiersBase = () => {
|
|
|
111
117
|
return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: loading, aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.dossiers.search.prompt') }), renderer: ({ item }, classRenderer) => renderer(item.item, classRenderer()), response: response, onSelect: (item) => navigate(`/dossiers/${item.id}/edit`), onCreate: () => navigate('/dossiers/create'), createPrompt: "route.dossiers.create", searchPrompt: "route.dossiers.manager.search", createIcon: _jsx(Topic, { sx: { mr: 1 } }) }));
|
|
112
118
|
};
|
|
113
119
|
const Dossiers = () => {
|
|
114
|
-
return (_jsx(TuiListProvider, { children: _jsx(DossiersBase, {}) }));
|
|
120
|
+
return (_jsx(TuiListProvider, { children: _jsx(SearchResponseProvider, { idField: "dossier_id", children: _jsx(DossiersBase, {}) }) }));
|
|
115
121
|
};
|
|
116
122
|
export default Dossiers;
|
|
@@ -25,7 +25,7 @@ const ActionIntroductionDocumentation = () => {
|
|
|
25
25
|
useEffect(() => {
|
|
26
26
|
api.action.operations
|
|
27
27
|
.get()
|
|
28
|
-
.then(_operations => _operations.filter(a => difference(a.roles, user.roles).length <
|
|
28
|
+
.then(_operations => _operations.filter(a => difference(a.roles, user.roles).length < a.roles.length))
|
|
29
29
|
.then(setOperations)
|
|
30
30
|
// eslint-disable-next-line no-console
|
|
31
31
|
.catch(console.debug);
|
|
@@ -49,7 +49,7 @@ const ApiDocumentation = () => {
|
|
|
49
49
|
.replace(/(\S+)\s+=>\s+(.+)/g, '\n`$1`: $2\n')
|
|
50
50
|
.replace(/(Data Block:\n)([\s\S]+)(Result Example:)/, (__, p1, p2, p3) => `${p1}\`\`\`\n${p2.trim()}\n\`\`\`\n${p3}`)
|
|
51
51
|
.replace(/(Result Example:\n)([\s\S]+)$/, (__, p1, p2) => `${p1}\`\`\`\n${p2.trim()}\n\`\`\``) }));
|
|
52
|
-
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { style: { marginBottom: '1rem' }, sx: [isLg && { '& > td': { borderBottom: 0 } }], children: [_jsx(TableCell, { children: _jsxs(Stack, { direction: "column", spacing: 1, alignItems: "start", children: [_jsx(Typography, { children: endpoint.name }), _jsx("code", { children: endpoint.path }), _jsxs(Stack, { direction: "row", spacing: 1, children: [endpoint.complete ? (_jsx(Chip, {
|
|
52
|
+
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { style: { marginBottom: '1rem' }, sx: [isLg && { '& > td': { borderBottom: 0 } }], children: [_jsx(TableCell, { children: _jsxs(Stack, { direction: "column", spacing: 1, alignItems: "start", children: [_jsx(Typography, { children: endpoint.name }), _jsx("code", { children: endpoint.path }), _jsxs(Stack, { direction: "row", spacing: 1, children: [endpoint.complete ? (_jsx(Chip, { label: "Stable", color: "success" })) : (_jsx(Chip, { label: "Unstable", color: "error" })), endpoint.protected ? (_jsx(Chip, { label: "Protected", color: "warning" })) : (_jsx(Chip, { label: "Unprotected" }))] }), _jsx(Stack, { spacing: 1, direction: "row", children: endpoint.methods.map(m => (_jsx(Chip, { size: "small", label: m }, m))) }), endpoint.ui_only && _jsx(Chip, { label: "UI Only" })] }) }), _jsx(TableCell, { children: _jsx(Stack, { spacing: 1, direction: "row", children: endpoint.required_type.map(type => (_jsx(Chip, { size: "small", label: type, color: user.roles?.includes(type) ? 'success' : 'default' }, type))) }) }), _jsx(TableCell, { children: _jsx(Stack, { spacing: 1, direction: "row", children: endpoint.required_priv.map((p) => (_jsx(Chip, { size: "small", label: t(APIKEY_LABELS[p]) }, p))) }) }), !isLg && _jsx(TableCell, { children: documentationCell })] }), isLg && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 3, sx: { '& pre': { whiteSpace: 'pre-wrap' } }, children: documentationCell }) }))] }, endpoint.id));
|
|
53
53
|
}) })] }) })] }) }));
|
|
54
54
|
};
|
|
55
55
|
export default ApiDocumentation;
|
|
@@ -5,7 +5,6 @@ import { useScrollRestoration } from '@cccsaurora/howler-ui/components/hooks/use
|
|
|
5
5
|
import { useCallback, useState } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { useSearchParams } from 'react-router-dom';
|
|
8
|
-
import BundleDocumentation from './BundleDocumentation';
|
|
9
8
|
import HelpTabs from './components/HelpTabs';
|
|
10
9
|
import HitBannerDocumentation from './HitBannerDocumentation';
|
|
11
10
|
import HitLabelsDocumentation from './HitLabelsDocumentation';
|
|
@@ -23,8 +22,7 @@ const HitDocumentation = () => {
|
|
|
23
22
|
searchParams.set('tab', _tab);
|
|
24
23
|
setSearchParams(new URLSearchParams(searchParams));
|
|
25
24
|
}, [searchParams, setSearchParams]);
|
|
26
|
-
return (_jsx(PageCenter, { margin: 4, width: "100%", maxWidth: "1750px", textAlign: "left", children: _jsxs(Stack, { sx: { flexDirection: useHorizontal ? 'column' : 'row', '& h1': { mt: 0 } }, children: [_jsxs(HelpTabs, { value: tab, children: [_jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.schema.title') }), value: "schema", onClick: () => onChange('schema') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.banner.title') }), value: "header", onClick: () => onChange('header') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.
|
|
27
|
-
bundle: () => _jsx(BundleDocumentation, {}),
|
|
25
|
+
return (_jsx(PageCenter, { margin: 4, width: "100%", maxWidth: "1750px", textAlign: "left", children: _jsxs(Stack, { sx: { flexDirection: useHorizontal ? 'column' : 'row', '& h1': { mt: 0 } }, children: [_jsxs(HelpTabs, { value: tab, children: [_jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.schema.title') }), value: "schema", onClick: () => onChange('schema') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.banner.title') }), value: "header", onClick: () => onChange('header') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.links.title') }), value: "links", onClick: () => onChange('links') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.labels.title') }), value: "labels", onClick: () => onChange('labels') })] }), _jsx(Box, { children: {
|
|
28
26
|
header: () => _jsx(HitBannerDocumentation, {}),
|
|
29
27
|
links: () => _jsx(HitLinksDocumentation, {}),
|
|
30
28
|
labels: () => _jsx(HitLabelsDocumentation, {}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export default "# Howler Client Documentation\n\nThis documentation will outline how to interact with the howler API using the howler client in both Java and python development environments. We will outline the basic process of creating a new hit in each environment as well as searching howler for hits matching your query.\n\n## Getting started\n\n### Installation\n\nIn order to use the howler client, you need to list it as a dependency in your project.\n\n#### **Python**\n\nSimply install through pip:\n\n```bash\npip install howler-client\n```\n\nYou can also add it to your requirements.txt, or whatever dependency management system you use.\n\n### Authentication\n\nAs outlined in the [Authentication Documentation](/help/auth), there's a number of ways users can choose to authenticate. In order to interface with the howler client, however, the suggested flow is to use an API key. So before we start, let's generate a key.\n\n1. Open the Howler UI you'd like to interface with.\n2. Log in, then click your profile in the top right.\n3. Under user menu, click Settings.\n4. Under User Security, press the (+) icon on the API Keys row.\n5. Name your key, and give it the requisite permissions.\n6. Press Create, and copy the supplied string somewhere safe. **You will not see this string again.**\n\nThis API Key will be supplied to your code later on.\n\n## Python Client\n\nIn order to connect with howler using the python client, there is a fairly simple process to follow:\n\n```python\nfrom howler_client import get_client\n\nUSERNAME = 'user' # Obtain this from the user settings page of the Howler UI\nAPIKEY = 'apikey_name:apikey_data'\n\napikey = (USERNAME, APIKEY)\n\nhowler = get_client(\"$CURRENT_URL\", apikey=apikey)\n```\n\n```alert\nYou can skip generating an API Key and providing it if you're executing this code within HOGWARTS (i.e., on jupyterhub or airflow). OBO will handle authentication for you!\n```\n\nThat's it! You can now use the `howler` object to interact with the server. So what does that actually look like?\n\n### Creating hits in Python\n\nFor the python client, you can create hits using either the `howler.hit.create` or `howler.hit.create_from_map` functions.\n\n#### `create`\n\nThis function takes in a single argument - either a single hit, or a list of them, conforming to the [Howler Schema](/help/hit?tab=schema). Here is a simple example:\n\n```python\n# Some bogus data in the Howler Schema format\nexample_hit = {\n \"howler\": {\n \"analytic\": \"example\",\n \"score\": 10.0\n },\n \"event\": {\n \"reason\": \"Example hit\"\n }\n}\n\nhowler.hit.create(example_hit)\n```\n\nYou can also ingest data in a flat format:\n\n```python\nexample_hit = {\n \"howler.analytic\": \"example\",\n \"howler.score\": 10.0,\n \"event.reason\": \"Example hit\"\n}\n\nhowler.hit.create(example_hit)\n```\n\n#### `create_from_map`\n\nThis function takes in three arguments:\n\n- `tool name`: The name of the analytic creating the hit\n- `map`: A mapping between the raw data you have and the howler schema\n - The format is a dictionary where the keys are the flattened path of the raw data, and the values are a list of flattened paths for Howler's fields where the data will be copied into.\n- `documents`: The raw data you want to add to howler\n\nHere is a simple example:\n\n```python\n# The mapping from our data to howler's schema\nhwl_map = {\n \"file.sha256\": [\"file.hash.sha256\", \"howler.hash\"],\n \"file.name\": [\"file.name\"],\n \"src_ip\": [\"source.ip\", \"related.ip\"],\n \"dest_ip\": [\"destination.ip\", \"related.ip\"],\n \"time.created\": [\"event.start\"],\n}\n\n# Some bogus data in a custom format we want to add to howler\nexample_hit = {\n \"src_ip\": \"0.0.0.0\",\n \"dest_ip\": \"8.8.8.8\",\n \"file\": {\n \"name\": \"hello.exe\",\n \"sha256\": sha256(str(\"hello.exe\").encode()).hexdigest()\n },\n \"time\": {\n \"created\": datetime.now().isoformat()\n },\n}\n\n# Note that the third argument is of type list!\nhowler.hit.create_from_map(\"example_ingestor\", hwl_map, [example_hit])\n```\n\n### Querying Hits\n\nQuerying hits using the howler python client is done using the `howler.search.hit` function. It has a number of required and optional arguments:\n\n- Required:\n - `query`: lucene query (string)\n- Optional:\n - `filters`: Additional lucene queries used to filter the data (list of strings)\n - `fl`: List of fields to return (comma separated string of fields)\n - `offset`: Offset at which the query items should start (integer)\n - `rows`: Number of records to return (integer)\n - `sort`: Field used for sorting with direction (string: ex. 'id desc')\n - `timeout`: Max amount of milliseconds the query will run (integer)\n - `
|
|
1
|
+
export default "# Howler Client Documentation\n\nThis documentation will outline how to interact with the howler API using the howler client in both Java and python development environments. We will outline the basic process of creating a new hit in each environment as well as searching howler for hits matching your query.\n\n## Getting started\n\n### Installation\n\nIn order to use the howler client, you need to list it as a dependency in your project.\n\n#### **Python**\n\nSimply install through pip:\n\n```bash\npip install howler-client\n```\n\nYou can also add it to your requirements.txt, or whatever dependency management system you use.\n\n### Authentication\n\nAs outlined in the [Authentication Documentation](/help/auth), there's a number of ways users can choose to authenticate. In order to interface with the howler client, however, the suggested flow is to use an API key. So before we start, let's generate a key.\n\n1. Open the Howler UI you'd like to interface with.\n2. Log in, then click your profile in the top right.\n3. Under user menu, click Settings.\n4. Under User Security, press the (+) icon on the API Keys row.\n5. Name your key, and give it the requisite permissions.\n6. Press Create, and copy the supplied string somewhere safe. **You will not see this string again.**\n\nThis API Key will be supplied to your code later on.\n\n## Python Client\n\nIn order to connect with howler using the python client, there is a fairly simple process to follow:\n\n```python\nfrom howler_client import get_client\n\nUSERNAME = 'user' # Obtain this from the user settings page of the Howler UI\nAPIKEY = 'apikey_name:apikey_data'\n\napikey = (USERNAME, APIKEY)\n\nhowler = get_client(\"$CURRENT_URL\", apikey=apikey)\n```\n\n```alert\nYou can skip generating an API Key and providing it if you're executing this code within HOGWARTS (i.e., on jupyterhub or airflow). OBO will handle authentication for you!\n```\n\nThat's it! You can now use the `howler` object to interact with the server. So what does that actually look like?\n\n### Creating hits in Python\n\nFor the python client, you can create hits using either the `howler.hit.create` or `howler.hit.create_from_map` functions.\n\n#### `create`\n\nThis function takes in a single argument - either a single hit, or a list of them, conforming to the [Howler Schema](/help/hit?tab=schema). Here is a simple example:\n\n```python\n# Some bogus data in the Howler Schema format\nexample_hit = {\n \"howler\": {\n \"analytic\": \"example\",\n \"score\": 10.0\n },\n \"event\": {\n \"reason\": \"Example hit\"\n }\n}\n\nhowler.hit.create(example_hit)\n```\n\nYou can also ingest data in a flat format:\n\n```python\nexample_hit = {\n \"howler.analytic\": \"example\",\n \"howler.score\": 10.0,\n \"event.reason\": \"Example hit\"\n}\n\nhowler.hit.create(example_hit)\n```\n\n#### `create_from_map`\n\nThis function takes in three arguments:\n\n- `tool name`: The name of the analytic creating the hit\n- `map`: A mapping between the raw data you have and the howler schema\n - The format is a dictionary where the keys are the flattened path of the raw data, and the values are a list of flattened paths for Howler's fields where the data will be copied into.\n- `documents`: The raw data you want to add to howler\n\nHere is a simple example:\n\n```python\n# The mapping from our data to howler's schema\nhwl_map = {\n \"file.sha256\": [\"file.hash.sha256\", \"howler.hash\"],\n \"file.name\": [\"file.name\"],\n \"src_ip\": [\"source.ip\", \"related.ip\"],\n \"dest_ip\": [\"destination.ip\", \"related.ip\"],\n \"time.created\": [\"event.start\"],\n}\n\n# Some bogus data in a custom format we want to add to howler\nexample_hit = {\n \"src_ip\": \"0.0.0.0\",\n \"dest_ip\": \"8.8.8.8\",\n \"file\": {\n \"name\": \"hello.exe\",\n \"sha256\": sha256(str(\"hello.exe\").encode()).hexdigest()\n },\n \"time\": {\n \"created\": datetime.now().isoformat()\n },\n}\n\n# Note that the third argument is of type list!\nhowler.hit.create_from_map(\"example_ingestor\", hwl_map, [example_hit])\n```\n\n### Querying Hits\n\nQuerying hits using the howler python client is done using the `howler.search.hit` function. It has a number of required and optional arguments:\n\n- Required:\n - `query`: lucene query (string)\n- Optional:\n - `filters`: Additional lucene queries used to filter the data (list of strings)\n - `fl`: List of fields to return (comma separated string of fields)\n - `offset`: Offset at which the query items should start (integer)\n - `rows`: Number of records to return (integer)\n - `sort`: Field used for sorting with direction (string: ex. 'id desc')\n - `timeout`: Max amount of milliseconds the query will run (integer)\n - `track_total_hits`: Number of hits to track (default: 10k)\n\nHere are some example queries:\n\n```python\n# Search for all hits created by assemblyline, show the first 50, and return only their ids\nhowler.search.hit(\"howler.analytic:assemblyline\", fl=\"howler.id\", rows=50)\n\n# Search for all resolved hits created in the last five days, returning their id and the analytic that created them. Show only ten, offset by 40\nhowler.search.hit(\"howler.status:resolved\", filters=['event.created:[now-5d TO now]'] fl=\"howler.id,howler.analytic\", rows=10, offset=40)\n\n# Search for all hits, timeout if the query takes more than 100ms\nhowler.search.hit(\"howler.id:*\", track_total_hits=100000000, timeout=100)\n```\n\n### Updating Hits\n\nIn order to update hits, there are a number of supported functions:\n\n- `howler.hit.update(...)`\n- `howler.hit.update_by_query(...)`\n- `howler.hit.overwrite(...)`\n\n#### `update()`\n\nIf you want to update a hit in a transactional way, you can use the following code:\n\n```python\nhit_to_update = client.search.hit(\"howler.id:*\", rows=1, sort=\"event.created desc\")[\"items\"][0]\n\nresult = client.hit.update(hit_to_update[\"howler\"][\"id\"], [(UPDATE_SET, \"howler.score\", hit_to_update[\"howler\"][\"score\"] + 100)])\n```\n\nThe following operations can be run to update a hit.\n\n**List Operations:**\n\n- `UPDATE_APPEND`: Used to append a value to a given list\n- `UPDATE_APPEND_IF_MISSING`: Used to append a value to a given list if the value isn't already in the list\n- `UPDATE_REMOVE`: Will remove a given value from a list\n\n**Numeric Operations:**\n\n- `UPDATE_DEC`: Decrement a numeric value by the specified amount\n- `UPDATE_INC`: Increment a numeric value by the specified amount\n- `UPDATE_MAX`: Will set a numeric value to the maximum of the existing value and the specified value\n- `UPDATE_MIN`: Will set a numeric value to the minimum of the existing value and the specified value\n\n**Multipurpose Operations:**\n\n- `UPDATE_SET`: Set a field's value to the given value\n- `UPDATE_DELETE`: Will delete a given field's value\n\n#### `update_by_query()`\n\nThis function allows you to update a large number of hits by a query:\n\n```python\nclient.hit.update_by_query(f'howler.analytic:\"Example Alert\"', [(UPDATE_INC, \"howler.score\", 100)])\n```\n\nThe same operations as in `update()` can be used.\n\n### `overwrite()`\n\nThis function allows you to directly overwrite a hit with a partial hit object. This is the most easy to use, but loses some of the validation and additional processing of the update functions.\n\n```python\nhit_to_update = client.search.hit(\"howler.id:*\", rows=1, sort=\"event.created desc\")[\"items\"][0]\n\nresult = client.hit.overwrite(hit_to_update[\"howler\"][\"id\"], {\"source.ip\": \"127.0.0.1\", \"destination.ip\": \"8.8.8.8\"})\n```\n"
|