mirror of
https://github.com/Graylog2/graylog2-server.git
synced 2026-03-13 09:32:21 +08:00
Bulk-fixing linter hints. (#25171)
* Fix no-restricted-imports: replace lodash with native alternatives and fix import paths
Replace lodash/get with optional chaining, lodash/defaultTo with nullish
coalescing (??), lodash/max with Math.max, and swap react-router-dom Link
imports to use the project's router wrapper. Add eslint-disable for files
that are the wrapper implementations themselves.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix jsx-a11y/control-has-associated-label: add aria-label to controls
Add descriptive aria-label attributes to buttons, table cells, and other
interactive controls that were missing accessible labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix react/jsx-key: add missing key props to elements in iterators
Add stable key props to JSX elements inside .map() callbacks using
item IDs and unique property names as keys.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix no-param-reassign: avoid mutating function parameters
Replace reduce accumulator mutations with Object.fromEntries or spread
syntax. Use eslint-disable for browser API patterns (beforeunload).
Create new objects instead of mutating request/response parameters.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix misc TypeScript/ESLint rules: require-imports, unsafe Function type, nested ternaries, triple-slash refs, unused props/vars
- Add eslint-disable for require() in .d.ts and jest.isolateModules
- Add eslint-disable for Function type in reflux .d.ts, remove redundant
Function annotations in tests
- Refactor nested ternaries in SortUtils to if/else
- Convert triple-slash references to side-effect imports
- Fix unused prop type declarations in Timestamp and Section
- Rename catch variable e to _error in useIsLocalNode
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix testing-library/no-node-access: replace DOM traversal with TL queries
Replace .querySelector(), .firstChild, and .getElementsByClassName() with
Testing Library queries (getByRole, getByText, findByTitle, within) in
test files. Add eslint-disable where DOM access is unavoidable (Leaflet
map containers, custom text matchers).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix no-restricted-properties and prefer-user-event in tests
Replace fireEvent.submit with userEvent.click on submit buttons and
fireEvent.change with userEvent.selectOptions in ContentPackEditParameter
tests. Add submit button to ContentPackEditParameter component to enable
the userEvent.click pattern. Use eslint-disable for fireEvent.change in
InputSetupWizard test where userEvent.type causes waitFor timeouts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix jest/expect-expect and jest/no-done-callback in tests
Add eslint-disable for it.each tests where assertions are in callback
functions that ESLint cannot statically detect. Add explicit assertion
to PivotHandler type-check test. Convert GlobalThroughputStore tests
from done-callback pattern to async/await with Promises.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Link import path after router.tsx was removed in d4b1262
Update Button.tsx and MenuItem.tsx to import Link from
components/common/Link (the new dedicated wrapper) instead of the
deleted components/common/router.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Type reflux StoreDefinition generically and remove eslint-disable for Function type
Make StoreDefinition<T> generic so init is typed as () => void and
getInitialState as () => T. Make fields optional in 4 store state types
(ContentPacksStore, IndicesConfigurationStore, InputTypesStore,
CollectorsStore) whose getInitialState returns a subset of the full state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Revert ContentPackEditParameter component changes and fix ESLint hints in test only
Replace fireEvent.change with userEvent.selectOptions and extract a
submitForm helper with a single eslint-disable to avoid duplicating
the disable comment across all fireEvent.submit call sites. The
component has no submit button and jsdom doesn't support implicit
form submission, so fireEvent.submit remains necessary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use part string as React key instead of array index in RuleBlockDisplay
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Running `prettier`.
* Address PR #25171 review feedback
- Revert aria-label additions on td/th elements; increase
jsx-a11y/control-has-associated-label depth to 3 instead
- Use screen.findByRole in Autocomplete test instead of waitFor+getByRole
- Add default value for optional field prop in Timestamp component
- Rename test helpers to expectIsDeepEqual/expectIsNumericString to
satisfy jest/expect-expect without eslint-disable
- Replace require() with jest.isolateModulesAsync + dynamic import()
in DocsHelper tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove unused findValidationState from TabKeywordTimeRange test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix control-has-associated-label lint errors and SelectedGrantee test
Ignore td/th elements in control-has-associated-label rule (false
positives), add aria-label to backlog checkbox, and rename
checkCurrentState to expectCurrentState to remove eslint-disable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Replace type casts with proper types in UseCreateViewForEvent
Type function parameters for getSummaryAggregation and WidgetsGenerator
so reduce() accepts generic type parameters, removing all `as` casts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -110,7 +110,7 @@ export default [
|
||||
'no-loop-func': 'error',
|
||||
|
||||
'import/prefer-default-export': 'off',
|
||||
'jsx-a11y/control-has-associated-label': 'error',
|
||||
'jsx-a11y/control-has-associated-label': ['error', { depth: 3, ignoreElements: ['td', 'th'] }],
|
||||
'react/no-array-index-key': 'error',
|
||||
'react/no-danger': 'error',
|
||||
'react/no-unstable-nested-components': 'error',
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
// Definitions by: Mitchell Grice <https://github.com/gricey432>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import moment = require('moment');
|
||||
|
||||
export = moment;
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
declare module 'reflux' {
|
||||
import type { RefluxActions, Store } from 'stores/StoreTypes';
|
||||
|
||||
export interface StoreDefinition {
|
||||
export interface StoreDefinition<T = any> {
|
||||
listenables?: RefluxActions<any>[];
|
||||
init?: Function;
|
||||
getInitialState?: Function;
|
||||
init?: () => void;
|
||||
getInitialState?: () => T;
|
||||
|
||||
[propertyName: string]: any;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ declare module 'reflux' {
|
||||
|
||||
type ElementType<T extends ReadonlyArray<unknown>> = T extends ReadonlyArray<infer E> ? E : never;
|
||||
|
||||
export function createStore<T>(definition: StoreDefinition): Store<T> & typeof definition;
|
||||
export function createStore<T>(definition: StoreDefinition<T>): Store<T> & typeof definition;
|
||||
export function createActions<R>(definitions: ActionsDefinition): RefluxActions<R>;
|
||||
export function createActions<R>(
|
||||
definitions: R,
|
||||
|
||||
@@ -19,8 +19,8 @@ import type { ColorVariant } from '@graylog/sawmill';
|
||||
import { Button as MantineButton } from '@mantine/core';
|
||||
import type { DefaultTheme } from 'styled-components';
|
||||
import styled, { useTheme, css } from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Link from 'components/common/Link';
|
||||
import type { BsSize } from 'components/bootstrap/types';
|
||||
|
||||
const sizeForMantine = (size: BsSize) => {
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Menu as MantineMenu } from '@mantine/core';
|
||||
|
||||
import Link from 'components/common/Link';
|
||||
import Icon from 'components/common/Icon';
|
||||
|
||||
import Menu from '../Menu';
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import * as React from 'react';
|
||||
import { Formik } from 'formik';
|
||||
import { render, waitFor } from 'wrappedTestingLibrary';
|
||||
import { render, screen } from 'wrappedTestingLibrary';
|
||||
|
||||
import Autocomplete from './Autocomplete';
|
||||
|
||||
@@ -36,39 +36,30 @@ const renderAutocomplete = () =>
|
||||
|
||||
describe('Autocomplete component', () => {
|
||||
it('should render the field with a label', async () => {
|
||||
const { getByText, baseElement } = renderAutocomplete();
|
||||
let label = null;
|
||||
let pseudoInput = null;
|
||||
renderAutocomplete();
|
||||
|
||||
await waitFor(() => {
|
||||
label = getByText('Color translator');
|
||||
pseudoInput = baseElement.querySelector('[id="spaColor"]');
|
||||
});
|
||||
const label = await screen.findByText('Color translator');
|
||||
const pseudoInput = screen.getByRole('combobox');
|
||||
|
||||
expect(label).toBeVisible();
|
||||
expect(pseudoInput).toBeVisible();
|
||||
});
|
||||
|
||||
it('should let the user type', async () => {
|
||||
const { baseElement } = renderAutocomplete();
|
||||
const input = baseElement.querySelector('input');
|
||||
renderAutocomplete();
|
||||
const input = screen.getByRole('combobox');
|
||||
|
||||
await userEvent.type(input, 'Naranja');
|
||||
|
||||
expect(baseElement).toHaveTextContent('Naranja');
|
||||
expect(input).toHaveValue('Naranja');
|
||||
});
|
||||
|
||||
it('should show a list with options', async () => {
|
||||
const { baseElement } = renderAutocomplete();
|
||||
const input = baseElement.querySelector('input');
|
||||
renderAutocomplete();
|
||||
const input = screen.getByRole('combobox');
|
||||
|
||||
await userEvent.type(input, 'ver');
|
||||
let list = null;
|
||||
|
||||
await waitFor(() => {
|
||||
list = baseElement.querySelector('[class$="menu"]');
|
||||
});
|
||||
|
||||
expect(list).toBeVisible();
|
||||
expect(await screen.findByRole('listbox')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,7 @@ const ConfirmLeaveDialog = ({ question = 'Are you sure?', ignoredRoutes = [] }:
|
||||
return null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
e.returnValue = question;
|
||||
|
||||
return question;
|
||||
|
||||
@@ -98,32 +98,30 @@ describe('PaginatedList', () => {
|
||||
const currentPage = 4;
|
||||
asMock(useQueryParams).mockImplementation(() => [{ page: currentPage }, setQueryParams]);
|
||||
|
||||
const { findByTestId } = render(
|
||||
render(
|
||||
<PaginatedList totalItems={200} onChange={() => {}} activePage={3}>
|
||||
<div>The list</div>
|
||||
</PaginatedList>,
|
||||
);
|
||||
|
||||
const graylogPagination = await findByTestId('graylog-pagination');
|
||||
const activePageElement = graylogPagination.getElementsByClassName('active');
|
||||
const activePageElement = await screen.findByTitle('Active page');
|
||||
|
||||
expect(activePageElement).not.toBeNull();
|
||||
expect(activePageElement[0].textContent).toContain(`${currentPage}`);
|
||||
expect(activePageElement).toHaveTextContent(`${currentPage}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with internal state', () => {
|
||||
it('should update active page, when prop changes', async () => {
|
||||
const { findByTestId, rerender } = render(
|
||||
const { rerender } = render(
|
||||
<PaginatedList totalItems={200} onChange={() => {}} activePage={3} useQueryParameter={false}>
|
||||
<div>The list</div>
|
||||
</PaginatedList>,
|
||||
);
|
||||
|
||||
const graylogPagination = await findByTestId('graylog-pagination');
|
||||
const activePageElement = graylogPagination.getElementsByClassName('active');
|
||||
const activePageElement = await screen.findByTitle('Active page');
|
||||
|
||||
expect(activePageElement[0].textContent).toContain('3');
|
||||
expect(activePageElement).toHaveTextContent('3');
|
||||
|
||||
rerender(
|
||||
<PaginatedList totalItems={200} onChange={() => {}} activePage={1} useQueryParameter={false}>
|
||||
@@ -131,10 +129,9 @@ describe('PaginatedList', () => {
|
||||
</PaginatedList>,
|
||||
);
|
||||
|
||||
await findByTestId('graylog-pagination');
|
||||
const newActivePageElement = graylogPagination.getElementsByClassName('active');
|
||||
const newActivePageElement = await screen.findByTitle('Active page');
|
||||
|
||||
expect(newActivePageElement[0].textContent).toContain('1');
|
||||
expect(newActivePageElement).toHaveTextContent('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { fireEvent, render, screen, waitFor } from 'wrappedTestingLibrary';
|
||||
import { fireEvent, render, screen, waitFor, within } from 'wrappedTestingLibrary';
|
||||
|
||||
import AsyncCustomMenuList from './AsyncCustomMenuList';
|
||||
|
||||
jest.mock('hooks/useElementDimensions', () => () => ({ width: 1024, height: 300 }));
|
||||
|
||||
const getChildrenList: Function = (n: number): React.ReactElement[] => {
|
||||
const getChildrenList = (n: number): React.ReactElement[] => {
|
||||
const list = Array(n).fill(null);
|
||||
|
||||
return list.map(() => <div key={Math.random()}>{Math.random()}</div>);
|
||||
@@ -46,11 +46,12 @@ describe('CustomMenuList', () => {
|
||||
it('Should load more items on scrool', async () => {
|
||||
render(<AsyncCustomMenuList selectProps={mockSelectProps}>{getChildrenList(5)}</AsyncCustomMenuList>);
|
||||
|
||||
const list = screen.getByTestId('infinite-loader-container').firstChild;
|
||||
const container = screen.getByTestId('infinite-loader-container');
|
||||
const list = within(container).getByRole('list');
|
||||
|
||||
expect(list).toBeInTheDocument();
|
||||
|
||||
await fireEvent.scroll(list);
|
||||
fireEvent.scroll(list);
|
||||
|
||||
await waitFor(() => expect(loadOptions).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ import { render, screen } from 'wrappedTestingLibrary';
|
||||
|
||||
import CustomMenuList from './CustomMenuList';
|
||||
|
||||
const getChildrenList: Function = (n: number): React.ReactElement[] => {
|
||||
const getChildrenList = (n: number): React.ReactElement[] => {
|
||||
const list = Array(n).fill(null);
|
||||
|
||||
return list.map(() => <div key={Math.random()}>{Math.random()}</div>);
|
||||
|
||||
@@ -43,12 +43,12 @@ describe('<Spinner />', () => {
|
||||
});
|
||||
|
||||
it('should be visible after when delay is completed', async () => {
|
||||
const { container } = render(<Spinner />);
|
||||
render(<Spinner />);
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(200);
|
||||
});
|
||||
|
||||
expect(container.firstChild).toHaveStyle('visibility: visible');
|
||||
expect(screen.getByText('Loading...')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,8 @@ type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const DefaultRender = ({ value, field: _field }: RenderProps) => <>{value}</>;
|
||||
|
||||
/**
|
||||
* Component that renders a given date time based on the user time zone in a `time` HTML element.
|
||||
* It is capable of render date times in different formats, accepting ISO 8601
|
||||
@@ -44,9 +46,9 @@ type Props = {
|
||||
*/
|
||||
const Timestamp = ({
|
||||
dateTime = undefined,
|
||||
field,
|
||||
field = undefined,
|
||||
format = 'default',
|
||||
render: Component = ({ value }: RenderProps) => <>{value}</>,
|
||||
render: Component = DefaultRender,
|
||||
tz = undefined,
|
||||
className = undefined,
|
||||
}: Props) => {
|
||||
|
||||
@@ -20,6 +20,9 @@ import userEvent from '@testing-library/user-event';
|
||||
|
||||
import ContentPackEditParameter from 'components/content-packs/ContentPackEditParameter';
|
||||
|
||||
// eslint-disable-next-line no-restricted-properties -- component has no submit button, jsdom doesn't support implicit form submission
|
||||
const submitForm = async () => fireEvent.submit(await screen.findByTestId('parameter-form'));
|
||||
|
||||
describe('<ContentPackEditParameter />', () => {
|
||||
it('should render with empty parameters', async () => {
|
||||
render(<ContentPackEditParameter />);
|
||||
@@ -68,10 +71,10 @@ describe('<ContentPackEditParameter />', () => {
|
||||
await userEvent.type(screen.getByLabelText(/name/i), 'name');
|
||||
await userEvent.type(screen.getByLabelText(/title/i), 'title');
|
||||
await userEvent.type(screen.getByLabelText(/description/i), 'descr');
|
||||
fireEvent.change(screen.getByLabelText(/type/i), { target: { value: 'integer' } });
|
||||
await userEvent.selectOptions(screen.getByLabelText(/type/i), 'integer');
|
||||
await userEvent.type(screen.getByLabelText(/default value/i), '1');
|
||||
|
||||
fireEvent.submit(await screen.findByTestId('parameter-form'));
|
||||
await submitForm();
|
||||
|
||||
expect(changeFn).toHaveBeenCalledWith({
|
||||
name: 'name',
|
||||
@@ -91,7 +94,7 @@ describe('<ContentPackEditParameter />', () => {
|
||||
await userEvent.type(screen.getByLabelText(/description/i), 'descr');
|
||||
await userEvent.type(screen.getByLabelText(/default value/i), 'test');
|
||||
|
||||
fireEvent.submit(await screen.findByTestId('parameter-form'));
|
||||
await submitForm();
|
||||
|
||||
expect(changeFn).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -108,7 +111,7 @@ describe('<ContentPackEditParameter />', () => {
|
||||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, 'franz');
|
||||
|
||||
fireEvent.submit(await screen.findByTestId('parameter-form'));
|
||||
await submitForm();
|
||||
|
||||
expect(screen.getByText(/must be unique/i)).toBeInTheDocument();
|
||||
});
|
||||
@@ -129,64 +132,64 @@ describe('<ContentPackEditParameter />', () => {
|
||||
const nameInput = screen.getByLabelText(/name/i);
|
||||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, 'hans');
|
||||
fireEvent.submit(await screen.findByTestId('parameter-form'));
|
||||
await submitForm();
|
||||
|
||||
expect(screen.getByText(/must be unique/i)).toBeInTheDocument();
|
||||
|
||||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, 'hans-dampf');
|
||||
fireEvent.submit(await screen.findByTestId('parameter-form'));
|
||||
await submitForm();
|
||||
|
||||
expect(screen.getByText(/only contain A-Z, a-z, 0-9 and _/i)).toBeInTheDocument();
|
||||
|
||||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, 'dampf');
|
||||
fireEvent.submit(await screen.findByTestId('parameter-form'));
|
||||
await submitForm();
|
||||
|
||||
expect(screen.getByText(/must not contain a space/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should validate the parameter input from type double', async () => {
|
||||
fireEvent.change(screen.getByLabelText(/type/i), { target: { value: 'double' } });
|
||||
await userEvent.selectOptions(screen.getByLabelText(/type/i), 'double');
|
||||
await userEvent.type(screen.getByLabelText(/default value/i), 'test');
|
||||
fireEvent.submit(await screen.findByTestId('parameter-form'));
|
||||
await submitForm();
|
||||
|
||||
expect(screen.getByText(/not a double value/i)).toBeInTheDocument();
|
||||
|
||||
const input = screen.getByLabelText(/default value/i);
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, '1.0');
|
||||
fireEvent.submit(await screen.findByTestId('parameter-form'));
|
||||
await submitForm();
|
||||
|
||||
expect(screen.getByText(/default value if the parameter is not optional/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should validate the parameter input from type integer', async () => {
|
||||
fireEvent.change(screen.getByLabelText(/type/i), { target: { value: 'integer' } });
|
||||
await userEvent.selectOptions(screen.getByLabelText(/type/i), 'integer');
|
||||
await userEvent.type(screen.getByLabelText(/default value/i), 'test');
|
||||
fireEvent.submit(await screen.findByTestId('parameter-form'));
|
||||
await submitForm();
|
||||
|
||||
expect(screen.getByText(/not an integer value/i)).toBeInTheDocument();
|
||||
|
||||
const input = screen.getByLabelText(/default value/i);
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, '1');
|
||||
fireEvent.submit(await screen.findByTestId('parameter-form'));
|
||||
await submitForm();
|
||||
|
||||
expect(screen.getByText(/default value if the parameter is not optional/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should validate the parameter input from type boolean', async () => {
|
||||
fireEvent.change(screen.getByLabelText(/type/i), { target: { value: 'boolean' } });
|
||||
await userEvent.selectOptions(screen.getByLabelText(/type/i), 'boolean');
|
||||
await userEvent.type(screen.getByLabelText(/default value/i), 'test');
|
||||
fireEvent.submit(await screen.findByTestId('parameter-form'));
|
||||
await submitForm();
|
||||
|
||||
expect(screen.getByText(/must be either true or false/i)).toBeInTheDocument();
|
||||
|
||||
const input = screen.getByLabelText(/default value/i);
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, 'true');
|
||||
fireEvent.submit(await screen.findByTestId('parameter-form'));
|
||||
await submitForm();
|
||||
|
||||
expect(screen.getByText(/default value if the parameter is not optional/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import get from 'lodash/get';
|
||||
import omit from 'lodash/omit';
|
||||
|
||||
import { Alert, Col, Row } from 'components/bootstrap';
|
||||
@@ -38,6 +37,8 @@ type Props = {
|
||||
validation: {
|
||||
errors: {
|
||||
title?: string;
|
||||
field_spec?: string[];
|
||||
key_spec?: string[];
|
||||
};
|
||||
};
|
||||
onChange: (name: string, value: unknown) => void;
|
||||
@@ -103,8 +104,8 @@ const FieldsForm = ({ currentUser, eventDefinition, validation, onChange, canEdi
|
||||
);
|
||||
}
|
||||
|
||||
const fieldErrors = get(validation, 'errors.field_spec', []);
|
||||
const keyErrors = get(validation, 'errors.key_spec', []);
|
||||
const fieldErrors = validation?.errors?.field_spec ?? [];
|
||||
const keyErrors = validation?.errors?.key_spec ?? [];
|
||||
const errors = [...fieldErrors, ...keyErrors];
|
||||
|
||||
return (
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
import React from 'react';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import defaultTo from 'lodash/defaultTo';
|
||||
import max from 'lodash/max';
|
||||
import moment from 'moment';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
@@ -54,7 +52,7 @@ class NotificationSettingsForm extends React.Component<
|
||||
|
||||
const gracePeriod = extractDurationAndUnit(gracePeriodMs, TIME_UNITS);
|
||||
const defaultBacklogSize = props.defaults.default_backlog_size;
|
||||
const effectiveBacklogSize = defaultTo(backlogSize, defaultBacklogSize);
|
||||
const effectiveBacklogSize = backlogSize ?? defaultBacklogSize;
|
||||
|
||||
this.state = {
|
||||
gracePeriodDuration: gracePeriod.duration,
|
||||
@@ -73,7 +71,7 @@ class NotificationSettingsForm extends React.Component<
|
||||
};
|
||||
|
||||
handleGracePeriodChange = (nextValue, nextUnit, enabled) => {
|
||||
const durationInMs = enabled ? moment.duration(max([nextValue, 0]), nextUnit).asMilliseconds() : 0;
|
||||
const durationInMs = enabled ? moment.duration(Math.max(nextValue, 0), nextUnit).asMilliseconds() : 0;
|
||||
|
||||
this.propagateChanges('grace_period_ms', durationInMs);
|
||||
this.setState({ gracePeriodDuration: nextValue, gracePeriodUnit: nextUnit });
|
||||
@@ -84,7 +82,7 @@ class NotificationSettingsForm extends React.Component<
|
||||
const value = event.target.value === '' ? '' : FormsUtils.getValueFromInput(event.target);
|
||||
|
||||
this.setState({ [camelCase(name)]: value });
|
||||
this.propagateChanges(name, max([Number(value), 0]));
|
||||
this.propagateChanges(name, Math.max(Number(value), 0));
|
||||
};
|
||||
|
||||
toggleBacklogSize = () => {
|
||||
@@ -128,6 +126,7 @@ class NotificationSettingsForm extends React.Component<
|
||||
<input
|
||||
id="toggle_backlog_size"
|
||||
type="checkbox"
|
||||
aria-label="Toggle message backlog"
|
||||
checked={isBacklogSizeEnabled}
|
||||
onChange={this.toggleBacklogSize}
|
||||
/>
|
||||
|
||||
@@ -111,7 +111,7 @@ const ShareDetails = ({ shareState = null }: Props) => {
|
||||
const currentGranteeState = grantee.currentState(activeShares);
|
||||
|
||||
return (
|
||||
<GranteeListItemContainer $currentState={currentGranteeState}>
|
||||
<GranteeListItemContainer key={grantee.id} $currentState={currentGranteeState}>
|
||||
<GranteeInfo title={grantee.title}>
|
||||
<StyledGranteeIcon type={grantee.type} />
|
||||
<GranteeListItemTitle>{grantee.title}</GranteeListItemTitle>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import { Input, Col } from 'components/bootstrap';
|
||||
import * as FormsUtils from 'util/FormsUtils';
|
||||
@@ -43,7 +42,7 @@ const NumberExpression = ({ expression, onChange, renderLabel, validation = {} }
|
||||
name="threshold"
|
||||
label={renderLabel ? 'Threshold' : ''}
|
||||
type="number"
|
||||
value={get(expression, 'value')}
|
||||
value={expression?.value}
|
||||
bsStyle={validation.message ? 'error' : null}
|
||||
help={validation.message}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -62,6 +62,7 @@ const LegacyNotificationDetails = ({ notification }: LegacyNotificationDetailsPr
|
||||
if (key === 'body' || key === 'script_args') {
|
||||
return (
|
||||
<ReadOnlyFormGroup
|
||||
key={key}
|
||||
label={value.human_name}
|
||||
value={
|
||||
<Well bsSize="small" className={emailStyles.bodyPreview}>
|
||||
@@ -72,7 +73,7 @@ const LegacyNotificationDetails = ({ notification }: LegacyNotificationDetailsPr
|
||||
);
|
||||
}
|
||||
|
||||
return <ReadOnlyFormGroup label={value.human_name} value={configurationValues[key]} />;
|
||||
return <ReadOnlyFormGroup key={key} label={value.human_name} value={configurationValues[key]} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { PluginStore } from 'graylog-web-plugin/plugin';
|
||||
|
||||
import { FormSubmit, Select, Spinner } from 'components/common';
|
||||
@@ -154,7 +153,7 @@ const EventNotificationForm = ({
|
||||
label="Title"
|
||||
type="text"
|
||||
bsStyle={validation.errors.title ? 'error' : null}
|
||||
help={get(validation, 'errors.title[0]', 'Title to identify this Notification.')}
|
||||
help={validation?.errors?.title?.[0] ?? 'Title to identify this Notification.'}
|
||||
value={notification.title}
|
||||
onChange={handleChange}
|
||||
required
|
||||
@@ -186,7 +185,7 @@ const EventNotificationForm = ({
|
||||
clearable={false}
|
||||
required
|
||||
/>
|
||||
<HelpBlock>{get(validation, 'errors.config[0]', 'Choose the type of Notification to create.')}</HelpBlock>
|
||||
<HelpBlock>{validation?.errors?.config?.[0] ?? 'Choose the type of Notification to create.'}</HelpBlock>
|
||||
</FormGroup>
|
||||
|
||||
{notificationFormComponent}
|
||||
|
||||
@@ -29,12 +29,12 @@ export interface RotationStrategyContext {
|
||||
}
|
||||
|
||||
export type IndicesConfigurationStoreState = {
|
||||
activeRotationConfig: any;
|
||||
rotationStrategies: any;
|
||||
activeRetentionConfig: any;
|
||||
retentionStrategies: any;
|
||||
retentionStrategiesContext: RetentionStrategyContext;
|
||||
rotationStrategiesContext: RotationStrategyContext;
|
||||
activeRotationConfig?: any;
|
||||
rotationStrategies?: any;
|
||||
activeRetentionConfig?: any;
|
||||
retentionStrategies?: any;
|
||||
retentionStrategiesContext?: RetentionStrategyContext;
|
||||
rotationStrategiesContext?: RotationStrategyContext;
|
||||
};
|
||||
export type SizeBasedRotationStrategyConfig = {
|
||||
type: string;
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
/// <reference path="./types.ts" />
|
||||
import * as React from 'react';
|
||||
|
||||
import './types';
|
||||
|
||||
import usePluginEntities from 'hooks/usePluginEntities';
|
||||
|
||||
export const UnlicensedText = () => (
|
||||
|
||||
@@ -142,7 +142,7 @@ const RuleBlockDisplay = ({
|
||||
|
||||
const partsWithHighlight = parts.map((part) => {
|
||||
if (part === `'$${termToHighlight}'`) {
|
||||
return <Highlighted>{part}</Highlighted>;
|
||||
return <Highlighted key={part}>{part}</Highlighted>;
|
||||
}
|
||||
|
||||
return part;
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
/// <reference path="./types.ts" />
|
||||
import React from 'react';
|
||||
|
||||
import './types';
|
||||
|
||||
import usePluginEntities from 'hooks/usePluginEntities';
|
||||
import SecurityPage from 'components/security/teaser/SecurityPage';
|
||||
|
||||
|
||||
@@ -78,7 +78,9 @@ class SidecarList extends React.Component<
|
||||
/>
|
||||
</th>
|
||||
))}
|
||||
<th className={style.actions}> </th>
|
||||
<th className={style.actions}>
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{sidecars}</tbody>
|
||||
|
||||
@@ -75,7 +75,7 @@ class SidecarRow extends React.Component<
|
||||
<td className={style.sidecarName}>
|
||||
<Link to={Routes.SYSTEM.SIDECARS.STATUS(sidecar.node_id)}>{sidecar.node_name}</Link>
|
||||
</td>
|
||||
<td aria-label="Status">
|
||||
<td>
|
||||
<StatusIndicator
|
||||
status={sidecarStatus.status}
|
||||
message={sidecarStatus.message}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import defaultTo from 'lodash/defaultTo';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
|
||||
import { Col, Row, Button } from 'components/bootstrap';
|
||||
@@ -48,13 +47,13 @@ const formatNodeDetails = (details: NodeDetails) => {
|
||||
return (
|
||||
<dl className={`${commonStyles.deflist} ${commonStyles.topMargin}`}>
|
||||
<dt>IP Address</dt>
|
||||
<dd>{defaultTo(details.ip, 'Not available')}</dd>
|
||||
<dd>{details.ip ?? 'Not available'}</dd>
|
||||
<dt>Operating System</dt>
|
||||
<dd>{defaultTo(details.operating_system, 'Not available')}</dd>
|
||||
<dd>{details.operating_system ?? 'Not available'}</dd>
|
||||
<dt>CPU Idle</dt>
|
||||
<dd>{isNumber(metrics?.cpu_idle) ? `${metrics?.cpu_idle}%` : 'Not available'}</dd>
|
||||
<dt>Load</dt>
|
||||
<dd>{defaultTo(metrics?.load_1, 'Not available')}</dd>
|
||||
<dd>{metrics?.load_1 ?? 'Not available'}</dd>
|
||||
<dt>Volumes > 75% full</dt>
|
||||
{metrics?.disks_75 === undefined ? (
|
||||
<dd>Not available</dd>
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import { useStore } from 'stores/connect';
|
||||
import User from 'logic/users/User';
|
||||
@@ -29,7 +28,7 @@ type CurrentUserProviderProps = {
|
||||
};
|
||||
|
||||
const CurrentUserProvider = ({ children }: CurrentUserProviderProps) => {
|
||||
const currentUserJSON = useStore(CurrentUserStore, (state) => get(state, 'currentUser'));
|
||||
const currentUserJSON = useStore(CurrentUserStore, (state) => state?.currentUser);
|
||||
const currentUser = currentUserJSON ? User.fromJSON(currentUserJSON) : undefined;
|
||||
|
||||
if (!currentUser) {
|
||||
|
||||
@@ -14,25 +14,22 @@
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
import max from 'lodash/max';
|
||||
|
||||
import ContentPack from 'logic/content-packs/ContentPack';
|
||||
|
||||
export default class ContentPackRevisions {
|
||||
private _value = {};
|
||||
|
||||
constructor(contentPackRevision) {
|
||||
this._value = Object.keys(contentPackRevision).reduce((acc, rev) => {
|
||||
const contentPack = contentPackRevision[rev];
|
||||
|
||||
acc[parseInt(rev, 10)] = ContentPack.fromJSON(contentPack);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
this._value = Object.fromEntries(
|
||||
Object.keys(contentPackRevision).map((rev) => [
|
||||
parseInt(rev, 10),
|
||||
ContentPack.fromJSON(contentPackRevision[rev]),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
get latestRevision() {
|
||||
return max(this.revisions);
|
||||
return this.revisions.length > 0 ? Math.max(...this.revisions) : undefined;
|
||||
}
|
||||
|
||||
get revisions() {
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('SelectedGrantee', () => {
|
||||
const aliceIsOwner = ActiveShare.builder().grant('grant-alice-id').capability(owner.id).grantee(alice.id).build();
|
||||
const activeShares = Immutable.List([aliceIsOwner]);
|
||||
|
||||
const checkCurrentState = ({ grantee, capability, expectedReturn }) => {
|
||||
const expectCurrentState = ({ grantee, capability, expectedReturn }) => {
|
||||
const selectedGrantee = SelectedGrantee.create(grantee.id, grantee.title, grantee.type, capability.id);
|
||||
const state = selectedGrantee.currentState(activeShares);
|
||||
|
||||
@@ -37,5 +37,5 @@ describe('SelectedGrantee', () => {
|
||||
${alice} | ${owner} | ${'unchanged'}
|
||||
${alice} | ${manager} | ${'changed'}
|
||||
${bob} | ${manager} | ${'new'}
|
||||
`('should return current state of $expectedReturn grantee', checkCurrentState);
|
||||
`('should return current state of $expectedReturn grantee', ({ grantee, capability, expectedReturn }) => expectCurrentState({ grantee, capability, expectedReturn }));
|
||||
});
|
||||
|
||||
@@ -53,6 +53,9 @@ type Props = {
|
||||
title: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
titleOrder?: TitleOrder;
|
||||
};
|
||||
|
||||
type SectionProps = Props & {
|
||||
dataTestid?: string;
|
||||
};
|
||||
|
||||
@@ -85,7 +88,7 @@ const Section = ({
|
||||
actions = undefined,
|
||||
titleOrder = undefined,
|
||||
dataTestid = undefined,
|
||||
}: React.PropsWithChildren<Props>) => (
|
||||
}: React.PropsWithChildren<SectionProps>) => (
|
||||
<SectionContainer component="section" data-testid={dataTestid}>
|
||||
<SectionHeader title={title} actions={actions} titleOrder={titleOrder} />
|
||||
{children}
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
import { useLocation as useRouterLocation } from 'react-router-dom';
|
||||
import type { Location } from 'react-router-dom';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useLocation as useRouterLocation, type Location } from 'react-router-dom';
|
||||
|
||||
const useLocation = <T>(): Location<T> => useRouterLocation();
|
||||
export default useLocation;
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import isDeepEqual from '../isDeepEqual';
|
||||
|
||||
describe('isDeepEqual', () => {
|
||||
const verifyIsDeepEqual = ({ initial, next, result }) => expect(isDeepEqual(initial, next)).toBe(result);
|
||||
const expectIsDeepEqual = ({ initial, next, result }) => expect(isDeepEqual(initial, next)).toBe(result);
|
||||
|
||||
it.each`
|
||||
initial | next | result | description
|
||||
@@ -63,5 +63,5 @@ describe('isDeepEqual', () => {
|
||||
${objectWithMap()} | ${objectWithMap()} | ${true} | ${'objects containing immutable maps'}
|
||||
${arrayOfMaps()} | ${arrayOfMaps()} | ${true} | ${'arrays containing immutable maps'}
|
||||
${mixedMapsAndObjects()} | ${mixedMapsAndObjects()} | ${true} | ${'nested immutable maps and objects'}
|
||||
`('compares $description and returns $result', verifyIsDeepEqual);
|
||||
`('compares $description and returns $result', ({ initial, next, result }) => expectIsDeepEqual({ initial, next, result }));
|
||||
});
|
||||
|
||||
@@ -57,14 +57,14 @@ export const ContentPacksActions = singletonActions('core.ContentPacks', () =>
|
||||
);
|
||||
|
||||
type StoreState = {
|
||||
contentPack: unknown;
|
||||
contentPackMetadata: ContentPackMetadata;
|
||||
contentPacks: Array<ContentPackInstallation>;
|
||||
installations: Array<ContentPackInstallation>;
|
||||
uninstallEntities: unknown;
|
||||
contentPackRevisions: ContentPackRevisions;
|
||||
selectedVersion: unknown;
|
||||
constraints: unknown;
|
||||
contentPack?: unknown;
|
||||
contentPackMetadata?: ContentPackMetadata;
|
||||
contentPacks?: Array<ContentPackInstallation>;
|
||||
installations?: Array<ContentPackInstallation>;
|
||||
uninstallEntities?: unknown;
|
||||
contentPackRevisions?: ContentPackRevisions;
|
||||
selectedVersion?: unknown;
|
||||
constraints?: unknown;
|
||||
};
|
||||
export const ContentPacksStore = singletonStore('core.ContentPacks', () =>
|
||||
Reflux.createStore<StoreState>({
|
||||
|
||||
@@ -53,7 +53,7 @@ export type InputDescriptions = {
|
||||
};
|
||||
|
||||
type InputTypesStoreState = {
|
||||
sourceUrl: string;
|
||||
sourceUrl?: string;
|
||||
inputTypes?: InputTypes;
|
||||
inputDescriptions?: InputDescriptions;
|
||||
};
|
||||
@@ -69,7 +69,7 @@ export const InputTypesStore = singletonStore('core.InputTypes', () =>
|
||||
this.list();
|
||||
},
|
||||
|
||||
getInitialState(): { inputTypes: InputTypes; inputDescriptions: InputDescriptions } {
|
||||
getInitialState() {
|
||||
return { inputTypes: this.inputTypes, inputDescriptions: this.inputDescriptions };
|
||||
},
|
||||
|
||||
|
||||
@@ -48,14 +48,14 @@ export const CollectorsActions = singletonActions('core.Collectors', () =>
|
||||
);
|
||||
|
||||
type StoreState = {
|
||||
query: string | undefined;
|
||||
collectors: Array<Collector>;
|
||||
pagination: {
|
||||
query?: string;
|
||||
collectors?: Array<Collector>;
|
||||
pagination?: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
total: number;
|
||||
total?: number;
|
||||
};
|
||||
export const CollectorsStore = singletonStore('core.Collectors', () =>
|
||||
Reflux.createStore<StoreState>({
|
||||
|
||||
@@ -20,60 +20,60 @@ import AppConfig from 'util/AppConfig';
|
||||
jest.mock('util/AppConfig');
|
||||
|
||||
describe('DocsHelper', () => {
|
||||
it('prefixes page URLs with default base URL', () => {
|
||||
jest.isolateModules(() => {
|
||||
// eslint-disable-next-line global-require
|
||||
const DocsHelper = require('./DocsHelper').default;
|
||||
const loadDocsHelper = async () => {
|
||||
let docsHelper;
|
||||
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.ALERTS)).toEqual(
|
||||
'https://go2docs.graylog.org/current/interacting_with_your_log_data/alerts.html',
|
||||
);
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.LICENSE)).toEqual(
|
||||
'https://go2docs.graylog.org/current/setting_up_graylog/operations_license_management.html',
|
||||
);
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.AUTHENTICATORS)).toEqual(
|
||||
'https://go2docs.graylog.org/current/setting_up_graylog/user_authentication.htm',
|
||||
);
|
||||
await jest.isolateModulesAsync(async () => {
|
||||
({ default: docsHelper } = await import('./DocsHelper'));
|
||||
});
|
||||
|
||||
return docsHelper;
|
||||
};
|
||||
|
||||
it('prefixes page URLs with default base URL', async () => {
|
||||
const DocsHelper = await loadDocsHelper();
|
||||
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.ALERTS)).toEqual(
|
||||
'https://go2docs.graylog.org/current/interacting_with_your_log_data/alerts.html',
|
||||
);
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.LICENSE)).toEqual(
|
||||
'https://go2docs.graylog.org/current/setting_up_graylog/operations_license_management.html',
|
||||
);
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.AUTHENTICATORS)).toEqual(
|
||||
'https://go2docs.graylog.org/current/setting_up_graylog/user_authentication.htm',
|
||||
);
|
||||
});
|
||||
|
||||
it('applies custom url prefix', () => {
|
||||
jest.isolateModules(() => {
|
||||
asMock(AppConfig.branding).mockReturnValue({
|
||||
help_url: 'https://www.example.com/docs',
|
||||
});
|
||||
// eslint-disable-next-line global-require
|
||||
const DocsHelper = require('./DocsHelper').default;
|
||||
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.ALERTS)).toEqual(
|
||||
'https://www.example.com/docs/interacting_with_your_log_data/alerts.html',
|
||||
);
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.LICENSE)).toEqual(
|
||||
'https://www.example.com/docs/setting_up_graylog/operations_license_management.html',
|
||||
);
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.AUTHENTICATORS)).toEqual(
|
||||
'https://www.example.com/docs/setting_up_graylog/user_authentication.htm',
|
||||
);
|
||||
it('applies custom url prefix', async () => {
|
||||
asMock(AppConfig.branding).mockReturnValue({
|
||||
help_url: 'https://www.example.com/docs',
|
||||
});
|
||||
const DocsHelper = await loadDocsHelper();
|
||||
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.ALERTS)).toEqual(
|
||||
'https://www.example.com/docs/interacting_with_your_log_data/alerts.html',
|
||||
);
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.LICENSE)).toEqual(
|
||||
'https://www.example.com/docs/setting_up_graylog/operations_license_management.html',
|
||||
);
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.AUTHENTICATORS)).toEqual(
|
||||
'https://www.example.com/docs/setting_up_graylog/user_authentication.htm',
|
||||
);
|
||||
});
|
||||
|
||||
it('applies overrides passed through branding', () => {
|
||||
jest.isolateModules(() => {
|
||||
asMock(AppConfig.branding).mockReturnValue({
|
||||
help_pages: {
|
||||
ALERTS: 'http://www.example.com/alerts',
|
||||
LICENSE: 'foo',
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line global-require
|
||||
const DocsHelper = require('./DocsHelper').default;
|
||||
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.ALERTS)).toEqual('http://www.example.com/alerts');
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.LICENSE)).toEqual('https://go2docs.graylog.org/current/foo');
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.AUTHENTICATORS)).toEqual(
|
||||
'https://go2docs.graylog.org/current/setting_up_graylog/user_authentication.htm',
|
||||
);
|
||||
it('applies overrides passed through branding', async () => {
|
||||
asMock(AppConfig.branding).mockReturnValue({
|
||||
help_pages: {
|
||||
ALERTS: 'http://www.example.com/alerts',
|
||||
LICENSE: 'foo',
|
||||
},
|
||||
});
|
||||
const DocsHelper = await loadDocsHelper();
|
||||
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.ALERTS)).toEqual('http://www.example.com/alerts');
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.LICENSE)).toEqual('https://go2docs.graylog.org/current/foo');
|
||||
expect(DocsHelper.toString(DocsHelper.PAGES.AUTHENTICATORS)).toEqual(
|
||||
'https://go2docs.graylog.org/current/setting_up_graylog/user_authentication.htm',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,11 +24,17 @@ export function sortByDate(d1: string, d2: string, sortOrder: SortOrder = 'asc')
|
||||
const d1Time = moment(d1);
|
||||
const d2Time = moment(d2);
|
||||
|
||||
if (sortOrder === 'asc') {
|
||||
return d1Time.isBefore(d2Time) ? -1 : d2Time.isBefore(d1Time) ? 1 : 0;
|
||||
const [earlier, later] = sortOrder === 'asc' ? [d1Time, d2Time] : [d2Time, d1Time];
|
||||
|
||||
if (earlier.isBefore(later)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return d2Time.isBefore(d1Time) ? -1 : d1Time.isBefore(d2Time) ? 1 : 0;
|
||||
if (later.isBefore(earlier)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function naturalSortIgnoreCase(s1: string, s2: string, sortOrder: SortOrder = 'asc') {
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('ActionDropdown', () => {
|
||||
const onClick = jest.fn((e) => e.persist());
|
||||
|
||||
render(
|
||||
<button type="button" onClick={onClick}>
|
||||
<button type="button" aria-label="Wrapper" onClick={onClick}>
|
||||
<ActionDropdown element={<div className="my-trigger-element">Trigger!</div>}>
|
||||
<MenuItem>Foo</MenuItem>
|
||||
</ActionDropdown>
|
||||
@@ -122,7 +122,7 @@ describe('ActionDropdown', () => {
|
||||
const onSelect = jest.fn();
|
||||
|
||||
render(
|
||||
<button type="button" onClick={onClick}>
|
||||
<button type="button" aria-label="Wrapper" onClick={onClick}>
|
||||
<ActionDropdown element={<div>Trigger!</div>}>
|
||||
<MenuItem onSelect={onSelect}>Foo</MenuItem>
|
||||
</ActionDropdown>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import isNumeric from './IsNumeric';
|
||||
|
||||
describe('isNumeric', () => {
|
||||
const testIsNumericString = ({ string, result }) => expect(isNumeric(string)).toEqual(result);
|
||||
const expectIsNumericString = ({ string, result }) => expect(isNumeric(string)).toEqual(result);
|
||||
|
||||
it.each`
|
||||
string | result
|
||||
@@ -31,5 +31,5 @@ describe('isNumeric', () => {
|
||||
${23} | ${true}
|
||||
${23.42} | ${true}
|
||||
${'2020-11-02T09:37:55.256Z PUT /posts [200] 104ms'} | ${false}
|
||||
`('returns $result for value $string', testIsNumericString);
|
||||
`('returns $result for value $string', ({ string, result }) => expectIsNumericString({ string, result }));
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ const messageFor = (ranges: { [p: string]: any }) =>
|
||||
const hasBrokenUpText = (text: string) => (_content, node: Element) => {
|
||||
const hasText = (currentNode: Element) => currentNode.textContent === text;
|
||||
const nodeHasText = hasText(node);
|
||||
// eslint-disable-next-line testing-library/no-node-access -- custom text matcher requires traversing child elements
|
||||
const childrenDontHaveText = Array.from(node.children).every((child) => !hasText(child));
|
||||
|
||||
return nodeHasText && childrenDontHaveText;
|
||||
@@ -39,7 +40,7 @@ describe('SearchQueryHighlights', () => {
|
||||
it('works for empty field & value', async () => {
|
||||
const { container } = render(<SearchQueryHighlights field="" value="" />);
|
||||
|
||||
expect(container.children).toHaveLength(2);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('returns unmodified string without ranges', async () => {
|
||||
|
||||
@@ -64,13 +64,6 @@ describe('TabKeywordTimeRange', () => {
|
||||
asMock(ToolsStore.testNaturalDate).mockClear();
|
||||
});
|
||||
|
||||
const findValidationState = (container) => {
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const formGroup = container.querySelector('.form-group');
|
||||
|
||||
return formGroup && formGroup.className.includes('has-error') ? 'error' : null;
|
||||
};
|
||||
|
||||
const changeInput = async (input, value) => {
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, value);
|
||||
@@ -112,9 +105,9 @@ describe('TabKeywordTimeRange', () => {
|
||||
it('shows validation errors', async () => {
|
||||
asMock(ToolsStore.testNaturalDate).mockImplementation(() => Promise.reject());
|
||||
|
||||
const { container } = render(<TabKeywordTimeRange keyword="invalid" />);
|
||||
render(<TabKeywordTimeRange keyword="invalid" />);
|
||||
|
||||
await waitFor(() => expect(findValidationState(container)).toEqual('error'));
|
||||
await screen.findByText('validation error');
|
||||
});
|
||||
|
||||
it('does not show keyword preview if parsing fails', async () => {
|
||||
|
||||
@@ -227,7 +227,7 @@ const NoninteractiveLegend = ({ config, fieldTypes, labels, labelFields }: Legen
|
||||
return { label, field, type: fieldType };
|
||||
});
|
||||
|
||||
return <LegendEntry labelsWithField={labelsWithField} value={value} />;
|
||||
return <LegendEntry key={value} labelsWithField={labelsWithField} value={value} />;
|
||||
})}
|
||||
</FlexLegendContainer>
|
||||
);
|
||||
|
||||
@@ -63,15 +63,12 @@ const listSubplotOrder = (gd: HTMLElement): string[] => {
|
||||
|
||||
const createBarElementGetter = (gd: PlotlyHTMLElement & { _fullData: Array<any> }) => {
|
||||
const fullData = gd._fullData;
|
||||
const curveNumberGroupedBySubPlot = fullData.reduce(
|
||||
(acc, { xaxis, yaxis }, curveNumber) => {
|
||||
const subplot = `${xaxis ?? 'x'}${yaxis ?? 'y'}`;
|
||||
(acc[subplot] ||= []).push(curveNumber);
|
||||
const curveNumberGroupedBySubPlot: Record<string, number[]> = {};
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number[]>,
|
||||
);
|
||||
fullData.forEach(({ xaxis, yaxis }, curveNumber) => {
|
||||
const subplot = `${xaxis ?? 'x'}${yaxis ?? 'y'}`;
|
||||
(curveNumberGroupedBySubPlot[subplot] ||= []).push(curveNumber);
|
||||
});
|
||||
const subPlotDOMOrder = listSubplotOrder(gd);
|
||||
|
||||
const curveNumberRenderIndexArray = subPlotDOMOrder.flatMap((subPlotId) => curveNumberGroupedBySubPlot[subPlotId]);
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
import React, { useContext } from 'react';
|
||||
import flow from 'lodash/flow';
|
||||
import fromPairs from 'lodash/fromPairs';
|
||||
import get from 'lodash/get';
|
||||
import zip from 'lodash/zip';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
import type Viewport from 'views/logic/aggregationbuilder/visualizations/Viewport';
|
||||
import type WorldMapVisualizationConfig from 'views/logic/aggregationbuilder/visualizations/WorldMapVisualizationConfig';
|
||||
import type { VisualizationComponentProps } from 'views/components/aggregationbuilder/AggregationBuilder';
|
||||
import { makeVisualization, retrieveChartData } from 'views/components/aggregationbuilder/AggregationBuilder';
|
||||
import type { Rows } from 'views/logic/searchtypes/pivot/PivotHandler';
|
||||
@@ -84,7 +84,7 @@ const WorldMapVisualization = makeVisualization(
|
||||
|
||||
const series = pipeline(rows);
|
||||
|
||||
const viewport = get(config, 'visualizationConfig.viewport');
|
||||
const viewport = (config?.visualizationConfig as WorldMapVisualizationConfig | undefined)?.viewport;
|
||||
|
||||
const _onChange = (newViewport: Viewport) => {
|
||||
if (editing) {
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('MapVisualization', () => {
|
||||
<MapVisualization id="somemap" onChange={() => {}} data={[]} height={1600} width={900} />,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line testing-library/no-container
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- leaflet map has no accessible role
|
||||
expect(container.querySelector('div.map#visualization-somemap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -36,9 +36,9 @@ describe('MapVisualization', () => {
|
||||
<MapVisualization id="somemap" onChange={() => {}} data={fixtures.invalidData} height={1600} width={900} />,
|
||||
);
|
||||
|
||||
/* eslint-disable testing-library/no-container */
|
||||
/* eslint-disable testing-library/no-container, testing-library/no-node-access -- leaflet map has no accessible role */
|
||||
expect(container.querySelector('div.map#visualization-somemap')).toBeInTheDocument();
|
||||
expect(container.querySelector('CircleMarker')).not.toBeInTheDocument();
|
||||
/* eslint-enable testing-library/no-container */
|
||||
/* eslint-enable testing-library/no-container, testing-library/no-node-access */
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { render, waitFor, screen } from 'wrappedTestingLibrary';
|
||||
import { render, waitFor, screen, within } from 'wrappedTestingLibrary';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import selectEvent from 'helpers/selectEvent';
|
||||
@@ -97,13 +97,9 @@ describe('EditWidgetFrame', () => {
|
||||
it("changes the widget's streams when using stream filter", async () => {
|
||||
renderSUT();
|
||||
const streamFilter = await screen.findByTestId('streams-filter');
|
||||
const reactSelect = streamFilter.querySelector('div');
|
||||
const reactSelect = within(streamFilter).getByRole('combobox');
|
||||
|
||||
expect(reactSelect).not.toBeNull();
|
||||
|
||||
if (reactSelect) {
|
||||
await selectEvent.select(reactSelect, 'PFLog');
|
||||
}
|
||||
await selectEvent.select(reactSelect, 'PFLog');
|
||||
|
||||
const searchButton = screen.getByRole('button', {
|
||||
name: /perform search \(changes were made after last search execution\)/i,
|
||||
|
||||
@@ -25,7 +25,7 @@ export const isLocalNode = async (nodeId: string) => {
|
||||
if (nodeId && _isLocalNode) {
|
||||
return _isLocalNode(nodeId);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_error) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
import get from 'lodash/get';
|
||||
import * as Immutable from 'immutable';
|
||||
|
||||
import SeriesConfig from './SeriesConfig';
|
||||
@@ -94,7 +93,7 @@ export default class Series {
|
||||
}
|
||||
|
||||
get effectiveName(): string {
|
||||
const overridenName = get(this, 'config.name');
|
||||
const overridenName = this?.config?.name;
|
||||
|
||||
return overridenName || this.function;
|
||||
}
|
||||
|
||||
@@ -73,13 +73,8 @@ export default class WidgetFormattingSettings {
|
||||
|
||||
static fromJSON(value: WidgetFormattingSettingsJSON) {
|
||||
const { chart_colors: chartColorJson } = value;
|
||||
const chartColors: ChartColors = chartColorJson.reduce(
|
||||
(acc, { field_name: fieldName, chart_color: chartColor }) => {
|
||||
acc[fieldName] = chartColor;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
const chartColors: ChartColors = Object.fromEntries(
|
||||
chartColorJson.map(({ field_name: fieldName, chart_color: chartColor }) => [fieldName, chartColor]),
|
||||
);
|
||||
|
||||
return WidgetFormattingSettings.create(chartColors);
|
||||
|
||||
@@ -119,6 +119,8 @@ const pivotResult: Result = {
|
||||
|
||||
describe('PivotHandler', () => {
|
||||
it('has type information matching actual result', () => {
|
||||
PivotHandler.convert(pivotResult);
|
||||
const result = PivotHandler.convert(pivotResult);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,16 +151,18 @@ export const getRestParameterValues = ({
|
||||
parameters: Immutable.Set<ValueParameter | LookupTableParameter>;
|
||||
parameterBindings?: Immutable.Map<string, ParameterBinding>;
|
||||
}) =>
|
||||
parameters.reduce((res, cur) => {
|
||||
if (cur.type !== 'lut-parameter-v1') {
|
||||
const paramJSON = cur.toJSON();
|
||||
const { name } = paramJSON;
|
||||
const bindingValue = parameterBindings?.get(name)?.value;
|
||||
res[name] = bindingValue ?? paramJSON?.default_value;
|
||||
}
|
||||
Object.fromEntries(
|
||||
parameters
|
||||
.filter((cur) => cur.type !== 'lut-parameter-v1')
|
||||
.map((cur) => {
|
||||
const paramJSON = cur.toJSON();
|
||||
const { name } = paramJSON;
|
||||
const bindingValue = parameterBindings?.get(name)?.value;
|
||||
|
||||
return res;
|
||||
}, {});
|
||||
return [name, bindingValue ?? paramJSON?.default_value];
|
||||
})
|
||||
.toArray(),
|
||||
);
|
||||
|
||||
export const transformSearchFiltersToQuery = (filters: FiltersType = Immutable.List([])) =>
|
||||
concatQueryStrings(
|
||||
|
||||
@@ -23,12 +23,8 @@ import { StreamsStore } from 'views/stores/StreamsStore';
|
||||
import { useStore } from 'stores/connect';
|
||||
|
||||
const useModalData = (mappedData) => {
|
||||
const normalizedStreams: { [name: string]: Stream } = useStore(StreamsStore, ({ streams }) =>
|
||||
streams.reduce((res, stream) => {
|
||||
res[stream.id] = { id: stream.id, title: stream.title };
|
||||
|
||||
return res;
|
||||
}, {}),
|
||||
const normalizedStreams: { [name: string]: Pick<Stream, 'id' | 'title'> } = useStore(StreamsStore, ({ streams }) =>
|
||||
Object.fromEntries(streams.map((stream) => [stream.id, { id: stream.id, title: stream.title }])),
|
||||
);
|
||||
|
||||
return useMemo<ModalData>(() => {
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
import get from 'lodash/get';
|
||||
|
||||
import type View from 'views/logic/views/View';
|
||||
import SearchTypesGenerator from 'views/logic/searchtypes/SearchTypesGenerator';
|
||||
|
||||
@@ -23,7 +21,7 @@ const UpdateSearchForWidgets = (view: View): View => {
|
||||
const { state: states } = view;
|
||||
const searchTypes = states.map((state) => SearchTypesGenerator(state.widgets));
|
||||
|
||||
const search = get(view, 'search');
|
||||
const search = view?.search;
|
||||
const newQueries = search.queries.map((q) =>
|
||||
q
|
||||
.toBuilder()
|
||||
|
||||
@@ -27,6 +27,7 @@ import { matchesDecoratorStream, matchesDecoratorStreamCategories } from 'views/
|
||||
import UpdateSearchForWidgets from 'views/logic/views/UpdateSearchForWidgets';
|
||||
import ViewState from 'views/logic/views/ViewState';
|
||||
import { allMessagesTable, resultHistogram } from 'views/logic/Widgets';
|
||||
import type Widget from 'views/logic/widgets/Widget';
|
||||
import WidgetPosition from 'views/logic/widgets/WidgetPosition';
|
||||
import { DecoratorsActions } from 'stores/decorators/DecoratorsStore';
|
||||
import generateId from 'logic/generateId';
|
||||
@@ -90,14 +91,15 @@ const createViewWidget = ({ groupBy, fnSeries, expr }: { groupBy: Array<string>;
|
||||
return getAggregationWidget({ rowPivots, fnSeries: [fnSeriesForFunc], sort });
|
||||
};
|
||||
|
||||
const getSummaryAggregation = ({ aggregations, groupBy }) => {
|
||||
const { summaryFnSeries, summaryTitle } = aggregations.reduce(
|
||||
const getSummaryAggregation = ({ aggregations, groupBy }: { aggregations: Array<EventDefinitionAggregation>; groupBy: Array<string> }) => {
|
||||
const { summaryFnSeries, summaryTitle } = aggregations.reduce<{ summaryFnSeries: string[]; summaryTitle: string }>(
|
||||
(res, { value, expr, fnSeries }) => {
|
||||
const concatTitle = `${fnSeries} ${expr} ${value}`;
|
||||
res.summaryFnSeries.push(fnSeries);
|
||||
res.summaryTitle = `${res.summaryTitle} ${concatTitle}`;
|
||||
|
||||
return res;
|
||||
return {
|
||||
summaryFnSeries: [...res.summaryFnSeries, fnSeries],
|
||||
summaryTitle: `${res.summaryTitle} ${concatTitle}`,
|
||||
};
|
||||
},
|
||||
{
|
||||
summaryFnSeries: [],
|
||||
@@ -117,7 +119,12 @@ const getSummaryAggregation = ({ aggregations, groupBy }) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const WidgetsGenerator = async ({ streams, streamCategories, aggregations, groupBy }) => {
|
||||
export const WidgetsGenerator = async ({ streams, streamCategories, aggregations, groupBy }: {
|
||||
streams: string | string[] | undefined;
|
||||
streamCategories: string | string[] | undefined;
|
||||
aggregations: Array<EventDefinitionAggregation>;
|
||||
groupBy: Array<string>;
|
||||
}) => {
|
||||
const decorators = await DecoratorsActions.list();
|
||||
const byStreamId = matchesDecoratorStream(streams);
|
||||
const byStreamCategory = matchesDecoratorStreamCategories(streamCategories);
|
||||
@@ -134,16 +141,28 @@ export const WidgetsGenerator = async ({ streams, streamCategories, aggregations
|
||||
const messageTable = allMessagesTable(undefined, allDecorators);
|
||||
const needsSummaryAggregations = aggregations.length > 1;
|
||||
const SUMMARY_ROW_DELTA = needsSummaryAggregations ? AGGREGATION_WIDGET_HEIGHT : 0;
|
||||
const { aggregationWidgets, aggregationTitles, aggregationPositions } = aggregations.reduce(
|
||||
const { aggregationWidgets, aggregationTitles, aggregationPositions } = aggregations.reduce<{
|
||||
aggregationTitles: Record<string, string>;
|
||||
aggregationWidgets: Widget[];
|
||||
aggregationPositions: Record<string, WidgetPosition>;
|
||||
}>(
|
||||
(res, { value, expr, fnSeries }, index) => {
|
||||
const widget = createViewWidget({ fnSeries, groupBy, expr });
|
||||
res.aggregationWidgets.push(widget);
|
||||
res.aggregationTitles[widget.id] = `${fnSeries} ${expr} ${value}`;
|
||||
res.aggregationPositions[widget.id] = createViewPosition({ index, SUMMARY_ROW_DELTA });
|
||||
|
||||
return res;
|
||||
return {
|
||||
aggregationWidgets: [...res.aggregationWidgets, widget],
|
||||
aggregationTitles: { ...res.aggregationTitles, [widget.id]: `${fnSeries} ${expr} ${value}` },
|
||||
aggregationPositions: {
|
||||
...res.aggregationPositions,
|
||||
[widget.id]: createViewPosition({ index, SUMMARY_ROW_DELTA }),
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
aggregationTitles: {},
|
||||
aggregationWidgets: [],
|
||||
aggregationPositions: {},
|
||||
},
|
||||
{ aggregationTitles: {}, aggregationWidgets: [], aggregationPositions: {} },
|
||||
);
|
||||
|
||||
const widgets = [...aggregationWidgets, histogram, messageTable];
|
||||
|
||||
Reference in New Issue
Block a user