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
3 changes: 3 additions & 0 deletions web/locales/en/plugin__netobserv-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"View alert details": "View alert details",
"View health dashboard": "View health dashboard",
"View FlowCollector status": "View FlowCollector status",
"Sampling is enabled": "Sampling is enabled",
"View sampling & resource usage": "View sampling & resource usage",
"Not all network flows are captured. Current sampling rate is 1:{{rate}}, meaning approximately 1 in every {{rate}} packets is captured.": "Not all network flows are captured. Current sampling rate is 1:{{rate}}, meaning approximately 1 in every {{rate}} packets is captured.",
"Name": "Name",
"Subnet label": "Subnet label",
"IP": "IP",
Expand Down
98 changes: 98 additions & 0 deletions web/src/components/alerts/__tests__/sampling-banner.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import * as React from 'react';

import { SamplingBanner } from '../sampling-banner';

// Mock the url module
const mockNavigate = jest.fn();
jest.mock('../../../utils/url', () => ({
flowCollectorSetupPath: '/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector/setup',
useNavigate: () => mockNavigate
}));

// Mock i18next
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, params?: Record<string, unknown>) => {
if (key === 'Sampling is enabled') return 'Sampling is enabled';
if (key === 'View sampling & resource usage') return 'View sampling & resource usage';
if (
key ===
'Not all network flows are captured. Current sampling rate is 1:{{rate}}, meaning approximately 1 in every {{rate}} packets is captured.'
) {
return `Not all network flows are captured. Current sampling rate is 1:${params?.rate}, meaning approximately 1 in every ${params?.rate} packets is captured.`;
}
return key;
}
})
}));

const SAMPLING_BANNER_DISMISSED_KEY = 'netobserv.sampling-banner-dismissed';

describe('<SamplingBanner />', () => {
beforeEach(() => {
localStorage.clear();
mockNavigate.mockClear();
});

it('should render when sampling > 1', () => {
const { container } = render(<SamplingBanner samplingValue={50} />);

expect(container.querySelector('[data-test="sampling-banner"]')).toBeTruthy();
expect(screen.getByText('Sampling is enabled')).toBeTruthy();
});

it('should not render when sampling = 0', () => {
const { container } = render(<SamplingBanner samplingValue={0} />);
expect(container.querySelector('[data-test="sampling-banner"]')).toBeFalsy();
});

it('should not render when sampling = 1', () => {
const { container } = render(<SamplingBanner samplingValue={1} />);
expect(container.querySelector('[data-test="sampling-banner"]')).toBeFalsy();
});

it('should dismiss and save to localStorage', async () => {
const { container, getByLabelText } = render(<SamplingBanner samplingValue={50} />);

expect(container.querySelector('[data-test="sampling-banner"]')).toBeTruthy();

// PF5 Alert close button has aria-label like "Close Info alert: alert: Sampling is enabled"
const closeButton = getByLabelText(/Close Info alert/);
expect(closeButton).toBeTruthy();

await act(async () => {
fireEvent.click(closeButton);
});

await waitFor(() => {
expect(container.querySelector('[data-test="sampling-banner"]')).toBeFalsy();
});

expect(localStorage.getItem(SAMPLING_BANNER_DISMISSED_KEY)).toBe('true');
});

it('should not render if dismissed', () => {
localStorage.setItem(SAMPLING_BANNER_DISMISSED_KEY, 'true');

const { container } = render(<SamplingBanner samplingValue={50} />);
expect(container.querySelector('[data-test="sampling-banner"]')).toBeFalsy();
});

it('should navigate on action link click', async () => {
const { container } = render(<SamplingBanner samplingValue={50} />);

expect(container.querySelector('[data-test="sampling-banner"]')).toBeTruthy();

const actionLink = container.querySelector('[data-test-id="sampling-action-link"]');
expect(actionLink).toBeTruthy();

await act(async () => {
fireEvent.click(actionLink!);
});

expect(mockNavigate).toHaveBeenCalledWith(
'/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector/setup?tab=consumption'
);
});
});
4 changes: 4 additions & 0 deletions web/src/components/alerts/banner.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@
position: absolute;
z-index: 999;
}

.netobserv-sampling-alert {
margin-bottom: var(--pf-global--spacer--md);
}
57 changes: 57 additions & 0 deletions web/src/components/alerts/sampling-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Alert, AlertActionCloseButton, AlertActionLink } from '@patternfly/react-core';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { flowCollectorSetupPath, useNavigate } from '../../utils/url';
import './banner.css';

const SAMPLING_BANNER_DISMISSED_KEY = 'netobserv.sampling-banner-dismissed';

export interface SamplingBannerProps {
samplingValue: number;
}

