@cccsaurora/howler-ui 3.1.0-dev.1627 → 3.1.0-dev.1634

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.
@@ -3,7 +3,7 @@ import { OpenInNew } from '@mui/icons-material';
3
3
  import { Box, Chip, Grid, Stack, Tooltip, Typography, chipClasses, useTheme } from '@mui/material';
4
4
  import { uniq } from 'lodash-es';
5
5
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
6
- import { Fragment, useCallback, useMemo } from 'react';
6
+ import { Fragment, useMemo } from 'react';
7
7
  import { Trans, useTranslation } from 'react-i18next';
8
8
  import { usePluginStore } from 'react-pluggable';
9
9
  import { getEscalationColor, getProviderColor } from '@cccsaurora/howler-ui/utils/utils';
@@ -15,6 +15,14 @@ import HitTimestamp from './elements/HitTimestamp';
15
15
  import HitBannerTooltip from './HitBannerTooltip';
16
16
  import { HitLayout } from './HitLayout';
17
17
  import RelatedRecords from './related/RelatedRecords';
18
+ const Wrapper = ({ compressed, hit, textVariant, i18nKey, value, field, ...typographyProps }) => {
19
+ const { t } = useTranslation();
20
+ const children = (_jsxs(Stack, { direction: "row", spacing: 1, flex: 1, sx: { lineHeight: '20px', '& .iconify': { maxHeight: '20px' } }, children: [_jsxs(Typography, { variant: textVariant, noWrap: compressed, fontWeight: "bold", textOverflow: compressed ? 'ellipsis' : 'wrap', ...typographyProps, sx: [
21
+ { display: 'flex', flexDirection: 'row' },
22
+ ...(typographyProps?.sx && Array.isArray(typographyProps.sx) ? typographyProps.sx : [typographyProps?.sx])
23
+ ], children: [t(i18nKey), ":"] }), (Array.isArray(value) ? value : [value]).map(val => (_jsx(PluginTypography, { component: "span", context: "banner", variant: textVariant, noWrap: compressed, textOverflow: compressed ? 'ellipsis' : 'wrap', ...typographyProps, value: val, field: field, obj: hit }, val)))] }));
24
+ return compressed ? (_jsx(Tooltip, { title: Array.isArray(value) ? (_jsx("div", { children: value.map(indicator => (_jsx("p", { style: { margin: 0, padding: 0 }, children: indicator }, indicator))) })) : (value), children: children })) : (children);
25
+ };
18
26
  const HitBanner = ({ hit, lazy = false, layout = HitLayout.NORMAL, showAssigned = true }) => {
19
27
  const { t } = useTranslation();
20
28
  const theme = useTheme();
@@ -22,19 +30,9 @@ const HitBanner = ({ hit, lazy = false, layout = HitLayout.NORMAL, showAssigned
22
30
  const compressed = useMemo(() => layout === HitLayout.DENSE, [layout]);
23
31
  const textVariant = useMemo(() => (layout === HitLayout.COMFY ? 'body1' : 'caption'), [layout]);
24
32
  const providerColor = getProviderColor(hit?.event?.provider);
25
- /**
26
- * The tooltips are necessary only when in the most compressed format
27
- */
28
- const Wrapper = useCallback(({ i18nKey, value, field, ...typographyProps }) => {
29
- const _children = (_jsxs(Stack, { direction: "row", spacing: 1, flex: 1, children: [_jsxs(Typography, { variant: textVariant, noWrap: compressed, fontWeight: "bold", textOverflow: compressed ? 'ellipsis' : 'wrap', ...typographyProps, sx: [
30
- { display: 'flex', flexDirection: 'row' },
31
- ...(typographyProps?.sx && Array.isArray(typographyProps.sx) ? typographyProps.sx : [typographyProps?.sx])
32
- ], children: [t(i18nKey), ":"] }), (Array.isArray(value) ? value : [value]).map(val => (_jsx(PluginTypography, { component: "span", context: "banner", variant: textVariant, noWrap: compressed, textOverflow: compressed ? 'ellipsis' : 'wrap', ...typographyProps, value: val, field: field, obj: hit }, val)))] }));
33
- return compressed ? (_jsx(Tooltip, { title: Array.isArray(value) ? (_jsx("div", { children: value.map(_indicator => (_jsx("p", { style: { margin: 0, padding: 0 }, children: _indicator }, _indicator))) })) : (value), children: _children })) : (_children);
34
- }, [compressed, hit, t, textVariant]);
35
- return (_jsx(Box, { sx: { width: '100%', ml: 0, overflow: 'hidden', color: 'text.primary' }, children: _jsxs(Stack, { spacing: layout !== HitLayout.COMFY ? 0.25 : 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, flexWrap: "wrap", alignItems: "center", children: [_jsx(HitBannerTooltip, { hit: hit, children: _jsx(Chip, { sx: { backgroundColor: providerColor, color: theme.palette.getContrastText(providerColor) }, label: hit.organization?.name ?? _jsx(Trans, { i18nKey: "unknown" }) }) }), _jsx(AnalyticLink, { lazy: lazy, hit: hit }), _jsx("div", { style: { flex: 1 } }), _jsx(EscalationChip, { hit: hit, layout: layout }), ['in-progress', 'on-hold'].includes(hit.howler.status ?? '') && (_jsx(Chip, { sx: { width: 'fit-content', display: 'inline-flex' }, label: hit.howler.status, color: "primary" })), _jsx(HitTimestamp, { hit: hit, layout: layout }), _jsx(Assigned, { hit: hit, layout: layout, showAssigned: showAssigned }), (hit.howler.related?.length ?? 0) > 0 && _jsx(RelatedRecords, { hit: hit }), howlerPluginStore.plugins.flatMap(plugin => (_jsx(Fragment, { children: pluginStore.executeFunction(`${plugin}.status`, { hit, layout }) }, plugin)))] }), hit.howler?.rationale && (_jsxs(Typography, { flex: 1, variant: textVariant, color: getEscalationColor(hit.howler.escalation) + '.main', sx: { fontWeight: 'bold' }, children: [t('hit.header.rationale'), ": ", hit.howler.rationale] })), hit.howler?.outline && (_jsxs(_Fragment, { children: [_jsxs(Grid, { container: true, spacing: layout !== HitLayout.COMFY ? 1 : 2, sx: { ml: `${theme.spacing(-1)} !important` }, children: [hit.howler.outline.threat && (_jsx(Grid, { children: _jsx(Wrapper, { i18nKey: "hit.header.threat", value: hit.howler.outline.threat, field: "howler.outline.threat" }) })), hit.howler.outline.target && (_jsx(Grid, { children: _jsx(Wrapper, { i18nKey: "hit.header.target", value: hit.howler.outline.target, field: "howler.outline.target" }) }))] }), (hit.howler.outline.indicators?.length ?? 0) > 0 && (_jsxs(Stack, { direction: "row", spacing: layout !== HitLayout.COMFY ? 0.25 : 1, children: [_jsxs(Typography, { component: "span", variant: textVariant, fontWeight: "bold", children: [t('hit.header.indicators'), ":"] }), _jsx(Grid, { container: true, spacing: 0.5, sx: { mt: `${theme.spacing(-0.5)} !important`, ml: `${theme.spacing(0.25)} !important` }, children: uniq(hit.howler.outline.indicators ?? []).map((_indicator, index) => {
33
+ return (_jsx(Box, { sx: { width: '100%', ml: 0, overflow: 'hidden', color: 'text.primary' }, children: _jsxs(Stack, { spacing: layout !== HitLayout.COMFY ? 0.25 : 1, children: [_jsxs(Stack, { direction: "row", spacing: 0.5, flexWrap: "wrap", alignItems: "center", children: [_jsx(HitBannerTooltip, { hit: hit, children: _jsx(Chip, { sx: { backgroundColor: providerColor, color: theme.palette.getContrastText(providerColor) }, label: hit.organization?.name ?? _jsx(Trans, { i18nKey: "unknown" }) }) }), _jsx(AnalyticLink, { lazy: lazy, hit: hit }), _jsx("div", { style: { flex: 1 } }), _jsx(EscalationChip, { hit: hit, layout: layout }), ['in-progress', 'on-hold'].includes(hit.howler.status ?? '') && (_jsx(Chip, { sx: { width: 'fit-content', display: 'inline-flex' }, label: hit.howler.status, color: "primary" })), _jsx(HitTimestamp, { hit: hit, layout: layout }), _jsx(Assigned, { hit: hit, layout: layout, showAssigned: showAssigned }), (hit.howler.related?.length ?? 0) > 0 && _jsx(RelatedRecords, { hit: hit }), howlerPluginStore.plugins.flatMap(plugin => (_jsx(Fragment, { children: pluginStore.executeFunction(`${plugin}.status`, { hit, layout }) }, plugin)))] }), hit.howler?.rationale && (_jsx(Wrapper, { fontWeight: "bold", color: getEscalationColor(hit.howler.escalation) + '.main', compressed: compressed, hit: hit, textVariant: textVariant, i18nKey: "hit.header.rationale", value: hit.howler.rationale, field: "howler.rationale" })), hit.howler?.outline && (_jsxs(_Fragment, { children: [hit.howler.outline.threat && (_jsx(Wrapper, { compressed: compressed, hit: hit, textVariant: textVariant, i18nKey: "hit.header.threat", value: hit.howler.outline.threat, field: "howler.outline.threat" })), hit.howler.outline.target && (_jsx(Wrapper, { compressed: compressed, hit: hit, textVariant: textVariant, i18nKey: "hit.header.target", value: hit.howler.outline.target, field: "howler.outline.target" })), (hit.howler.outline.indicators?.length ?? 0) > 0 && (_jsxs(Stack, { direction: "row", spacing: layout !== HitLayout.COMFY ? 0.25 : 1, children: [_jsxs(Typography, { component: "span", variant: textVariant, fontWeight: "bold", pr: 1, children: [t('hit.header.indicators'), ":"] }), _jsx(Grid, { container: true, spacing: 0.5, children: uniq(hit.howler.outline.indicators ?? []).map((_indicator, index) => {
36
34
  return (_jsx(Grid, { children: _jsxs(Stack, { direction: "row", children: [_jsx(PluginTypography, { context: "indicators", variant: textVariant, value: _indicator, children: _indicator }), index < (hit.howler.outline?.indicators?.length ?? 0) - 1 && (_jsx(Typography, { variant: textVariant, children: ',' }))] }) }, _indicator));
37
- }) })] })), hit.howler.outline.summary && (_jsx(Wrapper, { i18nKey: "hit.header.summary", value: hit.howler.outline.summary, paragraph: true, textOverflow: "wrap", sx: [compressed && { marginTop: `0 !important` }], field: "howler.outline.summary" })), hit.howler.links?.[0]?.href && (_jsx(Chip, { icon: _jsx(OpenInNew, {}), label: hit.howler.links[0].title || t('hit.header.link'), size: layout !== HitLayout.COMFY ? 'small' : 'medium', component: "a", href: hit.howler.links[0].href, target: "_blank", rel: "noopener noreferrer", sx: { [`.${chipClasses.label}`]: { cursor: 'pointer !important' }, alignSelf: 'start' }, onClick: e => {
35
+ }) })] })), hit.howler.outline.summary && (_jsx(Wrapper, { compressed: compressed, hit: hit, textVariant: textVariant, i18nKey: "hit.header.summary", value: hit.howler.outline.summary, textOverflow: "wrap", sx: [compressed && { marginTop: `0 !important` }], field: "howler.outline.summary" })), hit.howler.links?.[0]?.href && (_jsx(Chip, { icon: _jsx(OpenInNew, {}), label: hit.howler.links[0].title || t('hit.header.link'), size: layout !== HitLayout.COMFY ? 'small' : 'medium', component: "a", href: hit.howler.links[0].href, target: "_blank", rel: "noopener noreferrer", sx: { [`.${chipClasses.label}`]: { cursor: 'pointer !important' }, alignSelf: 'start' }, onClick: e => {
38
36
  e.stopPropagation();
39
37
  } }))] }))] }) }));
40
38
  };
@@ -117,7 +117,8 @@ describe('HitBanner', () => {
117
117
  renderHitBanner();
118
118
  expect(screen.getByText('Test Org')).toBeInTheDocument();
119
119
  expect(screen.getByTestId('analytic-link')).toBeInTheDocument();
120
- expect(screen.getByText('hit.header.rationale: Escalation rationale')).toBeInTheDocument();
120
+ expect(screen.getByText('hit.header.rationale:')).toBeInTheDocument();
121
+ expect(screen.getByText('Escalation rationale')).toBeInTheDocument();
121
122
  expect(screen.getByText('hit.header.threat:')).toBeInTheDocument();
122
123
  expect(screen.getByText('Threat value')).toBeInTheDocument();
123
124
  expect(screen.getByText('hit.header.target:')).toBeInTheDocument();
@@ -11,7 +11,7 @@ const Escalation = forwardRef(({ analytic, maxWidth = '45%' }, ref) => {
11
11
  const { doughnut } = useMyChart();
12
12
  const [loading, setLoading] = useState(false);
13
13
  const [escalationData, setEscalationData] = useState({});
14
- const escalationColors = useMemo(() => Object.keys(escalationData).map(escalation => getEscalationColor(escalation, 'rgba(255, 255, 255, 0.16)', theme)), [theme]);
14
+ const escalationColors = useMemo(() => Object.keys(escalationData).map(escalation => getEscalationColor(escalation, 'rgba(255, 255, 255, 0.16)', theme)), [escalationData, theme]);
15
15
  useEffect(() => {
16
16
  if (!analytic) {
17
17
  return;
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  ]
13
13
  },
14
14
  "dependencies": {
15
- "@cccsaurora/clue-ui": "1.4.2",
15
+ "@cccsaurora/clue-ui": "1.4.4",
16
16
  "@dnd-kit/core": "^6.3.1",
17
17
  "@dnd-kit/modifiers": "^7.0.0",
18
18
  "@dnd-kit/sortable": "^8.0.0",
@@ -97,7 +97,7 @@
97
97
  "url": "https://github.com/CybercentreCanada/howler"
98
98
  },
99
99
  "type": "module",
100
- "version": "3.1.0-dev.1627",
100
+ "version": "3.1.0-dev.1634",
101
101
  "exports": {
102
102
  "./i18n": "./i18n.js",
103
103
  "./index.css": "./index.css",
@@ -9,17 +9,17 @@ import { getStored } from '@cccsaurora/howler-ui/utils/localStorage';
9
9
  const Provider = ({ children, options = {} }) => {
10
10
  const { config } = useContext(ApiConfigContext);
11
11
  const features = config?.configuration?.features ?? {};
12
- const [database, setDatabase] = useState();
12
+ const [database, setDatabase] = useState(null);
13
13
  const defaultGetToken = useCallback(() => getStored(StorageKey.APP_TOKEN) ?? '', []);
14
14
  const baseURL = options.baseURL ?? `${location.origin}/api/v1/clue`;
15
15
  const getToken = options.getToken ?? defaultGetToken;
16
16
  const publicIconify = options.publicIconify ?? location.origin.includes('localhost');
17
17
  const customIconify = options.customIconify ?? location.origin.replace('howler', 'icons');
18
18
  useEffect(() => {
19
- let cancelled = false;
20
- if (!features.clue) {
19
+ if (!features.clue || database) {
21
20
  return;
22
21
  }
22
+ let cancelled = false;
23
23
  void buildDatabase({
24
24
  storageType: options.storageType ?? 'memory',
25
25
  replicate: !!options.replicate,
@@ -33,7 +33,7 @@ const Provider = ({ children, options = {} }) => {
33
33
  return () => {
34
34
  cancelled = true;
35
35
  };
36
- }, [baseURL, features.clue, getToken, options.storageType, options.replicate]);
36
+ }, [baseURL, features.clue, getToken, options.storageType, options.replicate, database]);
37
37
  return (_jsx(ClueProvider, { baseURL: baseURL, getToken: getToken, enabled: features.clue, publicIconify: publicIconify, customIconify: customIconify, defaultTimeout: options.defaultTimeout ?? 5, i18next: useTranslation('clue'), chunkSize: options.chunkSize ?? 50, database: database, includeContext: options.includeContext, children: children }));
38
38
  };
39
39
  export default Provider;
@@ -51,7 +51,7 @@ describe('Clue Provider', () => {
51
51
  renderProvider({ configuration: { features: { clue: false } } });
52
52
  expect(mocks.buildDatabase).not.toHaveBeenCalled();
53
53
  expect(getProviderProps().enabled).toBe(false);
54
- expect(getProviderProps().database).toBeUndefined();
54
+ expect(getProviderProps().database).toBeNull();
55
55
  expect(getProviderProps().getToken?.()).toBe('');
56
56
  });
57
57
  it('returns the current stored application token by default', () => {
@@ -114,6 +114,6 @@ describe('Clue Provider', () => {
114
114
  await act(async () => {
115
115
  resolveDatabase({ name: 'late' });
116
116
  });
117
- expect(getProviderProps().database).toBeUndefined();
117
+ expect(getProviderProps().database).toBeNull();
118
118
  });
119
119
  });
@@ -9,6 +9,7 @@ import useSetup from './setup';
9
9
  export type { CluePluginOptions } from './Provider';
10
10
  declare class CluePlugin extends HowlerPlugin {
11
11
  private readonly options;
12
+ private readonly providerComponent;
12
13
  constructor(options?: CluePluginOptions);
13
14
  name: string;
14
15
  version: string;
@@ -17,9 +17,11 @@ import Provider, {} from './Provider';
17
17
  import useSetup from './setup';
18
18
  class CluePlugin extends HowlerPlugin {
19
19
  options;
20
+ providerComponent;
20
21
  constructor(options = {}) {
21
22
  super();
22
23
  this.options = options;
24
+ this.providerComponent = ({ children }) => _jsx(Provider, { options: this.options, children: children });
23
25
  }
24
26
  name = 'CluePlugin';
25
27
  version = '0.0.1';
@@ -46,8 +48,7 @@ class CluePlugin extends HowlerPlugin {
46
48
  super.addPivot('clue', pivotForm, pivotRenderer);
47
49
  }
48
50
  provider() {
49
- const options = this.options;
50
- return ({ children }) => _jsx(Provider, { options: options, children: children });
51
+ return this.providerComponent;
51
52
  }
52
53
  setup() {
53
54
  return useSetup;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import CluePlugin from './index';
3
+ describe('Clue plugin', () => {
4
+ it('keeps its provider component identity stable', () => {
5
+ const plugin = new CluePlugin();
6
+ expect(plugin.provider()).toBe(plugin.provider());
7
+ });
8
+ });