Skip to content

Commit

Permalink
[frontend] add useOverviewLayoutCustomization hook (OpenCTI-Platform#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Goumies authored Jul 24, 2024
1 parent f198bf6 commit 87bab51
Show file tree
Hide file tree
Showing 17 changed files with 263 additions and 52 deletions.
9 changes: 9 additions & 0 deletions opencti-platform/opencti-front/src/private/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import React, { FunctionComponent, useMemo } from 'react';
import { graphql, PreloadedQuery, useFragment, usePreloadedQuery, useSubscription } from 'react-relay';
import { AnalyticsProvider } from 'use-analytics';
import Analytics from 'analytics';
import { EntitySettingSettings_entitySetting$key } from '@components/settings/sub_types/entity_setting/__generated__/EntitySettingSettings_entitySetting.graphql';
import { entitySettingFragment } from '@components/settings/sub_types/entity_setting/EntitySettingSettings';
import { ConnectedIntlProvider } from '../components/AppIntlProvider';
import { ConnectedThemeProvider } from '../components/AppThemeProvider';
import { SYSTEM_BANNER_HEIGHT } from '../public/components/SystemBanners';
Expand Down Expand Up @@ -277,6 +279,7 @@ const RootComponent: FunctionComponent<RootComponentProps> = ({ queryRef }) => {
const settings = useFragment<RootSettings$key>(rootSettingsFragment, settingsFragment);
const me = useFragment<RootMe_data$key>(meUserFragment, meFragment);

const entitySettingsData = entitySettings?.edges?.map((setting) => (useFragment<EntitySettingSettings_entitySetting$key>(entitySettingFragment, setting.node)));
const subConfig = useMemo(
() => ({
subscription,
Expand All @@ -303,6 +306,11 @@ const RootComponent: FunctionComponent<RootComponentProps> = ({ queryRef }) => {
const bannerSettings = computeBannerSettings(settings);
const platformModuleHelpers = platformModuleHelper(settings);
const platformAnalyticsConfiguration = generateAnalyticsConfig(settings);
const overviewLayoutCustomizationEntries = entitySettingsData
?.map(({ target_type, overviewLayoutCustomization }) => ({ key: target_type, values: overviewLayoutCustomization }))
.filter((entry) => !!entry.values)
.map(({ key: entityTypeKey, values: widgetsValues }) => [entityTypeKey, new Map(widgetsValues?.map(({ key, width }) => [key, width]))]);
const overviewLayoutCustomization = overviewLayoutCustomizationEntries ? new Map(overviewLayoutCustomizationEntries.map(([key, values]) => [key, values])) : new Map();
return (
<UserContext.Provider
value={{
Expand All @@ -312,6 +320,7 @@ const RootComponent: FunctionComponent<RootComponentProps> = ({ queryRef }) => {
entitySettings,
platformModuleHelpers,
schema,
overviewLayoutCustomization,
}}
>
<StyledEngineProvider injectFirst={true}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export const entitySettingFragment = graphql`
name
}
}
overviewLayoutCustomization {
key
width
}
}
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ThreatActorIndividual_ThreatActorIndividual$data,
ThreatActorIndividual_ThreatActorIndividual$key,
} from './__generated__/ThreatActorIndividual_ThreatActorIndividual.graphql';
import useOverviewLayoutCustomization from '../../../../utils/hooks/useOverviewLayoutCustomization';

// Deprecated - https://mui.com/system/styles/basics/
// Do not use it for new code.
Expand Down Expand Up @@ -156,6 +157,7 @@ const ThreatActorIndividualComponent = ({
threatActorIndividualFragment,
data,
);
const threatActorIndividualOverviewLayoutCustomization = useOverviewLayoutCustomization(threatActorIndividual.entity_type);
return (
<>
<Grid
Expand All @@ -164,55 +166,92 @@ const ThreatActorIndividualComponent = ({
rowSpacing={3}
classes={{ container: classes.gridContainer }}
>
<Grid item xs={6}>
<ThreatActorIndividualDetails
threatActorIndividualData={threatActorIndividual}
/>
</Grid>
<Grid item xs={6}>
<StixDomainObjectOverview stixDomainObject={threatActorIndividual} />
</Grid>
{hasDemographicsOrBiographics(threatActorIndividual) && (
<>
<Grid item xs={6}>
<ThreatActorIndividualDemographics
threatActorIndividual={threatActorIndividual}
/>
</Grid>
<Grid item xs={6}>
<ThreatActorIndividualBiographics
threatActorIndividual={threatActorIndividual}
/>
</Grid>
</>
)}
<Grid item xs={6}>
<SimpleStixObjectOrStixRelationshipStixCoreRelationships
stixObjectOrStixRelationshipId={threatActorIndividual.id}
stixObjectOrStixRelationshipLink={`/dashboard/threats/threat_actors_individual/${threatActorIndividual.id}/knowledge`}
/>
</Grid>
<Grid item xs={6}>
<StixCoreObjectOrStixRelationshipLastContainers
stixCoreObjectOrStixRelationshipId={threatActorIndividual.id}
/>
</Grid>
<Grid item xs={6}>
<StixCoreObjectExternalReferences
stixCoreObjectId={threatActorIndividual.id}
/>
</Grid>
<Grid item xs={6}>
<StixCoreObjectLatestHistory
stixCoreObjectId={threatActorIndividual.id}
/>
</Grid>
<Grid item xs={12}>
<StixCoreObjectOrStixCoreRelationshipNotes
stixCoreObjectOrStixCoreRelationshipId={threatActorIndividual.id}
defaultMarkings={threatActorIndividual.objectMarking ?? []}
/>
</Grid>
{
threatActorIndividualOverviewLayoutCustomization.map(({ key, width }) => {
switch (key) {
case 'details':
return (
<Grid key={key} item xs={width}>
<ThreatActorIndividualDetails
threatActorIndividualData={threatActorIndividual}
/>
</Grid>
);
case 'basicInformation':
return (
<Grid key={key} item xs={width}>
<StixDomainObjectOverview stixDomainObject={threatActorIndividual} />
</Grid>
);
case 'demographics':
if (hasDemographicsOrBiographics(threatActorIndividual)) {
return (
<Grid key={key} item xs={width}>
<ThreatActorIndividualDemographics
threatActorIndividual={threatActorIndividual}
/>
</Grid>
);
}
return undefined;
case 'biographics':
if (hasDemographicsOrBiographics(threatActorIndividual)) {
return (
<Grid key={key} item xs={width}>
<ThreatActorIndividualBiographics
threatActorIndividual={threatActorIndividual}
/>
</Grid>
);
}
return undefined;
case 'latestCreatedRelationships':
return (
<Grid key={key} item xs={width}>
<SimpleStixObjectOrStixRelationshipStixCoreRelationships
stixObjectOrStixRelationshipId={threatActorIndividual.id}
stixObjectOrStixRelationshipLink={`/dashboard/threats/threat_actors_individual/${threatActorIndividual.id}/knowledge`}
/>
</Grid>
);
case 'latestContainers':
return (
<Grid key={key} item xs={width}>
<StixCoreObjectOrStixRelationshipLastContainers
stixCoreObjectOrStixRelationshipId={threatActorIndividual.id}
/>
</Grid>
);
case 'externalReferences':
return (
<Grid key={key} item xs={width}>
<StixCoreObjectExternalReferences
stixCoreObjectId={threatActorIndividual.id}
/>
</Grid>
);
case 'mostRecentHistory':
return (
<Grid key={key} item xs={width}>
<StixCoreObjectLatestHistory
stixCoreObjectId={threatActorIndividual.id}
/>
</Grid>
);
case 'notes':
return (
<Grid key={key} item xs={width}>
<StixCoreObjectOrStixCoreRelationshipNotes
stixCoreObjectOrStixCoreRelationshipId={threatActorIndividual.id}
defaultMarkings={threatActorIndividual.objectMarking ?? []}
/>
</Grid>
);
default:
return null;
}
})
}
</Grid>
<Security needs={[KNOWLEDGE_KNUPDATE]}>
<ThreatActorIndividualEdition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10321,6 +10321,11 @@ type DefaultValueAttribute {
defaultValues: [DefaultValue!]!
}

type OverviewLayoutCustomization {
key: String!
width: Int!
}

type EntitySetting implements InternalObject & BasicObject {
id: ID!
entity_type: String!
Expand All @@ -10338,6 +10343,7 @@ type EntitySetting implements InternalObject & BasicObject {
availableSettings: [String!]!
created_at: DateTime!
updated_at: DateTime!
overviewLayoutCustomization: [OverviewLayoutCustomization!]
}

enum EntitySettingsOrdering {
Expand Down
17 changes: 15 additions & 2 deletions opencti-platform/opencti-front/src/utils/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,17 @@ export type SchemaType = {
filterKeysSchema: Map<string, Map<string, FilterDefinition>>
};

type OverviewLayoutCustomizationSettingsConfigurationWidgets = Map<string, number>;
type OverviewLayoutCustomization = Map<string, OverviewLayoutCustomizationSettingsConfigurationWidgets>;

export interface UserContextType {
me: RootMe_data$data | undefined;
settings: RootSettings$data | undefined;
bannerSettings: BannerSettings | undefined;
entitySettings: RootPrivateQuery$data['entitySettings'] | undefined;
platformModuleHelpers: ModuleHelper | undefined;
schema: SchemaType | undefined;
overviewLayoutCustomization: OverviewLayoutCustomization | undefined;
}

const defaultContext = {
Expand All @@ -49,15 +53,24 @@ const defaultContext = {
entitySettings: undefined,
platformModuleHelpers: undefined,
schema: undefined,
overviewLayoutCustomization: undefined,
};
export const UserContext = React.createContext<UserContextType>(defaultContext);

const useAuth = () => {
const { me, settings, bannerSettings, entitySettings, platformModuleHelpers, schema } = useContext(UserContext);
const {
me,
settings,
bannerSettings,
entitySettings,
platformModuleHelpers,
schema,
overviewLayoutCustomization,
} = useContext(UserContext);
if (!me || !settings || !bannerSettings || !entitySettings || !platformModuleHelpers || !schema) {
throw new Error('Invalid user context !');
}
return { me, settings, bannerSettings, entitySettings, platformModuleHelpers, schema };
return { me, settings, bannerSettings, entitySettings, platformModuleHelpers, schema, overviewLayoutCustomization };
};

export default useAuth;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import useAuth from './useAuth';

const useOverviewLayoutCustomization: (entityType: string) => Array<{ key: string, width: number }> = (entityType) => {
const { overviewLayoutCustomization } = useAuth();
return Array.from(overviewLayoutCustomization?.get(entityType)?.entries() ?? [])
.flatMap(([key, width]) => ({ key, width }));
};

export default useOverviewLayoutCustomization;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, it, expect } from 'vitest';
import { renderHook } from '@testing-library/react';
import React from 'react';
import useAuth from '../hooks/useAuth';
import { createMockUserContext, ProvidersWrapper, ProvidersWrapperProps } from './test-render';

describe('overviewLayoutCustomization', () => {
const threatActorIndividualEntityType = 'Threat-Actor-Individual';
const wrapper = ({ children }: ProvidersWrapperProps) => {
return (
<ProvidersWrapper
userContext={
createMockUserContext()
}
>
{children}
</ProvidersWrapper>
);
};
const { result } = renderHook(() => useAuth(), { wrapper });

it('should provide overview layout customization settings by block for a given entity type', () => {
const expectedWidgetsKeys = [
'details',
'basicInformation',
'demographics-biographics',
'latestCreatedRelationships',
'latestContainers',
'externalReferences',
'mostRecentHistory',
'notes',
];
const threatActorIndividualOverviewLayoutCustomization = result.current?.overviewLayoutCustomization?.get(threatActorIndividualEntityType);
expect(
Array.from(threatActorIndividualOverviewLayoutCustomization?.keys() ?? []),
).toEqual(
expectedWidgetsKeys,
);
});

it('should provide width for every widget', () => {
const threatActorIndividualOverviewLayoutCustomization = result.current?.overviewLayoutCustomization?.get(threatActorIndividualEntityType);
expect(
Array.from(threatActorIndividualOverviewLayoutCustomization?.values() ?? [])
.every((width) => !!width),
).toEqual(
true,
);
});
});
18 changes: 18 additions & 0 deletions opencti-platform/opencti-front/src/utils/tests/test-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,24 @@ interface CreateUserContextOptions {
entitySettings?: unknown,
platformModuleHelpers?: unknown,
schema?: unknown,
overviewLayoutCustomization?: unknown,
}

const threatActorIndividualConfiguration = new Map([
['details', 6],
['basicInformation', 6],
['demographics-biographics', 6],
['latestCreatedRelationships', 6],
['latestContainers', 6],
['externalReferences', 6],
['mostRecentHistory', 6],
['notes', 12],
]);
const fakeOverviewLayoutCustomization = new Map([
['Threat-Actor-Individual',
threatActorIndividualConfiguration],
]);

/**
* Create a fake user context to match your needs while testing.
* If no special need, you don't have to specify any option.
Expand All @@ -33,6 +49,7 @@ export const createMockUserContext = (options?: CreateUserContextOptions): UserC
entitySettings,
platformModuleHelpers,
schema,
overviewLayoutCustomization,
} = options ?? {};

return {
Expand All @@ -54,6 +71,7 @@ export const createMockUserContext = (options?: CreateUserContextOptions): UserC
entitySettings: (entitySettings ?? {}) as UserContextType['entitySettings'],
platformModuleHelpers: (platformModuleHelpers ?? {}) as UserContextType['platformModuleHelpers'],
schema: (schema ?? {}) as UserContextType['schema'],
overviewLayoutCustomization: (overviewLayoutCustomization ?? fakeOverviewLayoutCustomization) as UserContextType['overviewLayoutCustomization'],
};
};

Expand Down
Loading

0 comments on commit 87bab51

Please sign in to comment.