Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NETOBSERV-783 multiple page loads with quick filters #273

Merged
merged 3 commits into from
Jan 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions web/src/components/__tests-data__/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const ConfigResultSample = {
portNaming: {
enable: true,
portNames: new Map([['3100', 'loki']])
},
quickFilters: []
};
20 changes: 16 additions & 4 deletions web/src/components/__tests__/netflow-traffic.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@ import { mount, render, shallow } from 'enzyme';
import * as React from 'react';
import { waitFor } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { getFlows, getTopology } from '../../api/routes';
import { getConfig, getFlows, getTopology } from '../../api/routes';
import NetflowTraffic from '../netflow-traffic';
import { extensionsMock } from '../__tests-data__/extensions';
import { FlowsResultSample } from '../__tests-data__/flows';
import NetflowTrafficParent from '../netflow-traffic-parent';
import { TopologyResult } from '../../api/loki';
import { ConfigResultSample } from '../__tests-data__/config';

const useResolvedExtensionsMock = useResolvedExtensions as jest.Mock;

jest.mock('../../api/routes', () => ({
getConfig: jest.fn(() => Promise.resolve(ConfigResultSample)),
getFlows: jest.fn(() => Promise.resolve(FlowsResultSample)),
getTopology: jest.fn(() =>
Promise.resolve({ metrics: [], stats: { numQueries: 0, limitReached: false } } as TopologyResult)
)
}));
const getConfigMock = getConfig as jest.Mock;
const getFlowsMock = getFlows as jest.Mock;
const getTopologyMock = getTopology as jest.Mock;

Expand All @@ -41,13 +44,22 @@ describe('<NetflowTraffic />', () => {
});

