@cccsaurora/howler-ui 2.13.0-dev.164 → 2.13.0-dev.169
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.
|
@@ -8,10 +8,11 @@ import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/us
|
|
|
8
8
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
9
|
import { useTranslation } from 'react-i18next';
|
|
10
10
|
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
11
|
+
import { validateRegex } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
11
12
|
import Throttler from '@cccsaurora/howler-ui/utils/Throttler';
|
|
12
13
|
import { removeEmpty, searchObject } from '@cccsaurora/howler-ui/utils/utils';
|
|
13
14
|
const THROTTLER = new Throttler(150);
|
|
14
|
-
const JSONViewer = ({ data, collapse = true }) => {
|
|
15
|
+
const JSONViewer = ({ data, collapse = true, hideSearch = false, filter }) => {
|
|
15
16
|
const { t } = useTranslation();
|
|
16
17
|
const { isDark } = useAppTheme();
|
|
17
18
|
const [compact] = useMyLocalStorageItem(StorageKey.COMPACT_JSON, true);
|
|
@@ -21,19 +22,11 @@ const JSONViewer = ({ data, collapse = true }) => {
|
|
|
21
22
|
useEffect(() => {
|
|
22
23
|
THROTTLER.debounce(() => {
|
|
23
24
|
const filteredData = removeEmpty(data, compact);
|
|
24
|
-
const searchedData = searchObject(filteredData, query, flat);
|
|
25
|
+
const searchedData = searchObject(filteredData, filter ?? query, flat);
|
|
25
26
|
setResult(searchedData);
|
|
26
27
|
});
|
|
27
|
-
}, [compact, data, flat, query]);
|
|
28
|
-
const hasError = useMemo(() =>
|
|
29
|
-
try {
|
|
30
|
-
new RegExp(query);
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
catch (e) {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
}, [query]);
|
|
28
|
+
}, [compact, data, filter, flat, query]);
|
|
29
|
+
const hasError = useMemo(() => !validateRegex(filter ?? query), [query, filter]);
|
|
37
30
|
const shouldCollapse = useCallback((field) => {
|
|
38
31
|
return (field.name !== 'root' && field.type !== 'object') || field.namespace.length > 3;
|
|
39
32
|
}, []);
|
|
@@ -48,6 +41,6 @@ const JSONViewer = ({ data, collapse = true }) => {
|
|
|
48
41
|
// Type declaration is wrong - this is a valid prop
|
|
49
42
|
displayArrayKey: !compact
|
|
50
43
|
} })), [collapse, compact, isDark, result, shouldCollapse]);
|
|
51
|
-
return data ? (_jsxs(Stack, { direction: "column", spacing: 1, sx: { '& > div:first-of-type': { mt: 1, mr: 0.5 } }, children: [_jsx(Phrase, { value: query, onChange: setQuery, error: hasError, label: t('json.viewer.search.label'), placeholder: t('json.viewer.search.prompt'), disabled: !result, endAdornment: _jsx(IconButton, { onClick: () => setQuery(''), children: _jsx(Clear, {}) }) }), renderer] })) : (_jsx(Skeleton, { width: "100%", height: "95%", variant: "rounded" }));
|
|
44
|
+
return data ? (_jsxs(Stack, { direction: "column", spacing: 1, sx: { '& > div:first-of-type': { mt: 1, mr: 0.5 } }, children: [!hideSearch && (_jsx(Phrase, { value: query, onChange: setQuery, error: hasError, label: t('json.viewer.search.label'), placeholder: t('json.viewer.search.prompt'), disabled: !result, endAdornment: _jsx(IconButton, { onClick: () => setQuery(''), children: _jsx(Clear, {}) }) })), renderer] })) : (_jsx(Skeleton, { width: "100%", height: "95%", variant: "rounded" }));
|
|
52
45
|
};
|
|
53
46
|
export default JSONViewer;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Clear, Code, Comment, DataObject, History, LinkSharp, OpenInNew, QueryStats } from '@mui/icons-material';
|
|
3
|
-
import { Badge, Box, Divider, Skeleton, Stack, Tab, Tabs, Tooltip, useTheme } from '@mui/material';
|
|
3
|
+
import { Badge, Box, Divider, IconButton, Skeleton, Stack, Tab, Tabs, Tooltip, useTheme } from '@mui/material';
|
|
4
4
|
import TuiIconButton from '@cccsaurora/howler-ui/components/elements/addons/buttons/CustomIconButton';
|
|
5
5
|
import { Icon } from '@iconify/react/dist/iconify.js';
|
|
6
6
|
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
@@ -11,6 +11,7 @@ import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/Fle
|
|
|
11
11
|
import VSBox from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBox';
|
|
12
12
|
import VSBoxContent from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBoxContent';
|
|
13
13
|
import VSBoxHeader from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBoxHeader';
|
|
14
|
+
import Phrase from '@cccsaurora/howler-ui/components/elements/addons/search/phrase/Phrase';
|
|
14
15
|
import BundleButton from '@cccsaurora/howler-ui/components/elements/display/icons/BundleButton';
|
|
15
16
|
import SocketBadge from '@cccsaurora/howler-ui/components/elements/display/icons/SocketBadge';
|
|
16
17
|
import JSONViewer from '@cccsaurora/howler-ui/components/elements/display/json/JSONViewer';
|
|
@@ -38,6 +39,7 @@ import { usePluginStore } from 'react-pluggable';
|
|
|
38
39
|
import { useLocation } from 'react-router-dom';
|
|
39
40
|
import { useContextSelector } from 'use-context-selector';
|
|
40
41
|
import { getUserList } from '@cccsaurora/howler-ui/utils/hitFunctions';
|
|
42
|
+
import { validateRegex } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
41
43
|
import { tryParse } from '@cccsaurora/howler-ui/utils/utils';
|
|
42
44
|
import LeadRenderer from '../view/LeadRenderer';
|
|
43
45
|
const InformationPane = ({ onClose }) => {
|
|
@@ -55,6 +57,7 @@ const InformationPane = ({ onClose }) => {
|
|
|
55
57
|
const [tab, setTab] = useState('overview');
|
|
56
58
|
const [loading, setLoading] = useState(false);
|
|
57
59
|
const [dossiers, setDossiers] = useState([]);
|
|
60
|
+
const [filter, setFilter] = useState('');
|
|
58
61
|
const users = useMyUserList(userIds);
|
|
59
62
|
const hit = useContextSelector(HitContext, ctx => ctx.hits[selected]);
|
|
60
63
|
howlerPluginStore.plugins.forEach(plugin => {
|
|
@@ -128,8 +131,8 @@ const InformationPane = ({ onClose }) => {
|
|
|
128
131
|
overview: () => _jsx(HitOverview, { hit: hit }),
|
|
129
132
|
details: () => _jsx(HitDetails, { hit: hit }),
|
|
130
133
|
hit_comments: () => _jsx(HitComments, { hit: hit, users: users }),
|
|
131
|
-
hit_raw: () => _jsx(JSONViewer, { data: !loading && hit }),
|
|
132
|
-
hit_data: () => (_jsx(JSONViewer, { data: !loading && hit?.howler?.data?.map(entry => tryParse(entry)), collapse: false })),
|
|
134
|
+
hit_raw: () => _jsx(JSONViewer, { data: !loading && hit, hideSearch: true, filter: filter }),
|
|
135
|
+
hit_data: () => (_jsx(JSONViewer, { data: !loading && hit?.howler?.data?.map(entry => tryParse(entry)), collapse: false, hideSearch: true, filter: filter })),
|
|
133
136
|
hit_worklog: () => _jsx(HitWorklog, { hit: !loading && hit, users: users }),
|
|
134
137
|
hit_aggregate: () => _jsx(HitSummary, { query: `howler.bundles:(${hit?.howler?.id})` }),
|
|
135
138
|
hit_related: () => _jsx(HitRelated, { hit: hit }),
|
|
@@ -139,7 +142,8 @@ const InformationPane = ({ onClose }) => {
|
|
|
139
142
|
() => _jsx(LeadRenderer, { lead: _lead, hit: hit })
|
|
140
143
|
])))
|
|
141
144
|
}[tab]?.();
|
|
142
|
-
}, [dossiers, hit, loading, tab, users]);
|
|
145
|
+
}, [dossiers, filter, hit, loading, tab, users]);
|
|
146
|
+
const hasError = useMemo(() => !validateRegex(filter), [filter]);
|
|
143
147
|
return (_jsxs(VSBox, { top: 10, sx: { height: '100%', flex: 1 }, children: [_jsxs(Stack, { direction: "column", flex: 1, sx: { overflowY: 'auto', flexGrow: 1 }, position: "relative", spacing: 1, ml: 2, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, flexShrink: 0, pr: 2, sx: [hit?.howler?.is_bundle && { position: 'absolute', top: 1, right: 0, zIndex: 1100 }], children: [_jsx(FlexOne, {}), onClose && !location.pathname.startsWith('/bundles') && (_jsx(TuiIconButton, { size: "small", onClick: onClose, tooltip: t('hit.panel.details.exit'), children: _jsx(Clear, {}) })), _jsx(SocketBadge, { size: "small" }), analytic && (_jsx(TuiIconButton, { size: "small", tooltip: t('hit.panel.analytic.open'), disabled: !analytic || loading, route: `/analytics/${analytic.analytic_id}`, children: _jsx(QueryStats, {}) })), hit?.howler.bundles?.length > 0 && _jsx(BundleButton, { ids: hit.howler.bundles, disabled: loading }), !!hit && !hit.howler.is_bundle && (_jsx(TuiIconButton, { tooltip: t('hit.panel.open'), href: `/hits/${selected}`, disabled: !hit || loading, size: "small", target: "_blank", children: _jsx(OpenInNew, {}) }))] }), _jsx(Box, { pr: 2, children: header }), !!hit &&
|
|
144
148
|
!hit.howler.is_bundle &&
|
|
145
149
|
(!loading ? (_jsxs(_Fragment, { children: [_jsx(HitOutline, { hit: hit, layout: HitLayout.DENSE }), _jsx(HitLabels, { hit: hit })] })) : (_jsx(Skeleton, { height: 124 }))), (hit?.howler?.links?.length > 0 ||
|
|
@@ -149,39 +153,39 @@ const InformationPane = ({ onClose }) => {
|
|
|
149
153
|
.slice(0, 3)
|
|
150
154
|
.map(l => _jsx(RelatedLink, { compact: true, ...l }, l.href)), dossiers.flatMap(_dossier => (_dossier.pivots ?? []).map((_pivot, index) => (
|
|
151
155
|
// eslint-disable-next-line react/no-array-index-key
|
|
152
|
-
_jsx(PivotLink, { pivot: _pivot, hit: hit, compact: true }, _dossier.dossier_id + index))))] })),
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
156
|
+
_jsx(PivotLink, { pivot: _pivot, hit: hit, compact: true }, _dossier.dossier_id + index))))] })), _jsxs(VSBoxHeader, { ml: -1, mr: -1, pb: 1, sx: { top: '0px' }, children: [_jsxs(Tabs, { value: tab === 'overview' && !hasOverview ? 'details' : tab, sx: {
|
|
157
|
+
display: 'flex',
|
|
158
|
+
flexDirection: 'row',
|
|
159
|
+
pr: 2,
|
|
160
|
+
alignItems: 'center',
|
|
161
|
+
position: 'relative',
|
|
162
|
+
'& > .MuiTabScrollButton-root': {
|
|
163
|
+
position: 'absolute',
|
|
164
|
+
top: 0,
|
|
165
|
+
bottom: 0,
|
|
166
|
+
zIndex: 5,
|
|
167
|
+
backgroundColor: theme.palette.background.paper,
|
|
168
|
+
'&:not(.Mui-disabled)': {
|
|
169
|
+
opacity: 1
|
|
170
|
+
},
|
|
171
|
+
'&:first-of-type': {
|
|
172
|
+
left: 0
|
|
173
|
+
},
|
|
174
|
+
'&:last-of-type': {
|
|
175
|
+
right: 0
|
|
176
|
+
}
|
|
172
177
|
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
'
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
, { label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_lead.icon && _jsx(Icon, { icon: _lead.icon }), _jsx("span", { children: i18n.language === 'en' ? _lead.label.en : _lead.label.fr })] }), value: `external-lead:${dossierIndex}:${leadIndex}`, onClick: () => setTab(`external-lead:${dossierIndex}:${leadIndex}`) }, `external-lead:${dossierIndex}:${leadIndex}`)))), _jsx(FlexOne, {}), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.data'), children: _jsx(DataObject, {}) }), value: "hit_data", onClick: () => setTab('hit_data'), disabled: !hit?.howler?.data }), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.json'), children: _jsx(Code, {}) }), value: "hit_raw", onClick: () => setTab('hit_raw') }), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.worklog'), children: _jsx(History, {}) }), value: "hit_worklog", onClick: () => setTab('hit_worklog') }), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.related'), children: _jsx(LinkSharp, {}) }), value: "hit_related", onClick: () => setTab('hit_related') })] }) }), _jsx(ErrorBoundary, { children: _jsx(VSBoxContent, { mr: -1, ml: -1, height: "100%", children: _jsx(Stack, { height: "100%", flex: 1, children: tabContent }) }) })] }), !!hit && hit?.howler && (_jsxs(Box, { pr: 2, bgcolor: theme.palette.background.default, position: "relative", children: [_jsx(Divider, { orientation: "horizontal" }), _jsx(HitActions, { hit: hit })] }))] }));
|
|
178
|
+
}, variant: "scrollable", children: [_jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.comments'), children: _jsx(Badge, { sx: {
|
|
179
|
+
'& > .MuiBadge-badge': {
|
|
180
|
+
backgroundColor: theme.palette.divider,
|
|
181
|
+
zIndex: 1,
|
|
182
|
+
right: theme.spacing(-0.5)
|
|
183
|
+
},
|
|
184
|
+
'& > svg': { zIndex: 2 }
|
|
185
|
+
}, badgeContent: hit?.howler.comment?.length ?? 0, children: _jsx(Comment, {}) }) }), value: "hit_comments", onClick: () => setTab('hit_comments') }), hit?.howler?.is_bundle && (_jsx(Tab, { label: t('hit.viewer.aggregate'), value: "hit_aggregate", onClick: () => setTab('hit_aggregate') })), hasOverview && (_jsx(Tab, { label: t('hit.viewer.overview'), value: "overview", onClick: () => setTab('overview') })), _jsx(Tab, { label: t('hit.viewer.details'), value: "details", onClick: () => setTab('details') }), hit?.howler.dossier?.map((lead, index) => (_jsx(Tab
|
|
186
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
187
|
+
, { label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [lead.icon && _jsx(Icon, { icon: lead.icon }), _jsx("span", { children: i18n.language === 'en' ? lead.label.en : lead.label.fr })] }), value: 'lead:' + index, onClick: () => setTab('lead:' + index) }, 'lead:' + index))), dossiers.flatMap((_dossier, dossierIndex) => _dossier.leads?.map((_lead, leadIndex) => (_jsx(Tab
|
|
188
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
189
|
+
, { label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_lead.icon && _jsx(Icon, { icon: _lead.icon }), _jsx("span", { children: i18n.language === 'en' ? _lead.label.en : _lead.label.fr })] }), value: `external-lead:${dossierIndex}:${leadIndex}`, onClick: () => setTab(`external-lead:${dossierIndex}:${leadIndex}`) }, `external-lead:${dossierIndex}:${leadIndex}`)))), _jsx(FlexOne, {}), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.data'), children: _jsx(DataObject, {}) }), value: "hit_data", onClick: () => setTab('hit_data'), disabled: !hit?.howler?.data }), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.json'), children: _jsx(Code, {}) }), value: "hit_raw", onClick: () => setTab('hit_raw') }), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.worklog'), children: _jsx(History, {}) }), value: "hit_worklog", onClick: () => setTab('hit_worklog') }), _jsx(Tab, { sx: { px: 2, minWidth: 0 }, label: _jsx(Tooltip, { title: t('hit.viewer.related'), children: _jsx(LinkSharp, {}) }), value: "hit_related", onClick: () => setTab('hit_related') })] }), ['hit_raw', 'hit_data'].includes(tab) && (_jsx(Phrase, { sx: { mt: 1, pr: 1 }, value: filter, onChange: setFilter, error: hasError, label: t('json.viewer.search.label'), placeholder: t('json.viewer.search.prompt'), endAdornment: _jsx(IconButton, { onClick: () => setFilter(''), children: _jsx(Clear, {}) }) }))] }), _jsx(ErrorBoundary, { children: _jsx(VSBoxContent, { mr: -1, ml: -1, height: "100%", children: _jsx(Stack, { height: "100%", flex: 1, children: tabContent }) }) })] }), !!hit && hit?.howler && (_jsxs(Box, { pr: 2, bgcolor: theme.palette.background.default, position: "relative", children: [_jsx(Divider, { orientation: "horizontal" }), _jsx(HitActions, { hit: hit })] }))] }));
|
|
186
190
|
};
|
|
187
191
|
export default InformationPane;
|
package/package.json
CHANGED
package/utils/stringUtils.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export declare const safeFieldValueURI: (data: string | number | boolean) => str
|
|
|
5
5
|
export declare const sanitizeLuceneQuery: (query: string) => string;
|
|
6
6
|
export declare const safeStringPropertyCompare: (propertyPath: string) => (a: unknown, b: unknown) => any;
|
|
7
7
|
export declare const sanitizeMultilineLucene: (query: string) => string;
|
|
8
|
+
export declare const validateRegex: (regex: string) => boolean;
|
package/utils/stringUtils.js
CHANGED
|
@@ -36,3 +36,12 @@ export const safeStringPropertyCompare = (propertyPath) => {
|
|
|
36
36
|
export const sanitizeMultilineLucene = (query) => {
|
|
37
37
|
return query.replace(/#.+/g, '').replace(/\n{2,}/, '\n');
|
|
38
38
|
};
|
|
39
|
+
export const validateRegex = (regex) => {
|
|
40
|
+
try {
|
|
41
|
+
new RegExp(regex);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
};
|