Skip to content

Commit 1a45852

Browse files
committed
[frontend] Refactor to use FeatureFlagged and introduce CustomViewRedirector
1 parent 630943a commit 1a45852

9 files changed

Lines changed: 346 additions & 320 deletions

File tree

opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectMain.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { ReactElement, ReactNode } from 'react';
22
import { Route, Routes } from 'react-router-dom';
33
import StixDomainObjectTabsBox, { type StixDomainObjectTabsBoxTab } from './StixDomainObjectTabsBox';
44
import ErrorNotFound from '../../../../components/ErrorNotFound';
5-
import useCustomViewRoutes from '@components/custom_views/useCustomViewRoutes';
5+
import CustomViewRedirector from '@components/custom_views/CustomViewRedirector';
6+
import FeatureFlagged from '../../../../components/FeatureFlagged';
67

78
interface StixDomainObjectMainProps {
89
entityType: string;
@@ -20,7 +21,6 @@ const StixDomainObjectMain = ({
2021
extraRoutes,
2122
}: StixDomainObjectMainProps) => {
2223
const tabs = Object.keys(pages) as StixDomainObjectTabsBoxTab[];
23-
const customViewRoutes = useCustomViewRoutes({ entityType });
2424
return (
2525
<>
2626
<StixDomainObjectTabsBox
@@ -60,9 +60,22 @@ const StixDomainObjectMain = ({
6060
{tabs.includes('history') && (
6161
<Route path="/history" element={pages.history} />
6262
)}
63-
{...customViewRoutes}
6463
{extraRoutes}
65-
<Route path="*" element={<ErrorNotFound />} />
64+
<Route
65+
path="*"
66+
element={(
67+
<FeatureFlagged
68+
flags={['CUSTOM_VIEW']}
69+
Enabled={(
70+
<CustomViewRedirector
71+
entityType={entityType}
72+
Fallback={<ErrorNotFound />}
73+
/>
74+
)}
75+
Disabled={<ErrorNotFound />}
76+
/>
77+
)}
78+
/>
6679
</Routes>
6780
</>
6881
);

opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectTabsBox.tsx

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Tab from '@mui/material/Tab';
66
import Stack from '@mui/material/Stack';
77
import { getCurrentTab } from '../../../../utils/utils';
88
import { useFormatter } from '../../../../components/i18n';
9+
import FeatureFlagged from '../../../../components/FeatureFlagged';
910
import CustomViewTabsWrapper from '@components/custom_views/CustomViewTabsWrapper';
1011

1112
export type StixDomainObjectTabsBoxTab
@@ -98,33 +99,42 @@ const StixDomainObjectTabsBox = (props: StixDomainObjectTabsBoxProps) => {
9899
const { t_i18n } = useFormatter();
99100
const location = useLocation();
100101
const currentTab = getCurrentTab(location.pathname, basePath);
102+
const StaticTabs = TABS_INFO.map(({ tab, path, label }) =>
103+
tabs.includes(tab) && (
104+
<Tab
105+
key={tab}
106+
component={Link}
107+
to={path}
108+
value={path}
109+
label={t_i18n(label)}
110+
/>
111+
));
101112
return (
102113
<Box sx={CONTAINER_STYLE}>
103-
<CustomViewTabsWrapper
104-
basePath={basePath}
105-
entityType={entityType}
106-
render={({ CustomViewsTab, CustomViewsDropDown, currentCustomViewTab }) => {
107-
return (
108-
<>
109-
<Tabs value={currentCustomViewTab ?? currentTab}>
110-
{
111-
TABS_INFO.map(({ tab, path, label }) =>
112-
tabs.includes(tab) && (
113-
<Tab
114-
key={tab}
115-
component={Link}
116-
to={path}
117-
value={path}
118-
label={t_i18n(label)}
119-
/>
120-
))
121-
}
122-
{CustomViewsTab}
123-
</Tabs>
124-
{CustomViewsDropDown}
125-
</>
126-
);
127-
}}
114+
<FeatureFlagged
115+
flags={['CUSTOM_VIEW']}
116+
Enabled={(
117+
<CustomViewTabsWrapper
118+
basePath={basePath}
119+
entityType={entityType}
120+
render={({ CustomViewsTab, CustomViewsDropDown, currentCustomViewTab }) => {
121+
return (
122+
<>
123+
<Tabs value={currentCustomViewTab ?? currentTab}>
124+
{StaticTabs}
125+
{CustomViewsTab}
126+
</Tabs>
127+
{CustomViewsDropDown}
128+
</>
129+
);
130+
}}
131+
/>
132+
)}
133+
Disabled={(
134+
<Tabs value={currentTab}>
135+
{StaticTabs}
136+
</Tabs>
137+
)}
128138
/>
129139
{extraActions ? (
130140
<Stack gap={2} direction="row" justifyContent="space-between" alignItems="center">
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import { screen } from '@testing-library/react';
3+
import testRender, { createMockUserContext } from '../../../utils/tests/test-render';
4+
import CustomViewRedirector from './CustomViewRedirector';
5+
import { Route, Routes } from 'react-router-dom';
6+
7+
const CUSTOM_VIEW_MOCK_CONTENT = 'A great custom view page';
8+
9+
vi.mock('./Root', () => ({
10+
default: () => <span>{CUSTOM_VIEW_MOCK_CONTENT}</span>,
11+
__esModule: true,
12+
}));
13+
14+
describe('CustomViewRedirector', () => {
15+
it('renders custom view when on custom view route', () => {
16+
const customViewPath = 'my-custom-view-1504f07bee3f4c09ae66b9550eb3abe3';
17+
testRender(
18+
<Routes>
19+
<Route
20+
path="*"
21+
element={
22+
<CustomViewRedirector entityType="Intrusion-Set" Fallback="Not matched" />
23+
}
24+
/>
25+
</Routes>,
26+
{
27+
route: customViewPath,
28+
userContext: createMockUserContext({
29+
customViews: [{
30+
entity_type: 'Intrusion-Set',
31+
custom_views_info: [{
32+
id: '1504f07b-ee3f-4c09-ae66-b9550eb3abe3',
33+
name: 'My custom view',
34+
path: customViewPath,
35+
}],
36+
}],
37+
}),
38+
},
39+
);
40+
expect(screen.getByText(CUSTOM_VIEW_MOCK_CONTENT)).toBeInTheDocument();
41+
});
42+
43+
it('renders fallback when no match', () => {
44+
testRender(
45+
<Routes>
46+
<Route
47+
path="*"
48+
element={
49+
<CustomViewRedirector entityType="Intrusion-Set" Fallback="Not matched" />
50+
}
51+
/>
52+
</Routes>,
53+
{
54+
route: 'other-id-in-path-dc60eb35a6704b49804eef38e3655392',
55+
userContext: createMockUserContext({
56+
customViews: [{
57+
entity_type: 'Intrusion-Set',
58+
custom_views_info: [{
59+
id: 'dc60eb35-a670-4b49-804e-ef38e3655392',
60+
name: 'My custom view',
61+
path: 'my-custom-view-1504f07bee3f4c09ae66b9550eb3abe3',
62+
}],
63+
}],
64+
}),
65+
},
66+
);
67+
expect(screen.getByText(/Not matched/i)).toBeInTheDocument();
68+
});
69+
70+
it('renders fallback when no match because wrong entity type', () => {
71+
testRender(
72+
<Routes>
73+
<Route
74+
path="*"
75+
element={
76+
<CustomViewRedirector entityType="Case-Rft" Fallback="Not matched" />
77+
}
78+
/>
79+
</Routes>,
80+
{
81+
route: 'other-id-in-path-dc60eb35a6704b49804eef38e3655392',
82+
userContext: createMockUserContext({
83+
customViews: [{
84+
entity_type: 'Intrusion-Set',
85+
custom_views_info: [{
86+
id: 'dc60eb35-a670-4b49-804e-ef38e3655392',
87+
name: 'My custom view',
88+
path: 'my-custom-view-1504f07bee3f4c09ae66b9550eb3abe3',
89+
}],
90+
}],
91+
}),
92+
},
93+
);
94+
expect(screen.getByText(/Not matched/i)).toBeInTheDocument();
95+
});
96+
97+
it('renders custom view when the id in the path matches but not the slug', () => {
98+
testRender(
99+
<Routes>
100+
<Route
101+
path="*"
102+
element={
103+
<CustomViewRedirector entityType="Intrusion-Set" Fallback="Not matched" />
104+
}
105+
/>
106+
</Routes>,
107+
{
108+
route: 'old-slug-1504f07bee3f4c09ae66b9550eb3abe3',
109+
userContext: createMockUserContext({
110+
customViews: [{
111+
entity_type: 'Intrusion-Set',
112+
custom_views_info: [{
113+
id: '1504f07b-ee3f-4c09-ae66-b9550eb3abe3',
114+
name: 'My custom view',
115+
path: 'my-custom-view-1504f07bee3f4c09ae66b9550eb3abe3',
116+
}],
117+
}],
118+
}),
119+
},
120+
);
121+
expect(screen.getByText(CUSTOM_VIEW_MOCK_CONTENT)).toBeInTheDocument();
122+
});
123+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ReactNode, useMemo } from 'react';
2+
import RootCustomView from './Root';
3+
import { useCustomViews } from './useCustomViews';
4+
import { CustomViewsInfo } from './types';
5+
import NotionLikeRedirector, { type NotionLikePageInfo } from '../../../components/NotionLikeRedirector';
6+
7+
interface CustomViewRedirectorProps {
8+
entityType: string;
9+
Fallback: ReactNode;
10+
}
11+
12+
const renderMatch = (info: NotionLikePageInfo) =>
13+
<RootCustomView customViewId={(info as CustomViewsInfo[number]).id} />;
14+
15+
const CustomViewRedirector = ({ entityType, Fallback }: CustomViewRedirectorProps) => {
16+
const { customViews } = useCustomViews(entityType);
17+
const pagesInfo = useMemo(() => customViews.reduce(
18+
(acc, customViewInfo) => ({
19+
...acc,
20+
[customViewInfo.id.replaceAll('-', '')]: customViewInfo,
21+
}), {} as Record<string, CustomViewsInfo[number]>,
22+
), [customViews]);
23+
return (
24+
<NotionLikeRedirector
25+
renderMatch={renderMatch}
26+
NoMatch={Fallback}
27+
pagesInfo={pagesInfo}
28+
/>
29+
);
30+
};
31+
32+
export default CustomViewRedirector;

0 commit comments

Comments
 (0)