diff --git a/graylog2-web-interface/packages/eslint-config-graylog/index.js b/graylog2-web-interface/packages/eslint-config-graylog/index.js index a2852bf14d..605a791828 100644 --- a/graylog2-web-interface/packages/eslint-config-graylog/index.js +++ b/graylog2-web-interface/packages/eslint-config-graylog/index.js @@ -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', diff --git a/graylog2-web-interface/src/@types/moment/index.d.ts b/graylog2-web-interface/src/@types/moment/index.d.ts index 86c80a911a..6990bcf366 100644 --- a/graylog2-web-interface/src/@types/moment/index.d.ts +++ b/graylog2-web-interface/src/@types/moment/index.d.ts @@ -22,6 +22,7 @@ // Definitions by: Mitchell Grice // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// eslint-disable-next-line @typescript-eslint/no-require-imports import moment = require('moment'); export = moment; diff --git a/graylog2-web-interface/src/@types/reflux/index.d.ts b/graylog2-web-interface/src/@types/reflux/index.d.ts index 5c86332d22..f6f90f05c9 100644 --- a/graylog2-web-interface/src/@types/reflux/index.d.ts +++ b/graylog2-web-interface/src/@types/reflux/index.d.ts @@ -17,10 +17,10 @@ declare module 'reflux' { import type { RefluxActions, Store } from 'stores/StoreTypes'; - export interface StoreDefinition { + export interface StoreDefinition { listenables?: RefluxActions[]; - init?: Function; - getInitialState?: Function; + init?: () => void; + getInitialState?: () => T; [propertyName: string]: any; } @@ -31,7 +31,7 @@ declare module 'reflux' { type ElementType> = T extends ReadonlyArray ? E : never; - export function createStore(definition: StoreDefinition): Store & typeof definition; + export function createStore(definition: StoreDefinition): Store & typeof definition; export function createActions(definitions: ActionsDefinition): RefluxActions; export function createActions( definitions: R, diff --git a/graylog2-web-interface/src/components/bootstrap/Button.tsx b/graylog2-web-interface/src/components/bootstrap/Button.tsx index 2a089d2ba5..5ed1e714c4 100644 --- a/graylog2-web-interface/src/components/bootstrap/Button.tsx +++ b/graylog2-web-interface/src/components/bootstrap/Button.tsx @@ -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) => { diff --git a/graylog2-web-interface/src/components/bootstrap/menuitem/MenuItem.tsx b/graylog2-web-interface/src/components/bootstrap/menuitem/MenuItem.tsx index 1be48bb392..bfed847962 100644 --- a/graylog2-web-interface/src/components/bootstrap/menuitem/MenuItem.tsx +++ b/graylog2-web-interface/src/components/bootstrap/menuitem/MenuItem.tsx @@ -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'; diff --git a/graylog2-web-interface/src/components/common/Autocomplete/Autocomplete.test.tsx b/graylog2-web-interface/src/components/common/Autocomplete/Autocomplete.test.tsx index 6e7193191c..6b774b143b 100644 --- a/graylog2-web-interface/src/components/common/Autocomplete/Autocomplete.test.tsx +++ b/graylog2-web-interface/src/components/common/Autocomplete/Autocomplete.test.tsx @@ -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(); }); }); diff --git a/graylog2-web-interface/src/components/common/ConfirmLeaveDialog.tsx b/graylog2-web-interface/src/components/common/ConfirmLeaveDialog.tsx index 6257e3a3df..2b284f4a3f 100644 --- a/graylog2-web-interface/src/components/common/ConfirmLeaveDialog.tsx +++ b/graylog2-web-interface/src/components/common/ConfirmLeaveDialog.tsx @@ -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; diff --git a/graylog2-web-interface/src/components/common/PaginatedList.test.tsx b/graylog2-web-interface/src/components/common/PaginatedList.test.tsx index 462a81a415..749f3e3a61 100644 --- a/graylog2-web-interface/src/components/common/PaginatedList.test.tsx +++ b/graylog2-web-interface/src/components/common/PaginatedList.test.tsx @@ -98,32 +98,30 @@ describe('PaginatedList', () => { const currentPage = 4; asMock(useQueryParams).mockImplementation(() => [{ page: currentPage }, setQueryParams]); - const { findByTestId } = render( + render( {}} activePage={3}>
The list
, ); - 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( {}} activePage={3} useQueryParameter={false}>
The list
, ); - 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( {}} activePage={1} useQueryParameter={false}> @@ -131,10 +129,9 @@ describe('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'); }); }); }); diff --git a/graylog2-web-interface/src/components/common/Select/AsyncCustomMenuList.test.tsx b/graylog2-web-interface/src/components/common/Select/AsyncCustomMenuList.test.tsx index daf4002c82..7784a67968 100644 --- a/graylog2-web-interface/src/components/common/Select/AsyncCustomMenuList.test.tsx +++ b/graylog2-web-interface/src/components/common/Select/AsyncCustomMenuList.test.tsx @@ -15,13 +15,13 @@ * . */ 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(() =>
{Math.random()}
); @@ -46,11 +46,12 @@ describe('CustomMenuList', () => { it('Should load more items on scrool', async () => { render({getChildrenList(5)}); - 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()); }); diff --git a/graylog2-web-interface/src/components/common/Select/CustomMenuList.test.tsx b/graylog2-web-interface/src/components/common/Select/CustomMenuList.test.tsx index 22402f67c7..d7bde1c17f 100644 --- a/graylog2-web-interface/src/components/common/Select/CustomMenuList.test.tsx +++ b/graylog2-web-interface/src/components/common/Select/CustomMenuList.test.tsx @@ -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(() =>
{Math.random()}
); diff --git a/graylog2-web-interface/src/components/common/Spinner.test.tsx b/graylog2-web-interface/src/components/common/Spinner.test.tsx index edb4fa0f85..1b2cdaa924 100644 --- a/graylog2-web-interface/src/components/common/Spinner.test.tsx +++ b/graylog2-web-interface/src/components/common/Spinner.test.tsx @@ -43,12 +43,12 @@ describe('', () => { }); it('should be visible after when delay is completed', async () => { - const { container } = render(); + render(); act(() => { jest.advanceTimersByTime(200); }); - expect(container.firstChild).toHaveStyle('visibility: visible'); + expect(screen.getByText('Loading...')).toBeVisible(); }); }); diff --git a/graylog2-web-interface/src/components/common/Timestamp.tsx b/graylog2-web-interface/src/components/common/Timestamp.tsx index 1b82c41aac..1719741f62 100644 --- a/graylog2-web-interface/src/components/common/Timestamp.tsx +++ b/graylog2-web-interface/src/components/common/Timestamp.tsx @@ -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) => { diff --git a/graylog2-web-interface/src/components/content-packs/ContentPackEditParameter.test.tsx b/graylog2-web-interface/src/components/content-packs/ContentPackEditParameter.test.tsx index f1f34e1eb9..99bae9cf5f 100644 --- a/graylog2-web-interface/src/components/content-packs/ContentPackEditParameter.test.tsx +++ b/graylog2-web-interface/src/components/content-packs/ContentPackEditParameter.test.tsx @@ -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('', () => { it('should render with empty parameters', async () => { render(); @@ -68,10 +71,10 @@ describe('', () => { 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('', () => { 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('', () => { 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('', () => { 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(); }); diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-form/FieldsForm.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-form/FieldsForm.tsx index b140842043..09ead8551d 100644 --- a/graylog2-web-interface/src/components/event-definitions/event-definition-form/FieldsForm.tsx +++ b/graylog2-web-interface/src/components/event-definitions/event-definition-form/FieldsForm.tsx @@ -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 ( diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-form/NotificationSettingsForm.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-form/NotificationSettingsForm.tsx index 352baf98f1..872f061617 100644 --- a/graylog2-web-interface/src/components/event-definitions/event-definition-form/NotificationSettingsForm.tsx +++ b/graylog2-web-interface/src/components/event-definitions/event-definition-form/NotificationSettingsForm.tsx @@ -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< diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-form/ShareDetails.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-form/ShareDetails.tsx index bb66d473f9..b6d74715d1 100644 --- a/graylog2-web-interface/src/components/event-definitions/event-definition-form/ShareDetails.tsx +++ b/graylog2-web-interface/src/components/event-definitions/event-definition-form/ShareDetails.tsx @@ -111,7 +111,7 @@ const ShareDetails = ({ shareState = null }: Props) => { const currentGranteeState = grantee.currentState(activeShares); return ( - + {grantee.title} diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-types/AggregationConditionExpressions/NumberExpression.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-types/AggregationConditionExpressions/NumberExpression.tsx index 35537b3de5..9bb63bd8e4 100644 --- a/graylog2-web-interface/src/components/event-definitions/event-definition-types/AggregationConditionExpressions/NumberExpression.tsx +++ b/graylog2-web-interface/src/components/event-definitions/event-definition-types/AggregationConditionExpressions/NumberExpression.tsx @@ -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} diff --git a/graylog2-web-interface/src/components/event-notifications/event-notification-details/LegacyNotificationDetails.tsx b/graylog2-web-interface/src/components/event-notifications/event-notification-details/LegacyNotificationDetails.tsx index 2160367901..9d1c150d21 100644 --- a/graylog2-web-interface/src/components/event-notifications/event-notification-details/LegacyNotificationDetails.tsx +++ b/graylog2-web-interface/src/components/event-notifications/event-notification-details/LegacyNotificationDetails.tsx @@ -62,6 +62,7 @@ const LegacyNotificationDetails = ({ notification }: LegacyNotificationDetailsPr if (key === 'body' || key === 'script_args') { return ( @@ -72,7 +73,7 @@ const LegacyNotificationDetails = ({ notification }: LegacyNotificationDetailsPr ); } - return ; + return ; })} ); diff --git a/graylog2-web-interface/src/components/event-notifications/event-notification-form/EventNotificationForm.tsx b/graylog2-web-interface/src/components/event-notifications/event-notification-form/EventNotificationForm.tsx index 9d6d0dc200..03a4964d3c 100644 --- a/graylog2-web-interface/src/components/event-notifications/event-notification-form/EventNotificationForm.tsx +++ b/graylog2-web-interface/src/components/event-notifications/event-notification-form/EventNotificationForm.tsx @@ -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 /> - {get(validation, 'errors.config[0]', 'Choose the type of Notification to create.')} + {validation?.errors?.config?.[0] ?? 'Choose the type of Notification to create.'} {notificationFormComponent} diff --git a/graylog2-web-interface/src/components/indices/Types.ts b/graylog2-web-interface/src/components/indices/Types.ts index 3d60877d41..4fba568809 100644 --- a/graylog2-web-interface/src/components/indices/Types.ts +++ b/graylog2-web-interface/src/components/indices/Types.ts @@ -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; diff --git a/graylog2-web-interface/src/components/loggers/ClusterSupportBundleInfo.tsx b/graylog2-web-interface/src/components/loggers/ClusterSupportBundleInfo.tsx index 832cb69628..33dbc95980 100644 --- a/graylog2-web-interface/src/components/loggers/ClusterSupportBundleInfo.tsx +++ b/graylog2-web-interface/src/components/loggers/ClusterSupportBundleInfo.tsx @@ -14,9 +14,10 @@ * along with this program. If not, see * . */ -/// import * as React from 'react'; +import './types'; + import usePluginEntities from 'hooks/usePluginEntities'; export const UnlicensedText = () => ( diff --git a/graylog2-web-interface/src/components/rules/rule-builder/RuleBlockDisplay.tsx b/graylog2-web-interface/src/components/rules/rule-builder/RuleBlockDisplay.tsx index e2afdc6347..b1ad2c6198 100644 --- a/graylog2-web-interface/src/components/rules/rule-builder/RuleBlockDisplay.tsx +++ b/graylog2-web-interface/src/components/rules/rule-builder/RuleBlockDisplay.tsx @@ -142,7 +142,7 @@ const RuleBlockDisplay = ({ const partsWithHighlight = parts.map((part) => { if (part === `'$${termToHighlight}'`) { - return {part}; + return {part}; } return part; diff --git a/graylog2-web-interface/src/components/security/SecurityPageEntry.tsx b/graylog2-web-interface/src/components/security/SecurityPageEntry.tsx index 13517176d2..bded6dcba3 100644 --- a/graylog2-web-interface/src/components/security/SecurityPageEntry.tsx +++ b/graylog2-web-interface/src/components/security/SecurityPageEntry.tsx @@ -14,9 +14,10 @@ * along with this program. If not, see * . */ -/// import React from 'react'; +import './types'; + import usePluginEntities from 'hooks/usePluginEntities'; import SecurityPage from 'components/security/teaser/SecurityPage'; diff --git a/graylog2-web-interface/src/components/sidecars/sidecars/SidecarList.tsx b/graylog2-web-interface/src/components/sidecars/sidecars/SidecarList.tsx index 33f3e003f7..25676c3f7f 100644 --- a/graylog2-web-interface/src/components/sidecars/sidecars/SidecarList.tsx +++ b/graylog2-web-interface/src/components/sidecars/sidecars/SidecarList.tsx @@ -78,7 +78,9 @@ class SidecarList extends React.Component< /> ))} -   + +   + {sidecars} diff --git a/graylog2-web-interface/src/components/sidecars/sidecars/SidecarRow.tsx b/graylog2-web-interface/src/components/sidecars/sidecars/SidecarRow.tsx index 3decaa6430..db2d801faf 100644 --- a/graylog2-web-interface/src/components/sidecars/sidecars/SidecarRow.tsx +++ b/graylog2-web-interface/src/components/sidecars/sidecars/SidecarRow.tsx @@ -75,7 +75,7 @@ class SidecarRow extends React.Component< {sidecar.node_name} - + { return (
IP Address
-
{defaultTo(details.ip, 'Not available')}
+
{details.ip ?? 'Not available'}
Operating System
-
{defaultTo(details.operating_system, 'Not available')}
+
{details.operating_system ?? 'Not available'}
CPU Idle
{isNumber(metrics?.cpu_idle) ? `${metrics?.cpu_idle}%` : 'Not available'}
Load
-
{defaultTo(metrics?.load_1, 'Not available')}
+
{metrics?.load_1 ?? 'Not available'}
Volumes > 75% full
{metrics?.disks_75 === undefined ? (
Not available
diff --git a/graylog2-web-interface/src/contexts/CurrentUserProvider.tsx b/graylog2-web-interface/src/contexts/CurrentUserProvider.tsx index d54eb5110c..6a9134ac4f 100644 --- a/graylog2-web-interface/src/contexts/CurrentUserProvider.tsx +++ b/graylog2-web-interface/src/contexts/CurrentUserProvider.tsx @@ -15,7 +15,6 @@ * . */ 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) { diff --git a/graylog2-web-interface/src/logic/content-packs/ContentPackRevisions.ts b/graylog2-web-interface/src/logic/content-packs/ContentPackRevisions.ts index c457149772..356cac0eb5 100644 --- a/graylog2-web-interface/src/logic/content-packs/ContentPackRevisions.ts +++ b/graylog2-web-interface/src/logic/content-packs/ContentPackRevisions.ts @@ -14,25 +14,22 @@ * along with this program. If not, see * . */ -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() { diff --git a/graylog2-web-interface/src/logic/permissions/SelectedGrantee.test.ts b/graylog2-web-interface/src/logic/permissions/SelectedGrantee.test.ts index 3217759ab1..f7b6dae4e6 100644 --- a/graylog2-web-interface/src/logic/permissions/SelectedGrantee.test.ts +++ b/graylog2-web-interface/src/logic/permissions/SelectedGrantee.test.ts @@ -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 })); }); diff --git a/graylog2-web-interface/src/preflight/components/common/Section.tsx b/graylog2-web-interface/src/preflight/components/common/Section.tsx index 2b1b111139..a44af656a4 100644 --- a/graylog2-web-interface/src/preflight/components/common/Section.tsx +++ b/graylog2-web-interface/src/preflight/components/common/Section.tsx @@ -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) => ( +}: React.PropsWithChildren) => ( {children} diff --git a/graylog2-web-interface/src/routing/useLocation.ts b/graylog2-web-interface/src/routing/useLocation.ts index c25242f611..8faa7a04e2 100644 --- a/graylog2-web-interface/src/routing/useLocation.ts +++ b/graylog2-web-interface/src/routing/useLocation.ts @@ -14,8 +14,8 @@ * along with this program. If not, see * . */ -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 = (): Location => useRouterLocation(); export default useLocation; diff --git a/graylog2-web-interface/src/stores/__tests__/isDeepEqual.test.ts b/graylog2-web-interface/src/stores/__tests__/isDeepEqual.test.ts index 69ccd046f1..5457eac4a5 100644 --- a/graylog2-web-interface/src/stores/__tests__/isDeepEqual.test.ts +++ b/graylog2-web-interface/src/stores/__tests__/isDeepEqual.test.ts @@ -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 })); }); diff --git a/graylog2-web-interface/src/stores/content-packs/ContentPacksStore.ts b/graylog2-web-interface/src/stores/content-packs/ContentPacksStore.ts index d991e36907..ec6bf5298a 100644 --- a/graylog2-web-interface/src/stores/content-packs/ContentPacksStore.ts +++ b/graylog2-web-interface/src/stores/content-packs/ContentPacksStore.ts @@ -57,14 +57,14 @@ export const ContentPacksActions = singletonActions('core.ContentPacks', () => ); type StoreState = { - contentPack: unknown; - contentPackMetadata: ContentPackMetadata; - contentPacks: Array; - installations: Array; - uninstallEntities: unknown; - contentPackRevisions: ContentPackRevisions; - selectedVersion: unknown; - constraints: unknown; + contentPack?: unknown; + contentPackMetadata?: ContentPackMetadata; + contentPacks?: Array; + installations?: Array; + uninstallEntities?: unknown; + contentPackRevisions?: ContentPackRevisions; + selectedVersion?: unknown; + constraints?: unknown; }; export const ContentPacksStore = singletonStore('core.ContentPacks', () => Reflux.createStore({ diff --git a/graylog2-web-interface/src/stores/inputs/InputTypesStore.ts b/graylog2-web-interface/src/stores/inputs/InputTypesStore.ts index 1ed0623211..d57fc7a679 100644 --- a/graylog2-web-interface/src/stores/inputs/InputTypesStore.ts +++ b/graylog2-web-interface/src/stores/inputs/InputTypesStore.ts @@ -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 }; }, diff --git a/graylog2-web-interface/src/stores/sidecars/CollectorsStore.ts b/graylog2-web-interface/src/stores/sidecars/CollectorsStore.ts index 71276c3eb2..7edc2c06ca 100644 --- a/graylog2-web-interface/src/stores/sidecars/CollectorsStore.ts +++ b/graylog2-web-interface/src/stores/sidecars/CollectorsStore.ts @@ -48,14 +48,14 @@ export const CollectorsActions = singletonActions('core.Collectors', () => ); type StoreState = { - query: string | undefined; - collectors: Array; - pagination: { + query?: string; + collectors?: Array; + pagination?: { page: number; pageSize: number; total: number; }; - total: number; + total?: number; }; export const CollectorsStore = singletonStore('core.Collectors', () => Reflux.createStore({ diff --git a/graylog2-web-interface/src/util/DocsHelper.test.ts b/graylog2-web-interface/src/util/DocsHelper.test.ts index 6c22e93786..119a0c48fd 100644 --- a/graylog2-web-interface/src/util/DocsHelper.test.ts +++ b/graylog2-web-interface/src/util/DocsHelper.test.ts @@ -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', + ); }); }); diff --git a/graylog2-web-interface/src/util/SortUtils.ts b/graylog2-web-interface/src/util/SortUtils.ts index 14993c8225..ef2bdc00cb 100644 --- a/graylog2-web-interface/src/util/SortUtils.ts +++ b/graylog2-web-interface/src/util/SortUtils.ts @@ -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') { diff --git a/graylog2-web-interface/src/views/components/common/ActionDropdown.test.tsx b/graylog2-web-interface/src/views/components/common/ActionDropdown.test.tsx index e7c328cdbf..a46cc3cdae 100644 --- a/graylog2-web-interface/src/views/components/common/ActionDropdown.test.tsx +++ b/graylog2-web-interface/src/views/components/common/ActionDropdown.test.tsx @@ -44,7 +44,7 @@ describe('ActionDropdown', () => { const onClick = jest.fn((e) => e.persist()); render( -