@cccsaurora/howler-ui 3.1.0-dev.1445 → 3.1.0-dev.1463

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.
@@ -16,7 +16,7 @@ const Modal = () => {
16
16
  left: '50%',
17
17
  maxWidth: options.maxWidth || '1200px',
18
18
  maxHeight: options.maxHeight || '400px',
19
- height: has(options, 'height') ? options.height : '100%',
19
+ height: has(options, 'height') ? options.height : 'auto',
20
20
  transform: 'translate(-50%, -50%)',
21
21
  backgroundColor: 'background.paper',
22
22
  borderRadius: theme.shape.borderRadius,
@@ -1,6 +1,6 @@
1
- import type { FC } from 'react';
1
+ import type { FC, MouseEventHandler } from 'react';
2
2
  declare const ConfirmDeleteModal: FC<{
3
- onConfirm: () => void;
3
+ onConfirm: MouseEventHandler<HTMLButtonElement> | (() => void);
4
4
  title?: string;
5
5
  description?: string;
6
6
  preferDelete?: boolean;
@@ -6,8 +6,8 @@ import { useTranslation } from 'react-i18next';
6
6
  const ConfirmDeleteModal = ({ onConfirm, title, description, preferDelete, preferCancel }) => {
7
7
  const { t } = useTranslation();
8
8
  const { close } = useContext(ModalContext);
9
- const handleConfirm = useCallback(() => {
10
- onConfirm();
9
+ const handleConfirm = useCallback((e) => {
10
+ onConfirm(e);
11
11
  close();
12
12
  }, [close, onConfirm]);
13
13
  const modalTitle = title ?? t('modal.confirm.delete.title');
@@ -1,8 +1,9 @@
1
1
  import type { Overview } from '@cccsaurora/howler-ui/models/entities/generated/Overview';
2
- import type { FC } from 'react';
2
+ import { type FC } from 'react';
3
3
  declare const OverviewCard: FC<{
4
4
  overview: Overview;
5
5
  className?: string;
6
- onDelete?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>, id: string) => void;
6
+ error?: boolean;
7
+ onRemove?: (id: string) => Promise<void>;
7
8
  }>;
8
9
  export default OverviewCard;
@@ -1,16 +1,27 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Delete } from '@mui/icons-material';
3
- import { Card, IconButton, Stack, Tooltip, Typography } from '@mui/material';
2
+ import { Delete, ReportProblem } from '@mui/icons-material';
3
+ import { Button, Card, IconButton, Stack, Tooltip, Typography } from '@mui/material';
4
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
4
5
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
5
6
  import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
7
+ import ConfirmDeleteModal from '@cccsaurora/howler-ui/components/elements/display/modals/ConfirmDeleteModal';
8
+ import { useCallback, useContext } from 'react';
6
9
  import { useTranslation } from 'react-i18next';
7
- const OverviewCard = ({ overview, className, onDelete }) => {
10
+ const OverviewCard = ({ overview, error, className, onRemove }) => {
8
11
  const { t } = useTranslation();
12
+ const { showModal, withConfirmDeleteModal } = useContext(ModalContext);
13
+ const onDelete = useCallback((e, id) => {
14
+ e.preventDefault();
15
+ e.stopPropagation();
16
+ withConfirmDeleteModal(async () => {
17
+ await onRemove?.(id);
18
+ });
19
+ }, [onRemove, withConfirmDeleteModal]);
9
20
  return (_jsx(Card, { variant: "outlined", sx: { p: 1, mb: 1 }, className: className, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsxs(Stack, { children: [_jsxs(Typography, { variant: "body1", children: [t(overview.analytic), " - ", t(overview.detection ?? 'all')] }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: _jsx("code", { children: _jsx("pre", { children: overview.content
10
21
  .split('\n')
11
22
  .filter(line => !!line)
12
23
  .slice(0, 3)
13
24
  .map(content => content.replace(/(.{,64}).+/, '$1'))
14
- .join('\n') }) }) })] }), _jsx(FlexOne, {}), _jsx(HowlerAvatar, { sx: { height: '24px', width: '24px' }, userId: overview.owner }), onDelete && (_jsx(Tooltip, { title: t('route.overviews.manager.delete'), children: _jsx(IconButton, { onClick: e => onDelete(e, overview.overview_id), children: _jsx(Delete, {}) }) }))] }) }, overview.overview_id));
25
+ .join('\n') }) }) })] }), _jsx(FlexOne, {}), _jsx(HowlerAvatar, { sx: { height: '24px', width: '24px' }, userId: overview.owner }), onRemove && (_jsx(Tooltip, { title: t('route.overviews.manager.delete'), children: _jsx(IconButton, { onClick: e => onDelete?.(e, overview.overview_id), children: _jsx(Delete, {}) }) })), error && (_jsx(Stack, { direction: "row", justifyContent: "end", children: _jsx(Stack, { children: _jsx(Tooltip, { title: t('error.invalid_detection.action'), children: _jsx(Button, { startIcon: _jsx(ReportProblem, {}), color: "warning", onClick: () => showModal(_jsx(ConfirmDeleteModal, { onConfirm: () => onRemove?.(overview.overview_id), title: t('route.overviews.manager.error.modal.title'), description: t('route.overviews.manager.error.modal.description'), preferDelete: true })), children: t('error.invalid_detection.message') }) }) }) }))] }) }, overview.overview_id));
15
26
  };
