Skip to content

Commit

Permalink
[frontend] Feature flag for filters V2 (#1294)
Browse files Browse the repository at this point in the history
  • Loading branch information
RomuDeuxfois authored Aug 30, 2024
1 parent 0b34698 commit 09fe482
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ const inlineStyles: Record<string, CSSProperties> = {
},
'inject_status.tracking_sent_date': {
width: '15%',
cursor: 'default',
},
inject_status: {
width: '10%',
cursor: 'default',
},
inject_targets: {
width: '20%',
Expand Down
112 changes: 106 additions & 6 deletions openbas-front/src/admin/components/scenarios/Scenarios.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { makeStyles } from '@mui/styles';
import { List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { Card, CardActionArea, CardContent, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { MovieFilterOutlined } from '@mui/icons-material';
import React, { CSSProperties, useMemo, useState } from 'react';
import React, { CSSProperties, useEffect, useMemo, useState } from 'react';
import classNames from 'classnames';
import { useFormatter } from '../../../components/i18n';
import { useHelper } from '../../../store';
import type { TagHelper, UserHelper } from '../../../actions/helper';
import { searchScenarios } from '../../../actions/scenarios/scenario-actions';
import { fetchScenarioStatistic, searchScenarios } from '../../../actions/scenarios/scenario-actions';
import type { ScenarioStore } from '../../../actions/scenarios/Scenario';
import ScenarioCreation from './ScenarioCreation';
import Breadcrumbs from '../../../components/Breadcrumbs';
import { initSorting } from '../../../components/common/queryable/Page';
import ItemTags from '../../../components/ItemTags';
import ItemSeverity from '../../../components/ItemSeverity';
import PlatformIcon from '../../../components/PlatformIcon';
Expand All @@ -22,11 +22,28 @@ import { useAppDispatch } from '../../../utils/hooks';
import useQueryable from '../../../components/common/queryable/useQueryable';
import { buildSearchPagination } from '../../../components/common/queryable/QueryableUtils';
import ScenarioPopover from './scenario/ScenarioPopover';
import { fetchStatistics } from '../../../actions/Application';
import SortHeadersComponentV2 from '../../../components/common/queryable/sort/SortHeadersComponentV2';
import PaginationComponentV2 from '../../../components/common/queryable/pagination/PaginationComponentV2';
import type { Theme } from '../../../components/Theme';
import type { FilterGroup, ScenarioStatistic } from '../../../utils/api-types';
import { scenarioCategories } from './ScenarioForm';
import { buildEmptyFilter } from '../../../components/common/queryable/filter/FilterUtils';
import { initSorting } from '../../../components/common/queryable/Page';

const useStyles = makeStyles(() => ({
const useStyles = makeStyles((theme: Theme) => ({
card: {
overflow: 'hidden',
width: 250,
height: 100,
marginRight: 20,
},
cardSelected: {
border: `1px solid ${theme.palette.secondary.main}`,
},
area: {
width: '100%',
height: '100%',
},
itemHead: {
textTransform: 'uppercase',
cursor: 'pointer',
Expand Down Expand Up @@ -66,6 +83,7 @@ const inlineStyles: Record<string, CSSProperties> = {
},
scenario_tags: {
width: '18%',
cursor: 'default',
},
scenario_updated_at: {
width: '10%',
Expand Down Expand Up @@ -150,10 +168,67 @@ const Scenarios = () => {

const [scenarios, setScenarios] = useState<ScenarioStore[]>([]);

// Category filter
const CATEGORY_FILTER_KEY = 'scenario_category';
const scenarioFilter: FilterGroup = {
mode: 'and',
filters: [buildEmptyFilter(CATEGORY_FILTER_KEY, 'eq')],
};
const { queryableHelpers, searchPaginationInput } = useQueryable('scenarios', buildSearchPagination({
sorts: initSorting('scenario_updated_at', 'DESC'),
filterGroup: scenarioFilter,
}));

const handleOnClickCategory = (category?: string) => {
if (!category) {
// Clear filter
queryableHelpers.filterHelpers.handleAddMultipleValueFilter(
CATEGORY_FILTER_KEY,
[],
);
} else {
queryableHelpers.filterHelpers.handleAddSingleValueFilter(
CATEGORY_FILTER_KEY,
category,
);
}
};
const getCategoryValue = () => searchPaginationInput.filterGroup?.filters?.find((f) => f.key === CATEGORY_FILTER_KEY)?.values;
const hasCategory = (category: string) => getCategoryValue()?.includes(category);
const noCategory = () => getCategoryValue()?.length === 0;

// Statistic
const [statistic, setStatistic] = useState<ScenarioStatistic>();
const fetchStatistics = () => {
fetchScenarioStatistic().then((result: { data: ScenarioStatistic }) => setStatistic(result.data));
};
useEffect(() => {
fetchStatistics();
}, []);

const categoryCard = (category: string, count: number) => (
<Card
key={category}
classes={{ root: classes.card }} variant="outlined"
onClick={() => handleOnClickCategory(category)}
className={classNames({ [classes.cardSelected]: hasCategory(category) })}
>
<CardActionArea classes={{ root: classes.area }}>
<CardContent>
<div style={{ marginBottom: 10 }}>
<ItemCategory category={category} size="small" />
</div>
<div style={{ fontSize: 15, fontWeight: 600 }}>
{t(scenarioCategories.get(category) ?? category)}
</div>
<div style={{ marginTop: 10, fontSize: 12, fontWeight: 500 }}>
{count} {t('scenarios')}
</div>
</CardContent>
</CardActionArea>
</Card>
);

// Export
const exportProps = {
exportType: 'scenario',
Expand All @@ -173,6 +248,31 @@ const Scenarios = () => {
return (
<>
<Breadcrumbs variant="list" elements={[{ label: t('Scenarios'), current: true }]} />
<div style={{ display: 'flex', marginBottom: 30 }}>
<Card
key="all"
classes={{ root: classes.card }} variant="outlined"
onClick={() => handleOnClickCategory()}
className={classNames({ [classes.cardSelected]: noCategory() })}
>
<CardActionArea classes={{ root: classes.area }}>
<CardContent>
<div style={{ marginBottom: 10 }}>
<ItemCategory category="all" size="small" />
</div>
<div style={{ fontSize: 15, fontWeight: 600 }}>
{t('All categories')}
</div>
<div style={{ marginTop: 10, fontSize: 12, fontWeight: 500 }}>
{statistic?.scenarios_global_count ?? '-'} {t('scenarios')}
</div>
</CardContent>
</CardActionArea>
</Card>
{Object.entries(statistic?.scenarios_attack_scenario_count ?? {}).map(([key, value]) => (
categoryCard(key, value)
))}
</div>
<PaginationComponentV2
fetch={searchScenarios}
searchPaginationInput={searchPaginationInput}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,15 @@ const getInlineStyles = (variant: string): Record<string, CSSProperties> => ({
},
exercise_targets: {
width: variant === 'reduced-view' ? '15%' : '17%',
cursor: 'default',
},
exercise_global_score: {
width: variant === 'reduced-view' ? '16%' : '10%',
cursor: 'default',
},
exercise_tags: {
width: variant === 'reduced-view' ? '14%' : '19%',
cursor: 'default',
},
exercise_updated_at: {
width: variant === 'reduced-view' ? '12%' : '13%',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import type { AttackPatternStore } from '../../../../actions/attack_patterns/Att
import { QueryableHelpers } from '../QueryableHelpers';
import TextSearchComponent from '../textSearch/TextSearchComponent';
import TablePaginationComponent from './TablePaginationComponent';
import FilterAutocomplete, { OptionPropertySchema } from '../filter/FilterAutocomplete';
import { OptionPropertySchema } from '../filter/FilterAutocomplete';
import useFilterableProperties from '../filter/useFilterableProperties';
import FilterChips from '../filter/FilterChips';
import FilterModeChip from '../filter/FilterModeChip';
import KillChainPhasesFilter from '../../../../admin/components/common/filters/KillChainPhasesFilter';

const useStyles = makeStyles(() => ({
parameters: {
Expand Down Expand Up @@ -63,13 +63,10 @@ const PaginationComponentV2 = <T extends object>({
const classes = useStyles();
const { t } = useFormatter();

const [properties, setProperties] = useState<PropertySchemaDTO[]>([]);
const [options, setOptions] = useState<OptionPropertySchema[]>([]);
const [_properties, setProperties] = useState<PropertySchemaDTO[]>([]);
const [_options, setOptions] = useState<OptionPropertySchema[]>([]);

useEffect(() => {
// Retrieve input from uri
queryableHelpers.uriHelpers.retrieveFromUri();

if (entityPrefix) {
useFilterableProperties(entityPrefix, availableFilterNames).then((propertySchemas: PropertySchemaDTO[]) => {
const newOptions = propertySchemas.filter((property) => property.schema_property_name !== MITRE_FILTER_KEY)
Expand All @@ -83,9 +80,6 @@ const PaginationComponentV2 = <T extends object>({
}, []);

useEffect(() => {
// Modify URI
queryableHelpers.uriHelpers.updateUri();

// Fetch datas
fetch(searchPaginationInput).then((result: { data: Page<T> }) => {
const { data } = result;
Expand All @@ -95,7 +89,6 @@ const PaginationComponentV2 = <T extends object>({
}, [searchPaginationInput]);

// Filters
const [pristine, setPristine] = useState(true);
const [openMitreFilter, setOpenMitreFilter] = React.useState(false);

const computeAttackPatternNameForFilter = () => {
Expand All @@ -118,13 +111,9 @@ const PaginationComponentV2 = <T extends object>({
textSearchHelpers={queryableHelpers.textSearchHelpers}
/>
)}
<FilterAutocomplete
filterGroup={searchPaginationInput.filterGroup}
helpers={queryableHelpers.filterHelpers}
options={options}
setPristine={setPristine}
style={{ marginLeft: searchEnable ? 10 : 0 }}
/>
{availableFilterNames?.includes(`${entityPrefix}_kill_chain_phases`) && (
<KillChainPhasesFilter filterKey={`${entityPrefix}_kill_chain_phases`} helpers={queryableHelpers.filterHelpers} />
)}
{queryableHelpers.filterHelpers && availableFilterNames?.includes('injector_contract_attack_patterns') && (
<>
<div style={{ cursor: 'pointer' }} onClick={() => setOpenMitreFilter(true)}>
Expand Down Expand Up @@ -181,13 +170,6 @@ const PaginationComponentV2 = <T extends object>({
)}
</>
)}
<FilterChips
propertySchemas={properties}
filterGroup={searchPaginationInput.filterGroup}
availableFilterNames={availableFilterNames?.filter((n) => n !== MITRE_FILTER_KEY)}
helpers={queryableHelpers.filterHelpers}
pristine={pristine}
/>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useLocalStorage } from 'usehooks-ts';
import { useState } from 'react';
import useFiltersState from './filter/useFiltersState';
import type { FilterGroup, SearchPaginationInput, SortField } from '../../../utils/api-types';
import useTextSearchState from './textSearch/useTextSearchState';
Expand All @@ -8,10 +8,10 @@ import useSortState from './sort/useSortState';
import useUriState from './uri/useUriState';
import { buildSearchPagination } from './QueryableUtils';

const useQueryable = (localStorageKey: string, initSearchPaginationInput: Partial<SearchPaginationInput>) => {
const useQueryable = (_localStorageKey: string, initSearchPaginationInput: Partial<SearchPaginationInput>) => {
const finalSearchPaginationInput: SearchPaginationInput = buildSearchPagination(initSearchPaginationInput);

const [searchPaginationInput, setSearchPaginationInput] = useLocalStorage<SearchPaginationInput>(localStorageKey, finalSearchPaginationInput);
const [searchPaginationInput, setSearchPaginationInput] = useState<SearchPaginationInput>(finalSearchPaginationInput);

// Text Search
const textSearchHelpers = useTextSearchState(searchPaginationInput.textSearch, (textSearch: string, page: number) => setSearchPaginationInput({
Expand Down

0 comments on commit 09fe482

Please sign in to comment.