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:
Dennis Oelkers
2026-03-11 14:25:11 +01:00
committed by GitHub
parent 2f5bcf252d
commit 90a797f8ff
54 changed files with 252 additions and 251 deletions

View File

@@ -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',

View File

@@ -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;

View File

@@ -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,

View File

@@ -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) => {

View File

@@ -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';

View File

@@ -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();
});
});

View File

@@ -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;

View File

@@ -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');
});
});
});

View File

@@ -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());
});

View File

@@ -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>);

View File

@@ -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();
});
});

View File

@@ -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) => {

View File

@@ -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();
});

View File

@@ -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 (

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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]} />;
})}
</>
);

View File

@@ -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}

View File

@@ -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;

View File

@@ -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 = () => (

View File

@@ -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;

View File

@@ -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';

View File

@@ -78,7 +78,9 @@ class SidecarList extends React.Component<
/>
</th>
))}
<th className={style.actions}>&nbsp;</th>
<th className={style.actions}>
&nbsp;
</th>
</tr>
</thead>
<tbody>{sidecars}</tbody>

View File

@@ -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}

View File

@@ -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 &gt; 75% full</dt>
{metrics?.disks_75 === undefined ? (
<dd>Not available</dd>

View File

@@ -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) {

View File

@@ -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() {

View File

@@ -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 }));
});

View File

@@ -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}

View File

@@ -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;

View File

@@ -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 }));
});

View File

@@ -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>({

View File

@@ -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 };
},

View File

@@ -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>({

View File

@@ -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',
);
});
});

View File

@@ -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') {

View File

@@ -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>

View File

@@ -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 }));
});

View File

@@ -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 () => {

View File

@@ -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 () => {

View File

@@ -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>
);

View File

@@ -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]);

View File

@@ -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) {

View File

@@ -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 */
});
});

View File

@@ -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,

View File

@@ -25,7 +25,7 @@ export const isLocalNode = async (nodeId: string) => {
if (nodeId && _isLocalNode) {
return _isLocalNode(nodeId);
}
} catch (e) {
} catch (_error) {
// Do nothing
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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();
});
});

View File

@@ -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(

View File

@@ -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>(() => {

View File

@@ -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()

View File

@@ -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];