16
27
  export default OverviewCard;
@@ -1,7 +1,9 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /// <reference types="vitest" />
3
3
  import { render, screen } from '@testing-library/react';
4
4
  import userEvent, {} from '@testing-library/user-event';
5
+ import ModalProvider from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
6
+ import Modal from '@cccsaurora/howler-ui/components/elements/display/Modal';
5
7
  import i18n from '@cccsaurora/howler-ui/i18n';
6
8
  import { I18nextProvider } from 'react-i18next';
7
9
  import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -12,7 +14,7 @@ vi.mock('components/elements/display/HowlerAvatar', () => ({
12
14
  vi.mock('components/elements/addons/layout/FlexOne', () => ({
13
15
  default: () => _jsx("div", { id: "flex-one" })
14
16
  }));
15
- const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
17
+ const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: _jsxs(ModalProvider, { children: [children, _jsx(Modal, {})] }) }));
16
18
  describe('OverviewCard', () => {
17
19
  let user;
18
20
  beforeEach(() => {
@@ -87,10 +89,12 @@ describe('OverviewCard', () => {
87
89
  content: 'content',
88
90
  owner: 'testuser'
89
91
  };
90
- render(_jsx(OverviewCard, { overview: overview, onDelete: mockOnDelete }), { wrapper: Wrapper });
92
+ render(_jsx(OverviewCard, { overview: overview, onRemove: mockOnDelete }), { wrapper: Wrapper });
91
93
  const deleteButton = document.querySelector('[data-testid="DeleteIcon"]').closest('button');
92
94
  await user.click(deleteButton);
93
- expect(mockOnDelete).toHaveBeenCalledWith(expect.anything(), 'ov-1');
95
+ const confirmButton = screen.getByRole('button', { name: /delete/i });
96
+ await user.click(confirmButton);
97
+ expect(mockOnDelete).toHaveBeenCalledWith('ov-1');
94
98
  });
95
99
  it('should apply custom className', () => {
96
100
  const overview = {
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Article } 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';
5
+ import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
6
6
  import SearchResponseProvider, { SearchResponseContext } from '@cccsaurora/howler-ui/components/app/providers/SearchResponseProvider';
7
7
  import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
8
8
  import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
@@ -20,7 +20,6 @@ const OverviewsBase = () => {
20
20
  const navigate = useNavigate();
21
21
  const { dispatchApi } = useMyApi();
22
22
  const { showSuccessMessage } = useMySnackbar();
23
- const { withConfirmDeleteModal } = useContext(ModalContext);
24
23
  const [searchParams, setSearchParams] = useSearchParams();
25
24
  const { load } = useContext(TuiListMethodContext);
26
25
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
@@ -28,6 +27,7 @@ const OverviewsBase = () => {
28
27
  const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
29
28
  const [hasError, setHasError] = useState(false);
30
29
  const [loading, setLoading] = useState(false);
30
+ const { analytics } = useContext(AnalyticContext);
31
31
  const { response, request, remove, getSearchRequestData } = useContext(SearchResponseContext);
32
32
  const onSearch = useCallback(async () => {
33
33
  try {
@@ -58,17 +58,22 @@ const OverviewsBase = () => {
58
58
  }, [phrase, setSearchParams, searchParams, request, pageCount, offset]);
59
59
  // Load the items into list when response changes.
60
60
  // This hook should only trigger when the 'response' changes.
61
+ // or if the analytic list changes to refresh the disabled state
61
62
  useEffect(() => {
62
63
  if (response) {
63
64
  load(response.items.map((item) => ({
64
65
  id: item.overview_id,
65
66
  item,
66
67
  selected: false,
67
- cursor: false
68
+ cursor: false,
69
+ disabled: item.detection &&
70
+ !analytics
71
+ .find(v => v.name === item.analytic)
72
+ ?.detections?.map((s) => s.toLowerCase())
73
+ ?.includes(item.detection?.toLowerCase())
68
74
  })));
69
75
  }
70
- // eslint-disable-next-line react-hooks/exhaustive-deps
71
- }, [response, load]);
76
+ }, [response, load, analytics]);
72
77
  const onPageChange = useCallback((_offset) => {
73
78
  if (_offset !== offset) {
74
79
  const modifiedRequest = getSearchRequestData({ offset: _offset });
@@ -77,21 +82,17 @@ const OverviewsBase = () => {
77
82
  setOffset(modifiedRequest.offset);
78
83
  }
79
84
  }, [offset, searchParams, setSearchParams, getSearchRequestData]);
80
- const onDelete = useCallback((e, id) => {
81
- e.preventDefault();
82
- e.stopPropagation();
83
- withConfirmDeleteModal(async () => {
84
- try {
85
- await dispatchApi(api.overview.del(id), { throwError: true, showError: true });
86
- remove(id);
87
- showSuccessMessage(t('route.overviews.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]);
85
+ const removeOverview = useCallback(async (overviewId) => {
86
+ try {
87
+ await dispatchApi(api.overview.del(overviewId), { throwError: true, showError: true });
88
+ remove(overviewId);
89
+ showSuccessMessage(t('route.overviews.manager.delete.success'));
90
+ }
91
+ catch (_err) {
92
+ // eslint-disable-next-line no-console
93
+ console.warn(_err);
94
+ }
95
+ }, [dispatchApi, remove, showSuccessMessage, t]);
95
96
  useEffect(() => {
96
97
  void onSearch();
97
98
  if (!searchParams.has('offset')) {
@@ -113,8 +114,8 @@ const OverviewsBase = () => {
113
114
  }
114
115
  // eslint-disable-next-line react-hooks/exhaustive-deps
115
116
  }, [offset]);
116
- const renderer = useCallback((item, className) => _jsx(OverviewCard, { overview: item, className: className, onDelete: onDelete }), [onDelete]);
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.overviews.search.prompt') }), renderer: ({ item }, classRenderer) => renderer(item.item, classRenderer()), response: response, onSelect: (item) => navigate(`/overviews/view?analytic=${item.item.analytic}${item.item.detection ? '&detection=' + item.item.detection : ''}`), onCreate: () => navigate('/overviews/view'), createPrompt: "route.overviews.create", searchPrompt: "route.overviews.manager.search", createIcon: _jsx(Article, { sx: { mr: 1 } }) }));
117
+ const renderer = useCallback((item, error, className) => (_jsx(OverviewCard, { overview: item, error: error, className: className, onRemove: removeOverview })), [removeOverview]);
118
+ 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.overviews.search.prompt') }), renderer: ({ item }, classRenderer) => renderer(item.item, !!item.disabled, classRenderer()), response: response, onSelect: (item) => navigate(`/overviews/view?analytic=${item.item.analytic}${item.item.detection ? '&detection=' + item.item.detection : ''}`), onCreate: () => navigate('/overviews/view'), createPrompt: "route.overviews.create", searchPrompt: "route.overviews.manager.search", createIcon: _jsx(Article, { sx: { mr: 1 } }) }));
118
119
  };
119
120
  const Overviews = () => {
120
121
  return (_jsx(TuiListProvider, { children: _jsx(SearchResponseProvider, { idField: "overview_id", children: _jsx(OverviewsBase, {}) }) }));
@@ -12,6 +12,6 @@ const TemplateCard = ({ template, onRemove, error, className }) => {
12
12
  readonly: _jsx(Lock, {}),
13
13
  global: _jsx(Language, {}),
14
14
  personal: _jsx(Person, {})
15
- }[template.type] }), _jsx(Divider, { orientation: "vertical", flexItem: true }), _jsxs(Stack, { children: [_jsxs(Typography, { variant: "body1", children: [t(template.analytic), " - ", t(template.detection ?? 'all')] }), template.keys.map(key => (_jsx(Typography, { variant: "caption", sx: { ml: 1 }, children: _jsx("code", { children: key }) }, template.template_id + key)))] }), error && (_jsx(Stack, { direction: "row", justifyContent: "end", width: "100%", children: _jsx(Stack, { children: _jsx(Tooltip, { title: t('route.templates.manager.error.action'), children: _jsx(Button, { startIcon: _jsx(ReportProblem, {}), color: "warning", onClick: () => showModal(_jsx(ConfirmDeleteModal, { onConfirm: () => onRemove?.(template.template_id), title: t('route.templates.manager.error.modal.title'), description: t('route.templates.manager.error.modal.description'), preferDelete: true })), children: t('route.templates.manager.error.message') }) }) }) }))] }) }, template.template_id));
15
+ }[template.type] }), _jsx(Divider, { orientation: "vertical", flexItem: true }), _jsxs(Stack, { children: [_jsxs(Typography, { variant: "body1", children: [t(template.analytic), " - ", t(template.detection ?? 'all')] }), template.keys.map(key => (_jsx(Typography, { variant: "caption", sx: { ml: 1 }, children: _jsx("code", { children: key }) }, template.template_id + key)))] }), error && (_jsx(Stack, { direction: "row", justifyContent: "end", width: "100%", children: _jsx(Stack, { children: _jsx(Tooltip, { title: t('error.invalid_detection.action'), children: _jsx(Button, { startIcon: _jsx(ReportProblem, {}), color: "warning", onClick: () => showModal(_jsx(ConfirmDeleteModal, { onConfirm: () => onRemove?.(template.template_id), title: t('route.templates.manager.error.modal.title'), description: t('route.templates.manager.error.modal.description'), preferDelete: true })), children: t('error.invalid_detection.message') }) }) }) }))] }) }, template.template_id));
16
16
  };
