diff --git a/src/.eslintrc b/src/.eslintrc index e861c8caa6..75c762f2bb 100644 --- a/src/.eslintrc +++ b/src/.eslintrc @@ -3,12 +3,6 @@ "rules": { "react/jsx-uses-react": "off", "react/react-in-jsx-scope": "off", - "react-hooks/exhaustive-deps": [ - "warn", - { - "additionalHooks": "(useAutofetcher)", - }, - ], "valid-jsdoc": "off", "react/jsx-fragments": ["error", "element"], "no-restricted-syntax": [ diff --git a/src/components/MetricChart/MetricChart.tsx b/src/components/MetricChart/MetricChart.tsx index 1d6d22ef6e..2846b7b4e9 100644 --- a/src/components/MetricChart/MetricChart.tsx +++ b/src/components/MetricChart/MetricChart.tsx @@ -4,26 +4,14 @@ import ChartKit, {settings} from '@gravity-ui/chartkit'; import type {YagrSeriesData, YagrWidgetData} from '@gravity-ui/chartkit/yagr'; import {YagrPlugin} from '@gravity-ui/chartkit/yagr'; -import type {IResponseError} from '../../types/api/error'; import {cn} from '../../utils/cn'; -import {useAutofetcher} from '../../utils/hooks'; import type {TimeFrame} from '../../utils/timeframes'; import {ResponseError} from '../Errors/ResponseError'; import {Loader} from '../Loader'; import {colorToRGBA, colors} from './colors'; -import {convertResponse} from './convertResponse'; -import {getChartData} from './getChartData'; import {getDefaultDataFormatter} from './getDefaultDataFormatter'; -import i18n from './i18n'; -import { - chartReducer, - initialChartState, - setChartData, - setChartDataLoading, - setChartDataWasNotLoaded, - setChartError, -} from './reducer'; +import {chartApi} from './reducer'; import type { ChartOptions, MetricDescription, @@ -107,6 +95,8 @@ const prepareWidgetData = ( }; }; +const emptyChartData: PreparedMetricsData = {timeline: [], metrics: []}; + interface DiagnosticsChartProps { database: string; @@ -114,7 +104,7 @@ interface DiagnosticsChartProps { metrics: MetricDescription[]; timeFrame?: TimeFrame; - autorefresh?: boolean; + autorefresh?: number; height?: number; width?: number; @@ -143,90 +133,29 @@ export const MetricChart = ({ onChartDataStatusChange, isChartVisible, }: DiagnosticsChartProps) => { - const mounted = React.useRef(false); - - React.useEffect(() => { - mounted.current = true; - return () => { - mounted.current = false; - }; - }, []); - - const [{loading, wasLoaded, data, error}, dispatch] = React.useReducer( - chartReducer, - initialChartState, - ); - - React.useEffect(() => { - if (error) { - return onChartDataStatusChange?.('error'); - } - if (loading && !wasLoaded) { - return onChartDataStatusChange?.('loading'); - } - if (!loading && wasLoaded) { - return onChartDataStatusChange?.('success'); - } - - return undefined; - }, [loading, wasLoaded, error, onChartDataStatusChange]); - - const fetchChartData = React.useCallback( - async (isBackground: boolean) => { - dispatch(setChartDataLoading()); - - if (!isBackground) { - dispatch(setChartDataWasNotLoaded()); - } - - try { - // maxDataPoints param is calculated based on width - // should be width > maxDataPoints to prevent points that cannot be selected - // more px per dataPoint - easier to select, less - chart is smoother - const response = await getChartData({ - database, - metrics, - timeFrame, - maxDataPoints: width / 2, - }); - - // Hack to prevent setting value to state, if component unmounted - if (!mounted.current) { - return; - } - - // Response could be a plain html for ydb versions without charts support - // Or there could be an error in response with 200 status code - // It happens when request is OK, but chart data cannot be returned due to some reason - // Example: charts are not enabled in the DB ('GraphShard is not enabled' error) - if (Array.isArray(response)) { - const preparedData = convertResponse(response, metrics); - dispatch(setChartData(preparedData)); - } else { - const err = { - statusText: - typeof response === 'string' ? i18n('not-supported') : response.error, - }; - - throw err; - } - } catch (err) { - if (!mounted.current) { - return; - } - - dispatch(setChartError(err as IResponseError)); - } + const {currentData, error, isFetching, status} = chartApi.useGetChartDataQuery( + // maxDataPoints param is calculated based on width + // should be width > maxDataPoints to prevent points that cannot be selected + // more px per dataPoint - easier to select, less - chart is smoother + { + database, + metrics, + timeFrame, + maxDataPoints: width / 2, }, - [database, metrics, timeFrame, width], + {pollingInterval: autorefresh}, ); - useAutofetcher(fetchChartData, [fetchChartData], autorefresh); + const loading = isFetching && !currentData; + + React.useEffect(() => { + return onChartDataStatusChange?.(status === 'fulfilled' ? 'success' : 'loading'); + }, [status, onChartDataStatusChange]); - const convertedData = prepareWidgetData(data, chartOptions); + const convertedData = prepareWidgetData(currentData || emptyChartData, chartOptions); const renderContent = () => { - if (loading && !wasLoaded) { + if (loading) { return ; } @@ -237,7 +166,7 @@ export const MetricChart = ({ return (
- {error && } + {error ? : null}
); }; diff --git a/src/components/MetricChart/getChartData.ts b/src/components/MetricChart/getChartData.ts index 95117d3471..5bfc45557c 100644 --- a/src/components/MetricChart/getChartData.ts +++ b/src/components/MetricChart/getChartData.ts @@ -3,19 +3,17 @@ import type {TimeFrame} from '../../utils/timeframes'; import type {MetricDescription} from './types'; -interface GetChartDataParams { +export interface GetChartDataParams { database: string; metrics: MetricDescription[]; timeFrame: TimeFrame; maxDataPoints: number; } -export const getChartData = async ({ - database, - metrics, - timeFrame, - maxDataPoints, -}: GetChartDataParams) => { +export const getChartData = async ( + {database, metrics, timeFrame, maxDataPoints}: GetChartDataParams, + {signal}: {signal?: AbortSignal} = {}, +) => { const targetString = metrics.map((metric) => `target=${metric.target}`).join('&'); const until = Math.round(Date.now() / 1000); @@ -23,6 +21,6 @@ export const getChartData = async ({ return window.api.getChartData( {target: targetString, from, until, maxDataPoints, database}, - {concurrentId: `getChartData|${targetString}`}, + {signal}, ); }; diff --git a/src/components/MetricChart/reducer.ts b/src/components/MetricChart/reducer.ts index 014d76b40d..04d814d262 100644 --- a/src/components/MetricChart/reducer.ts +++ b/src/components/MetricChart/reducer.ts @@ -1,86 +1,38 @@ -import {createRequestActionTypes} from '../../store/utils'; -import type {IResponseError} from '../../types/api/error'; - -import type {PreparedMetricsData} from './types'; - -const FETCH_CHART_DATA = createRequestActionTypes('chart', 'FETCH_CHART_DATA'); -const SET_CHART_DATA_WAS_NOT_LOADED = 'chart/SET_DATA_WAS_NOT_LOADED'; - -export const setChartDataLoading = () => { - return { - type: FETCH_CHART_DATA.REQUEST, - } as const; -}; - -export const setChartData = (data: PreparedMetricsData) => { - return { - data, - type: FETCH_CHART_DATA.SUCCESS, - } as const; -}; - -export const setChartError = (error: IResponseError) => { - return { - error, - type: FETCH_CHART_DATA.FAILURE, - } as const; -}; - -export const setChartDataWasNotLoaded = () => { - return { - type: SET_CHART_DATA_WAS_NOT_LOADED, - } as const; -}; - -type ChartAction = - | ReturnType - | ReturnType - | ReturnType - | ReturnType; - -interface ChartState { - loading: boolean; - wasLoaded: boolean; - data: PreparedMetricsData; - error: IResponseError | undefined; -} - -export const initialChartState: ChartState = { - // Set chart initial state as loading, in order not to mount and unmount component in between requests - // as it leads to memory leak errors in console (not proper useEffect cleanups in chart component itself) - // TODO: possible fix (check needed): chart component is always present, but display: none for chart while loading - loading: true, - wasLoaded: false, - data: {timeline: [], metrics: []}, - error: undefined, -}; - -export const chartReducer = (state: ChartState, action: ChartAction) => { - switch (action.type) { - case FETCH_CHART_DATA.REQUEST: { - return {...state, loading: true}; - } - case FETCH_CHART_DATA.SUCCESS: { - return {...state, loading: false, wasLoaded: true, error: undefined, data: action.data}; - } - case FETCH_CHART_DATA.FAILURE: { - if (action.error?.isCancelled) { - return state; - } - - return { - ...state, - error: action.error, - // Clear data, so error will be displayed with empty chart - data: {timeline: [], metrics: []}, - loading: false, - wasLoaded: true, - }; - } - case SET_CHART_DATA_WAS_NOT_LOADED: { - return {...state, wasLoaded: false}; - } - default: - return state; - } -}; +import {api} from '../../store/reducers/api'; + +import {convertResponse} from './convertResponse'; +import type {GetChartDataParams} from './getChartData'; +import {getChartData} from './getChartData'; +import i18n from './i18n'; + +export const chartApi = api.injectEndpoints({ + endpoints: (builder) => ({ + getChartData: builder.query({ + queryFn: async (params: GetChartDataParams, {signal}) => { + try { + const response = await getChartData(params, {signal}); + + // Response could be a plain html for ydb versions without charts support + // Or there could be an error in response with 200 status code + // It happens when request is OK, but chart data cannot be returned due to some reason + // Example: charts are not enabled in the DB ('GraphShard is not enabled' error) + if (Array.isArray(response)) { + const preparedData = convertResponse(response, params.metrics); + return {data: preparedData}; + } + + return { + error: new Error( + typeof response === 'string' ? i18n('not-supported') : response.error, + ), + }; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + keepUnusedDataFor: 0, + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/containers/App/Content.tsx b/src/containers/App/Content.tsx index 6382561a22..687eb45483 100644 --- a/src/containers/App/Content.tsx +++ b/src/containers/App/Content.tsx @@ -10,7 +10,7 @@ import type {SlotComponent} from '../../components/slots/types'; import routes from '../../routes'; import type {RootState} from '../../store'; import {getUser} from '../../store/reducers/authentication/authentication'; -import {getNodesList} from '../../store/reducers/nodesList'; +import {nodesListApi} from '../../store/reducers/nodesList'; import {cn} from '../../utils/cn'; import {useTypedDispatch, useTypedSelector} from '../../utils/hooks'; import Authentication from '../Authentication/Authentication'; @@ -178,12 +178,7 @@ function GetUser() { } function GetNodesList() { - const dispatch = useTypedDispatch(); - - React.useEffect(() => { - dispatch(getNodesList()); - }, [dispatch]); - + nodesListApi.useGetNodesListQuery(undefined); return null; } diff --git a/src/containers/Heatmap/Heatmap.tsx b/src/containers/Heatmap/Heatmap.tsx index e338dfc025..671ffed8d2 100644 --- a/src/containers/Heatmap/Heatmap.tsx +++ b/src/containers/Heatmap/Heatmap.tsx @@ -4,12 +4,12 @@ import {Checkbox, Select} from '@gravity-ui/uikit'; import {ResponseError} from '../../components/Errors/ResponseError'; import {Loader} from '../../components/Loader'; -import {getTabletsInfo, setHeatmapOptions} from '../../store/reducers/heatmap'; +import {heatmapApi, setHeatmapOptions} from '../../store/reducers/heatmap'; import {hideTooltip, showTooltip} from '../../store/reducers/tooltip'; import type {IHeatmapMetricValue} from '../../types/store/heatmap'; import {cn} from '../../utils/cn'; import {formatNumber} from '../../utils/dataFormatters/dataFormatters'; -import {useAutofetcher, useTypedDispatch, useTypedSelector} from '../../utils/hooks'; +import {useTypedDispatch, useTypedSelector} from '../../utils/hooks'; import {HeatmapCanvas} from './HeatmapCanvas/HeatmapCanvas'; import {Histogram} from './Histogram/Histogram'; @@ -30,43 +30,16 @@ export const Heatmap = ({path}: HeatmapProps) => { const itemsContainer = React.createRef(); const {autorefresh} = useTypedSelector((state) => state.schema); - const { - loading, - wasLoaded, - error, - sort, - heatmap, - metrics, - currentMetric, - data: tablets = [], - } = useTypedSelector((state) => state.heatmap); - - const [selectedMetric, setSelectedMetric] = React.useState(['']); - - React.useEffect(() => { - if (!currentMetric && metrics && metrics.length) { - dispatch( - setHeatmapOptions({ - currentMetric: metrics[0].value, - }), - ); - } - if (currentMetric) { - setSelectedMetric([currentMetric]); - } - }, [currentMetric, metrics, dispatch]); - - const fetchData = React.useCallback( - (isBackground: boolean) => { - if (!isBackground) { - dispatch(setHeatmapOptions({wasLoaded: false})); - } - dispatch(getTabletsInfo({path})); - }, - [path, dispatch], + + const {currentData, isFetching, error} = heatmapApi.useGetHeatmapTabletsInfoQuery( + {path}, + {pollingInterval: autorefresh}, ); - useAutofetcher(fetchData, [fetchData], autorefresh); + const loading = isFetching && currentData === undefined; + + const {tablets = [], metrics} = currentData || {}; + const {sort, heatmap, currentMetric} = useTypedSelector((state) => state.heatmap); const onShowTooltip = (...args: Parameters) => { dispatch(showTooltip(...args)); @@ -137,7 +110,6 @@ export const Heatmap = ({path}: HeatmapProps) => { parentRef={itemsContainer} showTooltip={onShowTooltip} hideTooltip={onHideTooltip} - currentMetric={currentMetric} /> ); @@ -151,7 +123,7 @@ export const Heatmap = ({path}: HeatmapProps) => {
{ + dispatch(setAutorefreshInterval(Number(v))); + }} + width={85} + > + {i18n('None')} + {i18n('15 sec')} + {i18n('1 min')} + {i18n('2 min')} + {i18n('5 min')} + +
+ ); +} diff --git a/src/containers/Tenant/Diagnostics/Autorefresh/i18n/en.json b/src/containers/Tenant/Diagnostics/Autorefresh/i18n/en.json new file mode 100644 index 0000000000..03a64ebd3e --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Autorefresh/i18n/en.json @@ -0,0 +1,8 @@ +{ + "None": "None", + "15 sec": "15 sec", + "1 min": "1 min", + "2 min": "2 min", + "5 min": "5 min", + "Refresh": "Refresh" +} diff --git a/src/containers/Tenant/Diagnostics/Autorefresh/i18n/index.ts b/src/containers/Tenant/Diagnostics/Autorefresh/i18n/index.ts new file mode 100644 index 0000000000..be83c33511 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Autorefresh/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-diagnostics-autorefresh-control'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tenant/Diagnostics/Consumers/Consumers.tsx b/src/containers/Tenant/Diagnostics/Consumers/Consumers.tsx index b9d6eca593..ffc9bea812 100644 --- a/src/containers/Tenant/Diagnostics/Consumers/Consumers.tsx +++ b/src/containers/Tenant/Diagnostics/Consumers/Consumers.tsx @@ -7,15 +7,14 @@ import {ResponseError} from '../../../../components/Errors/ResponseError'; import {Loader} from '../../../../components/Loader'; import {Search} from '../../../../components/Search'; import { - getTopic, selectPreparedConsumersData, selectPreparedTopicStats, - setDataWasNotLoaded, + topicApi, } from '../../../../store/reducers/topic'; import type {EPathType} from '../../../../types/api/schema'; import {cn} from '../../../../utils/cn'; import {DEFAULT_TABLE_SETTINGS} from '../../../../utils/constants'; -import {useAutofetcher, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; +import {useTypedSelector} from '../../../../utils/hooks'; import {isCdcStreamEntityType} from '../../utils/schema'; import {ConsumersTopicStats} from './TopicStats'; @@ -34,28 +33,16 @@ interface ConsumersProps { export const Consumers = ({path, type}: ConsumersProps) => { const isCdcStream = isCdcStreamEntityType(type); - const dispatch = useTypedDispatch(); - const [searchValue, setSearchValue] = React.useState(''); const {autorefresh} = useTypedSelector((state) => state.schema); - const {loading, wasLoaded, error} = useTypedSelector((state) => state.topic); - - const consumers = useTypedSelector((state) => selectPreparedConsumersData(state)); - const topic = useTypedSelector((state) => selectPreparedTopicStats(state)); - - const fetchData = React.useCallback( - (isBackground: boolean) => { - if (!isBackground) { - dispatch(setDataWasNotLoaded); - } - - dispatch(getTopic(path)); - }, - [dispatch, path], + const {currentData, isFetching, error} = topicApi.useGetTopicQuery( + {path}, + {pollingInterval: autorefresh}, ); - - useAutofetcher(fetchData, [fetchData], autorefresh); + const loading = isFetching && currentData === undefined; + const consumers = useTypedSelector((state) => selectPreparedConsumersData(state, path)); + const topic = useTypedSelector((state) => selectPreparedTopicStats(state, path)); const dataToRender = React.useMemo(() => { if (!consumers) { @@ -73,7 +60,7 @@ export const Consumers = ({path, type}: ConsumersProps) => { setSearchValue(value); }; - if (loading && !wasLoaded) { + if (loading) { return ; } diff --git a/src/containers/Tenant/Diagnostics/Describe/Describe.tsx b/src/containers/Tenant/Diagnostics/Describe/Describe.tsx index cc55b51ec3..630cf8962d 100644 --- a/src/containers/Tenant/Diagnostics/Describe/Describe.tsx +++ b/src/containers/Tenant/Diagnostics/Describe/Describe.tsx @@ -1,20 +1,14 @@ -import React from 'react'; - +import {skipToken} from '@reduxjs/toolkit/query'; import JSONTree from 'react-json-inspector'; import {shallowEqual} from 'react-redux'; import {ResponseError} from '../../../../components/Errors/ResponseError'; import {Loader} from '../../../../components/Loader'; -import { - getDescribe, - getDescribeBatched, - setCurrentDescribePath, - setDataWasNotLoaded, -} from '../../../../store/reducers/describe'; +import {describeApi} from '../../../../store/reducers/describe'; import {selectSchemaMergedChildrenPaths} from '../../../../store/reducers/schema/schema'; import type {EPathType} from '../../../../types/api/schema'; import {cn} from '../../../../utils/cn'; -import {useAutofetcher, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; +import {useTypedSelector} from '../../../../utils/hooks'; import {isEntityWithMergedImplementation} from '../../utils/schema'; import './Describe.scss'; @@ -30,26 +24,6 @@ interface IDescribeProps { } const Describe = ({tenant, type}: IDescribeProps) => { - const dispatch = useTypedDispatch(); - - const {currentDescribe, error, loading, wasLoaded} = useTypedSelector( - (state) => state.describe, - ); - - const [preparedDescribeData, setPreparedDescribeData] = React.useState(); - - React.useEffect(() => { - if (currentDescribe) { - const paths = Object.keys(currentDescribe); - - if (paths.length === 1) { - setPreparedDescribeData(currentDescribe[paths[0]]); - } else { - setPreparedDescribeData(currentDescribe); - } - } - }, [currentDescribe]); - const {autorefresh, currentSchemaPath} = useTypedSelector((state) => state.schema); const isEntityWithMergedImpl = isEntityWithMergedImplementation(type); @@ -59,27 +33,30 @@ const Describe = ({tenant, type}: IDescribeProps) => { shallowEqual, ); - const fetchData = React.useCallback( - (isBackground: boolean) => { - if (!isBackground) { - dispatch(setDataWasNotLoaded()); - } - - const path = currentSchemaPath || tenant; - dispatch(setCurrentDescribePath(path)); - - if (!isEntityWithMergedImpl) { - dispatch(getDescribe({path})); - } else if (mergedChildrenPaths) { - dispatch(getDescribeBatched([path, ...mergedChildrenPaths])); - } - }, - [currentSchemaPath, tenant, mergedChildrenPaths, isEntityWithMergedImpl, dispatch], - ); - - useAutofetcher(fetchData, [fetchData], autorefresh); + const path = currentSchemaPath || tenant; + let paths: string[] | typeof skipToken = skipToken; + if (!isEntityWithMergedImpl) { + paths = [path]; + } else if (mergedChildrenPaths) { + paths = [path, ...mergedChildrenPaths]; + } + const {currentData, isFetching, error} = describeApi.useGetDescribeQuery(paths, { + pollingInterval: autorefresh, + }); + const loading = isFetching && currentData === undefined; + const currentDescribe = currentData; + + let preparedDescribeData: Object | undefined; + if (currentDescribe) { + const paths = Object.keys(currentDescribe); + if (paths.length === 1) { + preparedDescribeData = currentDescribe[paths[0]]; + } else { + preparedDescribeData = currentDescribe; + } + } - if ((loading && !wasLoaded) || (isEntityWithMergedImpl && !mergedChildrenPaths)) { + if (loading || (isEntityWithMergedImpl && !mergedChildrenPaths)) { return ; } diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index d36da5a768..962176b9dd 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {Switch, Tabs} from '@gravity-ui/uikit'; +import {Tabs} from '@gravity-ui/uikit'; import qs from 'qs'; import {Helmet} from 'react-helmet-async'; import {useLocation} from 'react-router'; @@ -8,8 +8,6 @@ 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'; import type {TenantDiagnosticsTab} from '../../../store/reducers/tenant/types'; @@ -24,12 +22,13 @@ import {Tablets} from '../../Tablets'; import {TenantTabsGroups} from '../TenantPages'; import {isDatabaseEntityType} from '../utils/schema'; +import {AutorefreshControl} from './Autorefresh/AutorefreshControl'; import {Consumers} from './Consumers'; import Describe from './Describe/Describe'; import DetailedOverview from './DetailedOverview/DetailedOverview'; import {DATABASE_PAGES, getPagesByType} from './DiagnosticsPages'; import {HotKeys} from './HotKeys/HotKeys'; -import Network from './Network/Network'; +import {Network} from './Network/Network'; import {Partitions} from './Partitions/Partitions'; import {TopQueries} from './TopQueries'; import {TopShards} from './TopShards'; @@ -48,7 +47,7 @@ function Diagnostics(props: DiagnosticsProps) { const container = React.useRef(null); const dispatch = useTypedDispatch(); - const {currentSchemaPath, autorefresh, wasLoaded} = useTypedSelector((state) => state.schema); + const {currentSchemaPath, wasLoaded} = useTypedSelector((state) => state.schema); const {diagnosticsTab = TENANT_DIAGNOSTICS_TABS_IDS.overview} = useTypedSelector( (state) => state.tenant, ); @@ -88,15 +87,6 @@ function Diagnostics(props: DiagnosticsProps) { return undefined; }, [pages, diagnosticsTab, wasLoaded]); - const onAutorefreshToggle = (value: boolean) => { - if (value) { - dispatch(api.util.invalidateTags(['All'])); - dispatch(enableAutorefresh()); - } else { - dispatch(disableAutorefresh()); - } - }; - const renderTabContent = () => { const {type} = props; @@ -184,11 +174,7 @@ function Diagnostics(props: DiagnosticsProps) { }} allowNotSelected={true} /> - + ); diff --git a/src/containers/Tenant/Diagnostics/Network/Network.js b/src/containers/Tenant/Diagnostics/Network/Network.js deleted file mode 100644 index fd5b2bd27f..0000000000 --- a/src/containers/Tenant/Diagnostics/Network/Network.js +++ /dev/null @@ -1,377 +0,0 @@ -import React from 'react'; - -import {Checkbox, Loader} from '@gravity-ui/uikit'; -import reduce from 'lodash/reduce'; -import PropTypes from 'prop-types'; -import {connect} from 'react-redux'; -import {Link} from 'react-router-dom'; - -import {Icon} from '../../../../components/Icon'; -import {Illustration} from '../../../../components/Illustration'; -import {ProblemFilter} from '../../../../components/ProblemFilter'; -import {getNetworkInfo, setDataWasNotLoaded} from '../../../../store/reducers/network/network'; -import {ProblemFilterValues, changeFilter} from '../../../../store/reducers/settings/settings'; -import {hideTooltip, showTooltip} from '../../../../store/reducers/tooltip'; -import {AutoFetcher} from '../../../../utils/autofetcher'; -import {cn} from '../../../../utils/cn'; -import {getDefaultNodePath} from '../../../Node/NodePages'; - -import NodeNetwork from './NodeNetwork/NodeNetwork'; -import {getConnectedNodesCount} from './utils'; - -import './Network.scss'; - -const b = cn('network'); - -class Network extends React.Component { - static propTypes = { - getNetworkInfo: PropTypes.func, - setDataWasNotLoaded: PropTypes.func, - netWorkInfo: PropTypes.object, - hideTooltip: PropTypes.func, - showTooltip: PropTypes.func, - error: PropTypes.object, - wasLoaded: PropTypes.bool, - loading: PropTypes.bool, - autorefresh: PropTypes.bool, - path: PropTypes.string, - filter: PropTypes.string, - changeFilter: PropTypes.func, - }; - - static defaultProps = {}; - - static renderLoader() { - return ( -
- -
- ); - } - - state = { - howNodeSeeOtherNodesSortType: '', - howOthersSeeNodeSortType: '', - howNodeSeeOtherSearch: '', - howOtherSeeNodeSearch: '', - hoveredNode: undefined, - clickedNode: undefined, - highlightedNodes: [], - showId: false, - showRacks: false, - }; - - autofetcher; - - componentDidMount() { - const {path, autorefresh, getNetworkInfo} = this.props; - this.autofetcher = new AutoFetcher(); - - if (autorefresh) { - this.autofetcher.start(); - this.autofetcher.fetch(() => getNetworkInfo(path)); - } - - getNetworkInfo(path); - } - - componentDidUpdate(prevProps) { - const {autorefresh, path, setDataWasNotLoaded, getNetworkInfo} = this.props; - - const restartAutorefresh = () => { - this.autofetcher.stop(); - this.autofetcher.start(); - this.autofetcher.fetch(() => getNetworkInfo(path)); - }; - - if (autorefresh && !prevProps.autorefresh) { - getNetworkInfo(path); - restartAutorefresh(); - } - if (!autorefresh && prevProps.autorefresh) { - this.autofetcher.stop(); - } - - if (path !== prevProps.path) { - setDataWasNotLoaded(); - getNetworkInfo(path); - restartAutorefresh(); - } - } - - componentWillUnmount() { - this.autofetcher.stop(); - } - - onChange = (field, num) => { - this.setState({[field]: num}); - }; - - handleSortChange = (sortItem) => { - this.setState({sort: sortItem}); - }; - - handleNodeClickWrap = (nodeInfo) => { - return () => { - const {clickedNode} = this.state; - const {NodeId} = nodeInfo; - if (!clickedNode) { - this.setState({ - clickedNode: nodeInfo, - rightNodes: this.groupNodesByField(nodeInfo.Peers, 'NodeType'), - }); - } else if (NodeId === clickedNode.nodeId) { - this.setState({clickedNode: undefined}); - } else { - this.setState({ - clickedNode: nodeInfo, - rightNodes: this.groupNodesByField(nodeInfo.Peers, 'NodeType'), - }); - } - }; - }; - - groupNodesByField = (nodes, field) => { - return reduce( - nodes, - (acc, node) => { - if (!acc[node[field]]) { - acc[node[field]] = [node]; - } else { - acc[node[field]].push(node); - } - return acc; - }, - {}, - ); - }; - - renderNodes = (nodes, isRight) => { - const {showId, showRacks, clickedNode} = this.state; - let problemNodesCount = 0; - const {showTooltip, hideTooltip, filter} = this.props; - const result = Object.keys(nodes).map((key, j) => { - const nodesGroupedByRack = this.groupNodesByField(nodes[key], 'Rack'); - return ( -
-
{key} nodes
-
- {showRacks - ? Object.keys(nodesGroupedByRack).map((key, i) => ( -
-
- {key === 'undefined' ? '?' : key} -
- {/* eslint-disable-next-line array-callback-return */} - {nodesGroupedByRack[key].map((nodeInfo, index) => { - let capacity, connected; - if (!isRight && nodeInfo?.Peers) { - capacity = Object.keys(nodeInfo?.Peers).length; - connected = getConnectedNodesCount(nodeInfo?.Peers); - } - - if ( - (filter === ProblemFilterValues.PROBLEMS && - capacity !== connected) || - filter === ProblemFilterValues.ALL || - isRight - ) { - problemNodesCount++; - return ( - - ); - } - })} -
- )) - : // eslint-disable-next-line array-callback-return - nodes[key].map((nodeInfo, index) => { - let capacity, connected; - if (!isRight) { - capacity = nodeInfo?.Peers?.length; - connected = getConnectedNodesCount(nodeInfo?.Peers); - } - - if ( - (filter === ProblemFilterValues.PROBLEMS && - capacity !== connected) || - filter === ProblemFilterValues.ALL || - isRight - ) { - problemNodesCount++; - return ( - - ); - } - })} -
-
- ); - }); - - if (filter === ProblemFilterValues.PROBLEMS && problemNodesCount === 0) { - return ; - } else { - return result; - } - }; - - renderContent = () => { - const {netWorkInfo, filter, changeFilter} = this.props; - - const {showId, showRacks, rightNodes} = this.state; - const {clickedNode} = this.state; - - const nodes = netWorkInfo.Tenants && netWorkInfo.Tenants[0].Nodes; - const nodesGroupedByType = this.groupNodesByField(nodes, 'NodeType'); - - if (!nodes?.length) { - return
no nodes data
; - } - - return ( -
-
-
-
-
-
- -
- this.onChange('showId', !showId)} - checked={showId} - > - ID - -
-
- this.onChange('showRacks', !showRacks)} - checked={showRacks} - > - Racks - -
-
-
- {this.renderNodes(nodesGroupedByType)} -
- -
- {clickedNode ? ( -
-
- Connectivity of node{' '} - - {clickedNode.NodeId} - {' '} - to other nodes -
-
- {this.renderNodes(rightNodes, true)} -
-
- ) : ( -
-
- -
- -
- Select node to see its connectivity to other nodes -
-
- )} -
-
-
-
- ); - }; - - render() { - const {loading, wasLoaded, error} = this.props; - if (loading && !wasLoaded) { - return Network.renderLoader(); - } else if (error) { - return
{error.statusText}
; - } else { - return this.renderContent(); - } - } -} - -const mapStateToProps = (state) => { - const {wasLoaded, loading, data: netWorkInfo = {}} = state.network; - const {autorefresh} = state.schema; - return { - netWorkInfo, - wasLoaded, - loading, - filter: state.settings.problemFilter, - autorefresh, - }; -}; - -const mapDispatchToProps = { - getNetworkInfo, - hideTooltip, - showTooltip, - changeFilter, - setDataWasNotLoaded, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Network); diff --git a/src/containers/Tenant/Diagnostics/Network/Network.tsx b/src/containers/Tenant/Diagnostics/Network/Network.tsx new file mode 100644 index 0000000000..0390d8766f --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Network/Network.tsx @@ -0,0 +1,324 @@ +import React from 'react'; + +import {Checkbox, Loader} from '@gravity-ui/uikit'; +import {Link} from 'react-router-dom'; + +import {ResponseError} from '../../../../components/Errors/ResponseError'; +import {Icon} from '../../../../components/Icon'; +import {Illustration} from '../../../../components/Illustration'; +import {ProblemFilter} from '../../../../components/ProblemFilter'; +import {networkApi} from '../../../../store/reducers/network/network'; +import { + ProblemFilterValues, + changeFilter, + selectProblemFilter, +} from '../../../../store/reducers/settings/settings'; +import {hideTooltip, showTooltip} from '../../../../store/reducers/tooltip'; +import type {TNetNodeInfo, TNetNodePeerInfo} from '../../../../types/api/netInfo'; +import {cn} from '../../../../utils/cn'; +import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; +import {getDefaultNodePath} from '../../../Node/NodePages'; + +import {NodeNetwork} from './NodeNetwork/NodeNetwork'; +import {getConnectedNodesCount} from './utils'; + +import './Network.scss'; + +const b = cn('network'); + +interface NetworkProps { + path: string; +} +export function Network({path}: NetworkProps) { + const {autorefresh} = useTypedSelector((state) => state.schema); + const filter = useTypedSelector(selectProblemFilter); + const dispatch = useTypedDispatch(); + + const [clickedNode, setClickedNode] = React.useState(); + const [showId, setShowId] = React.useState(false); + const [showRacks, setShowRacks] = React.useState(false); + + const {currentData, isFetching, error} = networkApi.useGetNetworkInfoQuery(path, { + pollingInterval: autorefresh, + }); + const loading = isFetching && currentData === undefined; + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ; + } + + const netWorkInfo = currentData; + const nodes = (netWorkInfo?.Tenants && netWorkInfo.Tenants[0].Nodes) ?? []; + if (nodes.length === 0) { + return
no nodes data
; + } + + const nodesGroupedByType = groupNodesByField(nodes, 'NodeType'); + const rightNodes = clickedNode ? groupNodesByField(clickedNode.Peers ?? [], 'NodeType') : {}; + + return ( +
+
+
+
+
+
+ { + dispatch(changeFilter(v)); + }} + className={b('problem-filter')} + /> +
+ { + setShowId(!showId); + }} + checked={showId} + > + ID + +
+
+ { + setShowRacks(!showRacks); + }} + checked={showRacks} + > + Racks + +
+
+
+ +
+ +
+ {clickedNode ? ( +
+
+ Connectivity of node{' '} + + {clickedNode.NodeId} + {' '} + to other nodes +
+
+ +
+
+ ) : ( +
+
+ +
+ +
+ Select node to see its connectivity to other nodes +
+
+ )} +
+
+
+
+ ); +} + +interface NodesProps { + nodes: Record; + isRight?: boolean; + showId?: boolean; + showRacks?: boolean; + clickedNode?: TNetNodeInfo; + onClickNode: (node: TNetNodeInfo | undefined) => void; +} +function Nodes({nodes, isRight, showId, showRacks, clickedNode, onClickNode}: NodesProps) { + const filter = useTypedSelector(selectProblemFilter); + const dispatch = useTypedDispatch(); + + let problemNodesCount = 0; + const result = Object.keys(nodes).map((key, j) => { + const nodesGroupedByRack = groupNodesByField(nodes[key], 'Rack'); + return ( +
+
{key} nodes
+
+ {showRacks + ? Object.keys(nodesGroupedByRack).map((key, i) => ( +
+
+ {key === 'undefined' ? '?' : key} +
+ {nodesGroupedByRack[key].map((nodeInfo, index) => { + let capacity, connected; + if (!isRight && 'Peers' in nodeInfo && nodeInfo.Peers) { + capacity = Object.keys(nodeInfo.Peers).length; + connected = getConnectedNodesCount(nodeInfo.Peers); + } + + if ( + (filter === ProblemFilterValues.PROBLEMS && + capacity !== connected) || + filter === ProblemFilterValues.ALL || + isRight + ) { + problemNodesCount++; + return ( + { + dispatch(showTooltip(...params)); + }} + onMouseLeave={() => { + dispatch(hideTooltip()); + }} + onClick={ + isRight + ? undefined + : () => { + onClickNode( + clickedNode && + nodeInfo.NodeId === + clickedNode.NodeId + ? undefined + : (nodeInfo as TNetNodeInfo), + ); + } + } + isBlurred={ + !isRight && + clickedNode && + clickedNode.NodeId !== nodeInfo.NodeId + } + /> + ); + } + return null; + })} +
+ )) + : nodes[key].map((nodeInfo, index) => { + let capacity, connected; + const peers = + nodeInfo && 'Peers' in nodeInfo ? nodeInfo.Peers : undefined; + if (!isRight && 'Peers' in nodeInfo && nodeInfo.Peers) { + capacity = nodeInfo.Peers.length; + connected = getConnectedNodesCount(peers); + } + + if ( + (filter === ProblemFilterValues.PROBLEMS && + capacity !== connected) || + filter === ProblemFilterValues.ALL || + isRight + ) { + problemNodesCount++; + return ( + { + dispatch(showTooltip(...params)); + }} + onMouseLeave={() => { + dispatch(hideTooltip()); + }} + onClick={ + isRight + ? undefined + : () => { + onClickNode( + clickedNode && + nodeInfo.NodeId === + clickedNode.NodeId + ? undefined + : (nodeInfo as TNetNodeInfo), + ); + } + } + isBlurred={ + !isRight && + clickedNode && + clickedNode.NodeId !== nodeInfo.NodeId + } + /> + ); + } + return null; + })} +
+
+ ); + }); + + if (filter === ProblemFilterValues.PROBLEMS && problemNodesCount === 0) { + return ; + } else { + return result; + } +} + +function groupNodesByField>( + nodes: T[], + field: 'NodeType' | 'Rack', +) { + return nodes.reduce>((acc, node) => { + if (acc[node[field]]) { + acc[node[field]].push(node); + } else { + acc[node[field]] = [node]; + } + return acc; + }, {}); +} diff --git a/src/containers/Tenant/Diagnostics/Network/NodeNetwork/NodeNetwork.js b/src/containers/Tenant/Diagnostics/Network/NodeNetwork/NodeNetwork.js deleted file mode 100644 index 5cfa07a961..0000000000 --- a/src/containers/Tenant/Diagnostics/Network/NodeNetwork/NodeNetwork.js +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; - -import PropTypes from 'prop-types'; - -import {EFlag} from '../../../../../types/api/enums'; -import {cn} from '../../../../../utils/cn'; - -import './NodeNetwork.scss'; - -const b = cn('node-network'); - -const getNodeModifier = (connected, capacity) => { - const percents = Math.floor((connected / capacity) * 100); - if (percents === 100) { - return EFlag.Green; - } else if (percents >= 70) { - return EFlag.Yellow; - } else if (percents >= 1) { - return EFlag.Red; - } else { - return EFlag.Grey; - } -}; - -export class NodeNetwork extends React.Component { - static propTypes = { - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func, - nodeId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - connected: PropTypes.number, - capacity: PropTypes.number, - rack: PropTypes.string, - status: PropTypes.string, - onClick: PropTypes.func, - showID: PropTypes.bool, - isBlurred: PropTypes.bool, - }; - - static defaultProps = { - onMouseEnter: () => {}, - onMouseLeave: () => {}, - onClick: () => {}, - }; - - node = React.createRef(); - - _onNodeHover = () => { - const {onMouseEnter, nodeId, connected, capacity, rack} = this.props; - const popupData = {nodeId, connected, capacity, rack}; - onMouseEnter(this.node.current, popupData, 'node'); - }; - _onNodeLeave = () => { - this.props.onMouseLeave(); - }; - - render() { - const {nodeId, connected, capacity, status, onClick, showID, isBlurred} = this.props; - - const color = status || getNodeModifier(connected, capacity); - - return ( -
onClick(nodeId)} - > - {showID && nodeId} -
- ); - } -} - -export default NodeNetwork; diff --git a/src/containers/Tenant/Diagnostics/Network/NodeNetwork/NodeNetwork.tsx b/src/containers/Tenant/Diagnostics/Network/NodeNetwork/NodeNetwork.tsx new file mode 100644 index 0000000000..fbc3a77b0f --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Network/NodeNetwork/NodeNetwork.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import {EFlag} from '../../../../../types/api/enums'; +import type {ITooltipTemplateType} from '../../../../../types/store/tooltip'; +import {cn} from '../../../../../utils/cn'; + +import './NodeNetwork.scss'; + +const b = cn('node-network'); + +function getNodeModifier(connected = 0, capacity = 0) { + const percents = Math.floor((connected / capacity) * 100); + if (percents === 100) { + return EFlag.Green; + } else if (percents >= 70) { + return EFlag.Yellow; + } else if (percents >= 1) { + return EFlag.Red; + } else { + return EFlag.Grey; + } +} + +function noop() {} + +interface NodeNetworkProps { + onMouseEnter?: (node: HTMLDivElement, data: any, type: ITooltipTemplateType) => void; + onMouseLeave?: () => void; + nodeId: number | string; + connected?: number; + capacity?: number; + rack: string; + status?: EFlag; + onClick?: (nodeId: number | string) => void; + showID?: boolean; + isBlurred?: boolean; +} + +export function NodeNetwork({ + nodeId, + connected, + capacity, + rack, + status, + onClick = noop, + onMouseEnter = noop, + onMouseLeave = noop, + showID, + isBlurred, +}: NodeNetworkProps) { + const ref = React.useRef(null); + const color = status || getNodeModifier(connected, capacity); + + return ( +
{ + onMouseEnter(ref.current!, {nodeId, connected, capacity, rack}, 'node'); + }} + onMouseLeave={() => { + onMouseLeave(); + }} + onClick={() => onClick(nodeId)} + > + {showID ? nodeId : null} +
+ ); +} diff --git a/src/containers/Tenant/Diagnostics/Overview/Overview.tsx b/src/containers/Tenant/Diagnostics/Overview/Overview.tsx index b8a8aabbd2..b56a72c65e 100644 --- a/src/containers/Tenant/Diagnostics/Overview/Overview.tsx +++ b/src/containers/Tenant/Diagnostics/Overview/Overview.tsx @@ -1,30 +1,21 @@ import React from 'react'; +import {skipToken} from '@reduxjs/toolkit/query'; import {shallowEqual} from 'react-redux'; import {ResponseError} from '../../../../components/Errors/ResponseError'; import {TableIndexInfo} from '../../../../components/InfoViewer/schemaInfo'; import {Loader} from '../../../../components/Loader'; -import { - getOlapStats, - resetLoadingState as resetOlapLoadingState, -} from '../../../../store/reducers/olapStats'; -import { - getOverview, - getOverviewBatched, - setCurrentOverviewPath, - setDataWasNotLoaded, -} from '../../../../store/reducers/overview/overview'; +import {olapApi} from '../../../../store/reducers/olapStats'; +import {overviewApi} from '../../../../store/reducers/overview/overview'; import {selectSchemaMergedChildrenPaths} from '../../../../store/reducers/schema/schema'; -import {getTopic} from '../../../../store/reducers/topic'; import {EPathType} from '../../../../types/api/schema'; -import {useAutofetcher, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; +import {useTypedSelector} from '../../../../utils/hooks'; import {ExternalDataSourceInfo} from '../../Info/ExternalDataSource/ExternalDataSource'; import {ExternalTableInfo} from '../../Info/ExternalTable/ExternalTable'; import { isColumnEntityType, isEntityWithMergedImplementation, - isPathTypeWithTopic, isTableType, } from '../../utils/schema'; @@ -38,21 +29,17 @@ interface OverviewProps { } function Overview({type, tenantName}: OverviewProps) { - const dispatch = useTypedDispatch(); - const {autorefresh, currentSchemaPath} = useTypedSelector((state) => state.schema); - const { - data: rawData, - additionalData, - loading: overviewLoading, - wasLoaded: overviewWasLoaded, - error: overviewError, - } = useTypedSelector((state) => state.overview); - const { - data: {result: olapStats} = {result: undefined}, - loading: olapStatsLoading, - wasLoaded: olapStatsWasLoaded, - } = useTypedSelector((state) => state.olapStats); + + const schemaPath = currentSchemaPath || tenantName; + const olapParams = + isTableType(type) && isColumnEntityType(type) ? {path: schemaPath} : skipToken; + const {currentData: olapData, isFetching: olapIsFetching} = olapApi.useGetOlapStatsQuery( + olapParams, + {pollingInterval: autorefresh}, + ); + const olapStatsLoading = olapIsFetching && olapData === undefined; + const {result: olapStats} = olapData || {result: undefined}; const isEntityWithMergedImpl = isEntityWithMergedImplementation(type); @@ -62,52 +49,27 @@ function Overview({type, tenantName}: OverviewProps) { shallowEqual, ); - const entityLoading = - (overviewLoading && !overviewWasLoaded) || (olapStatsLoading && !olapStatsWasLoaded); - const entityNotReady = isEntityWithMergedImpl && !mergedChildrenPaths; + let paths: string[] | typeof skipToken = skipToken; + if (schemaPath) { + if (!isEntityWithMergedImpl) { + paths = [schemaPath]; + } else if (mergedChildrenPaths) { + paths = [schemaPath, ...mergedChildrenPaths]; + } + } - const fetchData = React.useCallback( - (isBackground: boolean) => { - const schemaPath = currentSchemaPath || tenantName; - - if (!schemaPath) { - return; - } - - dispatch(setCurrentOverviewPath(schemaPath)); - - if (!isBackground) { - dispatch(setDataWasNotLoaded()); - } - - if (!isEntityWithMergedImpl) { - dispatch(getOverview({path: schemaPath})); - } else if (mergedChildrenPaths) { - dispatch(getOverviewBatched([schemaPath, ...mergedChildrenPaths])); - } - - if (isTableType(type) && isColumnEntityType(type)) { - if (!isBackground) { - dispatch(resetOlapLoadingState()); - } - dispatch(getOlapStats({path: schemaPath})); - } - - if (isPathTypeWithTopic(type)) { - dispatch(getTopic(currentSchemaPath)); - } - }, - [ - tenantName, - currentSchemaPath, - type, - isEntityWithMergedImpl, - mergedChildrenPaths, - dispatch, - ], - ); + const { + currentData, + isFetching, + error: overviewError, + } = overviewApi.useGetOverviewQuery(paths, { + pollingInterval: autorefresh, + }); + const overviewLoading = isFetching && currentData === undefined; + const {data: rawData, additionalData} = currentData || {}; - useAutofetcher(fetchData, [fetchData], autorefresh); + const entityLoading = overviewLoading || olapStatsLoading; + const entityNotReady = isEntityWithMergedImpl && !mergedChildrenPaths; const renderContent = () => { const data = rawData ?? undefined; diff --git a/src/containers/Tenant/Diagnostics/Overview/TopicStats/TopicStats.tsx b/src/containers/Tenant/Diagnostics/Overview/TopicStats/TopicStats.tsx index 4e3aa1c903..f496c44f59 100644 --- a/src/containers/Tenant/Diagnostics/Overview/TopicStats/TopicStats.tsx +++ b/src/containers/Tenant/Diagnostics/Overview/TopicStats/TopicStats.tsx @@ -5,7 +5,7 @@ import {LabelWithPopover} from '../../../../../components/LabelWithPopover'; import {LagPopoverContent} from '../../../../../components/LagPopoverContent'; import {Loader} from '../../../../../components/Loader'; import {SpeedMultiMeter} from '../../../../../components/SpeedMultiMeter'; -import {selectPreparedTopicStats} from '../../../../../store/reducers/topic'; +import {selectPreparedTopicStats, topicApi} from '../../../../../store/reducers/topic'; import type {IPreparedTopicStats} from '../../../../../types/store/topic'; import {cn} from '../../../../../utils/cn'; import {formatBps, formatBytes} from '../../../../../utils/dataFormatters/dataFormatters'; @@ -70,11 +70,15 @@ const prepareBytesWrittenInfo = (data: IPreparedTopicStats): Array { - const {error, loading, wasLoaded} = useTypedSelector((state) => state.topic); - - const data = useTypedSelector(selectPreparedTopicStats); + const {autorefresh, currentSchemaPath} = useTypedSelector((state) => state.schema); + const {currentData, isFetching, error} = topicApi.useGetTopicQuery( + {path: currentSchemaPath}, + {pollingInterval: autorefresh}, + ); + const loading = isFetching && currentData === undefined; + const data = useTypedSelector((state) => selectPreparedTopicStats(state, currentSchemaPath)); - if (loading && !wasLoaded) { + if (loading) { return (
diff --git a/src/containers/Tenant/Diagnostics/Partitions/Partitions.tsx b/src/containers/Tenant/Diagnostics/Partitions/Partitions.tsx index 0e914a9a7a..34a8bd659b 100644 --- a/src/containers/Tenant/Diagnostics/Partitions/Partitions.tsx +++ b/src/containers/Tenant/Diagnostics/Partitions/Partitions.tsx @@ -1,29 +1,16 @@ import React from 'react'; import DataTable from '@gravity-ui/react-data-table'; +import {skipToken} from '@reduxjs/toolkit/query'; import {ResponseError} from '../../../../components/Errors/ResponseError'; import {TableSkeleton} from '../../../../components/TableSkeleton/TableSkeleton'; -import {selectNodesMap} from '../../../../store/reducers/nodesList'; -import { - getPartitions, - setDataWasNotLoaded as setPartitionsDataWasNotLoaded, - setSelectedConsumer, -} from '../../../../store/reducers/partitions/partitions'; -import { - cleanTopicData, - getTopic, - selectConsumersNames, - setDataWasNotLoaded as setTopicDataWasNotLoaded, -} from '../../../../store/reducers/topic'; +import {nodesListApi, selectNodesMap} from '../../../../store/reducers/nodesList'; +import {partitionsApi, setSelectedConsumer} from '../../../../store/reducers/partitions/partitions'; +import {selectConsumersNames, topicApi} from '../../../../store/reducers/topic'; import {cn} from '../../../../utils/cn'; import {DEFAULT_TABLE_SETTINGS, PARTITIONS_HIDDEN_COLUMNS_KEY} from '../../../../utils/constants'; -import { - useAutofetcher, - useSetting, - useTypedDispatch, - useTypedSelector, -} from '../../../../utils/hooks'; +import {useSetting, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import {PartitionsControls} from './PartitionsControls/PartitionsControls'; import i18n from './i18n'; @@ -50,69 +37,58 @@ export const Partitions = ({path}: PartitionsProps) => { PreparedPartitionDataWithHosts[] >([]); - const consumers = useTypedSelector(selectConsumersNames); - const nodesMap = useTypedSelector(selectNodesMap); + const consumers = useTypedSelector((state) => selectConsumersNames(state, path)); const {autorefresh} = useTypedSelector((state) => state.schema); + const {selectedConsumer} = useTypedSelector((state) => state.partitions); const { - loading: partitionsLoading, - wasLoaded: partitionsWasLoaded, - error: partitionsError, - partitions: rawPartitions, - selectedConsumer, - } = useTypedSelector((state) => state.partitions); - const { - loading: topicLoading, - wasLoaded: topicWasLoaded, + currentData: topicData, + isFetching: topicIsFetching, error: topicError, - } = useTypedSelector((state) => state.topic); + } = topicApi.useGetTopicQuery({path}); + const topicLoading = topicIsFetching && topicData === undefined; const { - loading: nodesLoading, - wasLoaded: nodesWasLoaded, + currentData: nodesData, + isFetching: nodesIsFetching, error: nodesError, - } = useTypedSelector((state) => state.nodesList); + } = nodesListApi.useGetNodesListQuery(undefined); + const nodesLoading = nodesIsFetching && nodesData === undefined; + const nodesMap = useTypedSelector(selectNodesMap); const [hiddenColumns, setHiddenColumns] = useSetting(PARTITIONS_HIDDEN_COLUMNS_KEY); const [columns, columnsIdsForSelector] = useGetPartitionsColumns(selectedConsumer); React.useEffect(() => { - dispatch(cleanTopicData()); - dispatch(setTopicDataWasNotLoaded()); - - dispatch(getTopic(path)); - setComponentCurrentPath(path); }, [dispatch, path]); + const params = + !topicLoading && componentCurrentPath + ? {path: componentCurrentPath, consumerName: selectedConsumer} + : skipToken; + const { + currentData: partitionsData, + isFetching: partitionsIsFetching, + error: partitionsError, + } = partitionsApi.useGetPartitionsQuery(params, {pollingInterval: autorefresh}); + const partitionsLoading = partitionsIsFetching && partitionsData === undefined; + const rawPartitions = partitionsData; + const partitionsWithHosts = React.useMemo(() => { return addHostToPartitions(rawPartitions, nodesMap); }, [rawPartitions, nodesMap]); - const fetchData = React.useCallback( - (isBackground: boolean) => { - if (!isBackground) { - dispatch(setPartitionsDataWasNotLoaded()); - } - if (topicWasLoaded && componentCurrentPath) { - dispatch(getPartitions(componentCurrentPath, selectedConsumer)); - } - }, - [dispatch, selectedConsumer, componentCurrentPath, topicWasLoaded], - ); - - useAutofetcher(fetchData, [fetchData], autorefresh); - // Wrong consumer could be passed in search query // Reset consumer if it doesn't exist for current topic React.useEffect(() => { - const isTopicWithoutConsumers = topicWasLoaded && !consumers; + const isTopicWithoutConsumers = !topicLoading && !consumers; const wrongSelectedConsumer = selectedConsumer && consumers && !consumers.includes(selectedConsumer); if (isTopicWithoutConsumers || wrongSelectedConsumer) { dispatch(setSelectedConsumer('')); } - }, [dispatch, topicWasLoaded, selectedConsumer, consumers]); + }, [dispatch, topicLoading, selectedConsumer, consumers]); const columnsToShow = React.useMemo(() => { return columns.filter((column) => !hiddenColumns.includes(column.name)); @@ -126,10 +102,7 @@ export const Partitions = ({path}: PartitionsProps) => { dispatch(setSelectedConsumer(value)); }; - const loading = - (topicLoading && !topicWasLoaded) || - (nodesLoading && !nodesWasLoaded) || - (partitionsLoading && !partitionsWasLoaded); + const loading = topicLoading || nodesLoading || partitionsLoading; const error = nodesError || topicError || partitionsError; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx index 1044edb864..ea639d90b7 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx @@ -1,7 +1,6 @@ import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {topNodesApi} from '../../../../../store/reducers/tenantOverview/topNodes/topNodes'; import type {AdditionalNodesProps} from '../../../../../types/additionalProps'; -import {DEFAULT_POLLING_INTERVAL} from '../../../../../utils/constants'; import {useSearchQuery, useTypedSelector} from '../../../../../utils/hooks'; import {getTopNodesByCpuColumns} from '../../../../Nodes/getNodesColumns'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; @@ -22,7 +21,7 @@ export function TopNodesByCpu({path, additionalNodesProps}: TopNodesByCpuProps) const {currentData, isFetching, error} = topNodesApi.useGetTopNodesQuery( {tenant: path, sortValue: 'CPU'}, - {pollingInterval: autorefresh ? DEFAULT_POLLING_INTERVAL : 0}, + {pollingInterval: autorefresh}, ); const loading = isFetching && currentData === undefined; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByLoad.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByLoad.tsx index 5da25aa5b2..90380ca6a8 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByLoad.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByLoad.tsx @@ -1,7 +1,6 @@ import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {topNodesApi} from '../../../../../store/reducers/tenantOverview/topNodes/topNodes'; import type {AdditionalNodesProps} from '../../../../../types/additionalProps'; -import {DEFAULT_POLLING_INTERVAL} from '../../../../../utils/constants'; import {useSearchQuery, useTypedSelector} from '../../../../../utils/hooks'; import {getTopNodesByLoadColumns} from '../../../../Nodes/getNodesColumns'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; @@ -22,7 +21,7 @@ export function TopNodesByLoad({path, additionalNodesProps}: TopNodesByLoadProps const {currentData, isFetching, error} = topNodesApi.useGetTopNodesQuery( {tenant: path, sortValue: 'LoadAverage'}, - {pollingInterval: autorefresh ? DEFAULT_POLLING_INTERVAL : 0}, + {pollingInterval: autorefresh}, ); const loading = isFetching && currentData === undefined; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx index 0eeb7e14a2..16acbf42d1 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx @@ -11,7 +11,6 @@ import { TENANT_QUERY_TABS_ID, } from '../../../../../store/reducers/tenant/constants'; import {topQueriesApi} from '../../../../../store/reducers/tenantOverview/topQueries/tenantOverviewTopQueries'; -import {DEFAULT_POLLING_INTERVAL} from '../../../../../utils/constants'; import {useTypedDispatch, useTypedSelector} from '../../../../../utils/hooks'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; import {getTenantOverviewTopQueriesColumns} from '../../TopQueries/getTopQueriesColumns'; @@ -33,9 +32,9 @@ export function TopQueries({path}: TopQueriesProps) { const {autorefresh} = useTypedSelector((state) => state.schema); const columns = getTenantOverviewTopQueriesColumns(); - const {currentData, isFetching, error} = topQueriesApi.useGetTopQueriesQuery( + const {currentData, isFetching, error} = topQueriesApi.useGetOverviewTopQueriesQuery( {database: path}, - {pollingInterval: autorefresh ? DEFAULT_POLLING_INTERVAL : 0}, + {pollingInterval: autorefresh}, ); const loading = isFetching && currentData === undefined; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx index 8a6548e534..c9c50dda62 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx @@ -3,7 +3,6 @@ import {useLocation} from 'react-router'; import {parseQuery} from '../../../../../routes'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {topShardsApi} from '../../../../../store/reducers/tenantOverview/topShards/tenantOverviewTopShards'; -import {DEFAULT_POLLING_INTERVAL} from '../../../../../utils/constants'; import {useTypedSelector} from '../../../../../utils/hooks'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; import {getTopShardsColumns} from '../../TopShards/getTopShardsColumns'; @@ -24,7 +23,7 @@ export const TopShards = ({path}: TopShardsProps) => { const {currentData, isFetching, error} = topShardsApi.useGetTopShardsQuery( {database: path, path: currentSchemaPath}, - {pollingInterval: autorefresh ? DEFAULT_POLLING_INTERVAL : 0}, + {pollingInterval: autorefresh}, ); const loading = isFetching && currentData === undefined; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.tsx index 8a5fbbb53b..2831523e44 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.tsx @@ -39,7 +39,7 @@ export const TenantDashboard = ({database, charts}: TenantDashboardProps) => { const {autorefresh} = useTypedSelector((state) => state.schema); // Refetch data only if dashboard successfully loaded - const shouldRefresh = autorefresh && !isDashboardHidden; + const shouldRefresh = isDashboardHidden ? 0 : autorefresh; /** * Charts should be hidden, if they are not enabled: diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TopNodesByMemory.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TopNodesByMemory.tsx index 3e4634855b..0233aea8ef 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TopNodesByMemory.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TopNodesByMemory.tsx @@ -1,7 +1,6 @@ import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {topNodesApi} from '../../../../../store/reducers/tenantOverview/topNodes/topNodes'; import type {AdditionalNodesProps} from '../../../../../types/additionalProps'; -import {DEFAULT_POLLING_INTERVAL} from '../../../../../utils/constants'; import {useSearchQuery, useTypedSelector} from '../../../../../utils/hooks'; import {getTopNodesByMemoryColumns} from '../../../../Nodes/getNodesColumns'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; @@ -24,7 +23,7 @@ export function TopNodesByMemory({path, additionalNodesProps}: TopNodesByMemoryP const {currentData, isFetching, error} = topNodesApi.useGetTopNodesQuery( {tenant: path, sortValue: 'Memory'}, - {pollingInterval: autorefresh ? DEFAULT_POLLING_INTERVAL : 0}, + {pollingInterval: autorefresh}, ); const loading = isFetching && currentData === undefined; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx index 443cc1d11f..aadfcff788 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx @@ -5,7 +5,7 @@ import {TENANT_METRICS_TABS_IDS} from '../../../../store/reducers/tenant/constan import {tenantApi} from '../../../../store/reducers/tenant/tenant'; import {calculateTenantMetrics} from '../../../../store/reducers/tenants/utils'; import type {AdditionalNodesProps, AdditionalTenantsProps} from '../../../../types/additionalProps'; -import {DEFAULT_POLLING_INTERVAL, TENANT_DEFAULT_TITLE} from '../../../../utils/constants'; +import {TENANT_DEFAULT_TITLE} from '../../../../utils/constants'; import {useTypedSelector} from '../../../../utils/hooks'; import {mapDatabaseTypeToDBName} from '../../utils/schema'; @@ -46,7 +46,7 @@ export function TenantOverview({ const {currentData: tenant, isFetching} = tenantApi.useGetTenantInfoQuery( {path: tenantName}, { - pollingInterval: autorefresh ? DEFAULT_POLLING_INTERVAL : 0, + pollingInterval: autorefresh, }, ); const tenantLoading = isFetching && tenant === undefined; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopGroups.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopGroups.tsx index 7ece61bae6..d23786f055 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopGroups.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopGroups.tsx @@ -1,6 +1,5 @@ import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {topStorageGroupsApi} from '../../../../../store/reducers/tenantOverview/topStorageGroups/topStorageGroups'; -import {DEFAULT_POLLING_INTERVAL} from '../../../../../utils/constants'; import {useSearchQuery, useTypedSelector} from '../../../../../utils/hooks'; import {getStorageTopGroupsColumns} from '../../../../Storage/StorageGroups/getStorageGroupsColumns'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; @@ -21,7 +20,7 @@ export function TopGroups({tenant}: TopGroupsProps) { const {currentData, isFetching, error} = topStorageGroupsApi.useGetTopStorageGroupsQuery( {tenant}, - {pollingInterval: autorefresh ? DEFAULT_POLLING_INTERVAL : 0}, + {pollingInterval: autorefresh}, ); const loading = isFetching && currentData === undefined; const topGroups = currentData; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx index f0123e9040..b6050998e3 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx @@ -7,7 +7,6 @@ import {LinkToSchemaObject} from '../../../../../components/LinkToSchemaObject/L import {topTablesApi} from '../../../../../store/reducers/tenantOverview/executeTopTables/executeTopTables'; import type {KeyValueRow} from '../../../../../types/api/query'; import {formatBytes, getSizeWithSignificantDigits} from '../../../../../utils/bytesParsers'; -import {DEFAULT_POLLING_INTERVAL} from '../../../../../utils/constants'; import {useTypedSelector} from '../../../../../utils/hooks'; import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout'; import {getSectionTitle} from '../getSectionTitle'; @@ -26,7 +25,7 @@ export function TopTables({path}: TopTablesProps) { const {currentData, error, isFetching} = topTablesApi.useGetTopTablesQuery( {path}, - {pollingInterval: autorefresh ? DEFAULT_POLLING_INTERVAL : 0}, + {pollingInterval: autorefresh}, ); const loading = isFetching && currentData === undefined; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/useHealthcheck.ts b/src/containers/Tenant/Diagnostics/TenantOverview/useHealthcheck.ts index 4c88f16439..72cc767126 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/useHealthcheck.ts +++ b/src/containers/Tenant/Diagnostics/TenantOverview/useHealthcheck.ts @@ -6,7 +6,6 @@ import { import type {IssuesTree} from '../../../../store/reducers/healthcheckInfo/types'; import {SelfCheckResult} from '../../../../types/api/healthcheck'; import type {StatusFlag} from '../../../../types/api/healthcheck'; -import {DEFAULT_POLLING_INTERVAL} from '../../../../utils/constants'; import {useTypedSelector} from '../../../../utils/hooks'; interface HealthcheckParams { @@ -20,7 +19,7 @@ interface HealthcheckParams { export const useHealthcheck = ( tenantName: string, - {autorefresh}: {autorefresh?: boolean} = {}, + {autorefresh}: {autorefresh?: number} = {}, ): HealthcheckParams => { const { currentData: data, @@ -28,7 +27,7 @@ export const useHealthcheck = ( error, refetch, } = healthcheckApi.useGetHealthcheckInfoQuery(tenantName, { - pollingInterval: autorefresh ? DEFAULT_POLLING_INTERVAL : 0, + pollingInterval: autorefresh, }); const selfCheckResult = data?.self_check_result || SelfCheckResult.UNSPECIFIED; const issuesStatistics = useTypedSelector((state) => selectIssuesStatistics(state, tenantName)); diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx index b676faf801..ddce120f39 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx @@ -10,22 +10,18 @@ import {Search} from '../../../../components/Search'; import {parseQuery} from '../../../../routes'; import {changeUserInput} from '../../../../store/reducers/executeQuery'; import { - fetchTopQueries, setTopQueriesFilters, - setTopQueriesState, + topQueriesApi, } from '../../../../store/reducers/executeTopQueries/executeTopQueries'; -import type {ITopQueriesFilters} from '../../../../store/reducers/executeTopQueries/types'; import { TENANT_PAGE, TENANT_PAGES_IDS, TENANT_QUERY_TABS_ID, } from '../../../../store/reducers/tenant/constants'; import type {EPathType} from '../../../../types/api/schema'; -import type {IQueryResult} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; -import {HOUR_IN_SECONDS} from '../../../../utils/constants'; import {isSortableTopQueriesProperty} from '../../../../utils/diagnostics'; -import {useAutofetcher, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; +import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import {prepareQueryError} from '../../../../utils/query'; import {TenantTabsGroups, getTenantPath} from '../../TenantPages'; import {QUERY_TABLE_SETTINGS} from '../../utils/constants'; @@ -50,79 +46,23 @@ export const TopQueries = ({path, type}: TopQueriesProps) => { const {autorefresh} = useTypedSelector((state) => state.schema); - const { - loading, - wasLoaded, - error, - data: {result: data = undefined} = {}, - filters: storeFilters, - } = useTypedSelector((state) => state.executeTopQueries); - const rawColumns = getTopQueriesColumns(); - - const preventFetch = React.useRef(false); - - // some filters sync between redux state and URL - // component state is for default values, - // default values are determined from the query response, and should not propagate to URL - const [filters, setFilters] = React.useState(storeFilters); - - React.useEffect(() => { - dispatch(setTopQueriesFilters(filters)); - }, [dispatch, filters]); + const filters = useTypedSelector((state) => state.executeTopQueries); + const {currentData, isFetching, error} = topQueriesApi.useGetTopQueriesQuery( + { + database: path, + filters, + }, + {pollingInterval: autorefresh}, + ); + const loading = isFetching && currentData === undefined; + const {result: data} = currentData || {}; + const rawColumns = getTopQueriesColumns(); const columns = rawColumns.map((column) => ({ ...column, sortable: isSortableTopQueriesProperty(column.name), })); - const setDefaultFiltersFromResponse = (responseData?: IQueryResult) => { - const intervalEnd = responseData?.result?.[0]?.IntervalEnd; - - if (intervalEnd) { - const to = new Date(intervalEnd).getTime(); - const from = new Date(to - HOUR_IN_SECONDS * 1000).getTime(); - - setFilters((currentFilters) => { - // request without filters returns the latest interval with data - // only in this case should update filters in ui - // also don't update if user already interacted with controls - const shouldUpdateFilters = !currentFilters.from && !currentFilters.to; - - if (!shouldUpdateFilters) { - return currentFilters; - } - - preventFetch.current = true; - - return {...currentFilters, from, to}; - }); - } - }; - - useAutofetcher( - (isBackground) => { - if (preventFetch.current) { - preventFetch.current = false; - return; - } - - if (!isBackground) { - dispatch( - setTopQueriesState({ - wasLoaded: false, - data: undefined, - }), - ); - } - - dispatch(fetchTopQueries({database: path, filters})).then( - setDefaultFiltersFromResponse, - ); - }, - [dispatch, filters, path], - autorefresh, - ); - const handleRowClick = React.useCallback( (row: any) => { const {QueryText: input} = row; @@ -143,11 +83,11 @@ export const TopQueries = ({path, type}: TopQueriesProps) => { ); const handleTextSearchUpdate = (text: string) => { - setFilters((currentFilters) => ({...currentFilters, text})); + dispatch(setTopQueriesFilters({text})); }; const handleDateRangeChange = (value: DateRangeValues) => { - setFilters((currentFilters) => ({...currentFilters, ...value})); + dispatch(setTopQueriesFilters(value)); }; const renderLoader = () => { @@ -159,11 +99,11 @@ export const TopQueries = ({path, type}: TopQueriesProps) => { }; const renderContent = () => { - if (loading && !wasLoaded) { + if (loading) { return renderLoader(); } - if (error && !error.isCancelled) { + if (error && typeof error === 'object' && !(error as any).isCancelled) { return
{prepareQueryError(error)}
; } diff --git a/src/containers/Tenant/Diagnostics/TopShards/Filters/Filters.tsx b/src/containers/Tenant/Diagnostics/TopShards/Filters/Filters.tsx index c24fb3e096..c0b25a2ac2 100644 --- a/src/containers/Tenant/Diagnostics/TopShards/Filters/Filters.tsx +++ b/src/containers/Tenant/Diagnostics/TopShards/Filters/Filters.tsx @@ -3,7 +3,7 @@ import {RadioButton} from '@gravity-ui/uikit'; import type {DateRangeValues} from '../../../../../components/DateRange'; import {DateRange} from '../../../../../components/DateRange'; import {EShardsWorkloadMode} from '../../../../../store/reducers/shardsWorkload/types'; -import type {IShardsWorkloadFilters} from '../../../../../store/reducers/shardsWorkload/types'; +import type {ShardsWorkloadFilters} from '../../../../../store/reducers/shardsWorkload/types'; import {isEnumMember} from '../../../../../utils/typecheckers'; import {b} from '../TopShards'; import i18n from '../i18n'; @@ -11,8 +11,8 @@ import i18n from '../i18n'; import './Filters.scss'; interface FiltersProps { - value: IShardsWorkloadFilters; - onChange: (value: Partial) => void; + value: ShardsWorkloadFilters; + onChange: (value: Partial) => void; className?: string; } diff --git a/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx b/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx index d7be4c0fa6..3561b63857 100644 --- a/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx +++ b/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx @@ -6,19 +6,18 @@ import {Loader} from '@gravity-ui/uikit'; import {useLocation} from 'react-router'; import { - sendShardQuery, setShardsQueryFilters, - setShardsState, + shardApi, } from '../../../../store/reducers/shardsWorkload/shardsWorkload'; import {EShardsWorkloadMode} from '../../../../store/reducers/shardsWorkload/types'; -import type {IShardsWorkloadFilters} from '../../../../store/reducers/shardsWorkload/types'; +import type {ShardsWorkloadFilters} from '../../../../store/reducers/shardsWorkload/types'; import type {CellValue, KeyValueRow} from '../../../../types/api/query'; import type {EPathType} from '../../../../types/api/schema'; import {cn} from '../../../../utils/cn'; import {DEFAULT_TABLE_SETTINGS, HOUR_IN_SECONDS} from '../../../../utils/constants'; import {formatDateTime} from '../../../../utils/dataFormatters/dataFormatters'; import {isSortableTopShardsProperty} from '../../../../utils/diagnostics'; -import {useAutofetcher, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; +import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import {prepareQueryError} from '../../../../utils/query'; import {isColumnEntityType} from '../../utils/schema'; @@ -79,7 +78,7 @@ function dataTableToStringSortOrder(value: SortOrder | SortOrder[] = []) { return sortOrders.map(({columnId}) => columnId).join(','); } -function fillDateRangeFor(value: IShardsWorkloadFilters) { +function fillDateRangeFor(value: ShardsWorkloadFilters) { value.to = Date.now(); value.from = value.to - HOUR_IN_SECONDS * 1000; return value; @@ -96,17 +95,11 @@ export const TopShards = ({tenantPath, type}: TopShardsProps) => { const {autorefresh, currentSchemaPath} = useTypedSelector((state) => state.schema); - const { - loading, - data: {result: data = undefined} = {}, - filters: storeFilters, - error, - wasLoaded, - } = useTypedSelector((state) => state.shardsWorkload); + const storeFilters = useTypedSelector((state) => state.shardsWorkload); // default filters shouldn't propagate into URL until user interacts with the control // redux initial value can't be used, as it synchronizes with URL - const [filters, setFilters] = React.useState(() => { + const [filters, setFilters] = React.useState(() => { const defaultValue = {...storeFilters}; if (!defaultValue.mode) { @@ -121,31 +114,21 @@ export const TopShards = ({tenantPath, type}: TopShardsProps) => { }); const [sortOrder, setSortOrder] = React.useState(tableColumnsNames.CPUCores); - - useAutofetcher( - () => { - dispatch( - sendShardQuery({ - database: tenantPath, - path: currentSchemaPath, - sortOrder: stringToQuerySortOrder(sortOrder), - filters, - }), - ); + const { + data: result, + isFetching, + error, + } = shardApi.useSendShardQueryQuery( + { + database: tenantPath, + path: currentSchemaPath, + sortOrder: stringToQuerySortOrder(sortOrder), + filters, }, - [dispatch, tenantPath, currentSchemaPath, sortOrder, filters], - autorefresh, + {pollingInterval: autorefresh}, ); - - // don't show loader for requests triggered by table sort, only for path change - React.useEffect(() => { - dispatch( - setShardsState({ - wasLoaded: false, - data: undefined, - }), - ); - }, [dispatch, currentSchemaPath, tenantPath, filters]); + const loading = isFetching && result === undefined; + const {result: data} = result ?? {}; const onSort = (newSortOrder?: SortOrder | SortOrder[]) => { // omit information about sort order to disable ASC order, only DESC makes sense for top shards @@ -154,7 +137,7 @@ export const TopShards = ({tenantPath, type}: TopShardsProps) => { setSortOrder(dataTableToStringSortOrder(newSortOrder)); }; - const handleFiltersChange = (value: Partial) => { + const handleFiltersChange = (value: Partial) => { const newStateValue = {...value}; const isDateRangePristine = !storeFilters.from && !storeFilters.to && !value.from && !value.to; @@ -212,11 +195,11 @@ export const TopShards = ({tenantPath, type}: TopShardsProps) => { }; const renderContent = () => { - if (loading && !wasLoaded) { + if (loading) { return renderLoader(); } - if (error && !error.isCancelled) { + if (error && typeof error === 'object' && !(error as any).isCancelled) { return
{prepareQueryError(error)}
; } diff --git a/src/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx b/src/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx index 2067dc33a5..e3a00af7bc 100644 --- a/src/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx +++ b/src/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx @@ -16,7 +16,7 @@ import './ObjectGeneral.scss'; const b = cn('object-general'); interface ObjectGeneralProps { - type: EPathType; + type?: EPathType; tenantName: string; additionalTenantProps?: AdditionalTenantsProps; additionalNodesProps?: AdditionalNodesProps; diff --git a/src/containers/Tenant/Query/Preview/Preview.tsx b/src/containers/Tenant/Query/Preview/Preview.tsx index 949d208ed8..678efd1ed2 100644 --- a/src/containers/Tenant/Query/Preview/Preview.tsx +++ b/src/containers/Tenant/Query/Preview/Preview.tsx @@ -1,16 +1,14 @@ -import React from 'react'; - import {Button, Loader} from '@gravity-ui/uikit'; import EnableFullscreenButton from '../../../../components/EnableFullscreenButton/EnableFullscreenButton'; import Fullscreen from '../../../../components/Fullscreen/Fullscreen'; import {Icon} from '../../../../components/Icon'; import {QueryResultTable} from '../../../../components/QueryResultTable'; -import {sendQuery, setQueryOptions} from '../../../../store/reducers/preview'; +import {previewApi} from '../../../../store/reducers/preview'; import {setShowPreview} from '../../../../store/reducers/schema/schema'; import type {EPathType} from '../../../../types/api/schema'; import {cn} from '../../../../utils/cn'; -import {useAutofetcher, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; +import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import {prepareQueryError} from '../../../../utils/query'; import {isExternalTable, isTableType} from '../../utils/schema'; import i18n from '../i18n'; @@ -27,39 +25,16 @@ interface PreviewProps { export const Preview = ({database, type}: PreviewProps) => { const dispatch = useTypedDispatch(); - const {data = {}, loading, error, wasLoaded} = useTypedSelector((state) => state.preview); const {autorefresh, currentSchemaPath} = useTypedSelector((state) => state.schema); const isFullscreen = useTypedSelector((state) => state.fullscreen); - const sendQueryForPreview = React.useCallback( - (isBackground: boolean) => { - if (!isTableType(type)) { - return; - } - - if (!isBackground) { - dispatch( - setQueryOptions({ - wasLoaded: false, - data: undefined, - }), - ); - } - - const query = `--!syntax_v1\nselect * from \`${currentSchemaPath}\` limit 32`; - - dispatch( - sendQuery({ - query, - database, - action: isExternalTable(type) ? 'execute-query' : 'execute-scan', - }), - ); - }, - [dispatch, database, currentSchemaPath, type], + const query = `--!syntax_v1\nselect * from \`${currentSchemaPath}\` limit 32`; + const {currentData, isFetching, error} = previewApi.useSendQueryQuery( + {database, query, action: isExternalTable(type) ? 'execute-query' : 'execute-scan'}, + {pollingInterval: autorefresh}, ); - - useAutofetcher(sendQueryForPreview, [sendQueryForPreview], autorefresh); + const loading = isFetching && currentData === undefined; + const data = currentData ?? {}; const handleClosePreview = () => { dispatch(setShowPreview(false)); @@ -86,7 +61,7 @@ export const Preview = ({database, type}: PreviewProps) => { ); }; - if (loading && !wasLoaded) { + if (loading) { return (
diff --git a/src/containers/Tenant/Query/Query.tsx b/src/containers/Tenant/Query/Query.tsx index 6d819309b9..828081701b 100644 --- a/src/containers/Tenant/Query/Query.tsx +++ b/src/containers/Tenant/Query/Query.tsx @@ -22,7 +22,7 @@ const b = cn('ydb-query'); interface QueryProps { theme: string; path: string; - type: EPathType; + type?: EPathType; } export const Query = (props: QueryProps) => { diff --git a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx index e65e0116f7..f7fb431eb3 100644 --- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx +++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx @@ -77,7 +77,7 @@ interface QueryEditorProps { executeQuery: ExecuteQueryState; explainQuery: ExplainQueryState; theme: string; - type: EPathType; + type?: EPathType; showPreview: boolean; setShowPreview: (...args: Parameters) => void; saveQueryToHistory: (...args: Parameters) => void; @@ -470,7 +470,7 @@ interface ResultProps { resultVisibilityState: InitialPaneState; onExpandResultHandler: () => void; onCollapseResultHandler: () => void; - type: EPathType; + type?: EPathType; handleAstQuery: () => void; theme: string; resultType: ValueOf | undefined; diff --git a/src/containers/Tenant/Tenant.tsx b/src/containers/Tenant/Tenant.tsx index 34331db07a..e499ba508e 100644 --- a/src/containers/Tenant/Tenant.tsx +++ b/src/containers/Tenant/Tenant.tsx @@ -7,7 +7,7 @@ import {useLocation} from 'react-router'; import {AccessDenied} from '../../components/Errors/403'; import SplitPane from '../../components/SplitPane'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; -import {disableAutorefresh, getSchema} from '../../store/reducers/schema/schema'; +import {getSchema} from '../../store/reducers/schema/schema'; import type {AdditionalNodesProps, AdditionalTenantsProps} from '../../types/additionalProps'; import type {TEvDescribeSchemeResult} from '../../types/api/schema'; import {cn} from '../../utils/cn'; @@ -99,10 +99,6 @@ function Tenant(props: TenantProps) { } }, [currentSchemaPath, dispatch]); - React.useEffect(() => { - dispatch(disableAutorefresh()); - }, [currentSchemaPath, tenantName, dispatch]); - React.useEffect(() => { if (tenantName) { dispatch(setHeaderBreadcrumbs('tenant', {tenantName})); @@ -148,7 +144,6 @@ function Tenant(props: TenantProps) { isCollapsed={summaryVisibilityState.collapsed} /> { const dispatch = useTypedDispatch(); - const {error, loading, wasLoaded, tenants} = useTypedSelector((state) => state.tenants); + const {currentData, isFetching, error} = tenantsApi.useGetTenantsInfoQuery( + {clusterName}, + {pollingInterval: DEFAULT_POLLING_INTERVAL}, + ); + const loading = isFetching && currentData === undefined; + const tenants = currentData ?? []; + const searchValue = useTypedSelector(selectTenantsSearchValue); - const filteredTenants = useTypedSelector(selectFilteredTenants); + const filteredTenants = useTypedSelector((state) => selectFilteredTenants(state, clusterName)); const problemFilter = useTypedSelector(selectProblemFilter); - useAutofetcher( - () => { - dispatch(getTenantsInfo(clusterName)); - }, - [dispatch], - true, - ); - const handleProblemFilterChange = (value: ProblemFilterValue) => { dispatch(changeFilter(value)); }; @@ -83,7 +81,7 @@ export const Tenants = ({additionalTenantsProps}: TenantsProps) => { total={tenants.length} current={filteredTenants?.length || 0} label={'Databases'} - loading={loading && !wasLoaded} + loading={loading} /> ); @@ -167,7 +165,7 @@ export const Tenants = ({additionalTenantsProps}: TenantsProps) => { width: 80, render: ({row}) => { // Don't show values below 0.01 when formatted - if (row.cpu > 10_000) { + if (row.cpu && row.cpu > 10_000) { return formatCPU(row.cpu); } return '—'; @@ -261,7 +259,7 @@ export const Tenants = ({additionalTenantsProps}: TenantsProps) => { return ( {renderControls()} - + {renderTable()} diff --git a/src/containers/VDiskPage/VDiskPage.tsx b/src/containers/VDiskPage/VDiskPage.tsx index 5e9d0a719e..c136bd29c1 100644 --- a/src/containers/VDiskPage/VDiskPage.tsx +++ b/src/containers/VDiskPage/VDiskPage.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {Icon} from '@gravity-ui/uikit'; +import {skipToken} from '@reduxjs/toolkit/query'; import {Helmet} from 'react-helmet-async'; import {StringParam, useQueryParams} from 'use-query-params'; @@ -13,12 +14,13 @@ import {VDiskWithDonorsStack} from '../../components/VDisk/VDiskWithDonorsStack' import {VDiskInfo} from '../../components/VDiskInfo/VDiskInfo'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; import {selectNodesMap} from '../../store/reducers/nodesList'; -import {getVDiskData, setVDiskDataWasNotLoaded} from '../../store/reducers/vdisk/vdisk'; +import {vDiskApi} from '../../store/reducers/vdisk/vdisk'; import {valueIsDefined} from '../../utils'; import {cn} from '../../utils/cn'; +import {DEFAULT_POLLING_INTERVAL} from '../../utils/constants'; import {stringifyVdiskId} from '../../utils/dataFormatters/dataFormatters'; import {getSeverityColor} from '../../utils/disks/helpers'; -import {useAutofetcher, useTypedDispatch, useTypedSelector} from '../../utils/hooks'; +import {useTypedDispatch, useTypedSelector} from '../../utils/hooks'; import {vDiskPageKeyset} from './i18n'; @@ -32,8 +34,6 @@ export function VDiskPage() { const dispatch = useTypedDispatch(); const nodesMap = useTypedSelector(selectNodesMap); - const {vDiskData, groupData, loading, wasLoaded} = useTypedSelector((state) => state.vDisk); - const {NodeHost, NodeId, NodeType, NodeDC, PDiskId, PDiskType, Severity, VDiskId} = vDiskData; const [{nodeId, pDiskId, vDiskSlotId}] = useQueryParams({ nodeId: StringParam, @@ -45,20 +45,16 @@ export function VDiskPage() { dispatch(setHeaderBreadcrumbs('vDisk', {nodeId, pDiskId, vDiskSlotId})); }, [dispatch, nodeId, pDiskId, vDiskSlotId]); - const fetchData = React.useCallback( - async (isBackground?: boolean) => { - if (!isBackground) { - dispatch(setVDiskDataWasNotLoaded()); - } - if (valueIsDefined(nodeId) && valueIsDefined(pDiskId) && valueIsDefined(vDiskSlotId)) { - return dispatch(getVDiskData({nodeId, pDiskId, vDiskSlotId})); - } - return undefined; - }, - [dispatch, nodeId, pDiskId, vDiskSlotId], - ); - - useAutofetcher(fetchData, [fetchData], true); + const params = + valueIsDefined(nodeId) && valueIsDefined(pDiskId) && valueIsDefined(vDiskSlotId) + ? {nodeId, pDiskId, vDiskSlotId} + : skipToken; + const {currentData, isFetching, refetch} = vDiskApi.useGetVDiskDataQuery(params, { + pollingInterval: DEFAULT_POLLING_INTERVAL, + }); + const loading = isFetching && currentData === undefined; + const {vDiskData = {}, groupData} = currentData || {}; + const {NodeHost, NodeId, NodeType, NodeDC, PDiskId, PDiskType, Severity, VDiskId} = vDiskData; const handleEvictVDisk = async () => { const {GroupID, GroupGeneration, Ring, Domain, VDisk} = VDiskId || {}; @@ -83,7 +79,7 @@ export function VDiskPage() { }; const handleAfterEvictVDisk = async () => { - return fetchData(true); + return refetch(); }; const renderHelmet = () => { @@ -112,7 +108,7 @@ export function VDiskPage() { return ( ); @@ -175,7 +171,7 @@ export function VDiskPage() { }; const renderContent = () => { - if (loading && !wasLoaded) { + if (loading) { return ; } diff --git a/src/services/api.ts b/src/services/api.ts index f5e283bc79..5d792120b6 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -64,24 +64,32 @@ export class YdbEmbeddedAPI extends AxiosWrapper { {concurrentId: concurrentId || `getClusterInfo`, requestConfig: {signal}}, ); } - getClusterNodes({concurrentId}: AxiosOptions = {}) { + getClusterNodes({concurrentId, signal}: AxiosOptions = {}) { return this.get( this.getPath('/viewer/json/sysinfo'), {}, - {concurrentId: concurrentId || `getClusterNodes`}, + {concurrentId: concurrentId || `getClusterNodes`, requestConfig: {signal}}, ); } - getNodeInfo(id?: string | number) { - return this.get(this.getPath('/viewer/json/sysinfo?enums=true'), { - node_id: id, - }); + getNodeInfo(id?: string | number, {concurrentId, signal}: AxiosOptions = {}) { + return this.get( + this.getPath('/viewer/json/sysinfo?enums=true'), + { + node_id: id, + }, + {concurrentId, requestConfig: {signal}}, + ); } - getTenants(clusterName?: string) { - return this.get(this.getPath('/viewer/json/tenantinfo'), { - tablets: 1, - storage: 1, - cluster_name: clusterName, - }); + getTenants(clusterName?: string, {concurrentId, signal}: AxiosOptions = {}) { + return this.get( + this.getPath('/viewer/json/tenantinfo'), + { + tablets: 1, + storage: 1, + cluster_name: clusterName, + }, + {concurrentId, requestConfig: {signal}}, + ); } getTenantInfo({path}: {path: string}, {concurrentId, signal}: AxiosOptions = {}) { return this.get( @@ -116,14 +124,14 @@ export class YdbEmbeddedAPI extends AxiosWrapper { /** @deprecated use getNodes instead */ getCompute( {sortOrder, sortValue, ...params}: ComputeApiRequestParams, - {concurrentId}: AxiosOptions = {}, + {concurrentId, signal}: AxiosOptions = {}, ) { const sort = prepareSortValue(sortValue, sortOrder); return this.get( this.getPath('/viewer/json/compute?enums=true'), {sort, ...params}, - {concurrentId}, + {concurrentId, requestConfig: {signal}}, ); } getStorageInfo( @@ -155,45 +163,70 @@ export class YdbEmbeddedAPI extends AxiosWrapper { {concurrentId, requestConfig: {signal}}, ); } - getPdiskInfo(nodeId: string | number, pdiskId: string | number) { - return this.get(this.getPath('/viewer/json/pdiskinfo?enums=true'), { - filter: `(NodeId=${nodeId}${pdiskId ? `;PDiskId=${pdiskId}` : ''})`, - }); + getPDiskInfo( + {nodeId, pDiskId}: {nodeId: string | number; pDiskId: string | number}, + {concurrentId, signal}: AxiosOptions = {}, + ) { + return this.get( + this.getPath('/viewer/json/pdiskinfo?enums=true'), + { + filter: `(NodeId=${nodeId}${pDiskId ? `;PDiskId=${pDiskId}` : ''})`, + }, + {concurrentId, requestConfig: {signal}}, + ); } - getVdiskInfo({ - vDiskSlotId, - pDiskId, - nodeId, - }: { - vDiskSlotId: string | number; - pDiskId: string | number; - nodeId: string | number; - }) { - return this.get(this.getPath('/viewer/json/vdiskinfo?enums=true'), { - node_id: nodeId, - filter: `(PDiskId=${pDiskId};VDiskSlotId=${vDiskSlotId})`, - }); + getVDiskInfo( + { + vDiskSlotId, + pDiskId, + nodeId, + }: { + vDiskSlotId: string | number; + pDiskId: string | number; + nodeId: string | number; + }, + {concurrentId, signal}: AxiosOptions = {}, + ) { + return this.get( + this.getPath('/viewer/json/vdiskinfo?enums=true'), + { + node_id: nodeId, + filter: `(PDiskId=${pDiskId};VDiskSlotId=${vDiskSlotId})`, + }, + {concurrentId, requestConfig: {signal}}, + ); } - getGroupInfo(groupId: string | number) { - return this.get(this.getPath('/viewer/json/storage?enums=true'), { - group_id: groupId, - }); + getGroupInfo(groupId: string | number, {concurrentId, signal}: AxiosOptions = {}) { + return this.get( + this.getPath('/viewer/json/storage?enums=true'), + { + group_id: groupId, + }, + {concurrentId, requestConfig: {signal}}, + ); } - getHostInfo() { + getHostInfo({concurrentId, signal}: AxiosOptions = {}) { return this.get( this.getPath('/viewer/json/sysinfo?node_id=.&enums=true'), - {}, + {concurrentId, requestConfig: {signal}}, ); } - getTabletsInfo({nodes = [], path}: {nodes?: string[]; path?: string}) { + getTabletsInfo( + {nodes = [], path}: {nodes?: string[]; path?: string}, + {concurrentId, signal}: AxiosOptions = {}, + ) { const filter = nodes.length > 0 && `(NodeId=[${nodes.join(',')}])`; - return this.get(this.getPath('/viewer/json/tabletinfo'), { - filter, - path, - enums: true, - }); + return this.get( + this.getPath('/viewer/json/tabletinfo'), + { + filter, + path, + enums: true, + }, + {concurrentId, requestConfig: {signal}}, + ); } - getSchema({path}: {path: string}, {concurrentId}: AxiosOptions = {}) { + getSchema({path}: {path: string}, {concurrentId, signal}: AxiosOptions = {}) { return this.get>( this.getPath('/viewer/json/describe'), { @@ -206,10 +239,10 @@ export class YdbEmbeddedAPI extends AxiosWrapper { partitioning_info: true, subs: 1, }, - {concurrentId: concurrentId || `getSchema|${path}`}, + {concurrentId: concurrentId || `getSchema|${path}`, requestConfig: {signal}}, ); } - getDescribe({path}: {path: string}, {concurrentId}: AxiosOptions = {}) { + getDescribe({path}: {path: string}, {concurrentId, signal}: AxiosOptions = {}) { return this.get>( this.getPath('/viewer/json/describe'), { @@ -218,35 +251,43 @@ export class YdbEmbeddedAPI extends AxiosWrapper { partition_stats: true, subs: 0, }, - {concurrentId: concurrentId || `getDescribe|${path}`}, + {concurrentId: concurrentId || `getDescribe|${path}`, requestConfig: {signal}}, ); } - getSchemaAcl({path}: {path: string}) { + getSchemaAcl({path}: {path: string}, {concurrentId, signal}: AxiosOptions = {}) { return this.get( this.getPath('/viewer/json/acl'), { path, }, - {concurrentId: `getSchemaAcl`}, + {concurrentId: concurrentId || `getSchemaAcl`, requestConfig: {signal}}, ); } - getHeatmapData({path}: {path: string}) { - return this.get>(this.getPath('/viewer/json/describe'), { - path, - enums: true, - backup: false, - children: false, - partition_config: false, - partition_stats: true, - }); + getHeatmapData({path}: {path: string}, {concurrentId, signal}: AxiosOptions = {}) { + return this.get>( + this.getPath('/viewer/json/describe'), + { + path, + enums: true, + backup: false, + children: false, + partition_config: false, + partition_stats: true, + }, + {concurrentId, requestConfig: {signal}}, + ); } - getNetwork(path: string) { - return this.get(this.getPath('/viewer/json/netinfo'), { - enums: true, - path, - }); + getNetwork(path: string, {concurrentId, signal}: AxiosOptions = {}) { + return this.get( + this.getPath('/viewer/json/netinfo'), + { + enums: true, + path, + }, + {concurrentId, requestConfig: {signal}}, + ); } - getTopic({path}: {path?: string}, {concurrentId}: AxiosOptions = {}) { + getTopic({path}: {path?: string}, {concurrentId, signal}: AxiosOptions = {}) { return this.get( this.getPath('/viewer/json/describe_topic'), { @@ -254,12 +295,12 @@ export class YdbEmbeddedAPI extends AxiosWrapper { include_stats: true, path, }, - {concurrentId: concurrentId || 'getTopic'}, + {concurrentId, requestConfig: {signal}}, ); } getConsumer( {path, consumer}: {path: string; consumer: string}, - {concurrentId}: AxiosOptions = {}, + {concurrentId, signal}: AxiosOptions = {}, ) { return this.get( this.getPath('/viewer/json/describe_consumer'), @@ -269,40 +310,68 @@ export class YdbEmbeddedAPI extends AxiosWrapper { path, consumer, }, - {concurrentId: concurrentId || 'getConsumer'}, + {concurrentId: concurrentId || 'getConsumer', requestConfig: {signal}}, ); } - getPoolInfo(poolName: string) { - return this.get(this.getPath('/viewer/json/storage'), { - pool: poolName, - enums: true, - }); + getPoolInfo(poolName: string, {concurrentId, signal}: AxiosOptions = {}) { + return this.get( + this.getPath('/viewer/json/storage'), + { + pool: poolName, + enums: true, + }, + {concurrentId, requestConfig: {signal}}, + ); } - getTablet({id}: {id?: string}) { + getTablet({id}: {id?: string}, {concurrentId, signal}: AxiosOptions = {}) { return this.get( this.getPath(`/viewer/json/tabletinfo?filter=(TabletId=${id})`), { enums: true, }, + { + concurrentId, + requestConfig: {signal}, + }, ); } - getTabletHistory({id}: {id?: string}) { + getTabletHistory({id}: {id?: string}, {concurrentId, signal}: AxiosOptions = {}) { return this.get( this.getPath(`/viewer/json/tabletinfo?filter=(TabletId=${id})`), { enums: true, merge: false, }, + { + concurrentId, + requestConfig: {signal}, + }, ); } - getNodesList() { - return this.get(this.getPath('/viewer/json/nodelist'), {enums: true}); + getNodesList({concurrentId, signal}: AxiosOptions = {}) { + return this.get( + this.getPath('/viewer/json/nodelist'), + { + enums: true, + }, + { + concurrentId, + requestConfig: {signal}, + }, + ); } - getTenantsList() { - return this.get(this.getPath('/viewer/json/tenants'), { - enums: true, - state: 0, - }); + getTenantsList({concurrentId, signal}: AxiosOptions = {}) { + return this.get( + this.getPath('/viewer/json/tenants'), + { + enums: true, + state: 0, + }, + { + concurrentId, + requestConfig: {signal}, + }, + ); } sendQuery( { @@ -376,14 +445,14 @@ export class YdbEmbeddedAPI extends AxiosWrapper { {}, ); } - getHotKeys(path: string, enableSampling: boolean, {concurrentId}: AxiosOptions = {}) { + getHotKeys(path: string, enableSampling: boolean, {concurrentId, signal}: AxiosOptions = {}) { return this.get( this.getPath('/viewer/json/hotkeys'), { path, enable_sampling: enableSampling, }, - {concurrentId: concurrentId || 'getHotKeys'}, + {concurrentId: concurrentId || 'getHotKeys', requestConfig: {signal}}, ); } getHealthcheckInfo(database: string, {concurrentId, signal}: AxiosOptions = {}) { @@ -465,15 +534,19 @@ export class YdbEmbeddedAPI extends AxiosWrapper { {}, ); } - getTabletDescribe(tenantId: TDomainKey) { - return this.get>(this.getPath('/viewer/json/describe'), { - schemeshard_id: tenantId?.SchemeShard, - path_id: tenantId?.PathId, - }); + getTabletDescribe(tenantId: TDomainKey, {concurrentId, signal}: AxiosOptions = {}) { + return this.get>( + this.getPath('/viewer/json/describe'), + { + schemeshard_id: tenantId?.SchemeShard, + path_id: tenantId?.PathId, + }, + {concurrentId, requestConfig: {signal}}, + ); } getChartData( {target, from, until, maxDataPoints, database}: JsonRenderRequestParams, - {concurrentId}: AxiosOptions = {}, + {concurrentId, signal}: AxiosOptions = {}, ) { const requestString = `${target}&from=${from}&until=${until}&maxDataPoints=${maxDataPoints}&format=json`; @@ -486,6 +559,7 @@ export class YdbEmbeddedAPI extends AxiosWrapper { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, + requestConfig: {signal}, }, ); } @@ -533,10 +607,14 @@ export class YdbWebVersionAPI extends YdbEmbeddedAPI { ).then(parseMetaCluster); } - getTenants(clusterName: string) { - return this.get(`${META_BACKEND || ''}/meta/cp_databases`, { - cluster_name: clusterName, - }).then(parseMetaTenants); + getTenants(clusterName: string, {concurrentId, signal}: AxiosOptions = {}) { + return this.get( + `${META_BACKEND || ''}/meta/cp_databases`, + { + cluster_name: clusterName, + }, + {concurrentId, requestConfig: {signal}}, + ).then(parseMetaTenants); } } diff --git a/src/store/configureStore.ts b/src/store/configureStore.ts index 7185ebd7f3..c198aac16a 100644 --- a/src/store/configureStore.ts +++ b/src/store/configureStore.ts @@ -27,10 +27,12 @@ function _configureStore< preloadedState, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ - immutableCheck: {ignoredPaths: ['tooltip.currentHoveredRef']}, + immutableCheck: { + ignoredPaths: ['tooltip.currentHoveredRef'], + }, serializableCheck: { ignoredPaths: ['tooltip.currentHoveredRef', 'api'], - ignoredActions: [UPDATE_REF], + ignoredActions: [UPDATE_REF, 'api/executeQuery/rejected'], }, }).concat(locationMiddleware, ...middleware), }); diff --git a/src/store/reducers/api.ts b/src/store/reducers/api.ts index d29da43430..ebba359785 100644 --- a/src/store/reducers/api.ts +++ b/src/store/reducers/api.ts @@ -9,6 +9,7 @@ export const api = createApi({ */ endpoints: () => ({}), refetchOnMountOrArgChange: true, + invalidationBehavior: 'immediately', tagTypes: ['All'], }); diff --git a/src/store/reducers/cluster/cluster.ts b/src/store/reducers/cluster/cluster.ts index 058c906177..0ab88dd5d3 100644 --- a/src/store/reducers/cluster/cluster.ts +++ b/src/store/reducers/cluster/cluster.ts @@ -91,4 +91,5 @@ export const clusterApi = api.injectEndpoints({ providesTags: ['All'], }), }), + overrideExisting: 'throw', }); diff --git a/src/store/reducers/clusterNodes/clusterNodes.tsx b/src/store/reducers/clusterNodes/clusterNodes.tsx index f11b960cfd..3d3f42281e 100644 --- a/src/store/reducers/clusterNodes/clusterNodes.tsx +++ b/src/store/reducers/clusterNodes/clusterNodes.tsx @@ -27,4 +27,5 @@ export const clusterNodesApi = api.injectEndpoints({ providesTags: ['All'], }), }), + overrideExisting: 'throw', }); diff --git a/src/store/reducers/clusters/clusters.ts b/src/store/reducers/clusters/clusters.ts index 9444ac279a..7e3df18e95 100644 --- a/src/store/reducers/clusters/clusters.ts +++ b/src/store/reducers/clusters/clusters.ts @@ -41,4 +41,5 @@ export const clustersApi = api.injectEndpoints({ providesTags: ['All'], }), }), + overrideExisting: 'throw', }); diff --git a/src/store/reducers/describe.ts b/src/store/reducers/describe.ts index b1c80fee13..d821736b5a 100644 --- a/src/store/reducers/describe.ts +++ b/src/store/reducers/describe.ts @@ -1,144 +1,28 @@ -import type {Reducer} from '@reduxjs/toolkit'; - -import type { - IDescribeAction, - IDescribeData, - IDescribeHandledResponse, - IDescribeState, -} from '../../types/store/describe'; -import {createApiRequest, createRequestActionTypes} from '../utils'; - -export const FETCH_DESCRIBE = createRequestActionTypes('describe', 'FETCH_DESCRIBE'); -const SET_CURRENT_DESCRIBE_PATH = 'describe/SET_CURRENT_DESCRIBE_PATH'; -const SET_DATA_WAS_NOT_LOADED = 'describe/SET_DATA_WAS_NOT_LOADED'; - -const initialState = { - loading: false, - wasLoaded: false, - data: {}, - currentDescribe: undefined, - currentDescribePath: undefined, -}; - -const describe: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_DESCRIBE.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_DESCRIBE.SUCCESS: { - const isCurrentDescribePath = action.data.path === state.currentDescribePath; - const newData = {...state.data, ...action.data.data}; - - if (!isCurrentDescribePath) { - return { - ...state, - data: newData, - }; - } - - return { - ...state, - data: newData, - currentDescribe: action.data.currentDescribe, - loading: false, - wasLoaded: true, - error: undefined, - }; - } - - case FETCH_DESCRIBE.FAILURE: { - if (action.error?.isCancelled) { - return state; - } - - return { - ...state, - error: action.error, - loading: false, - }; - } - case SET_CURRENT_DESCRIBE_PATH: { - return { - ...state, - currentDescribePath: action.data, - }; - } - case SET_DATA_WAS_NOT_LOADED: { - return { - ...state, - wasLoaded: false, - }; - } - default: - return state; - } -}; - -export const setCurrentDescribePath = (path: string) => { - return { - type: SET_CURRENT_DESCRIBE_PATH, - data: path, - } as const; -}; - -export const setDataWasNotLoaded = () => { - return { - type: SET_DATA_WAS_NOT_LOADED, - } as const; -}; - -export function getDescribe({path}: {path: string}) { - const request = window.api.getDescribe({path}); - return createApiRequest({ - request, - actions: FETCH_DESCRIBE, - dataHandler: (data): IDescribeHandledResponse => { - const dataPath = data?.Path; - const currentDescribe: IDescribeData = {}; - const newData: IDescribeData = {}; - - if (dataPath) { - currentDescribe[dataPath] = data; - newData[dataPath] = data; - } - - return { - path: dataPath, - currentDescribe, - data: newData, - }; - }, - }); -} - -export function getDescribeBatched(paths: string[]) { - const requestsArray = paths.map((p) => window.api.getDescribe({path: p})); - - const request = Promise.all(requestsArray); - return createApiRequest({ - request, - actions: FETCH_DESCRIBE, - dataHandler: (data): IDescribeHandledResponse => { - const currentDescribe: IDescribeData = {}; - const newData: IDescribeData = {}; - - data.forEach((dataItem) => { - if (dataItem?.Path) { - newData[dataItem.Path] = dataItem; - currentDescribe[dataItem.Path] = dataItem; +import type {IDescribeData} from '../../types/store/describe'; + +import {api} from './api'; + +export const describeApi = api.injectEndpoints({ + endpoints: (build) => ({ + getDescribe: build.query({ + queryFn: async (paths: string[], {signal}) => { + try { + const response = await Promise.all( + paths.map((p) => window.api.getDescribe({path: p}, {signal})), + ); + const data = response.reduce((acc, item) => { + if (item?.Path) { + acc[item.Path] = item; + } + return acc; + }, {}); + return {data}; + } catch (error) { + return {error}; } - }); - - return { - path: data[0]?.Path, - currentDescribe, - data: newData, - }; - }, - }); -} - -export default describe; + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/executeTopQueries/executeTopQueries.ts b/src/store/reducers/executeTopQueries/executeTopQueries.ts index b8021ce1b9..4351d33fbf 100644 --- a/src/store/reducers/executeTopQueries/executeTopQueries.ts +++ b/src/store/reducers/executeTopQueries/executeTopQueries.ts @@ -1,24 +1,29 @@ -import type {AnyAction, Reducer, ThunkAction} from '@reduxjs/toolkit'; +import {createSlice} from '@reduxjs/toolkit'; +import type {PayloadAction} from '@reduxjs/toolkit'; -import type {RootState} from '../..'; -import type {IQueryResult} from '../../../types/store/query'; +import {HOUR_IN_SECONDS} from '../../../utils/constants'; import {parseQueryAPIExecuteResponse} from '../../../utils/query'; -import {createApiRequest, createRequestActionTypes} from '../../utils'; +import {api} from '../api'; -import type {ITopQueriesAction, ITopQueriesFilters, ITopQueriesState} from './types'; +import type {TopQueriesFilters} from './types'; import {getFiltersConditions} from './utils'; -export const FETCH_TOP_QUERIES = createRequestActionTypes('top-queries', 'FETCH_TOP_QUERIES'); -const SET_TOP_QUERIES_STATE = 'top-queries/SET_TOP_QUERIES_STATE'; -const SET_TOP_QUERIES_FILTERS = 'top-queries/SET_TOP_QUERIES_FILTERS'; +const initialState: TopQueriesFilters = {}; -const initialState = { - loading: false, - wasLoaded: false, - filters: {}, -}; +const slice = createSlice({ + name: 'executeTopQueries', + initialState, + reducers: { + setTopQueriesFilters: (state, action: PayloadAction) => { + return {...state, ...action.payload}; + }, + }, +}); + +export const {setTopQueriesFilters} = slice.actions; +export default slice.reducer; -const getQueryText = (path: string, filters?: ITopQueriesFilters) => { +const getQueryText = (path: string, filters?: TopQueriesFilters) => { const filterConditions = getFiltersConditions(path, filters); return ` SELECT @@ -35,87 +40,39 @@ WHERE ${filterConditions || 'true'} `; }; -const executeTopQueries: Reducer = ( - state = initialState, - action, -) => { - switch (action.type) { - case FETCH_TOP_QUERIES.REQUEST: { - return { - ...state, - loading: true, - error: undefined, - }; - } - case FETCH_TOP_QUERIES.SUCCESS: { - return { - ...state, - data: action.data, - loading: false, - error: undefined, - wasLoaded: true, - }; - } - // 401 Unauthorized error is handled by GenericAPI - case FETCH_TOP_QUERIES.FAILURE: { - return { - ...state, - error: action.error || 'Unauthorized', - loading: false, - }; - } - case SET_TOP_QUERIES_STATE: - return { - ...state, - ...action.data, - }; - case SET_TOP_QUERIES_FILTERS: - return { - ...state, - filters: { - ...state.filters, - ...action.filters, - }, - }; - default: - return state; - } -}; - -type FetchTopQueries = (params: { - database: string; - filters?: ITopQueriesFilters; -}) => ThunkAction, RootState, unknown, AnyAction>; - -export const fetchTopQueries: FetchTopQueries = ({database, filters}) => - createApiRequest({ - request: window.api.sendQuery( - { - schema: 'modern', - query: getQueryText(database, filters), - database, - action: 'execute-scan', +export const topQueriesApi = api.injectEndpoints({ + endpoints: (build) => ({ + getTopQueries: build.query({ + queryFn: async ( + {database, filters}: {database: string; filters?: TopQueriesFilters}, + {signal, dispatch}, + ) => { + try { + const response = await window.api.sendQuery( + { + schema: 'modern', + query: getQueryText(database, filters), + database, + action: 'execute-scan', + }, + {signal}, + ); + const data = parseQueryAPIExecuteResponse(response); + // FIXME: do we really need this? + if (!filters?.from && !filters?.to) { + const intervalEnd = data?.result?.[0]?.IntervalEnd; + if (intervalEnd) { + const to = new Date(intervalEnd).getTime(); + const from = new Date(to - HOUR_IN_SECONDS * 1000).getTime(); + dispatch(setTopQueriesFilters({from, to})); + } + } + return {data}; + } catch (error) { + return {error}; + } }, - { - concurrentId: 'executeTopQueries', - }, - ), - actions: FETCH_TOP_QUERIES, - dataHandler: parseQueryAPIExecuteResponse, - }); - -export function setTopQueriesState(state: Partial) { - return { - type: SET_TOP_QUERIES_STATE, - data: state, - } as const; -} - -export function setTopQueriesFilters(filters: Partial) { - return { - type: SET_TOP_QUERIES_FILTERS, - filters, - } as const; -} - -export default executeTopQueries; + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/executeTopQueries/types.ts b/src/store/reducers/executeTopQueries/types.ts index f0a106b258..c43fac2093 100644 --- a/src/store/reducers/executeTopQueries/types.ts +++ b/src/store/reducers/executeTopQueries/types.ts @@ -1,13 +1,4 @@ -import type {IQueryResult, QueryErrorResponse} from '../../../types/store/query'; -import type {ApiRequestAction} from '../../utils'; - -import type { - FETCH_TOP_QUERIES, - setTopQueriesFilters, - setTopQueriesState, -} from './executeTopQueries'; - -export interface ITopQueriesFilters { +export interface TopQueriesFilters { /** ms from epoch */ from?: number; /** ms from epoch */ @@ -15,19 +6,6 @@ export interface ITopQueriesFilters { text?: string; } -export interface ITopQueriesState { - loading: boolean; - wasLoaded: boolean; - data?: IQueryResult; - error?: QueryErrorResponse; - filters: ITopQueriesFilters; -} - -export type ITopQueriesAction = - | ApiRequestAction - | ReturnType - | ReturnType; - -export interface ITopQueriesRootStateSlice { - executeTopQueries: ITopQueriesState; +export interface TopQueriesRootStateSlice { + executeTopQueries: TopQueriesFilters; } diff --git a/src/store/reducers/executeTopQueries/utils.ts b/src/store/reducers/executeTopQueries/utils.ts index e9b72e4605..147f402092 100644 --- a/src/store/reducers/executeTopQueries/utils.ts +++ b/src/store/reducers/executeTopQueries/utils.ts @@ -1,4 +1,4 @@ -import type {ITopQueriesFilters} from './types'; +import type {TopQueriesFilters} from './types'; const endTimeColumn = 'EndTime'; const intervalEndColumn = 'IntervalEnd'; @@ -9,7 +9,7 @@ const getMaxIntervalSubquery = (path: string) => `( FROM \`${path}/.sys/top_queries_by_cpu_time_one_hour\` )`; -export function getFiltersConditions(path: string, filters?: ITopQueriesFilters) { +export function getFiltersConditions(path: string, filters?: TopQueriesFilters) { const conditions: string[] = []; if (filters?.from && filters?.to && filters.from > filters.to) { diff --git a/src/store/reducers/healthcheckInfo/healthcheckInfo.ts b/src/store/reducers/healthcheckInfo/healthcheckInfo.ts index d982730cf9..aacf026293 100644 --- a/src/store/reducers/healthcheckInfo/healthcheckInfo.ts +++ b/src/store/reducers/healthcheckInfo/healthcheckInfo.ts @@ -20,6 +20,7 @@ export const healthcheckApi = api.injectEndpoints({ providesTags: ['All'], }), }), + overrideExisting: 'throw', }); const mapStatusToPriority: Partial> = { diff --git a/src/store/reducers/heatmap.ts b/src/store/reducers/heatmap.ts index 3b99654067..ab5021b6e0 100644 --- a/src/store/reducers/heatmap.ts +++ b/src/store/reducers/heatmap.ts @@ -1,118 +1,117 @@ -import type {Reducer} from '@reduxjs/toolkit'; +import {createSlice} from '@reduxjs/toolkit'; +import type {PayloadAction} from '@reduxjs/toolkit'; +import type {TEvDescribeSchemeResult} from '../../types/api/schema'; +import type {TEvTabletStateResponse} from '../../types/api/tablet'; import type { - IHeatmapAction, IHeatmapApiRequestParams, + IHeatmapMetricValue, IHeatmapState, IHeatmapTabletData, } from '../../types/store/heatmap'; -import {createApiRequest, createRequestActionTypes} from '../utils'; +import type {Nullable} from '../../utils/typecheckers'; +import type {RootState} from '../defaultStore'; -export const FETCH_HEATMAP = createRequestActionTypes('heatmap', 'FETCH_HEATMAP'); +import {api} from './api'; -const SET_HEATMAP_OPTIONS = 'heatmap/SET_HEATMAP_OPTIONS'; - -export const initialState = { - loading: false, - wasLoaded: false, +export const initialState: IHeatmapState = { currentMetric: undefined, sort: false, heatmap: false, }; -const heatmap: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_HEATMAP.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_HEATMAP.SUCCESS: { - return { - ...state, - ...action.data, - loading: false, - wasLoaded: true, - error: undefined, - }; - } - case FETCH_HEATMAP.FAILURE: { - return { - ...state, - error: action.error, - loading: false, - wasLoaded: false, - }; - } - case SET_HEATMAP_OPTIONS: +const slice = createSlice({ + name: 'heatmap', + initialState, + reducers: { + setHeatmapOptions: (state, action: PayloadAction>) => { return { ...state, - ...action.data, + ...action.payload, }; - default: - return state; - } -}; + }, + }, +}); -export function getTabletsInfo({nodes, path}: IHeatmapApiRequestParams) { - return createApiRequest({ - request: Promise.all([ - window.api.getTabletsInfo({nodes, path}), - window.api.getHeatmapData({path}), - ]), - actions: FETCH_HEATMAP, - dataHandler: ([tabletsData = {}, describe]) => { - const {TabletStateInfo: tablets = []} = tabletsData; - const TabletsMap: Map = new Map(); - const {PathDescription = {}} = describe ?? {}; - const { - TablePartitions = [], - TablePartitionStats = [], - TablePartitionMetrics = [], - } = PathDescription; +export default slice.reducer; - tablets.forEach((item) => { - if (item.TabletId) { - TabletsMap.set(item.TabletId, item); - } - }); +export const {setHeatmapOptions} = slice.actions; + +export const heatmapApi = api.injectEndpoints({ + endpoints: (builder) => ({ + getHeatmapTabletsInfo: builder.query({ + queryFn: async ( + {nodes, path}: IHeatmapApiRequestParams, + {signal, getState, dispatch}, + ) => { + try { + const response = await Promise.all([ + window.api.getTabletsInfo({nodes, path}, {signal}), + window.api.getHeatmapData({path}, {signal}), + ]); + const data = transformResponse(response); - TablePartitions.forEach((item, index) => { - const metrics = Object.assign( - {}, - TablePartitionStats[index], - TablePartitionMetrics[index], - ); - if (item.DatashardId) { - TabletsMap.set(item.DatashardId, { - ...TabletsMap.get(item.DatashardId), - metrics, - }); + if (data.metrics?.length) { + const state = getState() as RootState; + const currentMetric = state.heatmap.currentMetric; + if ( + !currentMetric || + !data.metrics.find((item) => item.value === currentMetric) + ) { + dispatch(setHeatmapOptions({currentMetric: data.metrics[0].value})); + } + } + + return {data}; + } catch (error) { + return {error}; } - }); + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); - const preparedTablets = Array.from(TabletsMap.values()); - const selectMetrics = - preparedTablets[0] && - preparedTablets[0].metrics && - Object.keys(preparedTablets[0].metrics).map((item) => { - return { - value: item, - content: item, - }; - }); +function transformResponse([tabletsData, describe]: [ + TEvTabletStateResponse, + Nullable, +]) { + const {TabletStateInfo: tablets = []} = tabletsData; + const TabletsMap: Map = new Map(); + const {PathDescription = {}} = describe ?? {}; + const { + TablePartitions = [], + TablePartitionStats = [], + TablePartitionMetrics = [], + } = PathDescription; - return {data: preparedTablets, metrics: selectMetrics}; - }, + tablets.forEach((item) => { + if (item.TabletId) { + TabletsMap.set(item.TabletId, item); + } }); -} -export function setHeatmapOptions(options: Partial) { - return { - type: SET_HEATMAP_OPTIONS, - data: options, - } as const; -} + TablePartitions.forEach((item, index) => { + const metrics = Object.assign({}, TablePartitionStats[index], TablePartitionMetrics[index]); + if (item.DatashardId) { + TabletsMap.set(item.DatashardId, { + ...TabletsMap.get(item.DatashardId), + metrics, + }); + } + }); -export default heatmap; + const preparedTablets = Array.from(TabletsMap.values()); + const selectMetrics = + preparedTablets[0] && + preparedTablets[0].metrics && + (Object.keys(preparedTablets[0].metrics).map((item) => { + return { + value: item, + content: item, + }; + }) as {value: IHeatmapMetricValue; content: IHeatmapMetricValue}[]); + + return {tablets: preparedTablets, metrics: selectMetrics}; +} diff --git a/src/store/reducers/index.ts b/src/store/reducers/index.ts index f256d23ed3..cbff8b9adb 100644 --- a/src/store/reducers/index.ts +++ b/src/store/reducers/index.ts @@ -4,7 +4,6 @@ import {api} from './api'; import authentication from './authentication/authentication'; import cluster from './cluster/cluster'; import clusters from './clusters/clusters'; -import describe from './describe'; import executeQuery from './executeQuery'; import executeTopQueries from './executeTopQueries/executeTopQueries'; import explainQuery from './explainQuery'; @@ -13,15 +12,8 @@ import header from './header/header'; import heatmap from './heatmap'; import host from './host'; import hotKeys from './hotKeys/hotKeys'; -import network from './network/network'; -import node from './node/node'; import nodes from './nodes/nodes'; -import nodesList from './nodesList'; -import olapStats from './olapStats'; -import overview from './overview/overview'; import partitions from './partitions/partitions'; -import pDisk from './pdisk/pdisk'; -import preview from './preview'; import saveQuery from './saveQuery'; import schema from './schema/schema'; import schemaAcl from './schemaAcl/schemaAcl'; @@ -29,14 +21,11 @@ import settings from './settings/settings'; import shardsWorkload from './shardsWorkload/shardsWorkload'; import singleClusterMode from './singleClusterMode'; import storage from './storage/storage'; -import tablet from './tablet'; import tablets from './tablets'; import tabletsFilters from './tabletsFilters'; import tenant from './tenant/tenant'; import tenants from './tenants/tenants'; import tooltip from './tooltip'; -import topic from './topic'; -import vDisk from './vdisk/vdisk'; export const rootReducer = { [api.reducerPath]: api.reducer, @@ -45,27 +34,17 @@ export const rootReducer = { cluster, tenant, storage, - node, tooltip, tablets, schema, - overview, - olapStats, host, - network, tenants, - tablet, - topic, partitions, - pDisk, executeQuery, explainQuery, tabletsFilters, heatmap, settings, - preview, - nodesList, - describe, schemaAcl, executeTopQueries, shardsWorkload, @@ -75,7 +54,6 @@ export const rootReducer = { saveQuery, fullscreen, clusters, - vDisk, }; const combinedReducer = combineReducers({ diff --git a/src/store/reducers/network/network.ts b/src/store/reducers/network/network.ts index dc1af970c1..d5ce8ee987 100644 --- a/src/store/reducers/network/network.ts +++ b/src/store/reducers/network/network.ts @@ -1,68 +1,18 @@ -import type {Reducer} from '@reduxjs/toolkit'; - -import {createApiRequest, createRequestActionTypes} from '../../utils'; - -import type {NetworkAction, NetworkState} from './types'; - -export const FETCH_ALL_NODES_NETWORK = createRequestActionTypes( - 'network', - 'FETCH_ALL_NODES_NETWORK', -); - -const SET_DATA_WAS_NOT_LOADED = 'network/SET_DATA_WAS_NOT_LOADED'; - -const initialState = { - loading: false, - wasLoaded: false, -}; - -const network: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_ALL_NODES_NETWORK.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_ALL_NODES_NETWORK.SUCCESS: { - return { - ...state, - data: action.data, - loading: false, - wasLoaded: true, - error: undefined, - }; - } - case FETCH_ALL_NODES_NETWORK.FAILURE: { - return { - ...state, - error: action.error, - loading: false, - }; - } - - case SET_DATA_WAS_NOT_LOADED: { - return { - ...state, - wasLoaded: false, - }; - } - default: - return state; - } -}; - -export const setDataWasNotLoaded = () => { - return { - type: SET_DATA_WAS_NOT_LOADED, - } as const; -}; - -export const getNetworkInfo = (tenant: string) => { - return createApiRequest({ - request: window.api.getNetwork(tenant), - actions: FETCH_ALL_NODES_NETWORK, - }); -}; - -export default network; +import {api} from '../api'; + +export const networkApi = api.injectEndpoints({ + endpoints: (build) => ({ + getNetworkInfo: build.query({ + queryFn: async (tenant: string, {signal}) => { + try { + const data = await window.api.getNetwork(tenant, {signal}); + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/network/types.ts b/src/store/reducers/network/types.ts deleted file mode 100644 index fd2f39a3b7..0000000000 --- a/src/store/reducers/network/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type {IResponseError} from '../../../types/api/error'; -import type {TNetInfo} from '../../../types/api/netInfo'; -import type {ApiRequestAction} from '../../utils'; - -import type {FETCH_ALL_NODES_NETWORK, setDataWasNotLoaded} from './network'; - -export interface NetworkState { - loading: boolean; - wasLoaded: boolean; - data?: TNetInfo; - error?: IResponseError; -} - -export type NetworkAction = - | ApiRequestAction - | ReturnType; diff --git a/src/store/reducers/node/node.ts b/src/store/reducers/node/node.ts index a3ed6383e6..fe596452d1 100644 --- a/src/store/reducers/node/node.ts +++ b/src/store/reducers/node/node.ts @@ -1,103 +1,31 @@ -import type {Reducer} from '@reduxjs/toolkit'; +import {api} from '../api'; -import {createApiRequest, createRequestActionTypes} from '../../utils'; - -import type {NodeAction, NodeState} from './types'; import {prepareNodeData} from './utils'; -export const FETCH_NODE = createRequestActionTypes('node', 'FETCH_NODE'); -export const FETCH_NODE_STRUCTURE = createRequestActionTypes('node', 'FETCH_NODE_STRUCTURE'); - -const RESET_NODE = 'node/RESET_NODE'; - -const initialState = { - data: {}, - loading: true, - wasLoaded: false, - nodeStructure: {}, - loadingStructure: true, - wasLoadedStructure: false, -}; - -const node: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_NODE.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_NODE.SUCCESS: { - return { - ...state, - data: action.data, - loading: false, - wasLoaded: true, - error: undefined, - }; - } - case FETCH_NODE.FAILURE: { - return { - ...state, - error: action.error, - loading: false, - }; - } - case FETCH_NODE_STRUCTURE.REQUEST: { - return { - ...state, - loadingStructure: true, - }; - } - case FETCH_NODE_STRUCTURE.SUCCESS: { - return { - ...state, - nodeStructure: action.data, - loadingStructure: false, - wasLoadedStructure: true, - errorStructure: undefined, - }; - } - case FETCH_NODE_STRUCTURE.FAILURE: { - return { - ...state, - errorStructure: action.error, - loadingStructure: false, - }; - } - case RESET_NODE: { - return { - ...state, - data: {}, - wasLoaded: false, - nodeStructure: {}, - wasLoadedStructure: false, - }; - } - default: - return state; - } -}; - -export const getNodeInfo = (id: string) => { - return createApiRequest({ - request: window.api.getNodeInfo(id), - actions: FETCH_NODE, - dataHandler: prepareNodeData, - }); -}; - -export const getNodeStructure = (nodeId: string) => { - return createApiRequest({ - request: window.api.getStorageInfo({nodeId}, {concurrentId: 'getNodeStructure'}), - actions: FETCH_NODE_STRUCTURE, - }); -}; - -export function resetNode() { - return { - type: RESET_NODE, - } as const; -} - -export default node; +export const nodeApi = api.injectEndpoints({ + endpoints: (build) => ({ + getNodeInfo: build.query({ + queryFn: async ({nodeId}: {nodeId: string}, {signal}) => { + try { + const data = await window.api.getNodeInfo(nodeId, {signal}); + return {data: prepareNodeData(data)}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + getNodeStructure: build.query({ + queryFn: async ({nodeId}: {nodeId: string}, {signal}) => { + try { + const data = await window.api.getStorageInfo({nodeId}, {signal}); + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/node/selectors.ts b/src/store/reducers/node/selectors.ts index 84cfd3e9dc..b4b10ec722 100644 --- a/src/store/reducers/node/selectors.ts +++ b/src/store/reducers/node/selectors.ts @@ -1,22 +1,26 @@ -import type {Selector} from '@reduxjs/toolkit'; import {createSelector} from '@reduxjs/toolkit'; import {stringifyVdiskId} from '../../../utils/dataFormatters/dataFormatters'; import {preparePDiskData} from '../../../utils/disks/prepareDisks'; +import type {RootState} from '../../defaultStore'; -import type { - NodeStateSlice, - PreparedNodeStructure, - PreparedStructureVDisk, - RawNodeStructure, -} from './types'; +import {nodeApi} from './node'; +import type {PreparedNodeStructure, PreparedStructureVDisk, RawNodeStructure} from './types'; -const selectNodeId = (state: NodeStateSlice) => state.node?.data?.NodeId; +const getNodeStructureSelector = createSelector( + (nodeId: string) => nodeId, + (nodeId) => nodeApi.endpoints.getNodeStructure.select({nodeId}), +); -const selectRawNodeStructure = (state: NodeStateSlice) => state.node?.nodeStructure; +const selectGetNodeStructureData = createSelector( + (state: RootState) => state, + (_state: RootState, nodeId: string) => getNodeStructureSelector(nodeId), + (state, selectGetNodeStructure) => selectGetNodeStructure(state).data, +); -export const selectNodeStructure: Selector = createSelector( - [selectNodeId, selectRawNodeStructure], +export const selectNodeStructure = createSelector( + (_state: RootState, nodeId: string) => Number(nodeId), + (state: RootState, nodeId: string) => selectGetNodeStructureData(state, nodeId), (nodeId, storageInfo) => { const pools = storageInfo?.StoragePools; const structure: RawNodeStructure = {}; @@ -43,7 +47,7 @@ export const selectNodeStructure: Selector( + const structureWithVDisksArray = Object.keys(structure).reduce( (preparedStructure, el) => { const vDisks = structure[el].vDisks; const vDisksArray = Object.keys(vDisks).reduce( @@ -58,6 +62,6 @@ export const selectNodeStructure: Selector; @@ -25,24 +20,3 @@ export interface PreparedStructurePDisk extends PreparedPDisk { export type PreparedNodeStructure = Record; export interface PreparedNode extends Partial {} - -export interface NodeState { - data: PreparedNode; - loading: boolean; - wasLoaded: boolean; - error?: IResponseError; - - nodeStructure: TStorageInfo; - loadingStructure: boolean; - wasLoadedStructure: boolean; - errorStructure?: IResponseError; -} - -export type NodeAction = - | ApiRequestAction - | ApiRequestAction - | ReturnType; - -export interface NodeStateSlice { - node: NodeState; -} diff --git a/src/store/reducers/nodes/nodes.ts b/src/store/reducers/nodes/nodes.ts index c6083a8671..58104ceb27 100644 --- a/src/store/reducers/nodes/nodes.ts +++ b/src/store/reducers/nodes/nodes.ts @@ -80,4 +80,5 @@ export const nodesApi = api.injectEndpoints({ providesTags: ['All'], }), }), + overrideExisting: 'throw', }); diff --git a/src/store/reducers/nodesList.ts b/src/store/reducers/nodesList.ts index e721aa630b..a316f187a7 100644 --- a/src/store/reducers/nodesList.ts +++ b/src/store/reducers/nodesList.ts @@ -1,57 +1,30 @@ import {createSelector} from '@reduxjs/toolkit'; -import type {Reducer} from '@reduxjs/toolkit'; -import type { - NodesListAction, - NodesListRootStateSlice, - NodesListState, -} from '../../types/store/nodesList'; import {prepareNodesMap} from '../../utils/nodes'; -import {createApiRequest, createRequestActionTypes} from '../utils'; +import type {RootState} from '../defaultStore'; -export const FETCH_NODES_LIST = createRequestActionTypes('nodesList', 'FETCH_NODES_LIST'); +import {api} from './api'; -const initialState = {loading: true, wasLoaded: false, data: []}; +export const nodesListApi = api.injectEndpoints({ + endpoints: (build) => ({ + getNodesList: build.query({ + queryFn: async (_, {signal}) => { + try { + const data = await window.api.getNodesList({signal}); + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); -const nodesList: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_NODES_LIST.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_NODES_LIST.SUCCESS: { - return { - ...state, - data: action.data, - loading: false, - wasLoaded: true, - error: undefined, - }; - } - case FETCH_NODES_LIST.FAILURE: { - return { - ...state, - error: action.error, - loading: false, - }; - } - default: - return state; - } -}; - -export function getNodesList() { - return createApiRequest({ - request: window.api.getNodesList(), - actions: FETCH_NODES_LIST, - }); -} +const selectNodesList = nodesListApi.endpoints.getNodesList.select(undefined); export const selectNodesMap = createSelector( - (state: NodesListRootStateSlice) => state.nodesList.data, - (nodes) => prepareNodesMap(nodes), + (state: RootState) => selectNodesList(state).data, + (data) => prepareNodesMap(data), ); - -export default nodesList; diff --git a/src/store/reducers/olapStats.ts b/src/store/reducers/olapStats.ts index 6ace287b8a..600b5b4bbe 100644 --- a/src/store/reducers/olapStats.ts +++ b/src/store/reducers/olapStats.ts @@ -1,16 +1,6 @@ -import type {Reducer} from '@reduxjs/toolkit'; - -import type {OlapStatsAction, OlapStatsState} from '../../types/store/olapStats'; import {parseQueryAPIExecuteResponse} from '../../utils/query'; -import {createApiRequest, createRequestActionTypes} from '../utils'; - -export const FETCH_OLAP_STATS = createRequestActionTypes('query', 'SEND_OLAP_STATS_QUERY'); -const RESET_LOADING_STATE = 'olapStats/RESET_LOADING_STATE'; -const initialState = { - loading: false, - wasLoaded: false, -}; +import {api} from './api'; function createOlatStatsQuery(path: string) { return `SELECT * FROM \`${path}/.sys/primary_index_stats\``; @@ -18,59 +8,27 @@ function createOlatStatsQuery(path: string) { const queryAction = 'execute-scan'; -const olapStats: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_OLAP_STATS.REQUEST: { - return { - ...state, - loading: true, - error: undefined, - }; - } - case FETCH_OLAP_STATS.SUCCESS: { - return { - ...state, - data: action.data, - loading: false, - error: undefined, - wasLoaded: true, - }; - } - case FETCH_OLAP_STATS.FAILURE: { - return { - ...state, - error: action.error || 'Unauthorized', - loading: false, - }; - } - case RESET_LOADING_STATE: { - return { - ...state, - wasLoaded: initialState.wasLoaded, - }; - } - default: - return state; - } -}; - -export const getOlapStats = ({path = ''}) => { - return createApiRequest({ - request: window.api.sendQuery({ - schema: 'modern', - query: createOlatStatsQuery(path), - database: path, - action: queryAction, +export const olapApi = api.injectEndpoints({ + endpoints: (build) => ({ + getOlapStats: build.query({ + queryFn: async ({path = ''}: {path?: string} = {}, {signal}) => { + try { + const response = await window.api.sendQuery( + { + schema: 'modern', + query: createOlatStatsQuery(path), + database: path, + action: queryAction, + }, + {signal}, + ); + return {data: parseQueryAPIExecuteResponse(response)}; + } catch (error) { + return {error: error || new Error('Unauthorized')}; + } + }, + providesTags: ['All'], }), - actions: FETCH_OLAP_STATS, - dataHandler: parseQueryAPIExecuteResponse, - }); -}; - -export function resetLoadingState() { - return { - type: RESET_LOADING_STATE, - } as const; -} - -export default olapStats; + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/overview/overview.ts b/src/store/reducers/overview/overview.ts index b04152f5c7..b6bd392059 100644 --- a/src/store/reducers/overview/overview.ts +++ b/src/store/reducers/overview/overview.ts @@ -1,108 +1,18 @@ -import type {Reducer} from '@reduxjs/toolkit'; - -import {createApiRequest, createRequestActionTypes} from '../../utils'; - -import type {OverviewAction, OverviewHandledResponse, OverviewState} from './types'; - -export const FETCH_OVERVIEW = createRequestActionTypes('overview', 'FETCH_OVERVIEW'); -const SET_CURRENT_OVERVIEW_PATH = 'overview/SET_CURRENT_OVERVIEW_PATH'; -const SET_DATA_WAS_NOT_LOADED = 'overview/SET_DATA_WAS_NOT_LOADED'; - -export const initialState = { - loading: true, - wasLoaded: false, -}; - -const schema: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_OVERVIEW.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_OVERVIEW.SUCCESS: { - if (action.data.data?.Path !== state.currentOverviewPath) { - return state; - } - - return { - ...state, - error: undefined, - data: action.data.data, - additionalData: action.data.additionalData, - loading: false, - wasLoaded: true, - }; - } - case FETCH_OVERVIEW.FAILURE: { - if (action.error?.isCancelled) { - return state; - } - - return { - ...state, - error: action.error, - loading: false, - }; - } - case SET_CURRENT_OVERVIEW_PATH: { - return { - ...state, - currentOverviewPath: action.data, - }; - } - case SET_DATA_WAS_NOT_LOADED: { - return { - ...state, - wasLoaded: false, - }; - } - default: - return state; - } -}; - -export function getOverview({path}: {path: string}) { - const request = window.api.getDescribe({path}, {concurrentId: 'getOverview'}); - return createApiRequest({ - request, - actions: FETCH_OVERVIEW, - dataHandler: (data): OverviewHandledResponse => { - return {data}; - }, - }); -} - -export function getOverviewBatched(paths: string[]) { - const requestArray = paths.map((p) => - window.api.getDescribe({path: p}, {concurrentId: `getOverviewBatched|${p}`}), - ); - const request = Promise.all(requestArray); - - return createApiRequest({ - request, - actions: FETCH_OVERVIEW, - dataHandler: ([item, ...rest]): OverviewHandledResponse => { - return { - data: item, - additionalData: rest, - }; - }, - }); -} - -export function setDataWasNotLoaded() { - return { - type: SET_DATA_WAS_NOT_LOADED, - } as const; -} - -export const setCurrentOverviewPath = (path?: string) => { - return { - type: SET_CURRENT_OVERVIEW_PATH, - data: path, - } as const; -}; - -export default schema; +import {api} from '../api'; + +export const overviewApi = api.injectEndpoints({ + endpoints: (build) => ({ + getOverview: build.query({ + queryFn: async (paths: string[], {signal}) => { + try { + const [data, ...additionalData] = await Promise.all( + paths.map((p) => window.api.getDescribe({path: p}, {signal})), + ); + return {data: {data, additionalData}}; + } catch (error) { + return {error}; + } + }, + }), + }), +}); diff --git a/src/store/reducers/overview/types.ts b/src/store/reducers/overview/types.ts deleted file mode 100644 index 5744583013..0000000000 --- a/src/store/reducers/overview/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type {IResponseError} from '../../../types/api/error'; -import type {TEvDescribeSchemeResult} from '../../../types/api/schema'; -import type {Nullable} from '../../../utils/typecheckers'; -import type {ApiRequestAction} from '../../utils'; - -import type {FETCH_OVERVIEW, setCurrentOverviewPath, setDataWasNotLoaded} from './overview'; - -export interface OverviewState { - loading: boolean; - wasLoaded: boolean; - currentOverviewPath?: string; - data?: Nullable; - additionalData?: Nullable[]; - error?: IResponseError; -} - -export interface OverviewHandledResponse { - data: Nullable; - additionalData?: Nullable[]; -} - -export type OverviewAction = - | ApiRequestAction - | ReturnType - | ReturnType; diff --git a/src/store/reducers/partitions/partitions.ts b/src/store/reducers/partitions/partitions.ts index a771495615..fb3ec22a5e 100644 --- a/src/store/reducers/partitions/partitions.ts +++ b/src/store/reducers/partitions/partitions.ts @@ -1,102 +1,56 @@ -import type {Reducer} from '@reduxjs/toolkit'; +import {createSlice} from '@reduxjs/toolkit'; +import type {PayloadAction} from '@reduxjs/toolkit'; -import {createApiRequest, createRequestActionTypes} from '../../utils'; +import {api} from '../api'; -import type {PartitionsAction, PartitionsState} from './types'; +import type {PartitionsState} from './types'; import {prepareConsumerPartitions, prepareTopicPartitions} from './utils'; -export const FETCH_PARTITIONS = createRequestActionTypes('partitions', 'FETCH_PARTITIONS'); - -const SET_SELECTED_CONSUMER = 'partitions/SET_SELECTED_CONSUMER'; -const SET_DATA_WAS_NOT_LOADED = 'partitions/SET_DATA_WAS_NOT_LOADED'; - -const initialState = { - loading: false, - wasLoaded: false, +const initialState: PartitionsState = { selectedConsumer: '', }; -const partitions: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_PARTITIONS.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_PARTITIONS.SUCCESS: { - return { - ...state, - partitions: action.data, - loading: false, - wasLoaded: true, - error: undefined, - }; - } - case FETCH_PARTITIONS.FAILURE: { - if (action.error?.isCancelled) { - return state; - } - - return { - ...state, - error: action.error, - loading: false, - }; - } - case SET_SELECTED_CONSUMER: { - return { - ...state, - selectedConsumer: action.data, - }; - } - case SET_DATA_WAS_NOT_LOADED: { - return { - ...state, - wasLoaded: false, - }; - } - default: - return state; - } -}; - -export const setSelectedConsumer = (value: string) => { - return { - type: SET_SELECTED_CONSUMER, - data: value, - } as const; -}; - -export const setDataWasNotLoaded = () => { - return { - type: SET_DATA_WAS_NOT_LOADED, - } as const; -}; - -export function getPartitions(path: string, consumerName?: string) { - if (consumerName) { - return createApiRequest({ - request: window.api.getConsumer( - {path, consumer: consumerName}, - {concurrentId: 'getPartitions'}, - ), - actions: FETCH_PARTITIONS, - dataHandler: (data) => { - const rawPartitions = data.partitions; - return prepareConsumerPartitions(rawPartitions); - }, - }); - } - - return createApiRequest({ - request: window.api.getTopic({path}, {concurrentId: 'getPartitions'}), - actions: FETCH_PARTITIONS, - dataHandler: (data) => { - const rawPartitions = data.partitions; - return prepareTopicPartitions(rawPartitions); +const slice = createSlice({ + name: 'partitions', + initialState, + reducers: { + setSelectedConsumer: (state, action: PayloadAction) => { + state.selectedConsumer = action.payload; }, - }); -} - -export default partitions; + }, +}); + +export const {setSelectedConsumer} = slice.actions; +export default slice.reducer; + +export const partitionsApi = api.injectEndpoints({ + endpoints: (build) => ({ + getPartitions: build.query({ + queryFn: async ( + {path, consumerName}: {path: string; consumerName?: string}, + {signal}, + ) => { + try { + if (consumerName) { + const response = await window.api.getConsumer( + {path, consumer: consumerName}, + {signal}, + ); + const rawPartitions = response.partitions; + const data = prepareConsumerPartitions(rawPartitions); + return {data}; + } else { + const response = await window.api.getTopic({path}, {signal}); + const rawPartitions = response.partitions; + const data = prepareTopicPartitions(rawPartitions); + return {data}; + } + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/partitions/types.ts b/src/store/reducers/partitions/types.ts index 432e6c3eee..e821c0abe2 100644 --- a/src/store/reducers/partitions/types.ts +++ b/src/store/reducers/partitions/types.ts @@ -1,8 +1,4 @@ -import type {IResponseError} from '../../../types/api/error'; import type {ProcessSpeedStats} from '../../../utils/bytesParsers'; -import type {ApiRequestAction} from '../../utils'; - -import type {FETCH_PARTITIONS, setDataWasNotLoaded, setSelectedConsumer} from './partitions'; // Fields that could be undefined corresponds to partitions without consumers export interface PreparedPartitionData { @@ -34,14 +30,5 @@ export interface PreparedPartitionData { } export interface PartitionsState { - loading: boolean; - wasLoaded: boolean; selectedConsumer: string; - partitions?: PreparedPartitionData[]; - error?: IResponseError; } - -export type PartitionsAction = - | ApiRequestAction - | ReturnType - | ReturnType; diff --git a/src/store/reducers/pdisk/pdisk.ts b/src/store/reducers/pdisk/pdisk.ts index 4efb5a5be7..2ab33a0e20 100644 --- a/src/store/reducers/pdisk/pdisk.ts +++ b/src/store/reducers/pdisk/pdisk.ts @@ -1,117 +1,45 @@ -import type {Reducer} from '@reduxjs/toolkit'; - import {EVersion} from '../../../types/api/storage'; -import {createApiRequest, createRequestActionTypes} from '../../utils'; - -import type {PDiskAction, PDiskState} from './types'; -import {preparePDiksDataResponse, preparePDiskStorageResponse} from './utils'; - -export const FETCH_PDISK = createRequestActionTypes('pdisk', 'FETCH_PDISK'); -export const FETCH_PDISK_GROUPS = createRequestActionTypes('pdisk', 'FETCH_PDISK_GROUPS'); -const SET_PDISK_DATA_WAS_NOT_LOADED = 'pdisk/SET_PDISK_DATA_WAS_NOT_LOADED'; - -const initialState = { - pDiskLoading: false, - pDiskWasLoaded: false, - pDiskData: {}, - groupsLoading: false, - groupsWasLoaded: false, - groupsData: [], -}; - -const pdisk: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_PDISK.REQUEST: { - return { - ...state, - pDiskLoading: true, - }; - } - case FETCH_PDISK.SUCCESS: { - return { - ...state, - pDiskData: action.data, - pDiskLoading: false, - pDiskWasLoaded: true, - pDiskError: undefined, - }; - } - case FETCH_PDISK.FAILURE: { - return { - ...state, - pDiskError: action.error, - pDiskLoading: false, - }; - } - case FETCH_PDISK_GROUPS.REQUEST: { - return { - ...state, - groupsLoading: true, - }; - } - case FETCH_PDISK_GROUPS.SUCCESS: { - return { - ...state, - groupsData: action.data, - groupsLoading: false, - groupsWasLoaded: true, - groupsError: undefined, - }; - } - case FETCH_PDISK_GROUPS.FAILURE: { - return { - ...state, - groupsError: action.error, - groupsLoading: false, - }; - } - case SET_PDISK_DATA_WAS_NOT_LOADED: { - return { - ...state, - pDiskWasLoaded: false, - groupsWasLoaded: false, - }; - } - default: - return state; - } -}; +import {api} from '../api'; -export const setPDiskDataWasNotLoaded = () => { - return { - type: SET_PDISK_DATA_WAS_NOT_LOADED, - } as const; -}; +import {preparePDiskDataResponse, preparePDiskStorageResponse} from './utils'; -export const getPDiskData = ({ - nodeId, - pDiskId, -}: { +interface PDiskParams { nodeId: number | string; pDiskId: number | string; -}) => { - return createApiRequest({ - request: Promise.all([ - window.api.getPdiskInfo(nodeId, pDiskId), - window.api.getNodeInfo(nodeId), - ]), - actions: FETCH_PDISK, - dataHandler: preparePDiksDataResponse, - }); -}; - -export const getPDiskStorage = ({ - nodeId, - pDiskId, -}: { - nodeId: number | string; - pDiskId: number | string; -}) => { - return createApiRequest({ - request: window.api.getStorageInfo({nodeId, version: EVersion.v1}), - actions: FETCH_PDISK_GROUPS, - dataHandler: (data) => preparePDiskStorageResponse(data, pDiskId, nodeId), - }); -}; - -export default pdisk; +} + +export const pDiskApi = api.injectEndpoints({ + endpoints: (build) => ({ + getPdiskInfo: build.query({ + queryFn: async ({nodeId, pDiskId}: PDiskParams, {signal}) => { + try { + const response = await Promise.all([ + window.api.getPDiskInfo({nodeId, pDiskId}, {signal}), + window.api.getNodeInfo(nodeId, {signal}), + ]); + const data = preparePDiskDataResponse(response); + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + getStorageInfo: build.query({ + queryFn: async ({nodeId, pDiskId}: PDiskParams, {signal}) => { + try { + const response = await window.api.getStorageInfo( + {nodeId, version: EVersion.v1}, + {signal}, + ); + const data = preparePDiskStorageResponse(response, pDiskId, nodeId); + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/pdisk/types.ts b/src/store/reducers/pdisk/types.ts index 8fcd50d335..8faa8d36e2 100644 --- a/src/store/reducers/pdisk/types.ts +++ b/src/store/reducers/pdisk/types.ts @@ -1,30 +1,8 @@ -import type {IResponseError} from '../../../types/api/error'; import type {PreparedPDisk} from '../../../utils/disks/types'; -import type {ApiRequestAction} from '../../utils'; -import type {PreparedStorageGroup} from '../storage/types'; -import type {FETCH_PDISK, FETCH_PDISK_GROUPS, setPDiskDataWasNotLoaded} from './pdisk'; - -interface PDiskData extends PreparedPDisk { +export interface PDiskData extends PreparedPDisk { NodeId?: number; NodeHost?: string; NodeType?: string; NodeDC?: string; } - -export interface PDiskState { - pDiskLoading: boolean; - pDiskWasLoaded: boolean; - pDiskData: PDiskData; - pDiskError?: IResponseError; - - groupsLoading: boolean; - groupsWasLoaded: boolean; - groupsData: PreparedStorageGroup[]; - groupsError?: IResponseError; -} - -export type PDiskAction = - | ApiRequestAction - | ApiRequestAction - | ReturnType; diff --git a/src/store/reducers/pdisk/utils.ts b/src/store/reducers/pdisk/utils.ts index 38794e0964..1697a78bc7 100644 --- a/src/store/reducers/pdisk/utils.ts +++ b/src/store/reducers/pdisk/utils.ts @@ -6,10 +6,12 @@ import {prepareNodeSystemState} from '../../../utils/nodes'; import type {PreparedStorageGroup} from '../storage/types'; import {prepareStorageGroupData} from '../storage/utils'; -export function preparePDiksDataResponse([pdiskResponse, nodeResponse]: [ +import type {PDiskData} from './types'; + +export function preparePDiskDataResponse([pdiskResponse, nodeResponse]: [ TEvPDiskStateResponse, TEvSystemStateResponse, -]) { +]): PDiskData { const rawPDisk = pdiskResponse.PDiskStateInfo?.[0]; const preparedPDisk = preparePDiskData(rawPDisk); diff --git a/src/store/reducers/preview.ts b/src/store/reducers/preview.ts index 3a8f0418f3..554718c765 100644 --- a/src/store/reducers/preview.ts +++ b/src/store/reducers/preview.ts @@ -1,57 +1,7 @@ import type {ExecuteActions} from '../../types/api/query'; -import type {IQueryResult, QueryErrorResponse} from '../../types/store/query'; import {parseQueryAPIExecuteResponse} from '../../utils/query'; -import type {ApiRequestAction} from '../utils'; -import {createApiRequest, createRequestActionTypes} from '../utils'; -const SEND_QUERY = createRequestActionTypes('preview', 'SEND_QUERY'); -const SET_QUERY_OPTIONS = 'preview/SET_QUERY_OPTIONS'; - -const initialState = { - loading: false, - wasLoaded: false, -}; - -const preview = ( - state = initialState, - action: - | ApiRequestAction - | ReturnType, -) => { - switch (action.type) { - case SEND_QUERY.REQUEST: { - return { - ...state, - loading: true, - error: undefined, - }; - } - case SEND_QUERY.SUCCESS: { - return { - ...state, - data: action.data, - loading: false, - error: undefined, - wasLoaded: true, - }; - } - // 401 Unauthorized error is handled by GenericAPI - case SEND_QUERY.FAILURE: { - return { - ...state, - error: action.error || 'Unauthorized', - loading: false, - }; - } - case SET_QUERY_OPTIONS: - return { - ...state, - ...action.data, - }; - default: - return state; - } -}; +import {api} from './api'; interface SendQueryParams { query?: string; @@ -59,19 +9,22 @@ interface SendQueryParams { action?: ExecuteActions; } -export const sendQuery = ({query, database, action}: SendQueryParams) => { - return createApiRequest({ - request: window.api.sendQuery({schema: 'modern', query, database, action}), - actions: SEND_QUERY, - dataHandler: parseQueryAPIExecuteResponse, - }); -}; - -export function setQueryOptions(options: any) { - return { - type: SET_QUERY_OPTIONS, - data: options, - } as const; -} - -export default preview; +export const previewApi = api.injectEndpoints({ + endpoints: (build) => ({ + sendQuery: build.query({ + queryFn: async ({query, database, action}: SendQueryParams, {signal}) => { + try { + const response = await window.api.sendQuery( + {schema: 'modern', query, database, action}, + {signal}, + ); + return {data: parseQueryAPIExecuteResponse(response)}; + } catch (error) { + return {error: error || new Error('Unauthorized')}; + } + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/schema/schema.ts b/src/store/reducers/schema/schema.ts index a8fa666e7a..3074845de5 100644 --- a/src/store/reducers/schema/schema.ts +++ b/src/store/reducers/schema/schema.ts @@ -1,8 +1,10 @@ -import type {Reducer, Selector} from '@reduxjs/toolkit'; +import type {Dispatch, Reducer, Selector} from '@reduxjs/toolkit'; import {createSelector} from '@reduxjs/toolkit'; import {isEntityWithMergedImplementation} from '../../../containers/Tenant/utils/schema'; +import {settingsManager} from '../../../services/settings'; import type {EPathType} from '../../../types/api/schema'; +import {AUTO_REFRESH_INTERVAL} from '../../../utils/constants'; import {createApiRequest, createRequestActionTypes} from '../../utils'; import type { @@ -17,16 +19,17 @@ export const FETCH_SCHEMA = createRequestActionTypes('schema', 'FETCH_SCHEMA'); const PRELOAD_SCHEMAS = 'schema/PRELOAD_SCHEMAS'; const SET_SCHEMA = 'schema/SET_SCHEMA'; const SET_SHOW_PREVIEW = 'schema/SET_SHOW_PREVIEW'; -const ENABLE_AUTOREFRESH = 'schema/ENABLE_AUTOREFRESH'; -const DISABLE_AUTOREFRESH = 'schema/DISABLE_AUTOREFRESH'; +export const SET_AUTOREFRESH_INTERVAL = 'schema/SET_AUTOREFRESH_INTERVAL'; const RESET_LOADING_STATE = 'schema/RESET_LOADING_STATE'; +const autoRefreshLS = Number(settingsManager.readUserSettingsValue(AUTO_REFRESH_INTERVAL, 0)); + export const initialState = { loading: true, wasLoaded: false, data: {}, currentSchemaPath: undefined, - autorefresh: false, + autorefresh: isNaN(autoRefreshLS) ? 0 : autoRefreshLS, showPreview: false, }; @@ -88,16 +91,10 @@ const schema: Reducer = (state = initialState, action currentSchemaPath: action.data, }; } - case ENABLE_AUTOREFRESH: { - return { - ...state, - autorefresh: true, - }; - } - case DISABLE_AUTOREFRESH: { + case SET_AUTOREFRESH_INTERVAL: { return { ...state, - autorefresh: false, + autorefresh: action.data, }; } case SET_SHOW_PREVIEW: { @@ -142,15 +139,14 @@ export function setCurrentSchemaPath(currentSchemaPath: string) { data: currentSchemaPath, } as const; } -export function enableAutorefresh() { - return { - type: ENABLE_AUTOREFRESH, - } as const; -} -export function disableAutorefresh() { - return { - type: DISABLE_AUTOREFRESH, - } as const; +export function setAutorefreshInterval(interval: number) { + return (dispatch: Dispatch) => { + settingsManager.setUserSettingsValue(AUTO_REFRESH_INTERVAL, interval); + dispatch({ + type: SET_AUTOREFRESH_INTERVAL, + data: interval, + } as const); + }; } export function setShowPreview(value: boolean) { return { diff --git a/src/store/reducers/schema/types.ts b/src/store/reducers/schema/types.ts index 4439813eed..162f4c5cf2 100644 --- a/src/store/reducers/schema/types.ts +++ b/src/store/reducers/schema/types.ts @@ -4,8 +4,7 @@ import type {ApiRequestAction} from '../../utils'; import type { FETCH_SCHEMA, - disableAutorefresh, - enableAutorefresh, + SET_AUTOREFRESH_INTERVAL, preloadSchemas, resetLoadingState, setCurrentSchemaPath, @@ -20,7 +19,7 @@ export interface SchemaState { data: SchemaData; currentSchema?: TEvDescribeSchemeResult; currentSchemaPath?: string; - autorefresh: boolean; + autorefresh: number; showPreview: boolean; error?: IResponseError; } @@ -41,8 +40,7 @@ export type SchemaAction = | SchemaApiRequestAction | ( | ReturnType - | ReturnType - | ReturnType + | {type: typeof SET_AUTOREFRESH_INTERVAL; data: number} | ReturnType | ReturnType | ReturnType diff --git a/src/store/reducers/shardsWorkload/shardsWorkload.ts b/src/store/reducers/shardsWorkload/shardsWorkload.ts index ce0f44c655..02a498a2bf 100644 --- a/src/store/reducers/shardsWorkload/shardsWorkload.ts +++ b/src/store/reducers/shardsWorkload/shardsWorkload.ts @@ -1,20 +1,13 @@ -import type {Reducer} from '@reduxjs/toolkit'; +import {createSlice} from '@reduxjs/toolkit'; +import type {PayloadAction} from '@reduxjs/toolkit'; import {parseQueryAPIExecuteResponse} from '../../../utils/query'; -import {createApiRequest, createRequestActionTypes} from '../../utils'; +import {api} from '../api'; -import type {IShardsWorkloadAction, IShardsWorkloadFilters, IShardsWorkloadState} from './types'; +import type {ShardsWorkloadFilters} from './types'; import {EShardsWorkloadMode} from './types'; -export const SEND_SHARD_QUERY = createRequestActionTypes('query', 'SEND_SHARD_QUERY'); -const SET_SHARD_STATE = 'query/SET_SHARD_STATE'; -const SET_SHARD_QUERY_FILTERS = 'shardsWorkload/SET_SHARD_QUERY_FILTERS'; - -const initialState = { - loading: false, - wasLoaded: false, - filters: {}, -}; +const initialState: ShardsWorkloadFilters = {}; export interface SortOrder { columnId: string; @@ -25,7 +18,7 @@ function formatSortOrder({columnId, order}: SortOrder) { return `${columnId} ${order}`; } -function getFiltersConditions(filters?: IShardsWorkloadFilters) { +function getFiltersConditions(filters?: ShardsWorkloadFilters) { const conditions: string[] = []; if (filters?.from && filters?.to && filters.from > filters.to) { @@ -48,7 +41,7 @@ function getFiltersConditions(filters?: IShardsWorkloadFilters) { function createShardQueryHistorical( path: string, - filters?: IShardsWorkloadFilters, + filters?: ShardsWorkloadFilters, sortOrder?: SortOrder[], tenantName?: string, ) { @@ -104,104 +97,64 @@ LIMIT 20`; const queryAction = 'execute-scan'; -const shardsWorkload: Reducer = ( - state = initialState, - action, -) => { - switch (action.type) { - case SEND_SHARD_QUERY.REQUEST: { +const slice = createSlice({ + name: 'shardsWorkload', + initialState, + reducers: { + setShardsQueryFilters: (state, action: PayloadAction) => { return { ...state, - loading: true, - error: undefined, + ...action.payload, }; - } - case SEND_SHARD_QUERY.SUCCESS: { - return { - ...state, - data: action.data, - loading: false, - error: undefined, - wasLoaded: true, - }; - } - // 401 Unauthorized error is handled by GenericAPI - case SEND_SHARD_QUERY.FAILURE: { - if (action.error?.isCancelled) { - return state; - } + }, + }, +}); - return { - ...state, - error: action.error || 'Unauthorized', - loading: false, - }; - } - case SET_SHARD_STATE: - return { - ...state, - ...action.data, - }; - case SET_SHARD_QUERY_FILTERS: - return { - ...state, - filters: { - ...state.filters, - ...action.filters, - }, - }; - default: - return state; - } -}; +export const {setShardsQueryFilters} = slice.actions; +export default slice.reducer; interface SendShardQueryParams { database?: string; path?: string; sortOrder?: SortOrder[]; - filters?: IShardsWorkloadFilters; -} - -export const sendShardQuery = ({database, path = '', sortOrder, filters}: SendShardQueryParams) => { - try { - return createApiRequest({ - request: window.api.sendQuery( - { - schema: 'modern', - query: - filters?.mode === EShardsWorkloadMode.Immediate - ? createShardQueryImmediate(path, sortOrder, database) - : createShardQueryHistorical(path, filters, sortOrder, database), - database, - action: queryAction, - }, - { - concurrentId: 'shardsWorkload', - }, - ), - actions: SEND_SHARD_QUERY, - dataHandler: parseQueryAPIExecuteResponse, - }); - } catch (error) { - return { - type: SEND_SHARD_QUERY.FAILURE, - error, - }; - } -}; - -export function setShardsState(options: Partial) { - return { - type: SET_SHARD_STATE, - data: options, - } as const; -} - -export function setShardsQueryFilters(filters: Partial) { - return { - type: SET_SHARD_QUERY_FILTERS, - filters, - } as const; + filters?: ShardsWorkloadFilters; } -export default shardsWorkload; +export const shardApi = api.injectEndpoints({ + endpoints: (build) => ({ + sendShardQuery: build.query({ + queryFn: async ( + {database, path = '', sortOrder, filters}: SendShardQueryParams, + {signal}, + ) => { + try { + const response = await window.api.sendQuery( + { + schema: 'modern', + query: + filters?.mode === EShardsWorkloadMode.Immediate + ? createShardQueryImmediate(path, sortOrder, database) + : createShardQueryHistorical( + path, + filters, + sortOrder, + database, + ), + database, + action: queryAction, + }, + { + signal, + }, + ); + const data = parseQueryAPIExecuteResponse(response); + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/shardsWorkload/types.ts b/src/store/reducers/shardsWorkload/types.ts index 2254e70545..f2e6e5a571 100644 --- a/src/store/reducers/shardsWorkload/types.ts +++ b/src/store/reducers/shardsWorkload/types.ts @@ -1,14 +1,9 @@ -import type {IQueryResult, QueryErrorResponse} from '../../../types/store/query'; -import type {ApiRequestAction} from '../../utils'; - -import type {SEND_SHARD_QUERY, setShardsQueryFilters, setShardsState} from './shardsWorkload'; - export enum EShardsWorkloadMode { Immediate = 'immediate', History = 'history', } -export interface IShardsWorkloadFilters { +export interface ShardsWorkloadFilters { /** ms from epoch */ from?: number; /** ms from epoch */ @@ -16,19 +11,6 @@ export interface IShardsWorkloadFilters { mode?: EShardsWorkloadMode; } -export interface IShardsWorkloadState { - loading: boolean; - wasLoaded: boolean; - data?: IQueryResult; - error?: QueryErrorResponse; - filters: IShardsWorkloadFilters; -} - -export type IShardsWorkloadAction = - | ApiRequestAction - | ReturnType - | ReturnType; - -export interface IShardsWorkloadRootStateSlice { - shardsWorkload: IShardsWorkloadState; +export interface ShardsWorkloadRootStateSlice { + shardsWorkload: ShardsWorkloadFilters; } diff --git a/src/store/reducers/storage/storage.ts b/src/store/reducers/storage/storage.ts index c2eb6bbb33..6c336c7fa6 100644 --- a/src/store/reducers/storage/storage.ts +++ b/src/store/reducers/storage/storage.ts @@ -101,4 +101,5 @@ export const storageApi = api.injectEndpoints({ providesTags: ['All'], }), }), + overrideExisting: 'throw', }); diff --git a/src/store/reducers/tablet.ts b/src/store/reducers/tablet.ts index e3ea18584f..046768fc66 100644 --- a/src/store/reducers/tablet.ts +++ b/src/store/reducers/tablet.ts @@ -1,140 +1,72 @@ -import type {Reducer} from '@reduxjs/toolkit'; - import type {TDomainKey} from '../../types/api/tablet'; -import type { - ITabletAction, - ITabletDescribeHandledResponse, - ITabletHandledResponse, - ITabletPreparedHistoryItem, - ITabletState, -} from '../../types/store/tablet'; +import type {ITabletPreparedHistoryItem} from '../../types/store/tablet'; import {prepareNodesMap} from '../../utils/nodes'; -import {createApiRequest, createRequestActionTypes} from '../utils'; - -export const FETCH_TABLET = createRequestActionTypes('TABLET', 'FETCH_TABLET'); -export const FETCH_TABLET_DESCRIBE = createRequestActionTypes('TABLET', 'FETCH_TABLET_DESCRIBE'); - -const CLEAR_TABLET_DATA = 'tablet/CLEAR_TABLET_DATA'; - -const initialState = { - loading: false, - tenantPath: undefined, -}; - -const tablet: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_TABLET.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_TABLET.SUCCESS: { - const {tabletData, historyData} = action.data; - const {TabletId: id} = tabletData; - return { - ...state, - id, - data: tabletData, - history: historyData, - loading: false, - error: undefined, - }; - } - case FETCH_TABLET.FAILURE: { - return { - ...state, - error: action.error, - loading: false, - }; - } - case FETCH_TABLET_DESCRIBE.SUCCESS: { - const {tenantPath} = action.data; - - return { - ...state, - tenantPath, - error: undefined, - }; - } - case CLEAR_TABLET_DATA: { - return { - ...state, - id: undefined, - tenantPath: undefined, - data: undefined, - history: undefined, - }; - } - default: - return state; - } -}; - -export const getTablet = (id: string) => { - return createApiRequest({ - request: Promise.all([ - window.api.getTablet({id}), - window.api.getTabletHistory({id}), - window.api.getNodesList(), - ]), - actions: FETCH_TABLET, - dataHandler: ([ - tabletResponseData, - historyResponseData, - nodesList, - ]): ITabletHandledResponse => { - const nodesMap = prepareNodesMap(nodesList); - - const historyData = Object.keys(historyResponseData).reduce< - ITabletPreparedHistoryItem[] - >((list, nodeId) => { - const tabletInfo = historyResponseData[nodeId]?.TabletStateInfo; - if (tabletInfo && tabletInfo.length) { - const leaderTablet = tabletInfo.find((t) => t.Leader) || tabletInfo[0]; - - const {ChangeTime, Generation, State, Leader, FollowerId} = leaderTablet; - const fqdn = nodesMap && nodeId ? nodesMap.get(Number(nodeId)) : undefined; - - list.push({ - nodeId, - generation: Generation, - changeTime: ChangeTime, - state: State, - leader: Leader, - followerId: FollowerId, - fqdn, - }); +import {api} from './api'; + +export const tabletApi = api.injectEndpoints({ + endpoints: (build) => ({ + getTablet: build.query({ + queryFn: async ({id}: {id: string}, {signal}) => { + try { + const [tabletResponseData, historyResponseData, nodesList] = await Promise.all([ + window.api.getTablet({id}, {signal}), + window.api.getTabletHistory({id}, {signal}), + window.api.getNodesList({signal}), + ]); + const nodesMap = prepareNodesMap(nodesList); + + const historyData = Object.keys(historyResponseData).reduce< + ITabletPreparedHistoryItem[] + >((list, nodeId) => { + const tabletInfo = historyResponseData[nodeId]?.TabletStateInfo; + if (tabletInfo && tabletInfo.length) { + const leaderTablet = tabletInfo.find((t) => t.Leader) || tabletInfo[0]; + + const {ChangeTime, Generation, State, Leader, FollowerId} = + leaderTablet; + + const fqdn = + nodesMap && nodeId ? nodesMap.get(Number(nodeId)) : undefined; + + list.push({ + nodeId, + generation: Generation, + changeTime: ChangeTime, + state: State, + leader: Leader, + followerId: FollowerId, + fqdn, + }); + } + return list; + }, []); + + const {TabletStateInfo = []} = tabletResponseData; + const [tabletData = {}] = TabletStateInfo; + const {TabletId} = tabletData; + + return {data: {id: TabletId, data: tabletData, history: historyData}}; + } catch (error) { + return {error}; } - return list; - }, []); - - const {TabletStateInfo = []} = tabletResponseData; - const [tabletData = {}] = TabletStateInfo; - - return {tabletData, historyData}; - }, - }); -}; - -export const getTabletDescribe = (tenantId: TDomainKey = {}) => { - return createApiRequest({ - request: window.api.getTabletDescribe(tenantId), - actions: FETCH_TABLET_DESCRIBE, - dataHandler: (tabletDescribe): ITabletDescribeHandledResponse => { - const {SchemeShard, PathId} = tenantId; - const tenantPath = tabletDescribe?.Path || `${SchemeShard}:${PathId}`; - - return {tenantPath}; - }, - }); -}; - -export const clearTabletData = () => { - return { - type: CLEAR_TABLET_DATA, - } as const; -}; - -export default tablet; + }, + providesTags: ['All'], + }), + getTabletDescribe: build.query({ + queryFn: async ({tenantId}: {tenantId: TDomainKey}, {signal}) => { + try { + const tabletDescribe = await window.api.getTabletDescribe(tenantId, {signal}); + const {SchemeShard, PathId} = tenantId; + const tenantPath = tabletDescribe?.Path || `${SchemeShard}:${PathId}`; + + return {data: tenantPath}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/tablets.ts b/src/store/reducers/tablets.ts index 00cb758eb8..f77e1e07f6 100644 --- a/src/store/reducers/tablets.ts +++ b/src/store/reducers/tablets.ts @@ -1,98 +1,45 @@ -import type {Reducer} from '@reduxjs/toolkit'; +import {createSlice} from '@reduxjs/toolkit'; +import type {PayloadAction} from '@reduxjs/toolkit'; import type {ETabletState, EType} from '../../types/api/tablet'; -import type { - ITabletsAction, - ITabletsApiRequestParams, - ITabletsState, -} from '../../types/store/tablets'; -import {createApiRequest, createRequestActionTypes} from '../utils'; +import type {TabletsApiRequestParams, TabletsState} from '../../types/store/tablets'; -export const FETCH_TABLETS = createRequestActionTypes('tablets', 'FETCH_TABLETS'); +import {api} from './api'; -const CLEAR_WAS_LOADING_TABLETS = 'tablets/CLEAR_WAS_LOADING_TABLETS'; -const SET_STATE_FILTER = 'tablets/SET_STATE_FILTER'; -const SET_TYPE_FILTER = 'tablets/SET_TYPE_FILTER'; - -const initialState = { - loading: true, - wasLoaded: false, +const initialState: TabletsState = { stateFilter: [], typeFilter: [], }; -const tablets: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_TABLETS.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_TABLETS.SUCCESS: { - return { - ...state, - data: action.data, - loading: false, - error: undefined, - wasLoaded: true, - }; - } - case FETCH_TABLETS.FAILURE: { - return { - ...state, - error: action.error, - loading: false, - }; - } - case CLEAR_WAS_LOADING_TABLETS: { - return { - ...state, - wasLoaded: false, - loading: true, - }; - } - case SET_STATE_FILTER: { - return { - ...state, - stateFilter: action.data, - }; - } - case SET_TYPE_FILTER: { - return { - ...state, - typeFilter: action.data, - }; - } - default: - return state; - } -}; - -export const setStateFilter = (stateFilter: ETabletState[]) => { - return { - type: SET_STATE_FILTER, - data: stateFilter, - } as const; -}; - -export const setTypeFilter = (typeFilter: EType[]) => { - return { - type: SET_TYPE_FILTER, - data: typeFilter, - } as const; -}; - -export const clearWasLoadingFlag = () => - ({ - type: CLEAR_WAS_LOADING_TABLETS, - }) as const; - -export function getTabletsInfo(data: ITabletsApiRequestParams) { - return createApiRequest({ - request: window.api.getTabletsInfo(data), - actions: FETCH_TABLETS, - }); -} - -export default tablets; +const slice = createSlice({ + name: 'tablets', + initialState, + reducers: { + setStateFilter: (state, action: PayloadAction) => { + state.stateFilter = action.payload; + }, + setTypeFilter: (state, action: PayloadAction) => { + state.typeFilter = action.payload; + }, + }, +}); + +export const {setStateFilter, setTypeFilter} = slice.actions; +export default slice.reducer; + +export const tabletsApi = api.injectEndpoints({ + endpoints: (build) => ({ + getTabletsInfo: build.query({ + queryFn: async (params: TabletsApiRequestParams, {signal}) => { + try { + const data = await window.api.getTabletsInfo(params, {signal}); + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/tenant/tenant.ts b/src/store/reducers/tenant/tenant.ts index 5d465e818d..494ed9a97d 100644 --- a/src/store/reducers/tenant/tenant.ts +++ b/src/store/reducers/tenant/tenant.ts @@ -52,4 +52,5 @@ export const tenantApi = api.injectEndpoints({ providesTags: ['All'], }), }), + overrideExisting: 'throw', }); diff --git a/src/store/reducers/tenantOverview/executeTopTables/executeTopTables.ts b/src/store/reducers/tenantOverview/executeTopTables/executeTopTables.ts index 8624970bec..efad5982ac 100644 --- a/src/store/reducers/tenantOverview/executeTopTables/executeTopTables.ts +++ b/src/store/reducers/tenantOverview/executeTopTables/executeTopTables.ts @@ -35,4 +35,5 @@ export const topTablesApi = api.injectEndpoints({ providesTags: ['All'], }), }), + overrideExisting: 'throw', }); diff --git a/src/store/reducers/tenantOverview/topNodes/topNodes.ts b/src/store/reducers/tenantOverview/topNodes/topNodes.ts index 036327f6ca..73c099ca7e 100644 --- a/src/store/reducers/tenantOverview/topNodes/topNodes.ts +++ b/src/store/reducers/tenantOverview/topNodes/topNodes.ts @@ -26,4 +26,5 @@ export const topNodesApi = api.injectEndpoints({ providesTags: ['All'], }), }), + overrideExisting: 'throw', }); diff --git a/src/store/reducers/tenantOverview/topQueries/tenantOverviewTopQueries.ts b/src/store/reducers/tenantOverview/topQueries/tenantOverviewTopQueries.ts index faa029d03f..6dade227cb 100644 --- a/src/store/reducers/tenantOverview/topQueries/tenantOverviewTopQueries.ts +++ b/src/store/reducers/tenantOverview/topQueries/tenantOverviewTopQueries.ts @@ -15,7 +15,7 @@ LIMIT ${TENANT_OVERVIEW_TABLES_LIMIT} export const topQueriesApi = api.injectEndpoints({ endpoints: (builder) => ({ - getTopQueries: builder.query({ + getOverviewTopQueries: builder.query({ queryFn: async ({database}: {database: string}, {signal}) => { try { const data = await window.api.sendQuery( @@ -35,4 +35,5 @@ export const topQueriesApi = api.injectEndpoints({ providesTags: ['All'], }), }), + overrideExisting: 'throw', }); diff --git a/src/store/reducers/tenantOverview/topShards/tenantOverviewTopShards.ts b/src/store/reducers/tenantOverview/topShards/tenantOverviewTopShards.ts index 78ce8240ea..5569c6f168 100644 --- a/src/store/reducers/tenantOverview/topShards/tenantOverviewTopShards.ts +++ b/src/store/reducers/tenantOverview/topShards/tenantOverviewTopShards.ts @@ -43,4 +43,5 @@ export const topShardsApi = api.injectEndpoints({ providesTags: ['All'], }), }), + overrideExisting: 'throw', }); diff --git a/src/store/reducers/tenantOverview/topStorageGroups/topStorageGroups.ts b/src/store/reducers/tenantOverview/topStorageGroups/topStorageGroups.ts index 2205cc6ad4..10d7d9b5cd 100644 --- a/src/store/reducers/tenantOverview/topStorageGroups/topStorageGroups.ts +++ b/src/store/reducers/tenantOverview/topStorageGroups/topStorageGroups.ts @@ -29,4 +29,5 @@ export const topStorageGroupsApi = api.injectEndpoints({ providesTags: ['All'], }), }), + overrideExisting: 'throw', }); diff --git a/src/store/reducers/tenants/selectors.ts b/src/store/reducers/tenants/selectors.ts index a1e2c60917..ecd09d1bd3 100644 --- a/src/store/reducers/tenants/selectors.ts +++ b/src/store/reducers/tenants/selectors.ts @@ -1,4 +1,3 @@ -import type {Selector} from '@reduxjs/toolkit'; import {createSelector} from '@reduxjs/toolkit'; import escapeRegExp from 'lodash/escapeRegExp'; @@ -7,6 +6,7 @@ import {EFlag} from '../../../types/api/enums'; import {ProblemFilterValues, selectProblemFilter} from '../settings/settings'; import type {ProblemFilterValue} from '../settings/types'; +import {tenantsApi} from './tenants'; import type {PreparedTenant, TenantsStateSlice} from './types'; // ==== Filters ==== @@ -29,13 +29,22 @@ const filteredTenantsBySearch = (tenants: PreparedTenant[], searchQuery: string) }; // ==== Simple selectors ==== +const createGetTenantsInfoSelector = createSelector( + (clusterName: string | undefined) => clusterName, + (clusterName) => tenantsApi.endpoints.getTenantsInfo.select({clusterName}), +); -export const selectTenants = (state: TenantsStateSlice) => state.tenants.tenants; +export const selectTenants = createSelector( + (state: RootState) => state, + (_state: RootState, clusterName: string | undefined) => + createGetTenantsInfoSelector(clusterName), + (state: RootState, selectTenantsInfo) => selectTenantsInfo(state).data ?? [], +); export const selectTenantsSearchValue = (state: TenantsStateSlice) => state.tenants.searchValue; // ==== Complex selectors ==== -export const selectFilteredTenants: Selector = createSelector( +export const selectFilteredTenants = createSelector( [selectTenants, selectProblemFilter, selectTenantsSearchValue], (tenants, problemFilter, searchQuery) => { let result = filterTenantsByProblems(tenants, problemFilter); diff --git a/src/store/reducers/tenants/tenants.ts b/src/store/reducers/tenants/tenants.ts index cc017dae45..9cb363a148 100644 --- a/src/store/reducers/tenants/tenants.ts +++ b/src/store/reducers/tenants/tenants.ts @@ -1,72 +1,47 @@ -import type {Reducer} from '@reduxjs/toolkit'; +import {createSlice} from '@reduxjs/toolkit'; +import type {PayloadAction} from '@reduxjs/toolkit'; -import {createApiRequest, createRequestActionTypes} from '../../utils'; +import type {RootState} from '../../defaultStore'; +import {api} from '../api'; -import type {TenantsAction, TenantsState} from './types'; +import type {PreparedTenant, TenantsState} from './types'; import {prepareTenants} from './utils'; -export const FETCH_TENANTS = createRequestActionTypes('tenants', 'FETCH_TENANTS'); +const initialState: TenantsState = {searchValue: ''}; -const SET_SEARCH_VALUE = 'tenants/SET_SEARCH_VALUE'; - -const initialState = {loading: true, wasLoaded: false, searchValue: '', tenants: []}; - -const tenants: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_TENANTS.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_TENANTS.SUCCESS: { - return { - ...state, - tenants: action.data, - loading: false, - wasLoaded: true, - error: undefined, - }; - } - case FETCH_TENANTS.FAILURE: { - return { - ...state, - error: action.error, - loading: false, - }; - } - case SET_SEARCH_VALUE: { - return { - ...state, - searchValue: action.data, - }; - } - default: - return state; - } -}; - -export function getTenantsInfo(clusterName?: string) { - return createApiRequest({ - request: window.api.getTenants(clusterName), - actions: FETCH_TENANTS, - dataHandler: (response, getState) => { - const {singleClusterMode} = getState(); - - if (!response.TenantInfo) { - return []; - } - - return prepareTenants(response.TenantInfo, singleClusterMode); +const slice = createSlice({ + name: 'tenants', + initialState, + reducers: { + setSearchValue: (state, action: PayloadAction) => { + state.searchValue = action.payload; }, - }); -} - -export const setSearchValue = (value: string) => { - return { - type: SET_SEARCH_VALUE, - data: value, - } as const; -}; - -export default tenants; + }, +}); + +export const {setSearchValue} = slice.actions; +export default slice.reducer; + +export const tenantsApi = api.injectEndpoints({ + endpoints: (build) => ({ + getTenantsInfo: build.query({ + queryFn: async ({clusterName}: {clusterName?: string}, {signal, getState}) => { + try { + const response = await window.api.getTenants(clusterName, {signal}); + let data: PreparedTenant[]; + if (Array.isArray(response.TenantInfo)) { + const {singleClusterMode} = getState() as RootState; + data = prepareTenants(response.TenantInfo, singleClusterMode); + } else { + data = []; + } + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/tenants/types.ts b/src/store/reducers/tenants/types.ts index 36d885ebb8..81d665d1a3 100644 --- a/src/store/reducers/tenants/types.ts +++ b/src/store/reducers/tenants/types.ts @@ -1,34 +1,23 @@ -import type {IResponseError} from '../../../types/api/error'; import type {TTenant} from '../../../types/api/tenant'; import type {ValueOf} from '../../../types/common'; -import type {ApiRequestAction} from '../../utils'; import type {METRIC_STATUS} from './contants'; -import type {FETCH_TENANTS, setSearchValue} from './tenants'; export interface PreparedTenant extends TTenant { backend: string | undefined; sharedTenantName: string | undefined; controlPlaneName: string; - cpu: number; - memory: number; - storage: number; + cpu: number | undefined; + memory: number | undefined; + storage: number | undefined; nodesCount: number; groupsCount: number; } export interface TenantsState { - loading: boolean; - wasLoaded: boolean; searchValue: string; - tenants: PreparedTenant[]; - error?: IResponseError; } -export type TenantsAction = - | ApiRequestAction - | ReturnType; - export interface TenantsStateSlice { tenants: TenantsState; } diff --git a/src/store/reducers/tenants/utils.ts b/src/store/reducers/tenants/utils.ts index 7990f5881f..9a91770655 100644 --- a/src/store/reducers/tenants/utils.ts +++ b/src/store/reducers/tenants/utils.ts @@ -6,6 +6,7 @@ import {formatCPUWithLabel} from '../../../utils/dataFormatters/dataFormatters'; import {isNumeric} from '../../../utils/utils'; import {METRIC_STATUS} from './contants'; +import type {PreparedTenant} from './types'; const getControlPlaneValue = (tenant: TTenant) => { const parts = tenant.Name?.split('/'); @@ -174,7 +175,7 @@ const calculateTenantEntities = (tenant: TTenant) => { return {nodesCount, groupsCount}; }; -export const prepareTenants = (tenants: TTenant[], useNodeAsBackend: boolean) => { +export const prepareTenants = (tenants: TTenant[], useNodeAsBackend: boolean): PreparedTenant[] => { return tenants.map((tenant) => { const backend = useNodeAsBackend ? getTenantBackend(tenant) : undefined; const sharedTenantName = tenants.find((item) => item.Id === tenant.ResourceId)?.Name; diff --git a/src/store/reducers/topic.ts b/src/store/reducers/topic.ts index ef9eacbd29..9e8ea48976 100644 --- a/src/store/reducers/topic.ts +++ b/src/store/reducers/topic.ts @@ -1,112 +1,56 @@ /* eslint-disable camelcase */ -import type {Reducer, Selector} from '@reduxjs/toolkit'; import {createSelector} from '@reduxjs/toolkit'; -import type { - IPreparedConsumerData, - IPreparedTopicStats, - ITopicAction, - ITopicRootStateSlice, - ITopicState, -} from '../../types/store/topic'; import {convertBytesObjectToSpeed} from '../../utils/bytesParsers'; import {parseLag, parseTimestampToIdleTime} from '../../utils/timeParsers'; -import {createApiRequest, createRequestActionTypes} from '../utils'; - -export const FETCH_TOPIC = createRequestActionTypes('topic', 'FETCH_TOPIC'); - -const SET_DATA_WAS_NOT_LOADED = 'topic/SET_DATA_WAS_NOT_LOADED'; -const CLEAN_TOPIC_DATA = 'topic/CLEAN_TOPIC_DATA'; - -const initialState = { - loading: true, - wasLoaded: false, - data: undefined, -}; - -const topic: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_TOPIC.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_TOPIC.SUCCESS: { - // On older version it can return HTML page of Developer UI with an error - if (typeof action.data !== 'object') { - return {...state, loading: false, error: {}}; - } - - return { - ...state, - data: action.data, - loading: false, - wasLoaded: true, - error: undefined, - }; - } - case FETCH_TOPIC.FAILURE: { - if (action.error?.isCancelled) { - return state; - } - - return { - ...state, - error: action.error, - loading: false, - }; - } - case SET_DATA_WAS_NOT_LOADED: { - return { - ...state, - wasLoaded: false, - }; - } - case CLEAN_TOPIC_DATA: { - return { - ...state, - data: undefined, - }; - } - default: - return state; - } -}; - -export const setDataWasNotLoaded = () => { - return { - type: SET_DATA_WAS_NOT_LOADED, - } as const; -}; - -export const cleanTopicData = () => { - return { - type: CLEAN_TOPIC_DATA, - } as const; -}; - -export function getTopic(path?: string) { - return createApiRequest({ - request: window.api.getTopic({path}), - actions: FETCH_TOPIC, - }); -} - -const selectTopicStats = (state: ITopicRootStateSlice) => state.topic.data?.topic_stats; -const selectConsumers = (state: ITopicRootStateSlice) => state.topic.data?.consumers; +import type {RootState} from '../defaultStore'; + +import {api} from './api'; + +export const topicApi = api.injectEndpoints({ + endpoints: (build) => ({ + getTopic: build.query({ + queryFn: async (params: {path?: string}) => { + try { + const data = await window.api.getTopic(params); + // On older version it can return HTML page of Developer UI with an error + if (typeof data !== 'object') { + return {error: {}}; + } + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); -export const selectConsumersNames: Selector = - createSelector([selectConsumers], (consumers) => { - return consumers - ?.map((consumer) => consumer?.name) - .filter((consumer): consumer is string => consumer !== undefined); - }); +const createGetTopicSelector = createSelector( + (path?: string) => path, + (path) => topicApi.endpoints.getTopic.select({path}), +); + +const selectTopicStats = createSelector( + (state: RootState) => state, + (_state: RootState, path?: string) => createGetTopicSelector(path), + (state, selectGetTopic) => selectGetTopic(state).data?.topic_stats, +); +const selectConsumers = createSelector( + (state: RootState) => state, + (_state: RootState, path?: string) => createGetTopicSelector(path), + (state, selectGetTopic) => selectGetTopic(state).data?.consumers, +); + +export const selectConsumersNames = createSelector(selectConsumers, (consumers) => { + return consumers + ?.map((consumer) => consumer?.name) + .filter((consumer): consumer is string => consumer !== undefined); +}); -export const selectPreparedTopicStats: Selector< - ITopicRootStateSlice, - IPreparedTopicStats | undefined -> = createSelector([selectTopicStats], (rawTopicStats) => { +export const selectPreparedTopicStats = createSelector(selectTopicStats, (rawTopicStats) => { if (!rawTopicStats) { return undefined; } @@ -126,10 +70,7 @@ export const selectPreparedTopicStats: Selector< }; }); -export const selectPreparedConsumersData: Selector< - ITopicRootStateSlice, - IPreparedConsumerData[] | undefined -> = createSelector([selectConsumers], (consumers) => { +export const selectPreparedConsumersData = createSelector(selectConsumers, (consumers) => { return consumers?.map((consumer) => { const {name, consumer_stats} = consumer || {}; @@ -146,5 +87,3 @@ export const selectPreparedConsumersData: Selector< }; }); }); - -export default topic; diff --git a/src/store/reducers/vdisk/types.ts b/src/store/reducers/vdisk/types.ts index 729fb5a610..91829954f3 100644 --- a/src/store/reducers/vdisk/types.ts +++ b/src/store/reducers/vdisk/types.ts @@ -1,10 +1,6 @@ -import type {IResponseError} from '../../../types/api/error'; import type {PreparedVDisk} from '../../../utils/disks/types'; -import type {ApiRequestAction} from '../../utils'; import type {PreparedStorageGroup} from '../storage/types'; -import type {FETCH_VDISK, setVDiskDataWasNotLoaded} from './vdisk'; - export type VDiskGroup = Partial; export interface VDiskData extends PreparedVDisk { @@ -16,20 +12,3 @@ export interface VDiskData extends PreparedVDisk { PDiskId?: number; PDiskType?: string; } - -export interface VDiskState { - loading: boolean; - wasLoaded: boolean; - error?: IResponseError; - - vDiskData: VDiskData; - groupData?: VDiskGroup; -} - -export type VDiskAction = - | ApiRequestAction< - typeof FETCH_VDISK, - {vDiskData: VDiskData; groupData?: VDiskGroup}, - IResponseError - > - | ReturnType; diff --git a/src/store/reducers/vdisk/vdisk.ts b/src/store/reducers/vdisk/vdisk.ts index 4ca9bd0caa..159bf3ea36 100644 --- a/src/store/reducers/vdisk/vdisk.ts +++ b/src/store/reducers/vdisk/vdisk.ts @@ -1,76 +1,45 @@ -import type {Reducer} from '@reduxjs/toolkit'; - import {EVersion} from '../../../types/api/storage'; import {valueIsDefined} from '../../../utils'; -import {createApiRequest, createRequestActionTypes} from '../../utils'; +import {api} from '../api'; -import type {VDiskAction, VDiskGroup, VDiskState} from './types'; +import type {VDiskGroup} from './types'; import {prepareVDiskDataResponse, prepareVDiskGroupResponse} from './utils'; -export const FETCH_VDISK = createRequestActionTypes('vdisk', 'FETCH_VDISK'); -const SET_VDISK_DATA_WAS_NOT_LOADED = 'vdisk/SET_VDISK_DATA_WAS_NOT_LOADED'; - -const initialState = { - loading: false, - wasLoaded: false, - vDiskData: {}, -}; - -const vdisk: Reducer = (state = initialState, action) => { - switch (action.type) { - case FETCH_VDISK.REQUEST: { - return { - ...state, - loading: true, - }; - } - case FETCH_VDISK.SUCCESS: { - const {vDiskData, groupData} = action.data; - - return { - ...state, - vDiskData, - groupData, - loading: false, - wasLoaded: true, - error: undefined, - }; - } - case FETCH_VDISK.FAILURE: { - return { - ...state, - error: action.error, - loading: false, - }; - } - case SET_VDISK_DATA_WAS_NOT_LOADED: { - return { - ...state, - wasLoaded: false, - }; - } - default: - return state; - } -}; - -export const setVDiskDataWasNotLoaded = () => { - return { - type: SET_VDISK_DATA_WAS_NOT_LOADED, - } as const; -}; - interface VDiskDataRequestParams { nodeId: number | string; pDiskId: number | string; vDiskSlotId: number | string; } -const requestVDiskData = async ({nodeId, pDiskId, vDiskSlotId}: VDiskDataRequestParams) => { +export const vDiskApi = api.injectEndpoints({ + endpoints: (build) => ({ + getVDiskData: build.query({ + queryFn: async ({nodeId, pDiskId, vDiskSlotId}) => { + try { + const {vDiskData, groupData} = await requestVDiskData({ + nodeId, + pDiskId, + vDiskSlotId, + }); + return {data: {vDiskData, groupData}}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); + +async function requestVDiskData( + {nodeId, pDiskId, vDiskSlotId}: VDiskDataRequestParams, + {signal}: {signal?: AbortSignal} = {}, +) { const vDiskDataResponse = await Promise.all([ - window.api.getVdiskInfo({nodeId, pDiskId, vDiskSlotId}), - window.api.getPdiskInfo(nodeId, pDiskId), - window.api.getNodeInfo(nodeId), + window.api.getVDiskInfo({nodeId, pDiskId, vDiskSlotId}, {signal}), + window.api.getPDiskInfo({nodeId, pDiskId}, {signal}), + window.api.getNodeInfo(nodeId, {signal}), ]); const vDiskData = prepareVDiskDataResponse(vDiskDataResponse); @@ -81,23 +50,17 @@ const requestVDiskData = async ({nodeId, pDiskId, vDiskSlotId}: VDiskDataRequest let groupData: VDiskGroup | undefined; if (valueIsDefined(StoragePoolName) && valueIsDefined(GroupID)) { - const groupResponse = await window.api.getStorageInfo({ - nodeId, - poolName: StoragePoolName, - groupId: GroupID, - version: EVersion.v1, - }); + const groupResponse = await window.api.getStorageInfo( + { + nodeId, + poolName: StoragePoolName, + groupId: GroupID, + version: EVersion.v1, + }, + {signal}, + ); groupData = prepareVDiskGroupResponse(groupResponse, StoragePoolName, GroupID); } return {vDiskData, groupData}; -}; - -export const getVDiskData = (params: VDiskDataRequestParams) => { - return createApiRequest({ - request: requestVDiskData(params), - actions: FETCH_VDISK, - }); -}; - -export default vdisk; +} diff --git a/src/store/state-url-mapping.ts b/src/store/state-url-mapping.ts index 7ff5564002..45834d5a09 100644 --- a/src/store/state-url-mapping.ts +++ b/src/store/state-url-mapping.ts @@ -63,22 +63,22 @@ const paramSetup: ParamSetup = { stateKey: 'tenant.metricsTab', }, shardsMode: { - stateKey: 'shardsWorkload.filters.mode', + stateKey: 'shardsWorkload.mode', }, shardsDateFrom: { - stateKey: 'shardsWorkload.filters.from', + stateKey: 'shardsWorkload.from', type: 'number', }, shardsDateTo: { - stateKey: 'shardsWorkload.filters.to', + stateKey: 'shardsWorkload.to', type: 'number', }, topQueriesDateFrom: { - stateKey: 'executeTopQueries.filters.from', + stateKey: 'executeTopQueries.from', type: 'number', }, topQueriesDateTo: { - stateKey: 'executeTopQueries.filters.to', + stateKey: 'executeTopQueries.to', type: 'number', }, selectedConsumer: { @@ -107,6 +107,11 @@ const paramSetup: ParamSetup = { stateKey: 'nodes.searchValue', }, }, + '/cluster/tenants': { + search: { + stateKey: 'tenants.searchValue', + }, + }, }; function mergeLocationToState(state: S, location: Pick): S { diff --git a/src/types/api/netInfo.ts b/src/types/api/netInfo.ts index 342c3bab00..46585164ac 100644 --- a/src/types/api/netInfo.ts +++ b/src/types/api/netInfo.ts @@ -16,7 +16,7 @@ interface TNetTenantInfo { Nodes?: TNetNodeInfo[]; } -interface TNetNodeInfo { +export interface TNetNodeInfo { NodeId: number; Overall: EFlag; Peers?: TNetNodePeerInfo[]; diff --git a/src/types/store/describe.ts b/src/types/store/describe.ts index 49c8a32d83..3415ee2cd5 100644 --- a/src/types/store/describe.ts +++ b/src/types/store/describe.ts @@ -1,40 +1,3 @@ -import type { - FETCH_DESCRIBE, - setCurrentDescribePath, - setDataWasNotLoaded, -} from '../../store/reducers/describe'; -import type {ApiRequestAction} from '../../store/utils'; -import type {IResponseError} from '../api/error'; import type {TEvDescribeSchemeResult} from '../api/schema'; export type IDescribeData = Record; - -export interface IDescribeState { - loading: boolean; - wasLoaded: boolean; - data: IDescribeData; - currentDescribe?: IDescribeData; - currentDescribePath?: string; - error?: IResponseError; -} - -export interface IDescribeHandledResponse { - path: string | undefined; - data: IDescribeData | undefined; - currentDescribe: IDescribeData | undefined; -} - -type IDescribeApiRequestAction = ApiRequestAction< - typeof FETCH_DESCRIBE, - IDescribeHandledResponse, - IResponseError ->; - -export type IDescribeAction = - | IDescribeApiRequestAction - | ReturnType - | ReturnType; - -export interface IDescribeRootStateSlice { - describe: IDescribeState; -} diff --git a/src/types/store/heatmap.ts b/src/types/store/heatmap.ts index 289b11475c..71342b4910 100644 --- a/src/types/store/heatmap.ts +++ b/src/types/store/heatmap.ts @@ -1,6 +1,3 @@ -import type {FETCH_HEATMAP, setHeatmapOptions} from '../../store/reducers/heatmap'; -import type {ApiRequestAction} from '../../store/utils'; -import type {IResponseError} from '../api/error'; import type {TTableStats} from '../api/schema'; import type {TTabletStateInfo} from '../api/tablet'; import type {TMetrics} from '../api/tenant'; @@ -11,40 +8,13 @@ export interface IHeatmapTabletData extends TTabletStateInfo { export type IHeatmapMetricValue = keyof TTableStats | keyof TMetrics; -interface IHeatmapMetric { - value: IHeatmapMetricValue; - content: IHeatmapMetricValue; -} - export interface IHeatmapState { - loading: boolean; - wasLoaded: boolean; currentMetric?: IHeatmapMetricValue; sort: boolean; heatmap: boolean; - data?: IHeatmapTabletData[]; - metrics?: IHeatmapMetric[]; - error?: IResponseError; } export interface IHeatmapApiRequestParams { nodes?: string[]; path: string; } - -interface IHeatmapHandledResponse { - data: IHeatmapTabletData[]; - metrics?: IHeatmapMetric[]; -} - -type IHeatmapApiRequestAction = ApiRequestAction< - typeof FETCH_HEATMAP, - IHeatmapHandledResponse, - IResponseError ->; - -export type IHeatmapAction = IHeatmapApiRequestAction | ReturnType; - -export interface IHeatmapRootStateSlice { - heatmap: IHeatmapState; -} diff --git a/src/types/store/nodesList.ts b/src/types/store/nodesList.ts index ae0ad15a2d..ce28a71fce 100644 --- a/src/types/store/nodesList.ts +++ b/src/types/store/nodesList.ts @@ -1,23 +1 @@ -import type {FETCH_NODES_LIST} from '../../store/reducers/nodesList'; -import type {ApiRequestAction} from '../../store/utils'; -import type {IResponseError} from '../api/error'; -import type {TEvNodesInfo} from '../api/nodesList'; - -export interface NodesListState { - loading: boolean; - wasLoaded: boolean; - data?: TEvNodesInfo; - error?: IResponseError; -} - -export type NodesListAction = ApiRequestAction< - typeof FETCH_NODES_LIST, - TEvNodesInfo, - IResponseError ->; - export type NodesMap = Map; - -export interface NodesListRootStateSlice { - nodesList: NodesListState; -} diff --git a/src/types/store/olapStats.ts b/src/types/store/olapStats.ts deleted file mode 100644 index acc9768f18..0000000000 --- a/src/types/store/olapStats.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type {FETCH_OLAP_STATS, resetLoadingState} from '../../store/reducers/olapStats'; -import type {ApiRequestAction} from '../../store/utils'; - -import type {IQueryResult} from './query'; - -export interface OlapStatsState { - loading: boolean; - wasLoaded: boolean; - data?: IQueryResult; - error?: unknown; -} - -export type OlapStatsAction = - | ApiRequestAction - | ReturnType; diff --git a/src/types/store/tablet.ts b/src/types/store/tablet.ts index 51854bcd59..a9ac2a26c7 100644 --- a/src/types/store/tablet.ts +++ b/src/types/store/tablet.ts @@ -1,10 +1,3 @@ -import type { - FETCH_TABLET, - FETCH_TABLET_DESCRIBE, - clearTabletData, -} from '../../store/reducers/tablet'; -import type {ApiRequestAction} from '../../store/utils'; -import type {IResponseError} from '../api/error'; import type {ETabletState, TTabletStateInfo} from '../api/tablet'; export interface ITabletPreparedHistoryItem { @@ -17,15 +10,6 @@ export interface ITabletPreparedHistoryItem { fqdn: string | undefined; } -export interface ITabletState { - loading: boolean; - tenantPath?: string; - error?: IResponseError; - id?: string; - history?: ITabletPreparedHistoryItem[]; - data?: TTabletStateInfo; -} - export interface ITabletHandledResponse { tabletData: TTabletStateInfo; historyData: ITabletPreparedHistoryItem[]; @@ -34,24 +18,3 @@ export interface ITabletHandledResponse { export interface ITabletDescribeHandledResponse { tenantPath: string; } - -type ITabletApiRequestAction = ApiRequestAction< - typeof FETCH_TABLET, - ITabletHandledResponse, - IResponseError ->; - -type ITabletDescribeApiRequestAction = ApiRequestAction< - typeof FETCH_TABLET_DESCRIBE, - ITabletDescribeHandledResponse, - IResponseError ->; - -export type ITabletAction = - | ITabletApiRequestAction - | ITabletDescribeApiRequestAction - | ReturnType; - -export interface ITabletRootStateSlice { - tablet: ITabletState; -} diff --git a/src/types/store/tablets.ts b/src/types/store/tablets.ts index 30fb0e1cfb..fd74ef908e 100644 --- a/src/types/store/tablets.ts +++ b/src/types/store/tablets.ts @@ -1,41 +1,15 @@ -import type { - FETCH_TABLETS, - clearWasLoadingFlag, - setStateFilter, - setTypeFilter, -} from '../../store/reducers/tablets'; -import type {ApiRequestAction} from '../../store/utils'; -import type {IResponseError} from '../api/error'; -import type {ETabletState, EType, TEvTabletStateResponse} from '../api/tablet'; +import type {ETabletState, EType} from '../api/tablet'; -export interface ITabletsState { - loading: boolean; - wasLoaded: boolean; +export interface TabletsState { stateFilter: ETabletState[]; typeFilter: EType[]; - data?: TEvTabletStateResponse; - error?: IResponseError; } -export interface ITabletsApiRequestParams { +export interface TabletsApiRequestParams { nodes?: string[]; path?: string; } -type ITabletsApiRequestAction = ApiRequestAction< - typeof FETCH_TABLETS, - TEvTabletStateResponse, - IResponseError ->; - -export type ITabletsAction = - | ITabletsApiRequestAction - | ( - | ReturnType - | ReturnType - | ReturnType - ); - -export interface ITabletsRootStateSlice { - tablets: ITabletsState; +export interface TabletsRootStateSlice { + tablets: TabletsState; } diff --git a/src/types/store/topic.ts b/src/types/store/topic.ts index 5706b432c9..6b31711959 100644 --- a/src/types/store/topic.ts +++ b/src/types/store/topic.ts @@ -1,8 +1,4 @@ -import type {FETCH_TOPIC, cleanTopicData, setDataWasNotLoaded} from '../../store/reducers/topic'; -import type {ApiRequestAction} from '../../store/utils'; import type {ProcessSpeedStats} from '../../utils/bytesParsers'; -import type {IResponseError} from '../api/error'; -import type {DescribeTopicResult} from '../api/topic'; export interface IPreparedConsumerData { name: string | undefined; @@ -21,19 +17,3 @@ export interface IPreparedTopicStats { writeSpeed: ProcessSpeedStats; } - -export interface ITopicState { - loading: boolean; - wasLoaded: boolean; - data?: DescribeTopicResult; - error?: IResponseError; -} - -export type ITopicAction = - | ApiRequestAction - | ReturnType - | ReturnType; - -export interface ITopicRootStateSlice { - topic: ITopicState; -} diff --git a/src/utils/autofetcher.ts b/src/utils/autofetcher.ts deleted file mode 100644 index 64b9c449a4..0000000000 --- a/src/utils/autofetcher.ts +++ /dev/null @@ -1,62 +0,0 @@ -export class AutoFetcher { - static DEFAULT_TIMEOUT = 30000; - static MIN_TIMEOUT = 30000; - timeout: number; - active: boolean; - timer: undefined | ReturnType; - launchCounter: number; - - constructor() { - this.timeout = AutoFetcher.DEFAULT_TIMEOUT; - this.active = true; - this.timer = undefined; - this.launchCounter = 0; - } - - wait(ms: number) { - return new Promise((resolve) => { - this.timer = setTimeout(resolve, ms); - return; - }); - } - async fetch(request: Function) { - if (!this.active) { - return; - } - - const currentLaunch = this.launchCounter; - - await this.wait(this.timeout); - - if (this.active) { - const startTs = Date.now(); - await request(); - const finishTs = Date.now(); - - if (currentLaunch !== this.launchCounter) { - // autofetcher was restarted while request was in progress - // stop further fetches, we are in deprecated thread - return; - } - - const responseTime = finishTs - startTs; - const nextTimeout = - responseTime > AutoFetcher.MIN_TIMEOUT ? responseTime : AutoFetcher.MIN_TIMEOUT; - this.timeout = nextTimeout; - - this.fetch(request); - } else { - return; - } - } - stop() { - if (this.timer) { - clearTimeout(this.timer); - } - this.active = false; - } - start() { - this.launchCounter++; - this.active = true; - } -} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 7be1ab6899..68b652bfb4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -89,6 +89,7 @@ export const ASIDE_HEADER_COMPACT_KEY = 'asideHeaderCompact'; export const QUERIES_HISTORY_KEY = 'queries_history'; export const DATA_QA_TUNE_COLUMNS_POPUP = 'tune-columns-popup'; export const BINARY_DATA_IN_PLAIN_TEXT_DISPLAY = 'binaryDataInPlainTextDisplay'; +export const AUTO_REFRESH_INTERVAL = 'auto-refresh-interval'; export const DEFAULT_SIZE_RESULT_PANE_KEY = 'default-size-result-pane'; export const DEFAULT_SIZE_TENANT_SUMMARY_KEY = 'default-size-tenant-summary-pane'; diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index 7fe29f9291..658ed38467 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -1,4 +1,3 @@ -export * from './useAutofetcher'; export * from './useTypedSelector'; export * from './useTypedDispatch'; export * from './useSetting'; diff --git a/src/utils/hooks/useAutofetcher.ts b/src/utils/hooks/useAutofetcher.ts deleted file mode 100644 index 16272f75da..0000000000 --- a/src/utils/hooks/useAutofetcher.ts +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; - -import {AutoFetcher} from '../autofetcher'; - -export const useAutofetcher = ( - fetchData: (isBackground: boolean) => void, - deps: React.DependencyList, - enabled = true, -) => { - const ref = React.useRef(null); - - if (ref.current === null) { - ref.current = new AutoFetcher(); - } - - const autofetcher = ref.current; - - // initial fetch - React.useEffect(() => { - fetchData(false); - }, deps); // eslint-disable-line react-hooks/exhaustive-deps - - React.useEffect(() => { - autofetcher.stop(); - - if (enabled) { - autofetcher.start(); - autofetcher.fetch(() => fetchData(true)); - } - - return () => { - autofetcher.stop(); - }; - }, [enabled, ...deps]); // eslint-disable-line react-hooks/exhaustive-deps -}; diff --git a/src/utils/tooltip.js b/src/utils/tooltip.js index c022876b45..0b88129595 100644 --- a/src/utils/tooltip.js +++ b/src/utils/tooltip.js @@ -25,14 +25,14 @@ const NodeTooltip = (props) => { Rack {data.rack || '?'} - {data.connected && data.capacity && ( + {data.connected && data.capacity ? ( Net {`${data.connected} / ${data.capacity}`} - )} + ) : null}