mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] Refactor useSelector (#75297)
This commit is contained in:
parent
53d49381c8
commit
2defe88a2c
33 changed files with 135 additions and 131 deletions
|
@ -5,13 +5,13 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { APP_ID } from '../../../../common/constants';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../common/components/link_to';
|
||||
import { State } from '../../../common/store';
|
||||
import { setInsertTimeline } from '../../../timelines/store/timeline/actions';
|
||||
import { timelineSelectors } from '../../../timelines/store/timeline';
|
||||
|
||||
|
@ -34,7 +34,7 @@ export const useAllCasesModal = ({
|
|||
}: UseAllCasesModalProps): UseAllCasesModalReturnedValues => {
|
||||
const dispatch = useDispatch();
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
const timeline = useSelector((state: State) =>
|
||||
const timeline = useShallowEqualSelector((state) =>
|
||||
timelineSelectors.selectTimeline(state, timelineId)
|
||||
);
|
||||
|
||||
|
|
|
@ -4,17 +4,16 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import { makeMapStateToProps } from '../url_state/helpers';
|
||||
import { getSearch } from './helpers';
|
||||
import { SearchNavTab } from './types';
|
||||
|
||||
export const useGetUrlSearch = (tab: SearchNavTab) => {
|
||||
const mapState = makeMapStateToProps();
|
||||
const { urlState } = useSelector(mapState, isEqual);
|
||||
const { urlState } = useDeepEqualSelector(mapState);
|
||||
const urlSearch = useMemo(() => getSearch(tab, urlState), [tab, urlState]);
|
||||
return urlSearch;
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@ import { set } from '@elastic/safer-lodash-set/fp';
|
|||
import { keyBy, pick, isEmpty, isEqual, isUndefined } from 'lodash/fp';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IIndexPattern } from 'src/plugins/data/public';
|
||||
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
|
@ -20,11 +20,10 @@ import {
|
|||
BrowserFields,
|
||||
} from '../../../../common/search_strategy/index_fields';
|
||||
import { AbortError } from '../../../../../../../src/plugins/data/common';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import * as i18n from './translations';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { sourcererActions, sourcererSelectors } from '../../store/sourcerer';
|
||||
|
||||
import { State } from '../../store';
|
||||
import { DocValueFields } from '../../../../common/search_strategy/common';
|
||||
|
||||
export { BrowserField, BrowserFields, DocValueFields };
|
||||
|
@ -201,9 +200,8 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => {
|
|||
() => sourcererSelectors.getIndexNamesSelectedSelector(),
|
||||
[]
|
||||
);
|
||||
const indexNames = useSelector<State, string[]>(
|
||||
(state) => indexNamesSelectedSelector(state, sourcererScopeName),
|
||||
shallowEqual
|
||||
const indexNames = useShallowEqualSelector<string[]>((state) =>
|
||||
indexNamesSelectedSelector(state, sourcererScopeName)
|
||||
);
|
||||
|
||||
const setLoading = useCallback(
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../common/constants';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../common/constants';
|
||||
import { useShallowEqualSelector } from '../../hooks/use_selector';
|
||||
import { inputsSelectors } from '../../store';
|
||||
import { inputsActions } from '../../store/actions';
|
||||
|
||||
|
@ -29,8 +30,10 @@ export const resetScroll = () => {
|
|||
|
||||
export const useFullScreen = () => {
|
||||
const dispatch = useDispatch();
|
||||
const globalFullScreen = useSelector(inputsSelectors.globalFullScreenSelector) ?? false;
|
||||
const timelineFullScreen = useSelector(inputsSelectors.timelineFullScreenSelector) ?? false;
|
||||
const globalFullScreen =
|
||||
useShallowEqualSelector(inputsSelectors.globalFullScreenSelector) ?? false;
|
||||
const timelineFullScreen =
|
||||
useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false;
|
||||
|
||||
const setGlobalFullScreen = useCallback(
|
||||
(fullScreen: boolean) => {
|
||||
|
|
|
@ -5,15 +5,16 @@
|
|||
*/
|
||||
|
||||
import { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useShallowEqualSelector } from '../../hooks/use_selector';
|
||||
import { inputsSelectors } from '../../store';
|
||||
import { inputsActions } from '../../store/actions';
|
||||
import { SetQuery, DeleteQuery } from './types';
|
||||
|
||||
export const useGlobalTime = (clearAllQuery: boolean = true) => {
|
||||
const dispatch = useDispatch();
|
||||
const { from, to } = useSelector(inputsSelectors.globalTimeRangeSelector);
|
||||
const { from, to } = useShallowEqualSelector(inputsSelectors.globalTimeRangeSelector);
|
||||
const [isInitializing, setIsInitializing] = useState(true);
|
||||
|
||||
const setQuery = useCallback(
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { State } from '../store';
|
||||
|
||||
export type TypedUseSelectorHook = <TSelected, TState = State>(
|
||||
selector: (state: TState) => TSelected,
|
||||
equalityFn?: (left: TSelected, right: TSelected) => boolean
|
||||
) => TSelected;
|
||||
|
||||
export const useShallowEqualSelector: TypedUseSelectorHook = (selector) =>
|
||||
useSelector(selector, shallowEqual);
|
||||
|
||||
export const useDeepEqualSelector: TypedUseSelectorHook = (selector) =>
|
||||
useSelector(selector, deepEqual);
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { noop } from 'lodash/fp';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import {
|
||||
|
@ -26,7 +25,8 @@ import {
|
|||
} from '../../../../common/search_strategy';
|
||||
import { ESTermQuery } from '../../../../common/typed_json';
|
||||
|
||||
import { inputsModel, State } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { inputsModel } from '../../../common/store';
|
||||
import { createFilter } from '../../../common/containers/helpers';
|
||||
import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
@ -71,9 +71,8 @@ export const useAuthentications = ({
|
|||
skip,
|
||||
}: UseAuthentications): [boolean, AuthenticationArgs] => {
|
||||
const getAuthenticationsSelector = hostsSelectors.authenticationsSelector();
|
||||
const { activePage, limit } = useSelector(
|
||||
(state: State) => getAuthenticationsSelector(state, type),
|
||||
shallowEqual
|
||||
const { activePage, limit } = useShallowEqualSelector((state) =>
|
||||
getAuthenticationsSelector(state, type)
|
||||
);
|
||||
const { data, notifications } = useKibana().services;
|
||||
const refetch = useRef<inputsModel.Refetch>(noop);
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
import deepEqual from 'fast-deep-equal';
|
||||
import { noop } from 'lodash/fp';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { inputsModel, State } from '../../../common/store';
|
||||
import { createFilter } from '../../../common/containers/helpers';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { hostsModel, hostsSelectors } from '../../store';
|
||||
import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers';
|
||||
import {
|
||||
|
@ -69,7 +69,7 @@ export const useAllHost = ({
|
|||
type,
|
||||
}: UseAllHost): [boolean, HostsArgs] => {
|
||||
const getHostsSelector = hostsSelectors.hostsSelector();
|
||||
const { activePage, direction, limit, sortField } = useSelector((state: State) =>
|
||||
const { activePage, direction, limit, sortField } = useShallowEqualSelector((state: State) =>
|
||||
getHostsSelector(state, type)
|
||||
);
|
||||
const { data, notifications } = useKibana().services;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { networkActions, networkModel, networkSelectors } from '../../store';
|
||||
|
@ -16,6 +16,7 @@ import {
|
|||
NetworkDnsFields,
|
||||
} from '../../../../common/search_strategy';
|
||||
import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
|
||||
import { getNetworkDnsColumns } from './columns';
|
||||
import { IsPtrIncluded } from './is_ptr_included';
|
||||
|
@ -59,10 +60,7 @@ const NetworkDnsTableComponent: React.FC<NetworkDnsTableProps> = ({
|
|||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const getNetworkDnsSelector = networkSelectors.dnsSelector();
|
||||
const { activePage, isPtrIncluded, limit, sort } = useSelector(
|
||||
getNetworkDnsSelector,
|
||||
shallowEqual
|
||||
);
|
||||
const { activePage, isPtrIncluded, limit, sort } = useShallowEqualSelector(getNetworkDnsSelector);
|
||||
const updateLimitPagination = useCallback(
|
||||
(newLimit) =>
|
||||
dispatch(
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { networkActions, networkModel, networkSelectors } from '../../store';
|
||||
import { NetworkHttpEdges, NetworkHttpFields } from '../../../../common/search_strategy';
|
||||
import { State } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table';
|
||||
|
||||
import { getNetworkHttpColumns } from './columns';
|
||||
|
@ -51,9 +51,8 @@ const NetworkHttpTableComponent: React.FC<NetworkHttpTableProps> = ({
|
|||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const getNetworkHttpSelector = networkSelectors.httpSelector();
|
||||
const { activePage, limit, sort } = useSelector(
|
||||
(state: State) => getNetworkHttpSelector(state, type),
|
||||
shallowEqual
|
||||
const { activePage, limit, sort } = useShallowEqualSelector((state) =>
|
||||
getNetworkHttpSelector(state, type)
|
||||
);
|
||||
const tableType =
|
||||
type === networkModel.NetworkType.page
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { last } from 'lodash/fp';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { IIndexPattern } from 'src/plugins/data/public';
|
||||
|
||||
|
@ -18,7 +18,7 @@ import {
|
|||
NetworkTopTablesFields,
|
||||
SortField,
|
||||
} from '../../../../common/search_strategy';
|
||||
import { State } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
|
||||
import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table';
|
||||
|
||||
|
@ -67,9 +67,8 @@ const NetworkTopCountriesTableComponent: React.FC<NetworkTopCountriesTableProps>
|
|||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const getTopCountriesSelector = networkSelectors.topCountriesSelector();
|
||||
const { activePage, limit, sort } = useSelector(
|
||||
(state: State) => getTopCountriesSelector(state, type, flowTargeted),
|
||||
shallowEqual
|
||||
const { activePage, limit, sort } = useShallowEqualSelector((state) =>
|
||||
getTopCountriesSelector(state, type, flowTargeted)
|
||||
);
|
||||
|
||||
const headerTitle: string = useMemo(
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import { last } from 'lodash/fp';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import {
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
NetworkTopNFlowEdges,
|
||||
NetworkTopTablesFields,
|
||||
} from '../../../../common/search_strategy';
|
||||
import { State } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table';
|
||||
import { networkActions, networkModel, networkSelectors } from '../../store';
|
||||
import { getNFlowColumnsCurated } from './columns';
|
||||
|
@ -61,9 +61,8 @@ const NetworkTopNFlowTableComponent: React.FC<NetworkTopNFlowTableProps> = ({
|
|||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const getTopNFlowSelector = networkSelectors.topNFlowSelector();
|
||||
const { activePage, limit, sort } = useSelector(
|
||||
(state: State) => getTopNFlowSelector(state, type, flowTargeted),
|
||||
shallowEqual
|
||||
const { activePage, limit, sort } = useShallowEqualSelector((state) =>
|
||||
getTopNFlowSelector(state, type, flowTargeted)
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { networkActions, networkModel, networkSelectors } from '../../store';
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
NetworkTlsFields,
|
||||
SortField,
|
||||
} from '../../../../common/search_strategy';
|
||||
import { State } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import {
|
||||
Criteria,
|
||||
ItemsPerRow,
|
||||
|
@ -63,9 +63,8 @@ const TlsTableComponent: React.FC<TlsTableProps> = ({
|
|||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const getTlsSelector = networkSelectors.tlsSelector();
|
||||
const { activePage, limit, sort } = useSelector(
|
||||
(state: State) => getTlsSelector(state, type),
|
||||
shallowEqual
|
||||
const { activePage, limit, sort } = useShallowEqualSelector((state) =>
|
||||
getTlsSelector(state, type)
|
||||
);
|
||||
const tableType: networkModel.TopTlsTableType =
|
||||
type === networkModel.NetworkType.page
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { assertUnreachable } from '../../../../common/utility_types';
|
||||
import { networkActions, networkModel, networkSelectors } from '../../store';
|
||||
import {
|
||||
|
@ -68,7 +69,7 @@ const UsersTableComponent: React.FC<UsersTableProps> = ({
|
|||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const getUsersSelector = networkSelectors.usersSelector();
|
||||
const { activePage, sort, limit } = useSelector(getUsersSelector, shallowEqual);
|
||||
const { activePage, sort, limit } = useShallowEqualSelector(getUsersSelector);
|
||||
const updateLimitPagination = useCallback(
|
||||
(newLimit) =>
|
||||
dispatch(
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
import { noop } from 'lodash/fp';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { ESTermQuery } from '../../../../common/typed_json';
|
||||
import { inputsModel, State } from '../../../common/store';
|
||||
import { inputsModel } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { createFilter } from '../../../common/containers/helpers';
|
||||
import { NetworkDnsEdges, PageInfoPaginated } from '../../../../common/search_strategy';
|
||||
|
@ -68,10 +68,7 @@ export const useNetworkDns = ({
|
|||
type,
|
||||
}: UseNetworkDns): [boolean, NetworkDnsArgs] => {
|
||||
const getNetworkDnsSelector = networkSelectors.dnsSelector();
|
||||
const { activePage, sort, isPtrIncluded, limit } = useSelector(
|
||||
(state: State) => getNetworkDnsSelector(state),
|
||||
shallowEqual
|
||||
);
|
||||
const { activePage, sort, isPtrIncluded, limit } = useShallowEqualSelector(getNetworkDnsSelector);
|
||||
const { data, notifications } = useKibana().services;
|
||||
const refetch = useRef<inputsModel.Refetch>(noop);
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
import { noop } from 'lodash/fp';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { ESTermQuery } from '../../../../common/typed_json';
|
||||
import { inputsModel, State } from '../../../common/store';
|
||||
import { inputsModel } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { createFilter } from '../../../common/containers/helpers';
|
||||
import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers';
|
||||
|
@ -68,9 +68,8 @@ export const useNetworkHttp = ({
|
|||
type,
|
||||
}: UseNetworkHttp): [boolean, NetworkHttpArgs] => {
|
||||
const getHttpSelector = networkSelectors.httpSelector();
|
||||
const { activePage, limit, sort } = useSelector(
|
||||
(state: State) => getHttpSelector(state, type),
|
||||
shallowEqual
|
||||
const { activePage, limit, sort } = useShallowEqualSelector((state) =>
|
||||
getHttpSelector(state, type)
|
||||
);
|
||||
const { data, notifications } = useKibana().services;
|
||||
const refetch = useRef<inputsModel.Refetch>(noop);
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
import { noop } from 'lodash/fp';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { ESTermQuery } from '../../../../common/typed_json';
|
||||
import { inputsModel, State } from '../../../common/store';
|
||||
import { inputsModel } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { createFilter } from '../../../common/containers/helpers';
|
||||
import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers';
|
||||
|
@ -66,9 +66,8 @@ export const useNetworkTopCountries = ({
|
|||
type,
|
||||
}: UseNetworkTopCountries): [boolean, NetworkTopCountriesArgs] => {
|
||||
const getTopCountriesSelector = networkSelectors.topCountriesSelector();
|
||||
const { activePage, limit, sort } = useSelector(
|
||||
(state: State) => getTopCountriesSelector(state, type, flowTarget),
|
||||
shallowEqual
|
||||
const { activePage, limit, sort } = useShallowEqualSelector((state) =>
|
||||
getTopCountriesSelector(state, type, flowTarget)
|
||||
);
|
||||
const { data, notifications } = useKibana().services;
|
||||
const refetch = useRef<inputsModel.Refetch>(noop);
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
import { noop } from 'lodash/fp';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { ESTermQuery } from '../../../../common/typed_json';
|
||||
import { inputsModel, State } from '../../../common/store';
|
||||
import { inputsModel } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { createFilter } from '../../../common/containers/helpers';
|
||||
import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers';
|
||||
|
@ -66,9 +66,8 @@ export const useNetworkTopNFlow = ({
|
|||
type,
|
||||
}: UseNetworkTopNFlow): [boolean, NetworkTopNFlowArgs] => {
|
||||
const getTopNFlowSelector = networkSelectors.topNFlowSelector();
|
||||
const { activePage, limit, sort } = useSelector(
|
||||
(state: State) => getTopNFlowSelector(state, type, flowTarget),
|
||||
shallowEqual
|
||||
const { activePage, limit, sort } = useShallowEqualSelector((state) =>
|
||||
getTopNFlowSelector(state, type, flowTarget)
|
||||
);
|
||||
const { data, notifications } = useKibana().services;
|
||||
const refetch = useRef<inputsModel.Refetch>(noop);
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
import { noop } from 'lodash/fp';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { ESTermQuery } from '../../../../common/typed_json';
|
||||
import { inputsModel, State } from '../../../common/store';
|
||||
import { inputsModel } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { createFilter } from '../../../common/containers/helpers';
|
||||
import { PageInfoPaginated, FlowTargetSourceDest } from '../../../graphql/types';
|
||||
|
@ -67,9 +67,8 @@ export const useNetworkTls = ({
|
|||
type,
|
||||
}: UseNetworkTls): [boolean, NetworkTlsArgs] => {
|
||||
const getTlsSelector = networkSelectors.tlsSelector();
|
||||
const { activePage, limit, sort } = useSelector(
|
||||
(state: State) => getTlsSelector(state, type, flowTarget),
|
||||
shallowEqual
|
||||
const { activePage, limit, sort } = useShallowEqualSelector((state) =>
|
||||
getTlsSelector(state, type, flowTarget)
|
||||
);
|
||||
const { data, notifications } = useKibana().services;
|
||||
const refetch = useRef<inputsModel.Refetch>(noop);
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
import { noop } from 'lodash/fp';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { ESTermQuery } from '../../../../common/typed_json';
|
||||
import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
|
||||
import { inputsModel } from '../../../common/store';
|
||||
|
@ -66,7 +66,7 @@ export const useNetworkUsers = ({
|
|||
startDate,
|
||||
}: UseNetworkUsers): [boolean, NetworkUsersArgs] => {
|
||||
const getNetworkUsersSelector = networkSelectors.usersSelector();
|
||||
const { activePage, sort, limit } = useSelector(getNetworkUsersSelector, shallowEqual);
|
||||
const { activePage, sort, limit } = useShallowEqualSelector(getNetworkUsersSelector);
|
||||
const { data, notifications, uiSettings } = useKibana().services;
|
||||
const refetch = useRef<inputsModel.Refetch>(noop);
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
|
||||
import { EuiHorizontalRule, EuiSpacer, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { FlowTarget, LastEventIndexKey } from '../../../../common/search_strategy';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
import { FiltersGlobal } from '../../../common/components/filters_global';
|
||||
|
@ -58,8 +59,8 @@ const NetworkDetailsComponent: React.FC = () => {
|
|||
const getGlobalQuerySelector = inputsSelectors.globalQuerySelector();
|
||||
const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector();
|
||||
|
||||
const query = useSelector(getGlobalQuerySelector, shallowEqual);
|
||||
const filters = useSelector(getGlobalFiltersQuerySelector, shallowEqual);
|
||||
const query = useShallowEqualSelector(getGlobalQuerySelector);
|
||||
const filters = useShallowEqualSelector(getGlobalFiltersQuerySelector);
|
||||
|
||||
const type = networkModel.NetworkType.details;
|
||||
const narrowDateRange = useCallback(
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { noop } from 'lodash/fp';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { connect, ConnectedProps, useDispatch, useSelector } from 'react-redux';
|
||||
import { connect, ConnectedProps, useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { FULL_SCREEN } from '../timeline/body/column_headers/translations';
|
||||
|
@ -22,6 +22,7 @@ import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/tr
|
|||
import { DEFAULT_INDEX_KEY, FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants';
|
||||
import { useFullScreen } from '../../../common/containers/use_full_screen';
|
||||
import { State } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { TimelineId, TimelineType } from '../../../../common/types/timeline';
|
||||
import { timelineSelectors } from '../../store/timeline';
|
||||
import { timelineDefaults } from '../../store/timeline/defaults';
|
||||
|
@ -109,7 +110,7 @@ const GraphOverlayComponent = ({
|
|||
dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' }));
|
||||
}, [dispatch, timelineId]);
|
||||
|
||||
const currentTimeline = useSelector((state: State) =>
|
||||
const currentTimeline = useShallowEqualSelector((state) =>
|
||||
timelineSelectors.selectTimeline(state, timelineId)
|
||||
);
|
||||
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
|
||||
import ApolloClient from 'apollo-client';
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { connect, ConnectedProps, shallowEqual, useSelector } from 'react-redux';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../../graphql/types';
|
||||
import { sourcererSelectors, State } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model';
|
||||
import { timelineSelectors } from '../../../timelines/store/timeline';
|
||||
|
@ -114,10 +115,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
() => sourcererSelectors.getAllExistingIndexNamesSelector(),
|
||||
[]
|
||||
);
|
||||
const existingIndexNames = useSelector<State, string[]>(
|
||||
existingIndexNamesSelector,
|
||||
shallowEqual
|
||||
);
|
||||
const existingIndexNames = useShallowEqualSelector<string[]>(existingIndexNamesSelector);
|
||||
|
||||
const {
|
||||
customTemplateTimelineCount,
|
||||
|
|
|
@ -20,10 +20,11 @@ import {
|
|||
EuiInMemoryTable,
|
||||
} from '@elastic/eui';
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { State } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
|
||||
import { renderers } from './catalog';
|
||||
import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../../timelines/store/timeline/actions';
|
||||
|
@ -81,7 +82,7 @@ const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowser
|
|||
}) => {
|
||||
const tableRef = useRef<EuiInMemoryTable<{}>>();
|
||||
const dispatch = useDispatch();
|
||||
const excludedRowRendererIds = useSelector(
|
||||
const excludedRowRendererIds = useShallowEqualSelector(
|
||||
(state: State) => state.timeline.timelineById[timelineId]?.excludedRowRendererIds || []
|
||||
);
|
||||
const [show, setShow] = useState(false);
|
||||
|
|
|
@ -5,22 +5,20 @@
|
|||
*/
|
||||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { TestProviders, mockTimelineModel } from '../../../../../common/mock';
|
||||
import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants';
|
||||
import { Actions } from '.';
|
||||
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const origin = jest.requireActual('react-redux');
|
||||
return {
|
||||
...origin,
|
||||
useSelector: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('../../../../../common/hooks/use_selector', () => ({
|
||||
useShallowEqualSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Actions', () => {
|
||||
(useSelector as jest.Mock).mockReturnValue(mockTimelineModel);
|
||||
beforeEach(() => {
|
||||
(useShallowEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel);
|
||||
});
|
||||
|
||||
test('it renders a checkbox for selecting the event when `showCheckboxes` is `true`', () => {
|
||||
const wrapper = mount(
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import uuid from 'uuid';
|
||||
import { useSelector, shallowEqual } from 'react-redux';
|
||||
|
||||
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { Ecs } from '../../../../../../common/ecs';
|
||||
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
|
||||
import { Note } from '../../../../../common/lib/note';
|
||||
|
@ -28,7 +28,6 @@ import { AlertContextMenu } from '../../../../../detections/components/alerts_ta
|
|||
import { InvestigateInTimelineAction } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action';
|
||||
import { AddEventNoteAction } from '../actions/add_note_icon_item';
|
||||
import { PinEventAction } from '../actions/pin_event_action';
|
||||
import { StoreState } from '../../../../../common/store/types';
|
||||
import { inputsModel } from '../../../../../common/store';
|
||||
import { TimelineId } from '../../../../../../common/types/timeline';
|
||||
|
||||
|
@ -96,9 +95,8 @@ export const EventColumnView = React.memo<Props>(
|
|||
toggleShowNotes,
|
||||
updateNote,
|
||||
}) => {
|
||||
const { timelineType, status } = useSelector<StoreState, TimelineModel>(
|
||||
(state) => state.timeline.timelineById[timelineId],
|
||||
shallowEqual
|
||||
const { timelineType, status } = useShallowEqualSelector<TimelineModel>(
|
||||
(state) => state.timeline.timelineById[timelineId]
|
||||
);
|
||||
|
||||
const handlePinClicked = useCallback(
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
*/
|
||||
|
||||
import React, { useRef, useState, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import uuid from 'uuid';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
|
||||
import { BrowserFields, DocValueFields } from '../../../../../common/containers/source';
|
||||
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { useTimelineEventsDetails } from '../../../../containers/details';
|
||||
import {
|
||||
TimelineEventsDetailsItem,
|
||||
|
@ -37,7 +37,7 @@ import { getEventType } from '../helpers';
|
|||
import { NoteCards } from '../../../notes/note_cards';
|
||||
import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context';
|
||||
import { EventColumnView } from './event_column_view';
|
||||
import { inputsModel, StoreState } from '../../../../../common/store';
|
||||
import { inputsModel } from '../../../../../common/store';
|
||||
|
||||
interface Props {
|
||||
actionsColumnWidth: number;
|
||||
|
@ -136,7 +136,7 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
}) => {
|
||||
const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>({});
|
||||
const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({});
|
||||
const { status: timelineStatus } = useSelector<StoreState, TimelineModel>(
|
||||
const { status: timelineStatus } = useShallowEqualSelector<TimelineModel>(
|
||||
(state) => state.timeline.timelineById[timelineId]
|
||||
);
|
||||
const divElement = useRef<HTMLDivElement | null>(null);
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import '../../../../common/mock/match_media';
|
||||
import { mockBrowserFields } from '../../../../common/containers/source/mock';
|
||||
|
@ -28,13 +27,9 @@ const mockSort: Sort = {
|
|||
sortDirection: Direction.desc,
|
||||
};
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const origin = jest.requireActual('react-redux');
|
||||
return {
|
||||
...origin,
|
||||
useSelector: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('../../../../common/hooks/use_selector', () => ({
|
||||
useShallowEqualSelector: jest.fn().mockReturnValue(mockTimelineModel),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../common/components/link_to');
|
||||
|
||||
|
@ -87,7 +82,6 @@ describe('Body', () => {
|
|||
toggleColumn: jest.fn(),
|
||||
updateNote: jest.fn(),
|
||||
};
|
||||
(useSelector as jest.Mock).mockReturnValue(mockTimelineModel);
|
||||
|
||||
describe('rendering', () => {
|
||||
test('it renders the column headers', () => {
|
||||
|
|
|
@ -15,10 +15,11 @@ import {
|
|||
EuiContextMenuPanelItemDescriptor,
|
||||
} from '@elastic/eui';
|
||||
import uuid from 'uuid';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { BrowserFields } from '../../../../common/containers/source';
|
||||
import { TimelineType } from '../../../../../common/types/timeline';
|
||||
import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { StatefulEditDataProvider } from '../../edit_data_provider';
|
||||
import { addContentToTimeline } from './helpers';
|
||||
import { DataProviderType } from './data_provider';
|
||||
|
@ -36,7 +37,7 @@ const AddDataProviderPopoverComponent: React.FC<AddDataProviderPopoverProps> = (
|
|||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false);
|
||||
const timelineById = useSelector(timelineSelectors.timelineByIdSelector);
|
||||
const timelineById = useShallowEqualSelector(timelineSelectors.timelineByIdSelector);
|
||||
const { dataProviders, timelineType } = timelineById[timelineId] ?? {};
|
||||
|
||||
const handleOpenPopover = useCallback(() => setIsAddFilterPopoverOpen(true), [
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
|
||||
import { noop } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { TimelineType } from '../../../../../common/types/timeline';
|
||||
import { BrowserFields } from '../../../../common/containers/source';
|
||||
import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { timelineSelectors } from '../../../store/timeline';
|
||||
|
||||
import { OnDataProviderEdited } from '../events';
|
||||
|
@ -59,7 +60,7 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>(
|
|||
val,
|
||||
type = DataProviderType.default,
|
||||
}) => {
|
||||
const timelineById = useSelector(timelineSelectors.timelineByIdSelector);
|
||||
const timelineById = useShallowEqualSelector(timelineSelectors.timelineByIdSelector);
|
||||
const timelineType = timelineId ? timelineById[timelineId]?.timelineType : TimelineType.default;
|
||||
const { getManageTimelineById } = useManageTimeline();
|
||||
const isLoading = useMemo(() => getManageTimelineById(timelineId ?? '').isLoading, [
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
*/
|
||||
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { SecurityPageName } from '../../../../../common/constants';
|
||||
import { getTimelineUrl, useFormatUrl } from '../../../../common/components/link_to';
|
||||
import { CursorPosition } from '../../../../common/components/markdown_editor';
|
||||
|
@ -20,7 +22,7 @@ export const useInsertTimeline = (value: string, onChange: (newValue: string) =>
|
|||
end: 0,
|
||||
});
|
||||
|
||||
const insertTimeline = useSelector(timelineSelectors.selectInsertTimeline, shallowEqual);
|
||||
const insertTimeline = useShallowEqualSelector(timelineSelectors.selectInsertTimeline);
|
||||
|
||||
const handleOnTimelineChange = useCallback(
|
||||
(title: string, id: string | null, graphEventId?: string) => {
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import uuid from 'uuid';
|
||||
import styled from 'styled-components';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { APP_ID } from '../../../../../common/constants';
|
||||
import {
|
||||
|
@ -33,9 +33,9 @@ import {
|
|||
import { SecurityPageName } from '../../../../app/types';
|
||||
import { timelineSelectors } from '../../../../timelines/store/timeline';
|
||||
import { getCreateCaseUrl } from '../../../../common/components/link_to';
|
||||
import { State } from '../../../../common/store';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { Note } from '../../../../common/lib/note';
|
||||
import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
|
||||
import { Notes } from '../../notes';
|
||||
import { AssociateNote, UpdateNote } from '../../notes/helpers';
|
||||
|
@ -159,7 +159,7 @@ interface NewCaseProps {
|
|||
export const NewCase = React.memo<NewCaseProps>(
|
||||
({ compact, graphEventId, onClosePopover, timelineId, timelineStatus, timelineTitle }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { savedObjectId } = useSelector((state: State) =>
|
||||
const { savedObjectId } = useShallowEqualSelector((state) =>
|
||||
timelineSelectors.selectTimeline(state, timelineId)
|
||||
);
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
|
||||
|
||||
import { defaultHeaders } from '../body/column_headers/default_headers';
|
||||
import { timelineActions } from '../../../store/timeline';
|
||||
import { useFullScreen } from '../../../../common/containers/use_full_screen';
|
||||
|
@ -14,9 +15,9 @@ import {
|
|||
TimelineType,
|
||||
TimelineTypeLiteral,
|
||||
} from '../../../../../common/types/timeline';
|
||||
import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { inputsActions, inputsSelectors } from '../../../../common/store/inputs';
|
||||
import { sourcererActions, sourcererSelectors } from '../../../../common/store/sourcerer';
|
||||
import { State } from '../../../../common/store';
|
||||
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
|
||||
|
||||
export const useCreateTimelineButton = ({
|
||||
|
@ -33,9 +34,9 @@ export const useCreateTimelineButton = ({
|
|||
() => sourcererSelectors.getAllExistingIndexNamesSelector(),
|
||||
[]
|
||||
);
|
||||
const existingIndexNames = useSelector<State, string[]>(existingIndexNamesSelector, shallowEqual);
|
||||
const existingIndexNames = useShallowEqualSelector<string[]>(existingIndexNamesSelector);
|
||||
const { timelineFullScreen, setTimelineFullScreen } = useFullScreen();
|
||||
const globalTimeRange = useSelector(inputsSelectors.globalTimeRangeSelector);
|
||||
const globalTimeRange = useShallowEqualSelector(inputsSelectors.globalTimeRangeSelector);
|
||||
const createTimeline = useCallback(
|
||||
({ id, show }) => {
|
||||
if (id === TimelineId.active && timelineFullScreen) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue