From 3706e81e7edb970e869428fe5a4d9388d16651f2 Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko Date: Mon, 24 Jun 2024 15:33:44 +0200 Subject: [PATCH] feat(store): rewrite some reducers to rtk-query --- src/containers/App/Content.tsx | 2 +- src/containers/App/appSlots.tsx | 2 +- src/containers/Tablets/Tablets.tsx | 13 +- .../TabletsFilters/TabletsFilters.js | 367 ------------------ .../TabletsFilters/TabletsFilters.tsx | 245 ++++++++++++ src/containers/Tenant/Acl/Acl.tsx | 27 +- .../Tenant/Diagnostics/HotKeys/HotKeys.tsx | 105 ++--- src/routes.ts | 4 +- src/store/reducers/hotKeys/hotKeys.ts | 115 ++---- src/store/reducers/hotKeys/types.ts | 22 -- src/store/reducers/index.ts | 6 - src/store/reducers/schemaAcl/schemaAcl.ts | 89 +---- src/store/reducers/schemaAcl/types.ts | 17 - src/store/reducers/tablets.ts | 22 +- src/store/reducers/tabletsFilters.js | 126 ------ src/store/reducers/tabletsFilters.ts | 44 +++ 16 files changed, 407 insertions(+), 799 deletions(-) delete mode 100644 src/containers/TabletsFilters/TabletsFilters.js create mode 100644 src/containers/TabletsFilters/TabletsFilters.tsx delete mode 100644 src/store/reducers/hotKeys/types.ts delete mode 100644 src/store/reducers/schemaAcl/types.ts delete mode 100644 src/store/reducers/tabletsFilters.js create mode 100644 src/store/reducers/tabletsFilters.ts 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 ( -
-
- -
- -
- { + return ( +
+
{option.content}
+
+ {option.data} +
+
+ ); + }} + getOptionHeight={() => 40} + /> +
+ +
+ +
+
+ ); +} 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, + }, + }, +);