it('should refresh on button click', async () => {
act(() => {
const wrapper = mount(<NetflowTrafficParent />);
//should have called getTopology 4 times after click (2 for current scope and 2 for app)
const wrapper = mount(<NetflowTrafficParent />);
await waitFor(() => {
//config is get only once
expect(getConfigMock).toHaveBeenCalledTimes(1);
expect(getFlowsMock).toHaveBeenCalledTimes(0);
//should have called getTopology 2 times on render (1 for current scope and 1 for app)
expect(getTopologyMock).toHaveBeenCalledTimes(2);
});
await act(async () => {
wrapper.find('#refresh-button').at(0).simulate('click');
});
await waitFor(() => {
//config is get only once
expect(getConfigMock).toHaveBeenCalledTimes(1);
expect(getFlowsMock).toHaveBeenCalledTimes(0);
//should have called getTopology 4 times after click (2 for current scope and 2 for app)
expect(getTopologyMock).toHaveBeenCalledTimes(4);
});
});
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/filters/filters-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import './filters-toolbar.css';
export interface FiltersToolbarProps {
id: string;
filters?: Filter[];
forcedFilters?: Filter[];
forcedFilters?: Filter[] | null;
skipTipsDelay?: boolean;
setFilters: (v: Filter[]) => void;
clearFilters: () => void;
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/netflow-traffic-parent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class NetflowTrafficParent extends React.Component<Props, State> {
return this.props.children;
}
// else render default NetworkTraffic
return <NetflowTraffic />;
return <NetflowTraffic forcedFilters={null} />;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need a distinction null vs undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from netflow-tab, the prop will be undefined before being set.

As we force null in netflow-traffic-parent, we make the distinction between the loading state from tabs and the null state from page to skip a tick before confing loaded

}
}

Expand Down
93 changes: 59 additions & 34 deletions web/src/components/netflow-traffic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ import HistogramContainer from './metrics/histogram';
export type ViewId = 'overview' | 'table' | 'topology';

export const NetflowTraffic: React.FC<{
forcedFilters?: Filter[];
forcedFilters?: Filter[] | null;
isTab?: boolean;
}> = ({ forcedFilters, isTab }) => {
const { push } = useHistory();
Expand Down Expand Up @@ -207,7 +207,8 @@ export const NetflowTraffic: React.FC<{
const [selectedElement, setSelectedElement] = React.useState<GraphElementPeer | undefined>(undefined);
const searchRef = React.useRef<SearchHandle>(null);
const [searchEvent, setSearchEvent] = React.useState<SearchEvent | undefined>(undefined);
const isInit = React.useRef(true);
//use this ref to list any props / content loading state & events to skip tick function
const initState = React.useRef<Array<'initDone' | 'configLoading' | 'configLoaded' | 'forcedFiltersLoaded'>>([]);
const [panels, setSelectedPanels] = useLocalStorage<OverviewPanel[]>(
LOCAL_STORAGE_OVERVIEW_IDS_KEY,
getDefaultOverviewPanels(),
Expand All @@ -222,16 +223,15 @@ export const NetflowTraffic: React.FC<{
});
const [columnSizes, setColumnSizes] = useLocalStorage<ColumnSizeMap>(LOCAL_STORAGE_COLS_SIZES_KEY, {});

React.useEffect(() => {
loadConfig().then(setConfig);
}, [setConfig]);

const getQuickFilters = React.useCallback(() => parseQuickFilters(t, config.quickFilters), [t, config]);
const getQuickFilters = React.useCallback((c = config) => parseQuickFilters(t, c.quickFilters), [t, config]);

const getDefaultFilters = React.useCallback(() => {
const quickFilters = getQuickFilters();
return quickFilters.filter(qf => qf.default).flatMap(qf => qf.filters);
}, [getQuickFilters]);
const getDefaultFilters = React.useCallback(
(c = config) => {
const quickFilters = getQuickFilters(c);
return quickFilters.filter(qf => qf.default).flatMap(qf => qf.filters);
},
[config, getQuickFilters]
);

// updates table filters and clears up the table for proper visualization of the
// updating process
Expand All @@ -245,23 +245,12 @@ export const NetflowTraffic: React.FC<{
[setFilters, setFlows, setWarningMessage]
);

const resetDefaultFilters = React.useCallback(() => {
updateTableFilters(getDefaultFilters());
}, [getDefaultFilters, updateTableFilters]);

React.useEffect(() => {
// Init state from URL
if (!forcedFilters) {
const filtersPromise = getFiltersFromURL(t, disabledFilters);
if (filtersPromise) {
filtersPromise.then(updateTableFilters);
} else {
resetDefaultFilters();
}
}
// disabling exhaustive-deps: only for init
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [forcedFilters, config]);
const resetDefaultFilters = React.useCallback(
(c = config) => {
updateTableFilters(getDefaultFilters(c));
},
[config, getDefaultFilters, updateTableFilters]
);

const clearSelections = () => {
setTRModalOpen(false);
Expand Down Expand Up @@ -374,6 +363,14 @@ export const NetflowTraffic: React.FC<{
);

const tick = React.useCallback(() => {
// skip tick while forcedFilters & config are not loaded
// this check ensure tick will not be called during init
// as it's difficult to manage react state changes
if (!initState.current.includes('forcedFiltersLoaded') || !initState.current.includes('configLoaded')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to have a union type for initState such as array of 'forcedFiltersLoaded' | 'configLoaded' | ... (e.g. to avoid typos)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console.error('tick skipped', initState.current);
return;
}

setLoading(true);
setError(undefined);
const fq = buildFlowQuery();
Expand Down Expand Up @@ -455,21 +452,49 @@ export const NetflowTraffic: React.FC<{
setLoading(false);
break;
}
}, [buildFlowQuery, histogramRange, manageWarnings, range, selectedViewId, showHistogram]);
}, [buildFlowQuery, histogramRange, manageWarnings, range, selectedViewId, showHistogram, initState]);

usePoll(tick, interval);

// tick on state change
React.useEffect(() => {
// Skip on init if forcedFilters not set
if (isInit.current) {
isInit.current = false;
if (!forcedFilters) {
// init function will be triggered only once
if (!initState.current.includes('initDone')) {
initState.current.push('initDone');

// load config only once and track its state
if (!initState.current.includes('configLoading')) {
initState.current.push('configLoading');
loadConfig().then(v => {
initState.current.push('configLoaded');
setConfig(v);
if (forcedFilters === null) {
//set filters from url or freshly loaded quick filters defaults
const filtersPromise = getFiltersFromURL(t, disabledFilters);
if (filtersPromise) {
filtersPromise.then(updateTableFilters);
} else {
resetDefaultFilters(v);
}
}
});
}

// init will trigger this useEffect update loop as soon as config is loaded
return;
}

if (!initState.current.includes('forcedFiltersLoaded') && forcedFilters !== undefined) {
initState.current.push('forcedFiltersLoaded');
//in case forcedFilters are null, we only track config update
if (forcedFilters === null) {
return;
}
}

tick();
}, [forcedFilters, tick]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [forcedFilters, config, tick, setConfig]);

// Rewrite URL params on state change
React.useEffect(() => {
Expand Down