diff --git a/src/components/Illustration/Illustration.tsx b/src/components/Illustration/Illustration.tsx index d4b70e8acd..407bbe7ec0 100644 --- a/src/components/Illustration/Illustration.tsx +++ b/src/components/Illustration/Illustration.tsx @@ -42,5 +42,5 @@ export const Illustration = ({name, className, ...props}: IllustrationProps) => } }, [srcGetter]); - return {name}; + return src ? {name} : null; }; diff --git a/src/containers/Cluster/Cluster.tsx b/src/containers/Cluster/Cluster.tsx index c2d2523446..6c8ac5a22a 100644 --- a/src/containers/Cluster/Cluster.tsx +++ b/src/containers/Cluster/Cluster.tsx @@ -8,8 +8,8 @@ import {Redirect, Route, Switch, useLocation, useRouteMatch} from 'react-router' import {EntityStatus} from '../../components/EntityStatus/EntityStatus'; import {InternalLink} from '../../components/InternalLink'; import routes, {getLocationObjectFromHref} from '../../routes'; -import {getClusterInfo, updateDefaultClusterTab} from '../../store/reducers/cluster/cluster'; -import {getClusterNodes} from '../../store/reducers/clusterNodes/clusterNodes'; +import {clusterApi, updateDefaultClusterTab} from '../../store/reducers/cluster/cluster'; +import {clusterNodesApi} from '../../store/reducers/clusterNodes/clusterNodes'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; import type { AdditionalClusterProps, @@ -18,8 +18,8 @@ import type { AdditionalVersionsProps, } from '../../types/additionalProps'; import {cn} from '../../utils/cn'; -import {CLUSTER_DEFAULT_TITLE} from '../../utils/constants'; -import {useAutofetcher, useTypedDispatch, useTypedSelector} from '../../utils/hooks'; +import {CLUSTER_DEFAULT_TITLE, DEFAULT_POLLING_INTERVAL} from '../../utils/constants'; +import {useTypedDispatch, useTypedSelector} from '../../utils/hooks'; import {parseNodesToVersionsValues, parseVersionsToVersionToColorMap} from '../../utils/versions'; import {NodesWrapper} from '../Nodes/NodesWrapper'; import {StorageWrapper} from '../Storage/StorageWrapper'; @@ -57,34 +57,24 @@ function Cluster({ const queryParams = qs.parse(location.search, { ignoreQueryPrefix: true, }); - const {clusterName} = queryParams; + const {clusterName, backend} = queryParams; const { - data: cluster = {}, - loading: clusterLoading, - wasLoaded: clusterWasLoaded, - error: clusterError, - groupsStats, - } = useTypedSelector((state) => state.cluster); - const { - nodes, - loading: nodesLoading, - wasLoaded: nodesWasLoaded, - } = useTypedSelector((state) => state.clusterNodes); + data: {clusterData: cluster = {}, groupsStats} = {}, + isLoading: isClusterLoading, + error, + } = clusterApi.useGetClusterInfoQuery(clusterName ? String(clusterName) : undefined, { + pollingInterval: DEFAULT_POLLING_INTERVAL, + }); - const {Name} = cluster; + const clusterError = error && typeof error === 'object' ? error : undefined; - const infoLoading = (clusterLoading && !clusterWasLoaded) || (nodesLoading && !nodesWasLoaded); + const {data: nodes = [], isLoading: isNodesLoading} = + clusterNodesApi.useGetClusterNodesQuery(undefined); - React.useEffect(() => { - dispatch(getClusterNodes()); - }, [dispatch]); + const infoLoading = isClusterLoading || isNodesLoading; - useAutofetcher( - () => dispatch(getClusterInfo(clusterName ? String(clusterName) : undefined)), - [dispatch, clusterName], - true, - ); + const {Name} = cluster; React.useEffect(() => { dispatch(setHeaderBreadcrumbs('cluster', {})); @@ -138,7 +128,7 @@ function Cluster({ activeTab={activeTabId} items={clusterTabs} wrapTo={({id}, node) => { - const path = getClusterPath(id as ClusterTab, queryParams); + const path = getClusterPath(id as ClusterTab, {clusterName, backend}); return ( state.singleClusterMode); const {page, pageBreadcrumbsOptions} = useTypedSelector((state) => state.header); - const {data} = useTypedSelector((state) => state.cluster); const queryParams = parseQuery(location); const clusterNameFromQuery = queryParams.clusterName?.toString(); + const {currentData: {clusterData: data} = {}} = + clusterApi.useGetClusterInfoQuery(clusterNameFromQuery); const clusterNameFinal = data?.Name || clusterNameFromQuery; - React.useEffect(() => { - dispatch(getClusterInfo(clusterNameFromQuery)); - }, [dispatch, clusterNameFromQuery]); - const breadcrumbItems = React.useMemo(() => { const rawBreadcrumbs: RawBreadcrumbItem[] = []; let options = pageBreadcrumbsOptions; diff --git a/src/containers/Nodes/Nodes.tsx b/src/containers/Nodes/Nodes.tsx index addba471ca..59ca59daa9 100644 --- a/src/containers/Nodes/Nodes.tsx +++ b/src/containers/Nodes/Nodes.tsx @@ -2,6 +2,7 @@ import React from 'react'; import DataTable from '@gravity-ui/react-data-table'; import {ASCENDING} from '@gravity-ui/react-data-table/build/esm/lib/constants'; +import {skipToken} from '@reduxjs/toolkit/query'; import {EntitiesCount} from '../../components/EntitiesCount'; import {AccessDenied} from '../../components/Errors/403'; @@ -12,28 +13,24 @@ import {Search} from '../../components/Search'; import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/TableWithControlsLayout'; import {UptimeFilter} from '../../components/UptimeFIlter'; import { - getComputeNodes, - getNodes, - resetNodesState, - setDataWasNotLoaded, - setNodesUptimeFilter, + nodesApi, + setInitialState, setSearchValue, setSort, + setUptimeFilter, } from '../../store/reducers/nodes/nodes'; -import {selectFilteredNodes} from '../../store/reducers/nodes/selectors'; +import {filterNodes} from '../../store/reducers/nodes/selectors'; import type {NodesSortParams} from '../../store/reducers/nodes/types'; import {ProblemFilterValues, changeFilter} from '../../store/reducers/settings/settings'; import type {ProblemFilterValue} from '../../store/reducers/settings/types'; import type {AdditionalNodesProps} from '../../types/additionalProps'; import {cn} from '../../utils/cn'; -import {DEFAULT_TABLE_SETTINGS, USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY} from '../../utils/constants'; import { - useAutofetcher, - useSetting, - useTableSort, - useTypedDispatch, - useTypedSelector, -} from '../../utils/hooks'; + DEFAULT_POLLING_INTERVAL, + DEFAULT_TABLE_SETTINGS, + USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY, +} from '../../utils/constants'; +import {useSetting, useTableSort, useTypedDispatch, useTypedSelector} from '../../utils/hooks'; import { NodesUptimeFilterValues, isSortableNodesProperty, @@ -57,47 +54,28 @@ export const Nodes = ({path, additionalNodesProps = {}}: NodesProps) => { const isClusterNodes = !path; - // Since Nodes component is used in several places, - // we need to reset filters, searchValue and loading state - // in nodes reducer when path changes - React.useEffect(() => { - dispatch(resetNodesState()); - }, [dispatch, path]); - const { - wasLoaded, - loading, - error, - nodesUptimeFilter, + uptimeFilter, searchValue, sortOrder = ASCENDING, sortValue = 'NodeId', - totalNodes, } = useTypedSelector((state) => state.nodes); const problemFilter = useTypedSelector((state) => state.settings.problemFilter); const {autorefresh} = useTypedSelector((state) => state.schema); - const nodes = useTypedSelector(selectFilteredNodes); - const [useNodesEndpoint] = useSetting(USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY); - const fetchNodes = React.useCallback( - (isBackground: boolean) => { - if (!isBackground) { - dispatch(setDataWasNotLoaded()); - } + const useAutoRefresh = isClusterNodes ? true : autorefresh; + // If there is no path, it's cluster Nodes tab + const useGetComputeNodes = path && !useNodesEndpoint; + const nodesQuery = nodesApi.useGetNodesQuery(useGetComputeNodes ? skipToken : {path}, { + pollingInterval: useAutoRefresh ? DEFAULT_POLLING_INTERVAL : 0, + }); + const computeQuery = nodesApi.useGetComputeNodesQuery(useGetComputeNodes ? {path} : skipToken, { + pollingInterval: useAutoRefresh ? DEFAULT_POLLING_INTERVAL : 0, + }); - // If there is no path, it's cluster Nodes tab - if (path && !useNodesEndpoint) { - dispatch(getComputeNodes({path})); - } else { - dispatch(getNodes({path})); - } - }, - [dispatch, path, useNodesEndpoint], - ); - - useAutofetcher(fetchNodes, [fetchNodes], isClusterNodes ? true : autorefresh); + const {currentData: data, isLoading, error} = useGetComputeNodes ? computeQuery : nodesQuery; const [sort, handleSort] = useTableSort({sortValue, sortOrder}, (sortParams) => dispatch(setSort(sortParams as NodesSortParams)), @@ -112,9 +90,25 @@ export const Nodes = ({path, additionalNodesProps = {}}: NodesProps) => { }; const handleUptimeFilterChange = (value: NodesUptimeFilterValues) => { - dispatch(setNodesUptimeFilter(value)); + dispatch(setUptimeFilter(value)); }; + // Since Nodes component is used in several places, + // we need to reset filters, searchValue + // in nodes reducer when path changes + React.useEffect(() => { + return () => { + // Clean data on component unmount + dispatch(setInitialState()); + }; + }, [dispatch, path]); + + const nodes = React.useMemo(() => { + return filterNodes(data?.Nodes, {searchValue, uptimeFilter, problemFilter}); + }, [data, searchValue, uptimeFilter, problemFilter]); + + const totalNodes = data?.TotalNodes || 0; + const renderControls = () => { return ( @@ -125,12 +119,12 @@ export const Nodes = ({path, additionalNodesProps = {}}: NodesProps) => { value={searchValue} /> - + ); @@ -148,7 +142,7 @@ export const Nodes = ({path, additionalNodesProps = {}}: NodesProps) => { if (nodes && nodes.length === 0) { if ( problemFilter !== ProblemFilterValues.ALL || - nodesUptimeFilter !== NodesUptimeFilterValues.All + uptimeFilter !== NodesUptimeFilterValues.All ) { return ; } @@ -169,7 +163,7 @@ export const Nodes = ({path, additionalNodesProps = {}}: NodesProps) => { }; if (error) { - if (error.status === 403) { + if ((error as any).status === 403) { return ; } return ; @@ -178,7 +172,7 @@ export const Nodes = ({path, additionalNodesProps = {}}: NodesProps) => { return ( {renderControls()} - + {renderTable()} diff --git a/src/containers/Storage/Storage.tsx b/src/containers/Storage/Storage.tsx index 21a47ca218..a470def884 100644 --- a/src/containers/Storage/Storage.tsx +++ b/src/containers/Storage/Storage.tsx @@ -7,25 +7,22 @@ import type {NodesSortParams} from '../../store/reducers/nodes/types'; import {selectNodesMap} from '../../store/reducers/nodesList'; import {STORAGE_TYPES, VISIBLE_ENTITIES} from '../../store/reducers/storage/constants'; import { - selectEntitiesCount, - selectFilteredGroups, - selectFilteredNodes, + filterGroups, + filterNodes, + getUsageFilterOptions, selectGroupsSortParams, selectNodesSortParams, - selectUsageFilterOptions, } from '../../store/reducers/storage/selectors'; import { - getStorageGroupsInfo, - getStorageNodesInfo, - setDataWasNotLoaded, setGroupsSortParams, setInitialState, setNodesSortParams, - setNodesUptimeFilter, setStorageTextFilter, setStorageType, + setUptimeFilter, setUsageFilter, setVisibleEntities, + storageApi, } from '../../store/reducers/storage/storage'; import type { StorageSortParams, @@ -33,9 +30,8 @@ import type { VisibleEntities, } from '../../store/reducers/storage/types'; import type {AdditionalNodesProps} from '../../types/additionalProps'; -import {DEFAULT_TABLE_SETTINGS} from '../../utils/constants'; +import {DEFAULT_POLLING_INTERVAL, DEFAULT_TABLE_SETTINGS} from '../../utils/constants'; import { - useAutofetcher, useNodesRequestParams, useStorageRequestParams, useTableSort, @@ -62,20 +58,14 @@ export const Storage = ({additionalNodesProps, tenant, nodeId}: StorageProps) => const {autorefresh} = useTypedSelector((state) => state.schema); const { - loading, - wasLoaded, - error, type, visible: visibleEntities, filter, usageFilter, - nodesUptimeFilter, + uptimeFilter, } = useTypedSelector((state) => state.storage); - const storageNodes = useTypedSelector(selectFilteredNodes); - const storageGroups = useTypedSelector(selectFilteredGroups); - const entitiesCount = useTypedSelector(selectEntitiesCount); + const nodesMap = useTypedSelector(selectNodesMap); - const usageFilterOptions = useTypedSelector(selectUsageFilterOptions); const nodesSortParams = useTypedSelector(selectNodesSortParams); const groupsSortParams = useTypedSelector(selectGroupsSortParams); @@ -83,16 +73,9 @@ export const Storage = ({additionalNodesProps, tenant, nodeId}: StorageProps) => const isNodePage = nodeId !== undefined; const storageType = isNodePage ? STORAGE_TYPES.groups : type; - React.useEffect(() => { - return () => { - // Clean data on component unmount - dispatch(setInitialState()); - }; - }, [dispatch]); - const nodesRequestParams = useNodesRequestParams({ filter, - nodesUptimeFilter, + nodesUptimeFilter: uptimeFilter, ...nodesSortParams, }); const storageRequestParams = useStorageRequestParams({ @@ -100,42 +83,56 @@ export const Storage = ({additionalNodesProps, tenant, nodeId}: StorageProps) => ...groupsSortParams, }); - const [nodesSort, handleNodesSort] = useTableSort(nodesSortParams, (params) => - dispatch(setNodesSortParams(params as NodesSortParams)), + const autoRefreshEnabled = tenant ? autorefresh : true; + + const nodesQuery = storageApi.useGetStorageNodesInfoQuery( + {tenant, visibleEntities, ...nodesRequestParams}, + { + skip: storageType !== STORAGE_TYPES.nodes, + pollingInterval: autoRefreshEnabled ? DEFAULT_POLLING_INTERVAL : 0, + }, ); - const [groupsSort, handleGroupsSort] = useTableSort(groupsSortParams, (params) => - dispatch(setGroupsSortParams(params as StorageSortParams)), + const groupsQuery = storageApi.useGetStorageGroupsInfoQuery( + {tenant, visibleEntities, nodeId, ...storageRequestParams}, + { + skip: storageType !== STORAGE_TYPES.groups, + pollingInterval: autoRefreshEnabled ? DEFAULT_POLLING_INTERVAL : 0, + }, ); - const fetchData = React.useCallback( - (isBackground: boolean) => { - if (!isBackground) { - dispatch(setDataWasNotLoaded()); - } + const {currentData, isFetching, error} = + storageType === STORAGE_TYPES.nodes ? nodesQuery : groupsQuery; - const nodesParams = nodesRequestParams || {}; - const storageParams = storageRequestParams || {}; + const {currentData: {nodes = []} = {}} = nodesQuery; + const {currentData: {groups = []} = {}} = groupsQuery; + const {nodes: _, groups: __, ...entitiesCount} = currentData ?? {found: 0, total: 0}; - if (storageType === STORAGE_TYPES.nodes) { - dispatch(getStorageNodesInfo({tenant, visibleEntities, ...nodesParams})); - } else { - dispatch(getStorageGroupsInfo({tenant, visibleEntities, nodeId, ...storageParams})); - } - }, - [ - dispatch, - tenant, - nodeId, - visibleEntities, - storageType, - storageRequestParams, - nodesRequestParams, - ], + const isLoading = currentData === undefined && isFetching; + + const storageNodes = React.useMemo( + () => filterNodes(nodes, filter, uptimeFilter), + [filter, nodes, uptimeFilter], + ); + const storageGroups = React.useMemo( + () => filterGroups(groups, filter, usageFilter), + [filter, groups, usageFilter], ); - const autorefreshEnabled = tenant ? autorefresh : true; + const usageFilterOptions = React.useMemo(() => getUsageFilterOptions(groups), [groups]); - useAutofetcher(fetchData, [fetchData], autorefreshEnabled); + React.useEffect(() => { + return () => { + // Clean data on component unmount + dispatch(setInitialState()); + }; + }, [dispatch]); + + const [nodesSort, handleNodesSort] = useTableSort(nodesSortParams, (params) => + dispatch(setNodesSortParams(params as NodesSortParams)), + ); + const [groupsSort, handleGroupsSort] = useTableSort(groupsSortParams, (params) => + dispatch(setGroupsSortParams(params as StorageSortParams)), + ); const handleUsageFilterChange = (value: string[]) => { dispatch(setUsageFilter(value)); @@ -154,7 +151,7 @@ export const Storage = ({additionalNodesProps, tenant, nodeId}: StorageProps) => }; const handleUptimeFilterChange = (value: NodesUptimeFilterValues) => { - dispatch(setNodesUptimeFilter(value)); + dispatch(setUptimeFilter(value)); }; const handleShowAllNodes = () => { @@ -167,6 +164,7 @@ export const Storage = ({additionalNodesProps, tenant, nodeId}: StorageProps) => {storageType === STORAGE_TYPES.groups && ( )} {storageType === STORAGE_TYPES.nodes && ( handleStorageTypeChange={handleStorageTypeChange} visibleEntities={visibleEntities} handleVisibleEntitiesChange={handleGroupVisibilityChange} - nodesUptimeFilter={nodesUptimeFilter} + nodesUptimeFilter={uptimeFilter} handleNodesUptimeFilterChange={handleUptimeFilterChange} groupsUsageFilter={usageFilter} groupsUsageFilterOptions={usageFilterOptions} @@ -213,13 +212,13 @@ export const Storage = ({additionalNodesProps, tenant, nodeId}: StorageProps) => : storageNodes.length } entitiesCountTotal={entitiesCount.total} - entitiesLoading={loading && !wasLoaded} + entitiesLoading={isLoading} /> ); }; if (error) { - if (error.status === 403) { + if ((error as any).status === 403) { return ; } @@ -229,7 +228,7 @@ export const Storage = ({additionalNodesProps, tenant, nodeId}: StorageProps) => return ( {renderControls()} - + {renderDataTable()} diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index d2ba24f0ef..d36da5a768 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -8,6 +8,7 @@ import {Link} from 'react-router-dom'; import {Loader} from '../../../components/Loader'; import routes, {createHref} from '../../../routes'; +import {api} from '../../../store/reducers/api'; import {disableAutorefresh, enableAutorefresh} from '../../../store/reducers/schema/schema'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../store/reducers/tenant/constants'; import {setDiagnosticsTab} from '../../../store/reducers/tenant/tenant'; @@ -89,6 +90,7 @@ function Diagnostics(props: DiagnosticsProps) { const onAutorefreshToggle = (value: boolean) => { if (value) { + dispatch(api.util.invalidateTags(['All'])); dispatch(enableAutorefresh()); } else { dispatch(disableAutorefresh()); diff --git a/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.tsx b/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.tsx index 1454fdead5..c499210805 100644 --- a/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.tsx +++ b/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {TreeView} from 'ydb-ui-components'; -import type {PreparedClusterNode} from '../../../store/reducers/clusterNodes/types'; +import type {PreparedClusterNode} from '../../../store/reducers/clusterNodes/clusterNodes'; import type {VersionValue} from '../../../types/versions'; import {cn} from '../../../utils/cn'; import {NodesTable} from '../NodesTable/NodesTable'; diff --git a/src/containers/Versions/NodesTable/NodesTable.tsx b/src/containers/Versions/NodesTable/NodesTable.tsx index 21a60d2ffb..e0c8f38129 100644 --- a/src/containers/Versions/NodesTable/NodesTable.tsx +++ b/src/containers/Versions/NodesTable/NodesTable.tsx @@ -4,7 +4,7 @@ import DataTable from '@gravity-ui/react-data-table'; import {EntityStatus} from '../../../components/EntityStatus/EntityStatus'; import {PoolsGraph} from '../../../components/PoolsGraph/PoolsGraph'; import {ProgressViewer} from '../../../components/ProgressViewer/ProgressViewer'; -import type {PreparedClusterNode} from '../../../store/reducers/clusterNodes/types'; +import type {PreparedClusterNode} from '../../../store/reducers/clusterNodes/clusterNodes'; import {DEFAULT_TABLE_SETTINGS} from '../../../utils/constants'; import {formatBytes} from '../../../utils/dataFormatters/dataFormatters'; import {isUnavailableNode} from '../../../utils/nodes'; diff --git a/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx b/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx index 4b930f24da..5f58f7c034 100644 --- a/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx +++ b/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx @@ -1,7 +1,7 @@ import {Progress} from '@gravity-ui/uikit'; import {ClipboardButton} from '../../../components/ClipboardButton'; -import type {PreparedClusterNode} from '../../../store/reducers/clusterNodes/types'; +import type {PreparedClusterNode} from '../../../store/reducers/clusterNodes/clusterNodes'; import type {VersionValue} from '../../../types/versions'; import {cn} from '../../../utils/cn'; import type {GroupedNodesItem} from '../types'; diff --git a/src/containers/Versions/Versions.tsx b/src/containers/Versions/Versions.tsx index 292044dd72..3a53ce62b3 100644 --- a/src/containers/Versions/Versions.tsx +++ b/src/containers/Versions/Versions.tsx @@ -3,10 +3,10 @@ import React from 'react'; import {Checkbox, RadioButton} from '@gravity-ui/uikit'; import {Loader} from '../../components/Loader'; -import {getClusterNodes} from '../../store/reducers/clusterNodes/clusterNodes'; +import {clusterNodesApi} from '../../store/reducers/clusterNodes/clusterNodes'; import type {VersionToColorMap} from '../../types/versions'; import {cn} from '../../utils/cn'; -import {useAutofetcher, useTypedDispatch, useTypedSelector} from '../../utils/hooks'; +import {DEFAULT_POLLING_INTERVAL} from '../../utils/constants'; import {GroupedNodesTree} from './GroupedNodesTree/GroupedNodesTree'; import {getGroupedStorageNodes, getGroupedTenantNodes, getOtherNodes} from './groupNodes'; @@ -21,11 +21,10 @@ interface VersionsProps { } export const Versions = ({versionToColor}: VersionsProps) => { - const dispatch = useTypedDispatch(); - - const {nodes = [], loading, wasLoaded} = useTypedSelector((state) => state.clusterNodes); - - useAutofetcher(() => dispatch(getClusterNodes()), [dispatch], true); + const {data: nodes = [], isLoading: isNodesLoading} = clusterNodesApi.useGetClusterNodesQuery( + undefined, + {pollingInterval: DEFAULT_POLLING_INTERVAL}, + ); const [groupByValue, setGroupByValue] = React.useState(GroupByValue.VERSION); const [expanded, setExpanded] = React.useState(false); @@ -64,7 +63,7 @@ export const Versions = ({versionToColor}: VersionsProps) => { ); }; - if (loading && !wasLoaded) { + if (isNodesLoading) { return ; } diff --git a/src/containers/Versions/groupNodes.ts b/src/containers/Versions/groupNodes.ts index c4df31825c..f49902d3ed 100644 --- a/src/containers/Versions/groupNodes.ts +++ b/src/containers/Versions/groupNodes.ts @@ -1,6 +1,6 @@ import groupBy from 'lodash/groupBy'; -import type {PreparedClusterNode} from '../../store/reducers/clusterNodes/types'; +import type {PreparedClusterNode} from '../../store/reducers/clusterNodes/clusterNodes'; import type {VersionToColorMap} from '../../types/versions'; import {getMinorVersion, parseNodesToVersionsValues} from '../../utils/versions'; diff --git a/src/containers/Versions/types.ts b/src/containers/Versions/types.ts index 0e766cb0fb..324d6c81f1 100644 --- a/src/containers/Versions/types.ts +++ b/src/containers/Versions/types.ts @@ -1,4 +1,4 @@ -import type {PreparedClusterNode} from '../../store/reducers/clusterNodes/types'; +import type {PreparedClusterNode} from '../../store/reducers/clusterNodes/clusterNodes'; import type {VersionValue} from '../../types/versions'; export interface GroupedNodesItem { diff --git a/src/services/api.ts b/src/services/api.ts index 9a5e6b412d..4c1930ca21 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -47,20 +47,21 @@ import {settingsManager} from './settings'; type AxiosOptions = { concurrentId?: string; + signal?: AbortSignal; }; export class YdbEmbeddedAPI extends AxiosWrapper { getPath(path: string) { return `${BACKEND ?? ''}${path}`; } - getClusterInfo(clusterName?: string, {concurrentId}: AxiosOptions = {}) { + getClusterInfo(clusterName?: string, {concurrentId, signal}: AxiosOptions = {}) { return this.get( this.getPath('/viewer/json/cluster'), { name: clusterName, tablets: true, }, - {concurrentId: concurrentId || `getClusterInfo`}, + {concurrentId: concurrentId || `getClusterInfo`, requestConfig: {signal}}, ); } getClusterNodes({concurrentId}: AxiosOptions = {}) { @@ -102,14 +103,14 @@ export class YdbEmbeddedAPI extends AxiosWrapper { sortValue, ...params }: NodesApiRequestParams, - {concurrentId}: AxiosOptions = {}, + {concurrentId, signal}: AxiosOptions = {}, ) { const sort = prepareSortValue(sortValue, sortOrder); return this.get( this.getPath('/viewer/json/nodes?enums=true'), {with: visibleEntities, type, tablets, sort, ...params}, - {concurrentId}, + {concurrentId, requestConfig: {signal}}, ); } /** @deprecated use getNodes instead */ @@ -521,13 +522,13 @@ export class YdbEmbeddedAPI extends AxiosWrapper { } export class YdbWebVersionAPI extends YdbEmbeddedAPI { - getClusterInfo(clusterName: string) { + getClusterInfo(clusterName: string, {signal}: AxiosOptions = {}) { return this.get( `${META_BACKEND || ''}/meta/cluster`, { name: clusterName, }, - {concurrentId: `getCluster${clusterName}`}, + {concurrentId: `getCluster${clusterName}`, requestConfig: {signal}}, ).then(parseMetaCluster); } diff --git a/src/store/configureStore.ts b/src/store/configureStore.ts index 1efc00cec0..7185ebd7f3 100644 --- a/src/store/configureStore.ts +++ b/src/store/configureStore.ts @@ -29,7 +29,7 @@ function _configureStore< getDefaultMiddleware({ immutableCheck: {ignoredPaths: ['tooltip.currentHoveredRef']}, serializableCheck: { - ignoredPaths: ['tooltip.currentHoveredRef'], + ignoredPaths: ['tooltip.currentHoveredRef', 'api'], ignoredActions: [UPDATE_REF], }, }).concat(locationMiddleware, ...middleware), diff --git a/src/store/reducers/api.ts b/src/store/reducers/api.ts index f335375e8f..d29da43430 100644 --- a/src/store/reducers/api.ts +++ b/src/store/reducers/api.ts @@ -8,6 +8,8 @@ export const api = createApi({ * which is why no endpoints are shown below. */ endpoints: () => ({}), + refetchOnMountOrArgChange: true, + tagTypes: ['All'], }); export const _NEVER = Symbol(); diff --git a/src/store/reducers/cluster/cluster.ts b/src/store/reducers/cluster/cluster.ts index 93a9a0a0fc..058c906177 100644 --- a/src/store/reducers/cluster/cluster.ts +++ b/src/store/reducers/cluster/cluster.ts @@ -1,131 +1,94 @@ -import type {Dispatch, Reducer} from '@reduxjs/toolkit'; +import {createSlice} from '@reduxjs/toolkit'; +import type {Dispatch, PayloadAction} from '@reduxjs/toolkit'; import type {ClusterTab} from '../../../containers/Cluster/utils'; import {clusterTabsIds, isClusterTab} from '../../../containers/Cluster/utils'; +import type {TClusterInfo} from '../../../types/api/cluster'; import {DEFAULT_CLUSTER_TAB_KEY} from '../../../utils/constants'; -import {createApiRequest, createRequestActionTypes} from '../../utils'; +import {api} from '../api'; -import type {ClusterAction, ClusterState} from './types'; +import type {ClusterGroupsStats, ClusterState} from './types'; import {createSelectClusterGroupsQuery, parseGroupsStatsQueryResponse} from './utils'; -const SET_DEFAULT_CLUSTER_TAB = 'cluster/SET_DEFAULT_CLUSTER_TAB'; - -export const FETCH_CLUSTER = createRequestActionTypes('cluster', 'FETCH_CLUSTER'); - const defaultClusterTabLS = localStorage.getItem(DEFAULT_CLUSTER_TAB_KEY); -let defaultClusterTab; +let defaultClusterTab: ClusterTab; if (isClusterTab(defaultClusterTabLS)) { defaultClusterTab = defaultClusterTabLS; } else { defaultClusterTab = clusterTabsIds.overview; } -const initialState = {loading: true, wasLoaded: false, defaultClusterTab}; - -const cluster: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_CLUSTER.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_CLUSTER.SUCCESS: { - const {clusterData, groupsStats} = action.data; - - return { - ...state, - data: clusterData, - groupsStats, - loading: false, - wasLoaded: true, - error: undefined, - }; - } - case FETCH_CLUSTER.FAILURE: { - if (action.error?.isCancelled) { - return state; - } - - return { - ...state, - error: action.error, - loading: false, - }; - } - case SET_DEFAULT_CLUSTER_TAB: { - return { - ...state, - defaultClusterTab: action.data, - }; - } - default: - return state; - } +const initialState: ClusterState = { + defaultClusterTab, }; - -export function setDefaultClusterTab(tab: ClusterTab) { - return { - type: SET_DEFAULT_CLUSTER_TAB, - data: tab, - } as const; -} +const clusterSlice = createSlice({ + name: 'cluster', + initialState, + reducers: { + setDefaultClusterTab(state, action: PayloadAction) { + state.defaultClusterTab = action.payload; + }, + }, +}); export function updateDefaultClusterTab(tab: string) { return (dispatch: Dispatch) => { if (isClusterTab(tab)) { localStorage.setItem(DEFAULT_CLUSTER_TAB_KEY, tab); - dispatch(setDefaultClusterTab(tab)); + dispatch(clusterSlice.actions.setDefaultClusterTab(tab)); } }; } -export function getClusterInfo(clusterName?: string) { - async function requestClusterData() { - // Error here is handled by createApiRequest - const clusterData = await window.api.getClusterInfo(clusterName); - - try { - const clusterRoot = clusterData.Domain; - - // Without domain we cannot get stats from system tables - if (!clusterRoot) { - return { - clusterData, - }; - } - - const query = createSelectClusterGroupsQuery(clusterRoot); - - // Normally query request should be fulfilled within 300-400ms even on very big clusters - // Table with stats is supposed to be very small (less than 10 rows) - // So we batch this request with cluster request to prevent possible layout shifts, if data is missing - const groupsStatsResponse = await window.api.sendQuery({ - schema: 'modern', - query: query, - database: clusterRoot, - action: 'execute-scan', - }); - - return { - clusterData, - groupsStats: parseGroupsStatsQueryResponse(groupsStatsResponse), - }; - } catch { - // Doesn't return groups stats on error - // It could happen if user doesn't have access rights - // Or there are no system tables in cluster root - return { - clusterData, - }; - } - } - - return createApiRequest({ - request: requestClusterData(), - actions: FETCH_CLUSTER, - }); -} - -export default cluster; +export default clusterSlice.reducer; + +export const clusterApi = api.injectEndpoints({ + endpoints: (builder) => ({ + getClusterInfo: builder.query({ + queryFn: async ( + clusterName = '', + {signal}, + ): Promise< + | {data: {clusterData: TClusterInfo; groupsStats?: ClusterGroupsStats}} + | {error: unknown} + > => { + try { + const clusterData = await window.api.getClusterInfo(clusterName, {signal}); + const clusterRoot = clusterData.Domain; + + // Without domain we cannot get stats from system tables + if (!clusterRoot) { + return {data: {clusterData}}; + } + + try { + const query = createSelectClusterGroupsQuery(clusterRoot); + + // Normally query request should be fulfilled within 300-400ms even on very big clusters + // Table with stats is supposed to be very small (less than 10 rows) + // So we batch this request with cluster request to prevent possible layout shifts, if data is missing + const groupsStatsResponse = await window.api.sendQuery({ + schema: 'modern', + query: query, + database: clusterRoot, + action: 'execute-scan', + }); + + return { + data: { + clusterData, + groupsStats: parseGroupsStatsQueryResponse(groupsStatsResponse), + }, + }; + } catch { + return {data: {clusterData}}; + } + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), +}); diff --git a/src/store/reducers/cluster/types.ts b/src/store/reducers/cluster/types.ts index e21a944b96..ca4030f53e 100644 --- a/src/store/reducers/cluster/types.ts +++ b/src/store/reducers/cluster/types.ts @@ -1,9 +1,5 @@ import type {ClusterTab} from '../../../containers/Cluster/utils'; import type {TClusterInfo} from '../../../types/api/cluster'; -import type {IResponseError} from '../../../types/api/error'; -import type {ApiRequestAction} from '../../utils'; - -import type {FETCH_CLUSTER, setDefaultClusterTab} from './cluster'; export interface DiskErasureGroupsStats { diskType: string; @@ -21,11 +17,6 @@ export type DiskGroupsStats = Record; export type ClusterGroupsStats = Record; export interface ClusterState { - loading: boolean; - wasLoaded: boolean; - data?: TClusterInfo; - error?: IResponseError; - groupsStats?: ClusterGroupsStats; defaultClusterTab: ClusterTab; } @@ -33,7 +24,3 @@ export interface HandledClusterResponse { clusterData: TClusterInfo; groupsStats: ClusterGroupsStats; } - -export type ClusterAction = - | ApiRequestAction - | ReturnType; diff --git a/src/store/reducers/clusterNodes/clusterNodes.tsx b/src/store/reducers/clusterNodes/clusterNodes.tsx index f8b61c8798..f11b960cfd 100644 --- a/src/store/reducers/clusterNodes/clusterNodes.tsx +++ b/src/store/reducers/clusterNodes/clusterNodes.tsx @@ -1,66 +1,30 @@ -import type {Reducer} from '@reduxjs/toolkit'; - +import type {TSystemStateInfo} from '../../../types/api/nodes'; import {calcUptime} from '../../../utils/dataFormatters/dataFormatters'; -import {createApiRequest, createRequestActionTypes} from '../../utils'; - -import type {ClusterNodesAction, ClusterNodesState, PreparedClusterNode} from './types'; - -export const FETCH_CLUSTER_NODES = createRequestActionTypes('cluster', 'FETCH_CLUSTER_NODES'); - -const initialState = {loading: false, wasLoaded: false}; - -const clusterNodes: Reducer = ( - state = initialState, - action, -) => { - switch (action.type) { - case FETCH_CLUSTER_NODES.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_CLUSTER_NODES.SUCCESS: { - const {data = []} = action; - - return { - ...state, - nodes: data, - loading: false, - wasLoaded: true, - error: undefined, - }; - } - case FETCH_CLUSTER_NODES.FAILURE: { - if (action.error?.isCancelled) { - return state; - } - - return { - ...state, - error: action.error, - loading: false, - }; - } - default: - return state; - } -}; +import {api} from '../api'; -export function getClusterNodes() { - return createApiRequest({ - request: window.api.getClusterNodes(), - actions: FETCH_CLUSTER_NODES, - dataHandler: (data): PreparedClusterNode[] => { - const {SystemStateInfo: nodes = []} = data; - return nodes.map((node) => { - return { - ...node, - uptime: calcUptime(node.StartTime), - }; - }); - }, - }); +export interface PreparedClusterNode extends TSystemStateInfo { + uptime: string; } -export default clusterNodes; +export const clusterNodesApi = api.injectEndpoints({ + endpoints: (builder) => ({ + getClusterNodes: builder.query({ + queryFn: async () => { + try { + const result = await window.api.getClusterNodes(); + const {SystemStateInfo: nodes = []} = result; + const data: PreparedClusterNode[] = nodes.map((node) => { + return { + ...node, + uptime: calcUptime(node.StartTime), + }; + }); + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), +}); diff --git a/src/store/reducers/clusterNodes/types.ts b/src/store/reducers/clusterNodes/types.ts deleted file mode 100644 index 92bfc1dc8f..0000000000 --- a/src/store/reducers/clusterNodes/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type {IResponseError} from '../../../types/api/error'; -import type {TSystemStateInfo} from '../../../types/api/nodes'; -import type {ApiRequestAction} from '../../utils'; - -import type {FETCH_CLUSTER_NODES} from './clusterNodes'; - -export interface PreparedClusterNode extends TSystemStateInfo { - uptime: string; -} - -export interface ClusterNodesState { - loading: boolean; - wasLoaded: boolean; - nodes?: PreparedClusterNode[]; - error?: IResponseError; -} - -export type ClusterNodesAction = ApiRequestAction< - typeof FETCH_CLUSTER_NODES, - PreparedClusterNode[], - IResponseError ->; diff --git a/src/store/reducers/clusters/clusters.ts b/src/store/reducers/clusters/clusters.ts index 109c7e67c0..9444ac279a 100644 --- a/src/store/reducers/clusters/clusters.ts +++ b/src/store/reducers/clusters/clusters.ts @@ -38,6 +38,7 @@ export const clustersApi = api.injectEndpoints({ return {error}; } }, + providesTags: ['All'], }), }), }); diff --git a/src/store/reducers/index.ts b/src/store/reducers/index.ts index cccd7677a7..926c0c5053 100644 --- a/src/store/reducers/index.ts +++ b/src/store/reducers/index.ts @@ -3,7 +3,6 @@ import {combineReducers} from '@reduxjs/toolkit'; import {api} from './api'; import authentication from './authentication/authentication'; import cluster from './cluster/cluster'; -import clusterNodes from './clusterNodes/clusterNodes'; import clusters from './clusters/clusters'; import describe from './describe'; import executeQuery from './executeQuery'; @@ -55,7 +54,6 @@ export const rootReducer = { topNodesByCpu, topNodesByMemory, cluster, - clusterNodes, tenant, storage, topStorageGroups, diff --git a/src/store/reducers/nodes/nodes.ts b/src/store/reducers/nodes/nodes.ts index 2289fd4e41..c6083a8671 100644 --- a/src/store/reducers/nodes/nodes.ts +++ b/src/store/reducers/nodes/nodes.ts @@ -1,148 +1,83 @@ -import type {Reducer} from '@reduxjs/toolkit'; +import {createSlice} from '@reduxjs/toolkit'; +import type {PayloadAction} from '@reduxjs/toolkit'; import {EVersion} from '../../../types/api/compute'; import {NodesUptimeFilterValues} from '../../../utils/nodes'; -import {createApiRequest, createRequestActionTypes} from '../../utils'; +import {api} from '../api'; import type { ComputeApiRequestParams, - NodesAction, NodesApiRequestParams, NodesSortParams, NodesState, } from './types'; import {prepareComputeNodesData, prepareNodesData} from './utils'; -export const FETCH_NODES = createRequestActionTypes('nodes', 'FETCH_NODES'); - -const RESET_NODES_STATE = 'nodes/RESET_NODES_STATE'; -const SET_NODES_UPTIME_FILTER = 'nodes/SET_NODES_UPTIME_FILTER'; -const SET_DATA_WAS_NOT_LOADED = 'nodes/SET_DATA_WAS_NOT_LOADED'; -const SET_SEARCH_VALUE = 'nodes/SET_SEARCH_VALUE'; -const SET_SORT = 'nodes/SET_SORT'; - -const initialState = { - loading: false, - wasLoaded: false, - nodesUptimeFilter: NodesUptimeFilterValues.All, +const initialState: NodesState = { + uptimeFilter: NodesUptimeFilterValues.All, searchValue: '', }; -const nodes: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_NODES.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_NODES.SUCCESS: { - return { - ...state, - data: action.data?.Nodes, - totalNodes: action.data?.TotalNodes, - loading: false, - wasLoaded: true, - error: undefined, - }; - } - case FETCH_NODES.FAILURE: { - if (action.error?.isCancelled) { - return state; - } - - return { - ...state, - error: action.error, - loading: false, - }; - } - case RESET_NODES_STATE: { - return { - ...state, - loading: initialState.loading, - wasLoaded: initialState.wasLoaded, - nodesUptimeFilter: initialState.nodesUptimeFilter, - searchValue: initialState.searchValue, - }; - } - case SET_NODES_UPTIME_FILTER: { - return { - ...state, - nodesUptimeFilter: action.data, - }; - } - case SET_SEARCH_VALUE: { - return { - ...state, - searchValue: action.data, - }; - } - case SET_SORT: { - return { - ...state, - sortValue: action.data.sortValue, - sortOrder: action.data.sortOrder, - }; - } - case SET_DATA_WAS_NOT_LOADED: { - return { - ...state, - wasLoaded: false, - }; - } - default: - return state; - } -}; -const concurrentId = 'getNodes'; +const slice = createSlice({ + name: 'nodes', + initialState, + reducers: { + setUptimeFilter: (state, action: PayloadAction) => { + state.uptimeFilter = action.payload; + }, + setSearchValue: (state, action: PayloadAction) => { + state.searchValue = action.payload; + }, + setSort: (state, action: PayloadAction) => { + state.sortValue = action.payload.sortValue; + state.sortOrder = action.payload.sortOrder; + }, + setInitialState: () => { + return initialState; + }, + }, +}); -export function getNodes({type = 'any', storage = false, ...params}: NodesApiRequestParams) { - return createApiRequest({ - request: window.api.getNodes({type, storage, ...params}, {concurrentId}), - actions: FETCH_NODES, - dataHandler: prepareNodesData, - }); -} - -export function getComputeNodes({version = EVersion.v2, ...params}: ComputeApiRequestParams) { - return createApiRequest({ - request: window.api.getCompute({version, ...params}, {concurrentId}), - actions: FETCH_NODES, - dataHandler: prepareComputeNodesData, - }); -} - -export const resetNodesState = () => { - return { - type: RESET_NODES_STATE, - } as const; -}; +export default slice.reducer; -export const setNodesUptimeFilter = (value: NodesUptimeFilterValues) => - ({ - type: SET_NODES_UPTIME_FILTER, - data: value, - }) as const; - -export const setDataWasNotLoaded = () => { - return { - type: SET_DATA_WAS_NOT_LOADED, - } as const; -}; - -export const setSearchValue = (value: string) => { - return { - type: SET_SEARCH_VALUE, - data: value, - } as const; -}; - -export const setSort = (sortParams: NodesSortParams) => { - return { - type: SET_SORT, - data: sortParams, - } as const; -}; +export const {setUptimeFilter, setSearchValue, setSort, setInitialState} = slice.actions; -export default nodes; +export const nodesApi = api.injectEndpoints({ + endpoints: (builder) => ({ + getNodes: builder.query({ + queryFn: async (params: NodesApiRequestParams, {signal}) => { + try { + const data = await window.api.getNodes( + { + type: 'any', + storage: false, + ...params, + }, + {signal}, + ); + return {data: prepareNodesData(data)}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + getComputeNodes: builder.query({ + queryFn: async (params: ComputeApiRequestParams, {signal}) => { + try { + const data = await window.api.getCompute( + { + version: EVersion.v2, + ...params, + }, + {signal}, + ); + return {data: prepareComputeNodesData(data)}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), +}); diff --git a/src/store/reducers/nodes/selectors.ts b/src/store/reducers/nodes/selectors.ts index 35408b86e7..2dabacdfa9 100644 --- a/src/store/reducers/nodes/selectors.ts +++ b/src/store/reducers/nodes/selectors.ts @@ -1,6 +1,3 @@ -import type {Selector} from '@reduxjs/toolkit'; -import {createSelector} from '@reduxjs/toolkit'; - import {EFlag} from '../../../types/api/enums'; import {HOUR_IN_SECONDS} from '../../../utils/constants'; import {calcUptimeInSeconds} from '../../../utils/dataFormatters/dataFormatters'; @@ -9,7 +6,7 @@ import {NodesUptimeFilterValues} from '../../../utils/nodes'; import {ProblemFilterValues} from '../settings/settings'; import type {ProblemFilterValue} from '../settings/types'; -import type {NodesPreparedEntity, NodesStateSlice} from './types'; +import type {NodesPreparedEntity} from './types'; // ==== Filters ==== @@ -49,27 +46,21 @@ const filterNodesBySearchValue = (nodesList: NodesPreparedEntity[] = [], searchV }); }; -// ==== Simple selectors ==== - -const selectNodesUptimeFilter = (state: NodesStateSlice) => state.nodes.nodesUptimeFilter; -const selectSearchValue = (state: NodesStateSlice) => state.nodes.searchValue; -const selectNodesList = (state: NodesStateSlice) => state.nodes.data; - -// ==== Complex selectors ==== - -export const selectFilteredNodes: Selector = - createSelector( - [ - selectNodesList, - selectNodesUptimeFilter, - selectSearchValue, - (state) => state.settings.problemFilter, - ], - (nodesList, uptimeFilter, searchValue, problemFilter) => { - let result = filterNodesByUptime(nodesList, uptimeFilter); - result = filterNodesByProblemsStatus(result, problemFilter); - result = filterNodesBySearchValue(result, searchValue); +export function filterNodes( + nodesList: NodesPreparedEntity[] = [], + { + uptimeFilter, + searchValue, + problemFilter, + }: { + uptimeFilter: NodesUptimeFilterValues; + searchValue: string; + problemFilter: ProblemFilterValue; + }, +) { + let result = filterNodesByUptime(nodesList, uptimeFilter); + result = filterNodesByProblemsStatus(result, problemFilter); + result = filterNodesBySearchValue(result, searchValue); - return result; - }, - ); + return result; +} diff --git a/src/store/reducers/nodes/types.ts b/src/store/reducers/nodes/types.ts index dc76a939d3..b40ae0f571 100644 --- a/src/store/reducers/nodes/types.ts +++ b/src/store/reducers/nodes/types.ts @@ -5,22 +5,11 @@ import type { TTabletStateInfo as TComputeTabletStateInfo, } from '../../../types/api/compute'; import type {EFlag} from '../../../types/api/enums'; -import type {IResponseError} from '../../../types/api/error'; import type {TEndpoint, TPoolStats} from '../../../types/api/nodes'; import type {TTabletStateInfo as TFullTabletStateInfo} from '../../../types/api/tablet'; import type {NodesSortValue, NodesUptimeFilterValues} from '../../../utils/nodes'; -import type {ApiRequestAction} from '../../utils'; import type {VisibleEntities} from '../storage/types'; -import type { - FETCH_NODES, - resetNodesState, - setDataWasNotLoaded, - setNodesUptimeFilter, - setSearchValue, - setSort, -} from './nodes'; - // Since nodes from different endpoints can have different types, // This type describes fields, that are expected by tables with nodes export interface NodesPreparedEntity { @@ -52,15 +41,10 @@ export interface NodesPreparedEntity { } export interface NodesState { - loading: boolean; - wasLoaded: boolean; - nodesUptimeFilter: NodesUptimeFilterValues; + uptimeFilter: NodesUptimeFilterValues; searchValue: string; sortValue?: NodesSortValue; sortOrder?: OrderType; - data?: NodesPreparedEntity[]; - totalNodes?: number; - error?: IResponseError; } export type NodeType = 'static' | 'dynamic' | 'any'; @@ -100,22 +84,6 @@ export interface NodesHandledResponse { FoundNodes?: number; } -type NodesApiRequestAction = ApiRequestAction< - typeof FETCH_NODES, - NodesHandledResponse, - IResponseError ->; - -export type NodesAction = - | NodesApiRequestAction - | ( - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - ); - export interface NodesStateSlice { nodes: NodesState; } diff --git a/src/store/reducers/nodesList.ts b/src/store/reducers/nodesList.ts index 4e71928c3b..e721aa630b 100644 --- a/src/store/reducers/nodesList.ts +++ b/src/store/reducers/nodesList.ts @@ -1,3 +1,4 @@ +import {createSelector} from '@reduxjs/toolkit'; import type {Reducer} from '@reduxjs/toolkit'; import type { @@ -48,7 +49,9 @@ export function getNodesList() { }); } -export const selectNodesMap = (state: NodesListRootStateSlice) => - prepareNodesMap(state.nodesList.data); +export const selectNodesMap = createSelector( + (state: NodesListRootStateSlice) => state.nodesList.data, + (nodes) => prepareNodesMap(nodes), +); export default nodesList; diff --git a/src/store/reducers/storage/selectors.ts b/src/store/reducers/storage/selectors.ts index 12faeb5fb0..7e8bed06fa 100644 --- a/src/store/reducers/storage/selectors.ts +++ b/src/store/reducers/storage/selectors.ts @@ -1,10 +1,8 @@ import type {OrderType} from '@gravity-ui/react-data-table'; import {ASCENDING, DESCENDING} from '@gravity-ui/react-data-table/build/esm/lib/constants'; -import type {Selector} from '@reduxjs/toolkit'; -import {createSelector} from '@reduxjs/toolkit'; import {NODES_SORT_VALUES} from '../../../utils/nodes'; -import type {NodesSortValue} from '../../../utils/nodes'; +import type {NodesSortValue, NodesUptimeFilterValues} from '../../../utils/nodes'; import {STORAGE_SORT_VALUES, getUsage} from '../../../utils/storage'; import type {StorageSortValue} from '../../../utils/storage'; import {filterNodesByUptime} from '../nodes/selectors'; @@ -61,21 +59,35 @@ const filterGroupsByUsage = (entities: PreparedStorageGroup[], usage?: string[]) }); }; -// ==== Simple selectors ==== - -export const selectEntitiesCount = (state: StorageStateSlice) => ({ - total: state.storage.total, - found: state.storage.found, -}); - -export const selectStorageGroups = (state: StorageStateSlice) => state.storage.groups; -export const selectStorageNodes = (state: StorageStateSlice) => state.storage.nodes; +export function filterNodes( + storageNodes: PreparedStorageNode[], + textFilter: string, + uptimeFilter: NodesUptimeFilterValues, +) { + let result = storageNodes || []; + result = filterNodesByText(result, textFilter); + result = filterNodesByUptime(result, uptimeFilter); + + return result; +} + +export function filterGroups( + storageGroups: PreparedStorageGroup[], + textFilter: string, + usageFilter: string[], +) { + let result = storageGroups || []; + result = filterGroupsByText(result, textFilter); + result = filterGroupsByUsage(result, usageFilter); + + return result; +} +// ==== Simple selectors ==== export const selectStorageFilter = (state: StorageStateSlice) => state.storage.filter; export const selectUsageFilter = (state: StorageStateSlice) => state.storage.usageFilter; export const selectVisibleEntities = (state: StorageStateSlice) => state.storage.visible; -export const selectNodesUptimeFilter = (state: StorageStateSlice) => - state.storage.nodesUptimeFilter; +export const selectNodesUptimeFilter = (state: StorageStateSlice) => state.storage.uptimeFilter; export const selectStorageType = (state: StorageStateSlice) => state.storage.type; // ==== Sort params selectors ==== @@ -112,50 +124,21 @@ export const selectGroupsSortParams = (state: StorageStateSlice) => { }; // ==== Complex selectors ==== -export const selectUsageFilterOptions: Selector = createSelector( - selectStorageGroups, - (groups) => { - const items: Record = {}; - - groups?.forEach((group) => { - // Get groups usage with step 5 - const usage = getUsage(group, 5); - - if (!Object.prototype.hasOwnProperty.call(items, usage)) { - items[usage] = 0; - } - - items[usage] += 1; - }); - - return Object.entries(items) - .map(([threshold, count]) => ({threshold: Number(threshold), count})) - .sort((a, b) => b.threshold - a.threshold); - }, -); - -// ==== Complex selectors with filters ==== - -export const selectFilteredNodes: Selector = - createSelector( - [selectStorageNodes, selectStorageFilter, selectNodesUptimeFilter], - (storageNodes, textFilter, uptimeFilter) => { - let result = storageNodes || []; - result = filterNodesByText(result, textFilter); - result = filterNodesByUptime(result, uptimeFilter); - - return result; - }, - ); - -export const selectFilteredGroups: Selector = - createSelector( - [selectStorageGroups, selectStorageFilter, selectUsageFilter], - (storageGroups, textFilter, usageFilter) => { - let result = storageGroups || []; - result = filterGroupsByText(result, textFilter); - result = filterGroupsByUsage(result, usageFilter); - - return result; - }, - ); +export function getUsageFilterOptions(groups: PreparedStorageGroup[]): UsageFilter[] { + const items: Record = {}; + + groups?.forEach((group) => { + // Get groups usage with step 5 + const usage = getUsage(group, 5); + + if (!Object.prototype.hasOwnProperty.call(items, usage)) { + items[usage] = 0; + } + + items[usage] += 1; + }); + + return Object.entries(items) + .map(([threshold, count]) => ({threshold: Number(threshold), count})) + .sort((a, b) => b.threshold - a.threshold); +} diff --git a/src/store/reducers/storage/storage.ts b/src/store/reducers/storage/storage.ts index 2c9b077fad..c2eb6bbb33 100644 --- a/src/store/reducers/storage/storage.ts +++ b/src/store/reducers/storage/storage.ts @@ -1,13 +1,13 @@ -import type {Reducer} from '@reduxjs/toolkit'; +import {createSlice} from '@reduxjs/toolkit'; +import type {PayloadAction} from '@reduxjs/toolkit'; import {EVersion} from '../../../types/api/storage'; import {NodesUptimeFilterValues} from '../../../utils/nodes'; -import {createApiRequest, createRequestActionTypes} from '../../utils'; +import {api} from '../api'; import type {NodesApiRequestParams, NodesSortParams} from '../nodes/types'; import {STORAGE_TYPES, VISIBLE_ENTITIES} from './constants'; import type { - StorageAction, StorageApiRequestParams, StorageSortParams, StorageState, @@ -16,221 +16,89 @@ import type { } from './types'; import {prepareStorageGroupsResponse, prepareStorageNodesResponse} from './utils'; -export const FETCH_STORAGE = createRequestActionTypes('storage', 'FETCH_STORAGE'); - -const SET_INITIAL = 'storage/SET_INITIAL'; -const SET_FILTER = 'storage/SET_FILTER'; -const SET_USAGE_FILTER = 'storage/SET_USAGE_FILTER'; -const SET_VISIBLE_GROUPS = 'storage/SET_VISIBLE_GROUPS'; -const SET_STORAGE_TYPE = 'storage/SET_STORAGE_TYPE'; -const SET_NODES_UPTIME_FILTER = 'storage/SET_NODES_UPTIME_FILTER'; -const SET_DATA_WAS_NOT_LOADED = 'storage/SET_DATA_WAS_NOT_LOADED'; -const SET_NODES_SORT_PARAMS = 'storage/SET_NODES_SORT_PARAMS'; -const SET_GROUPS_SORT_PARAMS = 'storage/SET_GROUPS_SORT_PARAMS'; - -const initialState = { - loading: true, - wasLoaded: false, +const initialState: StorageState = { filter: '', usageFilter: [], visible: VISIBLE_ENTITIES.all, - nodesUptimeFilter: NodesUptimeFilterValues.All, + uptimeFilter: NodesUptimeFilterValues.All, type: STORAGE_TYPES.groups, }; -const storage: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_STORAGE.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_STORAGE.SUCCESS: { - return { - ...state, - nodes: action.data.nodes, - groups: action.data.groups, - total: action.data.total, - found: action.data.found, - loading: false, - wasLoaded: true, - error: undefined, - }; - } - case FETCH_STORAGE.FAILURE: { - if (action.error?.isCancelled) { - return state; - } - - return { - ...state, - error: action.error, - loading: false, - wasLoaded: true, - }; - } - case SET_INITIAL: { - return { - ...initialState, - }; - } - case SET_FILTER: { - return { - ...state, - filter: action.data, - }; - } - case SET_USAGE_FILTER: { - return { - ...state, - usageFilter: action.data, - }; - } - case SET_VISIBLE_GROUPS: { - return { - ...state, - visible: action.data, - usageFilter: [], - wasLoaded: false, - error: undefined, - }; - } - - case SET_NODES_UPTIME_FILTER: { - return { - ...state, - nodesUptimeFilter: action.data, - }; - } - case SET_STORAGE_TYPE: { - return { - ...state, - type: action.data, - filter: '', - usageFilter: [], - wasLoaded: false, - error: undefined, - }; - } - case SET_DATA_WAS_NOT_LOADED: { - return { - ...state, - wasLoaded: false, - }; - } - case SET_NODES_SORT_PARAMS: { - return { - ...state, - nodesSortValue: action.data.sortValue, - nodesSortOrder: action.data.sortOrder, - }; - } - case SET_GROUPS_SORT_PARAMS: { - return { - ...state, - groupsSortValue: action.data.sortValue, - groupsSortOrder: action.data.sortOrder, - }; - } - default: - return state; - } -}; - -const concurrentId = 'getStorageInfo'; - -export const getStorageNodesInfo = ({ - tenant, - visibleEntities, - ...params -}: Omit) => { - return createApiRequest({ - request: window.api.getNodes( - {tenant, visibleEntities, storage: true, type: 'static', ...params}, - {concurrentId}, - ), - actions: FETCH_STORAGE, - dataHandler: prepareStorageNodesResponse, - }); -}; - -export const getStorageGroupsInfo = ({ - tenant, - visibleEntities, - nodeId, - version = EVersion.v1, - ...params -}: StorageApiRequestParams) => { - return createApiRequest({ - request: window.api.getStorageInfo( - {tenant, visibleEntities, nodeId, version, ...params}, - {concurrentId}, - ), - actions: FETCH_STORAGE, - dataHandler: prepareStorageGroupsResponse, - }); -}; - -export function setInitialState() { - return { - type: SET_INITIAL, - } as const; -} - -export function setStorageType(value: StorageType) { - return { - type: SET_STORAGE_TYPE, - data: value, - } as const; -} - -export function setStorageTextFilter(value: string) { - return { - type: SET_FILTER, - data: value, - } as const; -} - -export function setUsageFilter(value: string[]) { - return { - type: SET_USAGE_FILTER, - data: value, - } as const; -} - -export function setVisibleEntities(value: VisibleEntities) { - return { - type: SET_VISIBLE_GROUPS, - data: value, - } as const; -} - -export function setNodesUptimeFilter(value: NodesUptimeFilterValues) { - return { - type: SET_NODES_UPTIME_FILTER, - data: value, - } as const; -} - -export const setDataWasNotLoaded = () => { - return { - type: SET_DATA_WAS_NOT_LOADED, - } as const; -}; - -export const setNodesSortParams = (sortParams: NodesSortParams) => { - return { - type: SET_NODES_SORT_PARAMS, - data: sortParams, - } as const; -}; - -export const setGroupsSortParams = (sortParams: StorageSortParams) => { - return { - type: SET_GROUPS_SORT_PARAMS, - data: sortParams, - } as const; -}; - -export default storage; +const slice = createSlice({ + name: 'storage', + initialState, + reducers: { + setUptimeFilter: (state, action: PayloadAction) => { + state.uptimeFilter = action.payload; + }, + setStorageType: (state, action: PayloadAction) => { + state.type = action.payload; + }, + setStorageTextFilter: (state, action: PayloadAction) => { + state.filter = action.payload; + }, + setUsageFilter: (state, action: PayloadAction) => { + state.usageFilter = action.payload; + }, + setVisibleEntities: (state, action: PayloadAction) => { + state.visible = action.payload; + }, + setNodesSortParams: (state, action: PayloadAction) => { + state.nodesSortValue = action.payload.sortValue; + state.nodesSortOrder = action.payload.sortOrder; + }, + setGroupsSortParams: (state, action: PayloadAction) => { + state.groupsSortValue = action.payload.sortValue; + state.groupsSortOrder = action.payload.sortOrder; + }, + setInitialState: () => { + return initialState; + }, + }, +}); + +export default slice.reducer; + +export const { + setInitialState, + setStorageTextFilter, + setUsageFilter, + setVisibleEntities, + setStorageType, + setUptimeFilter, + setNodesSortParams, + setGroupsSortParams, +} = slice.actions; + +export const storageApi = api.injectEndpoints({ + endpoints: (builder) => ({ + getStorageNodesInfo: builder.query({ + queryFn: async (params: Omit, {signal}) => { + try { + const result = await window.api.getNodes( + {storage: true, type: 'static', ...params}, + {signal}, + ); + return {data: prepareStorageNodesResponse(result)}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + getStorageGroupsInfo: builder.query({ + queryFn: async (params: StorageApiRequestParams, {signal}) => { + try { + const result = await window.api.getStorageInfo( + {version: EVersion.v1, ...params}, + {signal}, + ); + return {data: prepareStorageGroupsResponse(result)}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), +}); diff --git a/src/store/reducers/storage/types.ts b/src/store/reducers/storage/types.ts index 9b8b071095..f1a58132d1 100644 --- a/src/store/reducers/storage/types.ts +++ b/src/store/reducers/storage/types.ts @@ -1,27 +1,13 @@ import type {OrderType} from '@gravity-ui/react-data-table'; -import type {IResponseError} from '../../../types/api/error'; import type {TSystemStateInfo} from '../../../types/api/nodes'; import type {EVersion, TStorageGroupInfo} from '../../../types/api/storage'; import type {ValueOf} from '../../../types/common'; import type {PreparedPDisk, PreparedVDisk} from '../../../utils/disks/types'; import type {NodesSortValue, NodesUptimeFilterValues} from '../../../utils/nodes'; import type {StorageSortValue} from '../../../utils/storage'; -import type {ApiRequestAction} from '../../utils'; import type {STORAGE_TYPES, VISIBLE_ENTITIES} from './constants'; -import type { - FETCH_STORAGE, - setDataWasNotLoaded, - setGroupsSortParams, - setInitialState, - setNodesSortParams, - setNodesUptimeFilter, - setStorageTextFilter, - setStorageType, - setUsageFilter, - setVisibleEntities, -} from './storage'; export type VisibleEntities = ValueOf; export type StorageType = ValueOf; @@ -82,22 +68,15 @@ export interface StorageApiRequestParams extends StorageSortAndFilterParams { } export interface StorageState { - loading: boolean; - wasLoaded: boolean; filter: string; usageFilter: string[]; visible: VisibleEntities; - nodesUptimeFilter: NodesUptimeFilterValues; + uptimeFilter: NodesUptimeFilterValues; groupsSortValue?: StorageSortValue; groupsSortOrder?: OrderType; nodesSortValue?: NodesSortValue; nodesSortOrder?: OrderType; type: StorageType; - nodes?: PreparedStorageNode[]; - groups?: PreparedStorageGroup[]; - found?: number; - total?: number; - error?: IResponseError; } export interface PreparedStorageResponse { @@ -107,24 +86,6 @@ export interface PreparedStorageResponse { total: number | undefined; } -type GetStorageInfoApiRequestAction = ApiRequestAction< - typeof FETCH_STORAGE, - PreparedStorageResponse, - IResponseError ->; - -export type StorageAction = - | GetStorageInfoApiRequestAction - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType; - export interface StorageStateSlice { storage: StorageState; } diff --git a/src/store/state-url-mapping.ts b/src/store/state-url-mapping.ts index 6a4cd18e6f..7ff5564002 100644 --- a/src/store/state-url-mapping.ts +++ b/src/store/state-url-mapping.ts @@ -89,16 +89,24 @@ const paramSetup: ParamSetup = { storageType: { stateKey: 'storage.type', }, - storageVisibleType: { + visible: { stateKey: 'storage.visible', }, - storageNodesUptime: { - stateKey: 'storage.nodesUptimeFilter', + uptimeFilter: { + stateKey: 'storage.uptimeFilter', }, - storageFilter: { + search: { stateKey: 'storage.filter', }, }, + '/cluster/nodes': { + uptimeFilter: { + stateKey: 'nodes.uptimeFilter', + }, + search: { + stateKey: 'nodes.searchValue', + }, + }, }; function mergeLocationToState(state: S, location: Pick): S {