export const SamplingBanner: React.FC<SamplingBannerProps> = ({ samplingValue }) => {
const { t } = useTranslation('plugin__netobserv-plugin');
const navigate = useNavigate();
const [isDismissed, setIsDismissed] = React.useState(() => {
return localStorage.getItem(SAMPLING_BANNER_DISMISSED_KEY) === 'true';
});

const handleDismiss = () => {
localStorage.setItem(SAMPLING_BANNER_DISMISSED_KEY, 'true');
setIsDismissed(true);
};

// Don't show if sampling <= 1 (all flows captured) or already dismissed
if (samplingValue <= 1 || isDismissed) {
return null;
}

// Link to FlowCollector setup wizard Consumption tab (requires PR #1570)
const configLink = flowCollectorSetupPath + '?tab=consumption';

return (
<div className="netobserv-sampling-alert" data-test="sampling-banner">
<Alert
title={t('Sampling is enabled')}
isInline={true}
variant="info"
actionClose={<AlertActionCloseButton onClose={handleDismiss} />}
actionLinks={
<React.Fragment>
<AlertActionLink data-test-id="sampling-action-link" onClick={() => navigate(configLink)}>
{t('View sampling & resource usage')}
</AlertActionLink>
</React.Fragment>
}
>
{t(
'Not all network flows are captured. Current sampling rate is 1:{{rate}}, meaning approximately 1 in every {{rate}} packets is captured.',
{ rate: samplingValue }
)}
</Alert>
</div>
);
};

export default SamplingBanner;
20 changes: 19 additions & 1 deletion web/src/components/forms/flowCollector-wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ export const FlowCollectorWizard: FC<FlowCollectorWizardProps> = props => {
const params = useParams<{ name?: string }>();
const navigate = useNavigate();

// Get initial step from URL parameter
const getInitialStep = React.useCallback(() => {
const urlParams = new URLSearchParams(window.location.search);
const tabParam = urlParams.get('tab');
const validSteps = ['overview', 'processing', 'loki', 'consumption'];
if (tabParam && validSteps.includes(tabParam)) {
return validSteps.indexOf(tabParam) + 1;
}
return 1;
}, []);

const [startIndex] = React.useState(getInitialStep());

const form = React.useCallback(
(errors?: string[]) => {
if (!schema) {
Expand Down Expand Up @@ -151,7 +164,12 @@ export const FlowCollectorWizard: FC<FlowCollectorWizardProps> = props => {
</Title>
</div>
<div id="wizard-container">
<Wizard onStepChange={onStepChange} onSave={() => ctx.onSubmit(data)} onClose={() => navigateTo('/')}>
<Wizard
startIndex={startIndex}
onStepChange={onStepChange}
onSave={() => ctx.onSubmit(data)}
onClose={() => navigateTo('/')}
>
<WizardStep name={t('Overview')} id="overview">
<span className="co-pre-line">
{t(
Expand Down
3 changes: 3 additions & 0 deletions web/src/components/netflow-traffic-parent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { NamespaceBar } from '@openshift-console/dynamic-plugin-sdk';
import { Bullseye, PageSection, Spinner } from '@patternfly/react-core';
import * as React from 'react';
import { getRole } from '../api/routes';
import { config } from '../utils/config';
import AlertFetcher from './alerts/fetcher';
import SamplingBanner from './alerts/sampling-banner';
import { NetflowTraffic, NetflowTrafficProps } from './netflow-traffic';

type Props = NetflowTrafficProps & {};
Expand Down Expand Up @@ -46,6 +48,7 @@ class NetflowTrafficParent extends React.Component<Props, State> {
return (
<AlertFetcher>
<>
<SamplingBanner samplingValue={config.sampling} />
{!this.props.forcedNamespace && this.state.role === 'dev' && (
<NamespaceBar onNamespaceChange={ns => this.setState({ namespace: ns })} />
)}
Expand Down
8 changes: 5 additions & 3 deletions web/src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ export const useParams = <T extends Record<string, string | undefined> = Record<
};

export const netflowTrafficPath = '/netflow-traffic';
export const flowCollectorNewPath = '/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector/~new';
export const flowCollectorEditPath = '/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector/cluster';
export const flowCollectorStatusPath = '/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector/status';
export const flowCollectorBasePath = '/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector';
export const flowCollectorNewPath = `${flowCollectorBasePath}/~new`;
export const flowCollectorSetupPath = `${flowCollectorBasePath}/setup`;
export const flowCollectorEditPath = `${flowCollectorBasePath}/cluster`;
export const flowCollectorStatusPath = `${flowCollectorBasePath}/status`;
export const flowMetricNewPath = '/k8s/cluster/flows.netobserv.io~v1alpha1~FlowMetric/~new';
export const flowCollectorSliceNewPath = '/k8s/cluster/flows.netobserv.io~v1alpha1~FlowCollectorSlice/~new';

Expand Down
7 changes: 5 additions & 2 deletions web/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,8 +490,11 @@ if (process.env.FLAVOR === 'static') {
{
type: "console.page/route",
properties: {
// add FlowCollector wizard to 'Installed Operator' -> 'Create' action
path: "/k8s/ns/:namespace/operators.coreos.com~v1alpha1~ClusterServiceVersion/:operator/flows.netobserv.io~v1beta2~FlowCollector/~new",
path: [
// add FlowCollector wizard to 'Installed Operator' -> 'Create' action
"/k8s/ns/:namespace/operators.coreos.com~v1alpha1~ClusterServiceVersion/:operator/flows.netobserv.io~v1beta2~FlowCollector/~new",
"/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector/setup"
],
component: {
"$codeRef": "flowCollectorWizard.default"
}
Expand Down
Loading