Skip to content
Open
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
5 changes: 5 additions & 0 deletions changelog/unreleased/issue-25916.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type = "f"
message = "Add a traffic type selector to the cluster traffic graph so pre-7.1 history remains viewable via the outgoing traffic view."

issues = ["25916"]
pulls = ["26548"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import { render, screen } from 'wrappedTestingLibrary';

import asMock from 'helpers/mocking/AsMock';
import selectEvent from 'helpers/selectEvent';
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
import useGraphDays from 'components/common/Graph/contexts/useGraphDays';
import useGraphWidth from 'components/common/Graph/useGraphWidth';
import useLocation from 'routing/useLocation';
import useClusterTraffic from 'components/cluster/hooks/useClusterTraffic';

import ClusterTrafficGraph from './ClusterTrafficGraph';

jest.mock('logic/telemetry/useSendTelemetry');
jest.mock('components/common/Graph/contexts/useGraphDays');
jest.mock('components/common/Graph/useGraphWidth');
jest.mock('routing/useLocation');
jest.mock('components/cluster/hooks/useClusterTraffic');

const GIB = 1024 * 1024 * 1024;

describe('ClusterTrafficGraph', () => {
beforeEach(() => {
jest.clearAllMocks();

asMock(useSendTelemetry).mockReturnValue(jest.fn());
asMock(useGraphDays).mockReturnValue({
graphDays: 30,
setGraphDays: jest.fn(),
});
asMock(useGraphWidth).mockReturnValue({
graphWidth: 1000,
graphContainerRef: React.createRef(),
});
asMock(useLocation).mockReturnValue({
pathname: '/system/overview',
search: '',
hash: '',
state: undefined,
key: 'default',
});
asMock(useClusterTraffic).mockReturnValue({
isLoading: false,
traffic: {
from: '2022-09-01T00:00:00.000Z',
to: '2022-09-03T00:00:00.000Z',
input: {
'2022-09-01T00:00:00.000Z': 2 * GIB,
'2022-09-02T00:00:00.000Z': 2 * GIB,
},
output: {
'2022-09-01T00:00:00.000Z': 2 * GIB,
'2022-09-02T00:00:00.000Z': 4 * GIB,
},
decoded: {
'2022-09-01T00:00:00.000Z': 2 * GIB,
'2022-09-02T00:00:00.000Z': 2 * GIB,
},
input_indexed: {
'2022-09-01T00:00:00.000Z': GIB,
'2022-09-02T00:00:00.000Z': 2 * GIB,
},
},
});
});

it('shows incoming (input_indexed) traffic by default', async () => {
render(<ClusterTrafficGraph />);

await screen.findByRole('heading', { name: /Incoming traffic/ });
expect(screen.getByText(/Last 30 days: 3(\.0)?\s?GiB/)).toBeInTheDocument();
});

it('switches to output traffic when selecting the outgoing view', async () => {
render(<ClusterTrafficGraph />);

await selectEvent.select(await screen.findByLabelText('Show traffic type'), 'Outgoing traffic');

await screen.findByRole('heading', { name: /Outgoing traffic/ });
expect(screen.getByText(/Last 30 days: 6(\.0)?\s?GiB/)).toBeInTheDocument();
});

it('handles missing input_indexed data and still allows switching to outgoing traffic', async () => {
asMock(useClusterTraffic).mockReturnValue({
isLoading: false,
traffic: {
from: '2022-09-01T00:00:00.000Z',
to: '2022-09-03T00:00:00.000Z',
input: {
'2022-09-01T00:00:00.000Z': 2 * GIB,
},
output: {
'2022-09-01T00:00:00.000Z': 2 * GIB,
'2022-09-02T00:00:00.000Z': 4 * GIB,
},
decoded: {
'2022-09-01T00:00:00.000Z': 2 * GIB,
},
},
});

render(<ClusterTrafficGraph />);

await screen.findByRole('heading', { name: /Incoming traffic/ });
expect(screen.queryByText(/Last 30 days:/)).not.toBeInTheDocument();

await selectEvent.select(await screen.findByLabelText('Show traffic type'), 'Outgoing traffic');

await screen.findByRole('heading', { name: /Outgoing traffic/ });
expect(screen.getByText(/Last 30 days: 6(\.0)?\s?GiB/)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,24 @@
*/

import * as React from 'react';
import { useState } from 'react';

import { TrafficGraphWithDaySelect } from 'components/common/Graph';
import useGraphDays from 'components/common/Graph/contexts/useGraphDays';
import type { TrafficType } from 'components/common/Graph/types';

import useClusterTraffic from './hooks/useClusterTraffic';

const ClusterTrafficGraph = () => {
const { graphDays } = useGraphDays();
const { traffic } = useClusterTraffic(graphDays);
const [trafficType, setTrafficType] = useState<TrafficType>('input-indexed');

return <TrafficGraphWithDaySelect traffic={traffic?.input_indexed} trafficType="input-indexed" />;
const trafficSeries = trafficType === 'input-indexed' ? traffic?.input_indexed : traffic?.output;

return (
<TrafficGraphWithDaySelect traffic={trafficSeries} trafficType={trafficType} onTrafficTypeChange={setTrafficType} />
);
};

export default ClusterTrafficGraph;
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
*/
import React from 'react';
import * as Immutable from 'immutable';
import userEvent from '@testing-library/user-event';
import { render, screen, waitFor } from 'wrappedTestingLibrary';
import type { Permission } from 'graylog-web-plugin/plugin';

import { SystemClusterTraffic } from '@graylog/server-api';

import { adminUser } from 'fixtures/users';
import asMock from 'helpers/mocking/AsMock';
import selectEvent from 'helpers/selectEvent';
import useCurrentUser from 'hooks/useCurrentUser';

import GraylogClusterOverview from './GraylogClusterOverview';
Expand Down Expand Up @@ -123,18 +123,17 @@ describe('GraylogClusterOverview', () => {
it('renders GraylogClusterOverview', async () => {
render(<GraylogClusterOverview />);

expect(screen.getByText(/Incoming traffic/)).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /Incoming traffic/ })).toBeInTheDocument();

await waitFor(() => expect(SystemClusterTraffic.get).toHaveBeenCalledWith(30, false));

await screen.findByText(/Last 30 days/);
});

it('renders GraylogClusterOverview and change the days for the traffic graph', async () => {
const { getByLabelText } = render(<GraylogClusterOverview />);
const graphDaysSelect = getByLabelText('Days');
render(<GraylogClusterOverview />);

await userEvent.selectOptions(graphDaysSelect, '365');
await selectEvent.select(await screen.findByLabelText('Days'), '365');

await waitFor(() => expect(SystemClusterTraffic.get).toHaveBeenCalledWith(365, false));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ const sampleTraffic = {
};

describe('TrafficGraph', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('configures xaxis with tickformat and hoverformat to prevent millisecond display', () => {
render(<TrafficGraph traffic={sampleTraffic} width={600} />);

Expand All @@ -51,4 +55,75 @@ describe('TrafficGraph', () => {

expect(layout.xaxis.title).toEqual({ text: 'Date (UTC)' });
});

it('themes outside bar labels with the theme text color', () => {
render(<TrafficGraph traffic={sampleTraffic} width={600} />);

const { chartData } = asMock(GenericPlot).mock.calls[0][0];

expect(chartData[0].outsidetextfont).toEqual({ color: '#252D47' });
});

it('pins the yaxis so dragging selects a time range only, like search charts', () => {
render(<TrafficGraph traffic={sampleTraffic} width={600} />);

const { layout } = asMock(GenericPlot).mock.calls[0][0];

expect(layout.yaxis.fixedrange).toBe(true);
});

it('enables double-click to reset a zoomed time range', () => {
render(<TrafficGraph traffic={sampleTraffic} width={600} />);

const { config } = asMock(GenericPlot).mock.calls[0][0];

expect(config).toEqual({ doubleClick: 'reset' });
});

it('does not configure a plotly updatemenus widget', () => {
render(<TrafficGraph traffic={sampleTraffic} width={600} />);

expect(asMock(GenericPlot).mock.calls[0][0].layout.updatemenus).toBeUndefined();
});

it('fits the yaxis to the data when zoomedToData is set', () => {
const { rerender } = render(<TrafficGraph traffic={sampleTraffic} width={600} trafficLimit={1024 * 1024} />);

const initialRange = asMock(GenericPlot).mock.calls[0][0].layout.yaxis.range;

rerender(<TrafficGraph traffic={sampleTraffic} width={600} trafficLimit={1024 * 1024} zoomedToData />);

const { calls } = asMock(GenericPlot).mock;
const zoomedRange = calls[calls.length - 1][0].layout.yaxis.range;

expect(zoomedRange[1]).toBeLessThan(initialRange[1]);
});

it('passes uiRevision through as the plotly uirevision', () => {
render(<TrafficGraph traffic={sampleTraffic} width={600} uiRevision={7} />);

expect(asMock(GenericPlot).mock.calls[0][0].layout.uirevision).toBe(7);
});

it('reserves top margin so the traffic limit annotation is not clipped', () => {
render(<TrafficGraph traffic={sampleTraffic} width={600} trafficLimit={1024 * 1024} />);

expect(asMock(GenericPlot).mock.calls[0][0].layout.margin.t).toBe(28);
});

it('wires onUserZoom to the plot zoom event', () => {
const onUserZoom = jest.fn();

render(<TrafficGraph traffic={sampleTraffic} width={600} onUserZoom={onUserZoom} />);

expect(asMock(GenericPlot).mock.calls[0][0].onZoom).toBe(onUserZoom);
});

it('wires onUserZoomReset to the plot reset event', () => {
const onUserZoomReset = jest.fn();

render(<TrafficGraph traffic={sampleTraffic} width={600} onUserZoomReset={onUserZoomReset} />);

expect(asMock(GenericPlot).mock.calls[0][0].onZoomReset).toBe(onUserZoomReset);
});
});
Loading
Loading