diff --git a/src/containers/App/Content.tsx b/src/containers/App/Content.tsx
index 687eb45483..38204d933e 100644
--- a/src/containers/App/Content.tsx
+++ b/src/containers/App/Content.tsx
@@ -22,7 +22,7 @@ import type {RawBreadcrumbItem} from '../Header/breadcrumbs';
import Node from '../Node/Node';
import {PDiskPage} from '../PDiskPage/PDiskPage';
import {Tablet} from '../Tablet';
-import TabletsFilters from '../TabletsFilters/TabletsFilters';
+import {TabletsFilters} from '../TabletsFilters/TabletsFilters';
import Tenant from '../Tenant/Tenant';
import {VDiskPage} from '../VDiskPage/VDiskPage';
diff --git a/src/containers/App/appSlots.tsx b/src/containers/App/appSlots.tsx
index 7161f7e4b6..09a8bd7292 100644
--- a/src/containers/App/appSlots.tsx
+++ b/src/containers/App/appSlots.tsx
@@ -6,7 +6,7 @@ import type {Clusters} from '../Clusters/Clusters';
import type Node from '../Node/Node';
import type {PDiskPage} from '../PDiskPage/PDiskPage';
import type {Tablet} from '../Tablet';
-import type TabletsFilters from '../TabletsFilters/TabletsFilters';
+import type {TabletsFilters} from '../TabletsFilters/TabletsFilters';
import type Tenant from '../Tenant/Tenant';
import type {VDiskPage} from '../VDiskPage/VDiskPage';
diff --git a/src/containers/Tablets/Tablets.tsx b/src/containers/Tablets/Tablets.tsx
index b3b84bd7fe..1eb9bf4c42 100644
--- a/src/containers/Tablets/Tablets.tsx
+++ b/src/containers/Tablets/Tablets.tsx
@@ -147,19 +147,22 @@ interface TabletsProps {
export function Tablets({nodeId, path, className}: TabletsProps) {
const {autorefresh} = useTypedSelector((state) => state.schema);
- let params: TabletsApiRequestParams | typeof skipToken = skipToken;
+ let params: TabletsApiRequestParams = {};
const node = nodeId === undefined ? undefined : String(nodeId);
if (node !== undefined) {
params = {nodes: [String(node)]};
} else if (path) {
params = {path};
}
- const {currentData, isFetching, error} = tabletsApi.useGetTabletsInfoQuery(params, {
- pollingInterval: autorefresh,
- });
+ const {currentData, isFetching, error} = tabletsApi.useGetTabletsInfoQuery(
+ Object.keys(params).length === 0 ? skipToken : params,
+ {
+ pollingInterval: autorefresh,
+ },
+ );
const loading = isFetching && currentData === undefined;
- const tablets = useTypedSelector((state) => selectTabletsWithFqdn(state, node, path));
+ const tablets = useTypedSelector((state) => selectTabletsWithFqdn(state, params));
if (loading) {
return ;
diff --git a/src/containers/TabletsFilters/TabletsFilters.js b/src/containers/TabletsFilters/TabletsFilters.js
deleted file mode 100644
index 57ce1a94f9..0000000000
--- a/src/containers/TabletsFilters/TabletsFilters.js
+++ /dev/null
@@ -1,367 +0,0 @@
-import React from 'react';
-
-import {Loader, Select} from '@gravity-ui/uikit';
-import isEqual from 'lodash/isEqual';
-import map from 'lodash/map';
-import PropTypes from 'prop-types';
-import {Helmet} from 'react-helmet-async';
-import ReactList from 'react-list';
-import {connect} from 'react-redux';
-
-import {AccessDenied} from '../../components/Errors/403';
-import {Tablet} from '../../components/Tablet';
-import {parseQuery} from '../../routes';
-import {setHeaderBreadcrumbs} from '../../store/reducers/header/header';
-import {
- clearWasLoadingFlag,
- getFilteredTablets,
- getTablets,
- getTabletsInfo,
- setStateFilter,
- setTypeFilter,
-} from '../../store/reducers/tabletsFilters';
-import {cn} from '../../utils/cn';
-import {CLUSTER_DEFAULT_TITLE} from '../../utils/constants';
-import {tabletColorToTabletState, tabletStates} from '../../utils/tablet';
-
-import i18n from './i18n';
-
-import './TabletsFilters.scss';
-
-const b = cn('tablets-filters');
-
-export class TabletsFilters extends React.Component {
- static propTypes = {
- wasLoaded: PropTypes.bool,
- loading: PropTypes.bool,
- getTabletsInfo: PropTypes.func,
- timeoutForRequest: PropTypes.number,
- path: PropTypes.string,
- clearWasLoadingFlag: PropTypes.func,
- nodes: PropTypes.array,
- tablets: PropTypes.array,
- filteredTablets: PropTypes.array,
- setStateFilter: PropTypes.func,
- setTypeFilter: PropTypes.func,
- stateFilter: PropTypes.array,
- typeFilter: PropTypes.array,
- error: PropTypes.oneOf([PropTypes.string, PropTypes.object]),
- setHeader: PropTypes.func,
- };
-
- static renderLoader() {
- return (
-
-
-
- );
- }
-
- static parseNodes = (nodes) => {
- if (Array.isArray(nodes)) {
- return nodes.map(Number).filter(Number.isInteger);
- }
- };
-
- static getStateFiltersFromColor = (color) => {
- return tabletColorToTabletState[color] || [color];
- };
-
- static CONTROL_WIDTH = 220;
- static POPUP_WIDTH = 300;
-
- state = {
- nodeFilter: [],
- tenantPath: '',
- clusterName: '',
- };
-
- reloadDescriptor = -1;
-
- componentDidMount() {
- const {setStateFilter, setTypeFilter, setHeaderBreadcrumbs} = this.props;
-
- const queryParams = parseQuery(this.props.location);
- const {nodeIds, type, path, state, clusterName} = queryParams;
- const nodes = TabletsFilters.parseNodes(nodeIds);
-
- if (state) {
- const stateFilter = TabletsFilters.getStateFiltersFromColor(state);
- setStateFilter(stateFilter);
- }
-
- if (type) {
- setTypeFilter([type]);
- }
-
- this.setState({nodeFilter: nodes, tenantPath: path, clusterName}, () => {
- this.makeRequest();
- });
-
- setHeaderBreadcrumbs('tablets', {
- tenantName: path,
- });
- }
-
- componentDidUpdate(prevProps) {
- const {loading, error} = this.props;
- if (!error && prevProps.path && this.props.path && prevProps.path !== this.props.path) {
- this.props.clearWasLoadingFlag();
- this.getTablets();
- }
-
- if (!error && !loading && this.reloadDescriptor === -1) {
- this.getTablets();
- }
- }
-
- componentWillUnmount() {
- clearInterval(this.reloadDescriptor);
- }
-
- makeRequest = () => {
- const {nodeFilter, tenantPath} = this.state;
-
- this.props.getTabletsInfo({nodes: nodeFilter, path: [tenantPath]});
- };
-
- getTablets = () => {
- const {timeoutForRequest} = this.props;
- clearInterval(this.reloadDescriptor);
- this.reloadDescriptor = setTimeout(() => {
- this.makeRequest();
- this.reloadDescriptor = -1;
- }, timeoutForRequest);
- };
-
- handleNodeFilterChange = (nodeFilter) => {
- this.setState({nodeFilter}, () => {
- this.props.clearWasLoadingFlag();
- this.makeRequest();
- });
- };
-
- handleStateFilterChange = (stateFilter) => {
- const {setStateFilter} = this.props;
- setStateFilter(stateFilter);
- };
-
- handleTypeFilterChange = (typeFilter) => {
- const {setTypeFilter} = this.props;
- setTypeFilter(typeFilter);
- };
-
- renderTablet = (index, key) => {
- const {filteredTablets, size} = this.props;
-
- return (
-
- );
- };
-
- renderContent = () => {
- const {nodeFilter, tenantPath} = this.state;
- const {tablets, filteredTablets, nodes, stateFilter, typeFilter, error} = this.props;
-
- const states = tabletStates.map((item) => ({value: item, content: item}));
- const types = Array.from(new Set(...[map(tablets, (tblt) => tblt.Type)])).map((item) => ({
- value: item,
- content: item,
- }));
-
- const nodesForSelect = map(nodes, (node) => ({
- content: node.Id,
- value: node.Id,
- meta: node.Host,
- }));
-
- return (
-
- {tenantPath ? (
-
-
- Database: {tenantPath}
-
-
- ) : null}
-
-
- {error &&
{error}
}
-
- {filteredTablets.length > 0 ? (
-
-
-
- ) : (
- !error &&
no tablets
- )}
-
- );
- };
-
- renderView = () => {
- const {loading, wasLoaded, error} = this.props;
-
- if (loading && !wasLoaded) {
- return TabletsFilters.renderLoader();
- } else if (error && typeof error === 'object') {
- if (error.status === 403) {
- return ;
- }
-
- return {error.statusText}
;
- } else {
- return this.renderContent();
- }
- };
-
- render() {
- const {tenantPath, clusterName} = this.state;
-
- return (
-
-
- {`${i18n('page.title')} — ${
- tenantPath || clusterName || CLUSTER_DEFAULT_TITLE
- }`}
-
- {this.renderView()}
-
- );
- }
-}
-
-const Filters = ({
- nodesForSelect,
- nodeFilter,
- onChangeNodes,
-
- states,
- stateFilter,
- onChangeStates,
-
- types,
- typeFilter,
- onChangeTypes,
-}) => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-Filters.propTypes = {
- nodesForSelect: PropTypes.array,
- nodeFilter: PropTypes.array,
- onChangeNodes: PropTypes.func,
-
- states: PropTypes.array,
- stateFilter: PropTypes.array,
- onChangeStates: PropTypes.func,
-
- types: PropTypes.array,
- typeFilter: PropTypes.array,
- onChangeTypes: PropTypes.func,
-};
-
-const MemoizedFilters = React.memo(Filters, (prevProps, nextProps) => {
- return (
- isEqual(prevProps.nodeFilter, nextProps.nodeFilter) &&
- isEqual(prevProps.stateFilter, nextProps.stateFilter) &&
- isEqual(prevProps.typeFilter, nextProps.typeFilter)
- );
-});
-
-const mapStateToProps = (state) => {
- const {nodes, wasLoaded, loading, timeoutForRequest, stateFilter, typeFilter, error} =
- state.tabletsFilters;
-
- return {
- tablets: getTablets(state),
- filteredTablets: getFilteredTablets(state),
- nodes,
- timeoutForRequest,
- wasLoaded,
- loading,
- stateFilter,
- typeFilter,
- error,
- };
-};
-
-const mapDispatchToProps = {
- getTabletsInfo,
- clearWasLoadingFlag,
- setStateFilter,
- setTypeFilter,
- setHeaderBreadcrumbs,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(TabletsFilters);
diff --git a/src/containers/TabletsFilters/TabletsFilters.tsx b/src/containers/TabletsFilters/TabletsFilters.tsx
new file mode 100644
index 0000000000..cbc8b24c30
--- /dev/null
+++ b/src/containers/TabletsFilters/TabletsFilters.tsx
@@ -0,0 +1,245 @@
+import React from 'react';
+
+import type {SelectOption} from '@gravity-ui/uikit';
+import {Loader, Select} from '@gravity-ui/uikit';
+import map from 'lodash/map';
+import {Helmet} from 'react-helmet-async';
+import ReactList from 'react-list';
+import {ArrayParam, StringParam, useQueryParams} from 'use-query-params';
+import {z} from 'zod';
+
+import {AccessDenied} from '../../components/Errors/403';
+import {ResponseError} from '../../components/Errors/ResponseError';
+import {Tablet} from '../../components/Tablet';
+import {setHeaderBreadcrumbs} from '../../store/reducers/header/header';
+import {nodesListApi} from '../../store/reducers/nodesList';
+import {tabletsApi} from '../../store/reducers/tablets';
+import {getFilteredTablets} from '../../store/reducers/tabletsFilters';
+import type {EFlag} from '../../types/api/enums';
+import {cn} from '../../utils/cn';
+import {AUTO_RELOAD_INTERVAL, CLUSTER_DEFAULT_TITLE} from '../../utils/constants';
+import {useTypedDispatch, useTypedSelector} from '../../utils/hooks';
+import {tabletColorToTabletState, tabletStates} from '../../utils/tablet';
+
+import i18n from './i18n';
+
+import './TabletsFilters.scss';
+
+const b = cn('tablets-filters');
+
+const stringArrayParam = z.preprocess((arg) => {
+ if (Array.isArray(arg)) {
+ return arg.filter(Boolean).sort();
+ }
+ return [];
+}, z.string().array());
+
+const stateArrayParam = z.preprocess((arg) => {
+ if (Array.isArray(arg)) {
+ return arg
+ .flatMap((item) => tabletColorToTabletState[item as EFlag] || item)
+ .filter(Boolean);
+ }
+ return [];
+}, z.string().array());
+
+const CONTROL_WIDTH = 220;
+const POPUP_WIDTH = 300;
+
+export function TabletsFilters() {
+ const [params, setParams] = useQueryParams({
+ nodeIds: ArrayParam,
+ type: ArrayParam,
+ state: ArrayParam,
+ path: StringParam,
+ clusterName: StringParam,
+ });
+
+ const path = params.path ?? undefined;
+ const dispatch = useTypedDispatch();
+ React.useEffect(() => {
+ dispatch(
+ setHeaderBreadcrumbs('tablets', {
+ tenantName: path,
+ }),
+ );
+ }, [dispatch, path]);
+
+ const nodeIds = stringArrayParam.parse(params.nodeIds);
+
+ const {currentData, isFetching, error} = tabletsApi.useGetTabletsInfoQuery(
+ {nodes: nodeIds, path},
+ {
+ pollingInterval: AUTO_RELOAD_INTERVAL,
+ },
+ );
+
+ const {data: nodes} = nodesListApi.useGetNodesListQuery(
+ {},
+ {pollingInterval: AUTO_RELOAD_INTERVAL},
+ );
+
+ const loading = isFetching && currentData === undefined;
+
+ const stateFilter = stateArrayParam.parse(params.state);
+ const states = tabletStates.map((item) => ({value: item, content: item}));
+ const typeFilter = stringArrayParam.parse(params.type);
+ const types = Array.from(
+ new Set(...[map(currentData?.TabletStateInfo, (tblt) => tblt.Type)]),
+ ).map((item) => ({
+ value: String(item),
+ content: item,
+ }));
+
+ const filteredTablets = useTypedSelector((state) =>
+ getFilteredTablets(state, {nodes: nodeIds, path}, stateFilter, typeFilter),
+ );
+
+ const renderTablet = (index: number, key: string) => (
+
+ );
+
+ const nodesForSelect = map(nodes, (node) => ({
+ content: node.Id,
+ value: String(node.Id),
+ data: node.Host,
+ }));
+
+ const renderView = () => {
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+ if (error && typeof error === 'object' && 'status' in error && error.status === 403) {
+ return ;
+ }
+ return (
+
+ {path ? (
+
+ Database: {path}
+
+ ) : null}
+
setParams({nodeIds: n})}
+ states={states}
+ stateFilter={stateFilter}
+ onChangeStates={(s) => setParams({state: s})}
+ types={types}
+ typeFilter={typeFilter}
+ onChangeTypes={(t) => setParams({type: t})}
+ />
+
+ {error ? : null}
+
+ {filteredTablets.length > 0 ? (
+
+
+
+ ) : (
+ !error && no tablets
+ )}
+
+ );
+ };
+
+ return (
+
+
+ {`${i18n('page.title')} — ${
+ path || params.clusterName || CLUSTER_DEFAULT_TITLE
+ }`}
+
+ {renderView()}
+
+ );
+}
+
+interface TabletFilterProps {
+ nodesForSelect: SelectOption[];
+ nodeFilter: string[];
+ onChangeNodes: (nodes: string[]) => void;
+
+ states: SelectOption[];
+ stateFilter: string[];
+ onChangeStates: (states: string[]) => void;
+
+ types: SelectOption[];
+ typeFilter: string[];
+ onChangeTypes: (types: string[]) => void;
+}
+
+function Filters({
+ nodesForSelect,
+ nodeFilter,
+ onChangeNodes,
+
+ states,
+ stateFilter,
+ onChangeStates,
+
+ types,
+ typeFilter,
+ onChangeTypes,
+}: TabletFilterProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/containers/Tenant/Acl/Acl.tsx b/src/containers/Tenant/Acl/Acl.tsx
index cb07c99237..bbe9aba609 100644
--- a/src/containers/Tenant/Acl/Acl.tsx
+++ b/src/containers/Tenant/Acl/Acl.tsx
@@ -1,15 +1,16 @@
import React from 'react';
import type {Column} from '@gravity-ui/react-data-table';
+import {skipToken} from '@reduxjs/toolkit/query';
import {ResponseError} from '../../../components/Errors/ResponseError';
import {Loader} from '../../../components/Loader';
import {ResizeableDataTable} from '../../../components/ResizeableDataTable/ResizeableDataTable';
-import {getSchemaAcl, setAclWasNotLoaded} from '../../../store/reducers/schemaAcl/schemaAcl';
+import {schemaAclApi} from '../../../store/reducers/schemaAcl/schemaAcl';
import type {TACE} from '../../../types/api/acl';
import {cn} from '../../../utils/cn';
import {DEFAULT_TABLE_SETTINGS} from '../../../utils/constants';
-import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks';
+import {useTypedSelector} from '../../../utils/hooks';
import i18n from '../i18n';
import './Acl.scss';
@@ -71,21 +72,13 @@ const columns: Column[] = [
];
export const Acl = () => {
- const dispatch = useTypedDispatch();
-
const {currentSchemaPath} = useTypedSelector((state) => state.schema);
- const {loading, error, acl, owner, wasLoaded} = useTypedSelector((state) => state.schemaAcl);
-
- React.useEffect(() => {
- if (currentSchemaPath) {
- dispatch(getSchemaAcl({path: currentSchemaPath}));
- }
+ const {currentData, isFetching, error} = schemaAclApi.useGetSchemaAclQuery(
+ currentSchemaPath ? {path: currentSchemaPath} : skipToken,
+ );
- return () => {
- // Ensures correct acl on path change
- dispatch(setAclWasNotLoaded());
- };
- }, [currentSchemaPath, dispatch]);
+ const loading = isFetching && !currentData;
+ const {acl, owner} = currentData || {};
const renderTable = () => {
if (!acl || !acl.length) {
@@ -115,7 +108,7 @@ export const Acl = () => {
);
};
- if (loading && !wasLoaded) {
+ if (loading) {
return ;
}
@@ -123,7 +116,7 @@ export const Acl = () => {
return ;
}
- if (!loading && !acl && !owner) {
+ if (!acl && !owner) {
return {i18n('acl.empty')};
}
diff --git a/src/containers/Tenant/Diagnostics/HotKeys/HotKeys.tsx b/src/containers/Tenant/Diagnostics/HotKeys/HotKeys.tsx
index a0cff80a60..e616fdb756 100644
--- a/src/containers/Tenant/Diagnostics/HotKeys/HotKeys.tsx
+++ b/src/containers/Tenant/Diagnostics/HotKeys/HotKeys.tsx
@@ -7,17 +7,11 @@ import {Button, Card, Icon} from '@gravity-ui/uikit';
import {ResponseError} from '../../../../components/Errors/ResponseError';
import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable';
-import {
- setHotKeysData,
- setHotKeysDataWasNotLoaded,
- setHotKeysError,
- setHotKeysLoading,
-} from '../../../../store/reducers/hotKeys/hotKeys';
-import type {IResponseError} from '../../../../types/api/error';
+import {hotKeysApi} from '../../../../store/reducers/hotKeys/hotKeys';
import type {HotKey} from '../../../../types/api/hotkeys';
import {cn} from '../../../../utils/cn';
import {DEFAULT_TABLE_SETTINGS, IS_HOTKEYS_HELP_HIDDDEN_KEY} from '../../../../utils/constants';
-import {useSetting, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks';
+import {useSetting, useTypedSelector} from '../../../../utils/hooks';
import i18n from './i18n';
@@ -63,13 +57,9 @@ interface HotKeysProps {
}
export function HotKeys({path}: HotKeysProps) {
- const dispatch = useTypedDispatch();
+ const {currentData: data, isFetching, error} = hotKeysApi.useGetHotKeysQuery({path});
+ const loading = isFetching && data === undefined;
- const [helpHidden, setHelpHidden] = useSetting(IS_HOTKEYS_HELP_HIDDDEN_KEY);
-
- const collectSamplesTimerRef = React.useRef>();
-
- const {loading, wasLoaded, data, error} = useTypedSelector((state) => state.hotKeys);
const {loading: schemaLoading, data: schemaData} = useTypedSelector((state) => state.schema);
const keyColumnsIds = schemaData[path]?.PathDescription?.Table?.KeyColumnNames;
@@ -78,52 +68,9 @@ export function HotKeys({path}: HotKeysProps) {
return getHotKeysColumns(keyColumnsIds);
}, [keyColumnsIds]);
- React.useEffect(() => {
- const fetchHotkeys = async (enableSampling: boolean) => {
- // Set hotkeys error, but not data, since data is set conditionally
- try {
- const response = await window.api.getHotKeys(path, enableSampling);
- return response;
- } catch (err) {
- dispatch(setHotKeysError(err as IResponseError));
- return undefined;
- }
- };
-
- const fetchData = async () => {
- // If there is previous pending request for samples, cancel it
- if (collectSamplesTimerRef.current !== undefined) {
- window.clearInterval(collectSamplesTimerRef.current);
- }
-
- dispatch(setHotKeysDataWasNotLoaded());
- dispatch(setHotKeysLoading());
-
- // Send request that will trigger hot keys sampling (enable_sampling = true)
- const initialResponse = await fetchHotkeys(true);
-
- // If there are hotkeys in the initial request (hotkeys was collected before)
- // we could just use colleted samples (collected hotkeys are stored only for 30 seconds)
- if (initialResponse && initialResponse.hotkeys) {
- dispatch(setHotKeysData(initialResponse));
- } else if (initialResponse) {
- // Else wait for 5 seconds, while hot keys are being collected
- // And request these samples (enable_sampling = false)
- const timer = setTimeout(async () => {
- const responseWithSamples = await fetchHotkeys(false);
- if (responseWithSamples) {
- dispatch(setHotKeysData(responseWithSamples));
- }
- }, 5000);
- collectSamplesTimerRef.current = timer;
- }
- };
- fetchData();
- }, [dispatch, path]);
-
const renderContent = () => {
// It takes a while to collect hot keys. Display explicit status message, while collecting
- if ((loading && !wasLoaded) || schemaLoading) {
+ if (loading || schemaLoading) {
return {i18n('hot-keys-collecting')}
;
}
@@ -149,29 +96,31 @@ export function HotKeys({path}: HotKeysProps) {
);
};
- const renderHelpCard = () => {
- if (helpHidden) {
- return null;
- }
-
- return (
-
- {i18n('help')}
-
-
- );
- };
-
return (
- {renderHelpCard()}
+
{renderContent()}
);
}
+
+function HelpCard() {
+ const [helpHidden, setHelpHidden] = useSetting(IS_HOTKEYS_HELP_HIDDDEN_KEY);
+
+ if (helpHidden) {
+ return null;
+ }
+
+ return (
+
+ {i18n('help')}
+
+
+ );
+}
diff --git a/src/routes.ts b/src/routes.ts
index 6a50d66e25..11bd079963 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -69,7 +69,9 @@ export function createHref(
extendedQuery = {...extendedQuery, clusterName};
}
- const search = isEmpty(extendedQuery) ? '' : `?${qs.stringify(extendedQuery, {encode: false})}`;
+ const search = isEmpty(extendedQuery)
+ ? ''
+ : `?${qs.stringify(extendedQuery, {encode: false, arrayFormat: 'repeat'})}`;
const preparedRoute = prepareRoute(route);
diff --git a/src/store/reducers/hotKeys/hotKeys.ts b/src/store/reducers/hotKeys/hotKeys.ts
index 5163c68f5d..0b3db0a13f 100644
--- a/src/store/reducers/hotKeys/hotKeys.ts
+++ b/src/store/reducers/hotKeys/hotKeys.ts
@@ -1,76 +1,39 @@
-import type {Reducer} from '@reduxjs/toolkit';
-
-import type {IResponseError} from '../../../types/api/error';
-import type {JsonHotKeysResponse} from '../../../types/api/hotkeys';
-import {createRequestActionTypes} from '../../utils';
-
-import type {HotKeysAction, HotKeysState} from './types';
-
-export const FETCH_HOT_KEYS = createRequestActionTypes('hot_keys', 'FETCH_HOT_KEYS');
-const SET_DATA_WAS_NOT_LOADED = 'hot_keys/SET_DATA_WAS_NOT_LOADED';
-
-const initialState = {loading: true, wasLoaded: false, data: null};
-
-const hotKeys: Reducer = (state = initialState, action) => {
- switch (action.type) {
- case FETCH_HOT_KEYS.REQUEST: {
- return {
- ...state,
- loading: true,
- };
- }
- case FETCH_HOT_KEYS.SUCCESS: {
- return {
- ...state,
- data: action.data.hotkeys,
- loading: false,
- error: undefined,
- wasLoaded: true,
- };
- }
- case FETCH_HOT_KEYS.FAILURE: {
- if (action.error?.isCancelled) {
- return state;
- }
-
- return {
- ...state,
- error: action.error,
- loading: false,
- };
- }
- case SET_DATA_WAS_NOT_LOADED: {
- return {
- ...state,
- wasLoaded: false,
- };
- }
- default:
- return state;
- }
-};
-
-export function setHotKeysDataWasNotLoaded() {
- return {
- type: SET_DATA_WAS_NOT_LOADED,
- } as const;
-}
-export function setHotKeysLoading() {
- return {
- type: FETCH_HOT_KEYS.REQUEST,
- } as const;
-}
-export function setHotKeysData(data: JsonHotKeysResponse) {
- return {
- type: FETCH_HOT_KEYS.SUCCESS,
- data: data,
- } as const;
-}
-export function setHotKeysError(error: IResponseError) {
- return {
- type: FETCH_HOT_KEYS.FAILURE,
- error: error,
- } as const;
-}
-
-export default hotKeys;
+import type {HotKey} from '../../../types/api/hotkeys';
+import {api} from '../api';
+
+export const hotKeysApi = api.injectEndpoints({
+ endpoints: (builder) => ({
+ getHotKeys: builder.query({
+ queryFn: async ({path}, {signal}) => {
+ try {
+ // Send request that will trigger hot keys sampling (enable_sampling = true)
+ const initialResponse = await window.api.getHotKeys(path, true, {signal});
+
+ // If there are hotkeys in the initial request (hotkeys was collected before)
+ // we could just use colleted samples (collected hotkeys are stored only for 30 seconds)
+ if (Array.isArray(initialResponse.hotkeys)) {
+ return {data: initialResponse.hotkeys};
+ }
+
+ // Else wait for 5 seconds, while hot keys are being collected
+ await Promise.race([
+ new Promise((resolve) => {
+ setTimeout(resolve, 5000);
+ }),
+ new Promise((_, reject) => {
+ signal.addEventListener('abort', reject);
+ }),
+ ]);
+
+ // And request these samples (enable_sampling = false)
+ const response = await window.api.getHotKeys(path, false, {signal});
+ return {data: response.hotkeys ?? null};
+ } catch (error) {
+ return {error};
+ }
+ },
+ providesTags: ['All'],
+ }),
+ }),
+ overrideExisting: 'throw',
+});
diff --git a/src/store/reducers/hotKeys/types.ts b/src/store/reducers/hotKeys/types.ts
deleted file mode 100644
index 9c959f1d04..0000000000
--- a/src/store/reducers/hotKeys/types.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import type {IResponseError} from '../../../types/api/error';
-import type {HotKey} from '../../../types/api/hotkeys';
-
-import type {
- setHotKeysData,
- setHotKeysDataWasNotLoaded,
- setHotKeysError,
- setHotKeysLoading,
-} from './hotKeys';
-
-export interface HotKeysState {
- loading: boolean;
- wasLoaded: boolean;
- data: null | HotKey[];
- error?: IResponseError;
-}
-
-export type HotKeysAction =
- | ReturnType
- | ReturnType
- | ReturnType
- | ReturnType;
diff --git a/src/store/reducers/index.ts b/src/store/reducers/index.ts
index f7a657fcae..66f0b49315 100644
--- a/src/store/reducers/index.ts
+++ b/src/store/reducers/index.ts
@@ -9,16 +9,13 @@ import executeTopQueries from './executeTopQueries/executeTopQueries';
import fullscreen from './fullscreen';
import header from './header/header';
import heatmap from './heatmap';
-import hotKeys from './hotKeys/hotKeys';
import partitions from './partitions/partitions';
import saveQuery from './saveQuery';
import schema from './schema/schema';
-import schemaAcl from './schemaAcl/schemaAcl';
import settings from './settings/settings';
import shardsWorkload from './shardsWorkload/shardsWorkload';
import singleClusterMode from './singleClusterMode';
import tablets from './tablets';
-import tabletsFilters from './tabletsFilters';
import tenant from './tenant/tenant';
import tenants from './tenants/tenants';
import tooltip from './tooltip';
@@ -34,13 +31,10 @@ export const rootReducer = {
tenants,
partitions,
executeQuery,
- tabletsFilters,
heatmap,
settings,
- schemaAcl,
executeTopQueries,
shardsWorkload,
- hotKeys,
authentication,
header,
saveQuery,
diff --git a/src/store/reducers/schemaAcl/schemaAcl.ts b/src/store/reducers/schemaAcl/schemaAcl.ts
index cdef8b44d9..67d9700622 100644
--- a/src/store/reducers/schemaAcl/schemaAcl.ts
+++ b/src/store/reducers/schemaAcl/schemaAcl.ts
@@ -1,71 +1,18 @@
-import type {Reducer} from '@reduxjs/toolkit';
-
-import {createApiRequest, createRequestActionTypes} from '../../utils';
-
-import type {SchemaAclAction, SchemaAclState} from './types';
-
-export const FETCH_SCHEMA_ACL = createRequestActionTypes('schemaAcl', 'FETCH_SCHEMA_ACL');
-const SET_ACL_WAS_NOT_LOADED = 'schemaAcl/SET_DATA_WAS_NOT_LOADED';
-
-const initialState = {
- loading: false,
- wasLoaded: false,
-};
-
-const schemaAcl: Reducer = (state = initialState, action) => {
- switch (action.type) {
- case FETCH_SCHEMA_ACL.REQUEST: {
- return {
- ...state,
- loading: true,
- };
- }
- case FETCH_SCHEMA_ACL.SUCCESS: {
- const acl = action.data.Common?.ACL;
- const owner = action.data.Common?.Owner;
-
- return {
- ...state,
- acl,
- owner,
- loading: false,
- wasLoaded: true,
- error: undefined,
- };
- }
- case FETCH_SCHEMA_ACL.FAILURE: {
- if (action.error?.isCancelled) {
- return state;
- }
-
- return {
- ...state,
- error: action.error,
- loading: false,
- };
- }
- case SET_ACL_WAS_NOT_LOADED: {
- return {
- ...state,
- wasLoaded: false,
- };
- }
- default:
- return state;
- }
-};
-
-export function getSchemaAcl({path}: {path: string}) {
- return createApiRequest({
- request: window.api.getSchemaAcl({path}),
- actions: FETCH_SCHEMA_ACL,
- });
-}
-
-export const setAclWasNotLoaded = () => {
- return {
- type: SET_ACL_WAS_NOT_LOADED,
- } as const;
-};
-
-export default schemaAcl;
+import {api} from '../api';
+
+export const schemaAclApi = api.injectEndpoints({
+ endpoints: (build) => ({
+ getSchemaAcl: build.query({
+ queryFn: async ({path}: {path: string}, {signal}) => {
+ try {
+ const data = await window.api.getSchemaAcl({path}, {signal});
+ return {data: {acl: data.Common.ACL, owner: data.Common.Owner}};
+ } catch (error) {
+ return {error};
+ }
+ },
+ providesTags: ['All'],
+ }),
+ }),
+ overrideExisting: 'throw',
+});
diff --git a/src/store/reducers/schemaAcl/types.ts b/src/store/reducers/schemaAcl/types.ts
deleted file mode 100644
index ca9812d757..0000000000
--- a/src/store/reducers/schemaAcl/types.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import type {TACE, TMetaInfo} from '../../../types/api/acl';
-import type {IResponseError} from '../../../types/api/error';
-import type {ApiRequestAction} from '../../utils';
-
-import type {FETCH_SCHEMA_ACL, setAclWasNotLoaded} from './schemaAcl';
-
-export interface SchemaAclState {
- loading: boolean;
- wasLoaded: boolean;
- acl?: TACE[];
- owner?: string;
- error?: IResponseError;
-}
-
-export type SchemaAclAction =
- | ApiRequestAction
- | ReturnType;
diff --git a/src/store/reducers/tablets.ts b/src/store/reducers/tablets.ts
index de17e233e7..eeca708946 100644
--- a/src/store/reducers/tablets.ts
+++ b/src/store/reducers/tablets.ts
@@ -1,5 +1,6 @@
-import {createSelector, createSlice} from '@reduxjs/toolkit';
+import {createSelector, createSlice, lruMemoize} from '@reduxjs/toolkit';
import type {PayloadAction} from '@reduxjs/toolkit';
+import isEqual from 'lodash/isEqual';
import type {ETabletState, EType, TTabletStateInfo} from '../../types/api/tablet';
import type {TabletsApiRequestParams, TabletsState} from '../../types/store/tablets';
@@ -47,23 +48,22 @@ export const tabletsApi = api.injectEndpoints({
});
const getTabletsInfoSelector = createSelector(
- (nodeId: string | undefined, path: string | undefined) => ({nodeId, path}),
- ({nodeId, path}) =>
- tabletsApi.endpoints.getTabletsInfo.select(
- nodeId === undefined ? {path} : {nodes: [nodeId]},
- ),
+ (params: TabletsApiRequestParams) => params,
+ (params) => tabletsApi.endpoints.getTabletsInfo.select(params),
+ {
+ argsMemoize: lruMemoize,
+ argsMemoizeOptions: {equalityCheck: isEqual},
+ },
);
-const selectGetTabletsInfo = createSelector(
+export const selectGetTabletsInfo = createSelector(
(state: RootState) => state,
- (_state: RootState, nodeId: string | undefined, path: string | undefined) =>
- getTabletsInfoSelector(nodeId, path),
+ (_state: RootState, params: TabletsApiRequestParams) => getTabletsInfoSelector(params),
(state, selectTabletsInfo) => selectTabletsInfo(state).data,
);
export const selectTabletsWithFqdn = createSelector(
- (state: RootState, nodeId: string | undefined, path: string | undefined) =>
- selectGetTabletsInfo(state, nodeId, path),
+ (state: RootState, params: TabletsApiRequestParams) => selectGetTabletsInfo(state, params),
(state: RootState) => selectNodesMap(state),
(data, nodesMap): (TTabletStateInfo & {fqdn?: string})[] => {
if (!data?.TabletStateInfo) {
diff --git a/src/store/reducers/tabletsFilters.js b/src/store/reducers/tabletsFilters.js
deleted file mode 100644
index cc38bd3413..0000000000
--- a/src/store/reducers/tabletsFilters.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import {createSelector} from '@reduxjs/toolkit';
-
-import {AUTO_RELOAD_INTERVAL} from '../../utils/constants';
-import {createApiRequest, createRequestActionTypes} from '../utils';
-
-const FETCH_TABLETS_FILTERS = createRequestActionTypes('tabletsFilters', 'FETCH_TABLETS_FILTERS');
-
-const initialState = {
- data: undefined,
- loading: true,
- wasLoaded: false,
- stateFilter: [],
- typeFilter: [],
-};
-
-const tabletsFilters = function (state = initialState, action) {
- switch (action.type) {
- case FETCH_TABLETS_FILTERS.REQUEST: {
- return {
- ...state,
- loading: true,
- requestTime: new Date().getTime(),
- };
- }
- case FETCH_TABLETS_FILTERS.SUCCESS: {
- const timeout = new Date().getTime() - state.requestTime;
- const [tabletsData, nodes] = action.data;
- return {
- ...state,
- tabletsData,
- nodes,
- loading: false,
- wasLoaded: true,
- timeoutForRequest: timeout > AUTO_RELOAD_INTERVAL ? timeout : AUTO_RELOAD_INTERVAL,
- error: undefined,
- };
- }
-
- // The error with large uri is handled by GenericAPI
- case FETCH_TABLETS_FILTERS.FAILURE: {
- return {
- ...state,
- error: action.error || 'Request-URI Too Large. Please reload the page',
- loading: false,
- };
- }
- case 'CLEAR_WAS_LOADING_TABLETS': {
- const {stateFilter, typeFilter} = state;
- return {
- ...initialState,
- stateFilter,
- typeFilter,
- };
- }
- case 'SET_STATE_FILTER': {
- return {
- ...state,
- stateFilter: action.data,
- };
- }
- case 'SET_TYPE_FILTER': {
- return {
- ...state,
- typeFilter: action.data,
- };
- }
- default:
- return state;
- }
-};
-
-export const clearWasLoadingFlag = () => ({
- type: 'CLEAR_WAS_LOADING_TABLETS',
-});
-
-export const setStateFilter = (stateFilter) => {
- return {
- type: 'SET_STATE_FILTER',
- data: stateFilter,
- };
-};
-
-export const setTypeFilter = (typeFilter) => {
- return {
- type: 'SET_TYPE_FILTER',
- data: typeFilter,
- };
-};
-
-export function getTabletsInfo(data) {
- return createApiRequest({
- request: Promise.all([window.api.getTabletsInfo(data), window.api.getNodesList()]),
- actions: FETCH_TABLETS_FILTERS,
- });
-}
-
-export const getTablets = (state) => {
- const {tabletsData} = state.tabletsFilters;
- return tabletsData?.TabletStateInfo || [];
-};
-
-export const getFilteredTablets = createSelector(
- [
- getTablets,
- (state) => state.tabletsFilters.stateFilter,
- (state) => state.tabletsFilters.typeFilter,
- ],
- (tablets, stateFilter, typeFilter) => {
- let filteredTablets = tablets;
-
- if (typeFilter.length > 0) {
- filteredTablets = filteredTablets.filter((tblt) =>
- typeFilter.some((filter) => tblt.Type === filter),
- );
- }
- if (stateFilter.length > 0) {
- filteredTablets = filteredTablets.filter((tblt) =>
- stateFilter.some((filter) => tblt.State === filter),
- );
- }
-
- return filteredTablets;
- },
-);
-
-export default tabletsFilters;
diff --git a/src/store/reducers/tabletsFilters.ts b/src/store/reducers/tabletsFilters.ts
new file mode 100644
index 0000000000..03082a4c67
--- /dev/null
+++ b/src/store/reducers/tabletsFilters.ts
@@ -0,0 +1,44 @@
+import {createSelector, lruMemoize} from '@reduxjs/toolkit';
+import {shallowEqual} from 'react-redux';
+
+import type {TTabletStateInfo} from '../../types/api/tablet';
+import type {TabletsApiRequestParams} from '../../types/store/tablets';
+import type {RootState} from '../defaultStore';
+
+import {selectGetTabletsInfo} from './tablets';
+
+const EMPTY_ARRAY: TTabletStateInfo[] = [];
+
+export const getFilteredTablets = createSelector(
+ (state: RootState, params: TabletsApiRequestParams) =>
+ selectGetTabletsInfo(state, params)?.TabletStateInfo,
+ (_: RootState, _params: TabletsApiRequestParams, stateFilter: string[]) => stateFilter,
+ (
+ _: RootState,
+ _params: TabletsApiRequestParams,
+ _stateFilter: string[],
+ typeFilter: string[],
+ ) => typeFilter,
+ (tablets, stateFilter, typeFilter) => {
+ let filteredTablets = tablets ?? EMPTY_ARRAY;
+
+ if (typeFilter.length > 0) {
+ filteredTablets = filteredTablets?.filter((tblt) =>
+ typeFilter.some((filter) => tblt.Type === filter),
+ );
+ }
+ if (stateFilter.length > 0) {
+ filteredTablets = filteredTablets?.filter((tblt) =>
+ stateFilter.some((filter) => tblt.State === filter),
+ );
+ }
+
+ return filteredTablets.length > 0 ? filteredTablets : EMPTY_ARRAY;
+ },
+ {
+ argsMemoize: lruMemoize,
+ argsMemoizeOptions: {
+ equalityCheck: shallowEqual,
+ },
+ },
+);