17
17
  export default TemplateCard;
@@ -112,6 +112,8 @@
112
112
  "duplicates.omitted": "Some duplicate entries have been omitted.",
113
113
  "edit": "Edit",
114
114
  "enabled": "Enabled",
115
+ "error.invalid_detection.action": "Click to open quick fix options",
116
+ "error.invalid_detection.message": "Invalid Detection",
115
117
  "event.module": "Event Module",
116
118
  "event.open": "Open Event",
117
119
  "event.type": "Event Type",
@@ -806,6 +808,8 @@
806
808
  "route.overviews.detection": "Choose Detection",
807
809
  "route.overviews.manager.delete": "Delete Overview",
808
810
  "route.overviews.manager.delete.success": "Overview removed.",
811
+ "route.overviews.manager.error.modal.description": "The overview fields are read only and will not be used. Do you want to remove the template?",
812
+ "route.overviews.manager.error.modal.title": "Overview detection no longer exists",
809
813
  "route.overviews.manager.search": "Search Overviews",
810
814
  "route.overviews.prompt": "Activate autocomplete using [ctrl + space].",
811
815
  "route.overviews.search.prompt": "Search by title, content, analytic or detection.",
@@ -824,8 +828,6 @@
824
828
  "route.templates.detection": "Choose Detection",
825
829
  "route.templates.global": "Global",
826
830
  "route.templates.manager.delete.success": "Template removed.",
827
- "route.templates.manager.error.action": "Click to open quick fix options",
828
- "route.templates.manager.error.message": "Invalid Detection",
829
831
  "route.templates.manager.error.modal.description": "The template fields are read only and will not be used. Do you want to remove the template?",
830
832
  "route.templates.manager.error.modal.title": "Template detection no longer exists",
831
833
  "route.templates.manager.global": "Global",
@@ -112,6 +112,8 @@
112
112
  "duplicates.omitted": "Certains doublons ont été omis.",
113
113
  "edit": "Modifier",
114
114
  "enabled": "Activé",
115
+ "error.invalid_detection.action": "Cliquez ici pour afficher les options de correction rapide",
116
+ "error.invalid_detection.message": "Détection non valide",
115
117
  "event.module": "Module d'événement",
116
118
  "event.open": "Ouvrir événement",
117
119
  "event.type": "Type d'événement",
@@ -806,6 +808,8 @@
806
808
  "route.overviews.detection": "Choisir une détection",
807
809
  "route.overviews.manager.delete": "Supprimer la vue d'ensemble",
808
810
  "route.overviews.manager.delete.success": "Vue d'ensemble supprimée.",
811
+ "route.overviews.manager.error.modal.description": "Les clés de la vue d'ensemble sont en lecture seule et ne seront pas utilisées. Voulez-vous supprimer cette vue d'ensemble ?",
812
+ "route.overviews.manager.error.modal.title": "La détection de la vue d'ensemble n'existe plus",
809
813
  "route.overviews.manager.search": "Rechercher les vues d'ensemble",
810
814
  "route.overviews.prompt": "Activer l'autocomplétion en utilisant [ctrl + espace].",
811
815
  "route.overviews.search.prompt": "Recherche par titre, contenu, clé d'analyse ou de détection.",
@@ -824,9 +828,7 @@
824
828
  "route.templates.detection": "Choisir une détection",
825
829
  "route.templates.global": "Global",
826
830
  "route.templates.manager.delete.success": "Modèle supprimé.",
827
- "route.templates.manager.error.action": "Cliquez ici pour afficher les options de correction rapide",
828
- "route.templates.manager.error.message": "Détection non valide",
829
- "route.templates.manager.error.modal.description": "Les clés du modèle sont en lecture seule et ne seront pas utilisées. Voulez-vous supprimer le modèle ?",
831
+ "route.templates.manager.error.modal.description": "Les clés du modèle sont en lecture seule et ne seront pas utilisées. Voulez-vous supprimer ce modèle ?",
830
832
  "route.templates.manager.error.modal.title": "La détection du modèle n'existe plus",
831
833
  "route.templates.manager.global": "Global",
832
834
  "route.templates.manager.open": "Ouvrir la vue",
package/package.json CHANGED
@@ -33,7 +33,7 @@
33
33
  "chart.js": "^4.5.1",
34
34
  "chartjs-adapter-dayjs-4": "^1.0.4",
35
35
  "chartjs-plugin-zoom": "^2.2.0",
36
- "dayjs": "^1.11.21",
36
+ "dayjs": "^1.11.23",
37
37
  "dompurify": "^3.4.13",
38
38
  "flat": "^6.0.1",
39
39
  "fuse.js": "^7.5.0",
@@ -93,7 +93,7 @@
93
93
  "url": "https://github.com/CybercentreCanada/howler"
94
94
  },
95
95
  "type": "module",
96
- "version": "3.1.0-dev.1445",
96
+ "version": "3.1.0-dev.1463",
97
97
  "exports": {
98
98
  "./i18n": "./i18n.js",
99
99
  "./index.css": "./index.css",