diff --git a/src/components/EmptyState/EmptyState.tsx b/src/components/EmptyState/EmptyState.tsx
index b7d44d3fa..f04f03f0c 100644
--- a/src/components/EmptyState/EmptyState.tsx
+++ b/src/components/EmptyState/EmptyState.tsx
@@ -49,3 +49,20 @@ export const EmptyState = ({
);
};
+
+interface EmptyStateWrapperProps extends EmptyStateProps {
+ isEmpty?: boolean;
+ children: React.ReactNode;
+ className?: string;
+}
+
+export function EmptyStateWrapper({isEmpty, children, className, ...rest}: EmptyStateWrapperProps) {
+ if (isEmpty) {
+ return (
+
+
+
+ );
+ }
+ return children;
+}
diff --git a/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx b/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx
index eaae8966c..cb10221f0 100644
--- a/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx
+++ b/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx
@@ -1,6 +1,9 @@
+import React from 'react';
+
import {Skeleton} from '@gravity-ui/uikit';
import {cn} from '../../utils/cn';
+import {useDelayed} from '../../utils/hooks/useDelayed';
import './InfoViewerSkeleton.scss';
@@ -16,15 +19,27 @@ const SkeletonLabel = () => (
interface InfoViewerSkeletonProps {
className?: string;
rows?: number;
+ delay?: number;
}
-export const InfoViewerSkeleton = ({rows = 8, className}: InfoViewerSkeletonProps) => (
-
- {[...new Array(rows)].map((_, index) => (
-
-
-
-
- ))}
-
-);
+export const InfoViewerSkeleton = ({rows = 8, className, delay = 600}: InfoViewerSkeletonProps) => {
+ const show = useDelayed(delay);
+ let skeletons: React.ReactNode = (
+
+
+
+
+ );
+ if (!show) {
+ skeletons = null;
+ }
+ return (
+
+ {[...new Array(rows)].map((_, index) => (
+
+ {skeletons}
+
+ ))}
+
+ );
+};
diff --git a/src/components/Tablet/Tablet.tsx b/src/components/Tablet/Tablet.tsx
index 6d48a2c5d..3e2713acf 100644
--- a/src/components/Tablet/Tablet.tsx
+++ b/src/components/Tablet/Tablet.tsx
@@ -1,4 +1,4 @@
-import routes, {createHref} from '../../routes';
+import {getTabletPagePath} from '../../routes';
import type {TTabletStateInfo} from '../../types/api/tablet';
import {cn} from '../../utils/cn';
import {getTabletLabel} from '../../utils/constants';
@@ -20,8 +20,7 @@ export const Tablet = ({tablet = {}, tenantName}: TabletProps) => {
const {TabletId: id, NodeId, Type} = tablet;
const status = tablet.Overall?.toLowerCase();
- const tabletPath =
- id && createHref(routes.tablet, {id}, {nodeId: NodeId, tenantName, type: Type});
+ const tabletPath = id && getTabletPagePath(id, {nodeId: NodeId, tenantName, type: Type});
return (
{state};
+}
diff --git a/src/containers/Tablet/Tablet.scss b/src/containers/Tablet/Tablet.scss
index 0c0d91783..6b13f19c1 100644
--- a/src/containers/Tablet/Tablet.scss
+++ b/src/containers/Tablet/Tablet.scss
@@ -1,27 +1,10 @@
@import '../../styles/mixins.scss';
-.tablet-page {
+.ydb-tablet-page {
$_: &;
- display: flex;
- flex-direction: column;
-
+ height: 100%;
padding: 20px;
-
- &__tenant {
- margin-bottom: 20px;
- }
-
- &__pane-wrapper {
- display: flex;
- }
-
- &__left-pane {
- margin-right: 70px;
- }
- &__history-title {
- margin-bottom: 15px;
- @include body-2-typography();
- }
+ @include body-2-typography();
&__placeholder {
display: flex;
@@ -30,67 +13,7 @@
align-items: center;
}
- &__row {
- display: flex;
- align-items: center;
-
- &_header {
- margin-bottom: 20px;
-
- & #{$_}__link {
- margin: 0 10px 0 5px;
- }
- }
- }
-
- &__title {
- margin-right: 16px;
-
- font-weight: 500;
- text-transform: uppercase;
-
- @include body-2-typography();
- }
-
&__loader {
- width: 25px;
- }
-
- .info-viewer__items {
- grid-template-columns: auto;
- }
-
- &__link {
- @extend .link;
- }
-
- &__controls {
- margin: 20px 0 15px;
- }
-
- &__control {
- margin-right: 15px;
- }
-
- &__links {
- display: flex;
-
- margin: 5px 0 10px;
- padding: 0;
-
- list-style-type: none;
-
- & > * {
- margin: 0 10px 0 0;
- }
- }
-
- &__top-label {
- margin-right: 16px;
-
- font-weight: 500;
- text-transform: uppercase;
-
- @include body-2-typography();
+ margin-left: var(--g-spacing-2);
}
}
diff --git a/src/containers/Tablet/Tablet.tsx b/src/containers/Tablet/Tablet.tsx
index fdcdf0e29..a8498c6e2 100644
--- a/src/containers/Tablet/Tablet.tsx
+++ b/src/containers/Tablet/Tablet.tsx
@@ -1,42 +1,74 @@
import React from 'react';
-import {ArrowUpRightFromSquare} from '@gravity-ui/icons';
-import {Link as ExternalLink, Icon} from '@gravity-ui/uikit';
+import {Flex, Tabs} from '@gravity-ui/uikit';
import {skipToken} from '@reduxjs/toolkit/query';
import {Helmet} from 'react-helmet-async';
import {useLocation, useParams} from 'react-router-dom';
+import {StringParam, useQueryParams} from 'use-query-params';
+import {z} from 'zod';
-import {EmptyState} from '../../components/EmptyState';
-import {EntityStatus} from '../../components/EntityStatus/EntityStatus';
+import {EmptyStateWrapper} from '../../components/EmptyState';
+import {EntityPageTitle} from '../../components/EntityPageTitle/EntityPageTitle';
import {ResponseError} from '../../components/Errors/ResponseError';
-import {Loader} from '../../components/Loader';
-import {Tag} from '../../components/Tag';
-import {parseQuery} from '../../routes';
-import {backend} from '../../store';
+import {InternalLink} from '../../components/InternalLink';
+import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper';
+import {PageMetaWithAutorefresh} from '../../components/PageMeta/PageMeta';
+import {getTabletPagePath, parseQuery} from '../../routes';
+import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication';
import {setHeaderBreadcrumbs} from '../../store/reducers/header/header';
import {tabletApi} from '../../store/reducers/tablet';
+import {EFlag} from '../../types/api/enums';
import type {EType} from '../../types/api/tablet';
import {cn} from '../../utils/cn';
-import {CLUSTER_DEFAULT_TITLE, DEVELOPER_UI_TITLE} from '../../utils/constants';
-import {useAutoRefreshInterval, useTypedDispatch} from '../../utils/hooks';
+import {CLUSTER_DEFAULT_TITLE} from '../../utils/constants';
+import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../utils/hooks';
-import {TabletControls} from './TabletControls';
-import {TabletInfo} from './TabletInfo';
-import {TabletTable} from './TabletTable';
+import {TabletControls} from './components/TabletControls';
+import {TabletInfo} from './components/TabletInfo';
+import {TabletStorageInfo} from './components/TabletStorageInfo/TabletStorageInfo';
+import {TabletTable} from './components/TabletTable';
import i18n from './i18n';
+import {hasHive} from './utils';
import './Tablet.scss';
-export const b = cn('tablet-page');
+export const b = cn('ydb-tablet-page');
+
+const TABLET_TABS_IDS = {
+ history: 'history',
+ channels: 'channels',
+} as const;
+
+const TABLET_PAGE_TABS = [
+ {
+ id: TABLET_TABS_IDS.history,
+ get title() {
+ return i18n('label_tablet-history');
+ },
+ },
+ {
+ id: TABLET_TABS_IDS.channels,
+ get title() {
+ return i18n('label_tablet-channels');
+ },
+ isAdvanced: true,
+ },
+];
+
+const tabletTabSchema = z.nativeEnum(TABLET_TABS_IDS).catch(TABLET_TABS_IDS.history);
export const Tablet = () => {
- const isFirstDataFetchRef = React.useRef(true);
-
const dispatch = useTypedDispatch();
const location = useLocation();
+ const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges);
+
+ const {id} = useParams<{id: string}>();
+
+ const [{activeTab}, setParams] = useQueryParams({
+ activeTab: StringParam,
+ });
- const params = useParams<{id: string}>();
- const {id} = params;
+ const tabletTab = tabletTabSchema.parse(activeTab);
const {
nodeId: queryNodeId,
@@ -47,7 +79,7 @@ export const Tablet = () => {
const [autoRefreshInterval] = useAutoRefreshInterval();
const {currentData, isFetching, error, refetch} = tabletApi.useGetTabletQuery(
- {id},
+ {id, database: queryTenantName?.toString()},
{pollingInterval: autoRefreshInterval},
);
@@ -58,8 +90,38 @@ export const Tablet = () => {
tablet.TenantId ? {tenantId: tablet.TenantId} : skipToken,
);
+ const hasHiveId = hasHive(tablet.HiveId);
+
+ const noAdvancedInfo = !isUserAllowedToMakeChanges || !hasHiveId;
+
+ React.useEffect(() => {
+ if (loading) {
+ return;
+ }
+ if (noAdvancedInfo && TABLET_PAGE_TABS.find(({id}) => id === tabletTab)?.isAdvanced) {
+ setParams({activeTab: TABLET_TABS_IDS.history});
+ }
+ }, [noAdvancedInfo, tabletTab, setParams, loading]);
+
+ const {
+ currentData: advancedData,
+ refetch: refetchAdvancedInfo,
+ isFetching: isFetchingAdvancedData,
+ } = tabletApi.useGetAdvancedTableInfoQuery(
+ {id, hiveId: tablet.HiveId},
+ {
+ pollingInterval: autoRefreshInterval,
+ skip: noAdvancedInfo || activeTab !== 'channels',
+ },
+ );
+
+ const refetchTabletInfo = React.useCallback(async () => {
+ await Promise.all([refetch(), refetchAdvancedInfo()]);
+ }, [refetch, refetchAdvancedInfo]);
+
const nodeId = tablet.NodeId?.toString() || queryNodeId?.toString();
const tenantName = tenantPath || queryTenantName?.toString();
+
const type = tablet.Type || (queryTabletType?.toString() as EType | undefined);
React.useEffect(() => {
@@ -73,80 +135,106 @@ export const Tablet = () => {
);
}, [dispatch, tenantName, id, nodeId, type]);
- const renderExternalLinks = (link: {name: string; path: string}, index: number) => {
+ const renderPageMeta = () => {
+ const {Leader, Type} = tablet;
+ const database = tenantName ? `${i18n('tablet.meta-database')}: ${tenantName}` : undefined;
+ const type = Type ? Type : undefined;
+ const follower = Leader === false ? i18n('tablet.meta-follower').toUpperCase() : undefined;
+
+ return ;
+ };
+
+ const renderHelmet = () => {
+ return (
+
+ {`${id} — ${i18n('tablet.header')} — ${
+ tenantName || queryClusterName || CLUSTER_DEFAULT_TITLE
+ }`}
+
+ );
+ };
+
+ const renderTabs = () => {
return (
-
-
- {link.name}
-
-
+ //block wrapper for tabs
+
+
+ isAdvanced ? !noAdvancedInfo : true,
+ )}
+ activeTab={tabletTab}
+ wrapTo={({id}, tabNode) => {
+ const path = tabletId
+ ? getTabletPagePath(tabletId, {activeTab: id})
+ : undefined;
+ return (
+
+ {tabNode}
+
+ );
+ }}
+ />
+
);
};
- const renderView = () => {
- if (loading && id !== tabletId && isFirstDataFetchRef.current) {
- return ;
+ const renderTabsContent = () => {
+ switch (tabletTab) {
+ case 'channels': {
+ return (
+
+
+
+ );
+ }
+ case 'history': {
+ return ;
+ }
+ default:
+ return null;
}
+ };
+ const renderView = () => {
if (error && !currentData) {
return ;
}
- if (!tablet || !Object.keys(tablet).length) {
- return (
-
-
-
- );
- }
-
- const {TabletId, Overall, Leader} = tablet;
-
- const externalLinks = [
- {
- name: `${DEVELOPER_UI_TITLE} - tablet`,
- path: `/tablets?TabletID=${TabletId}`,
- },
- ];
-
+ const {TabletId, Overall} = tablet;
return (
-
+
{error ? : null}
-
-
-
{externalLinks.map(renderExternalLinks)}
-
-
{i18n('tablet.header')}
-
-
-
-
- {Leader &&
}
-
{loading && }
-
-
-
-
-
-
-
+
+
+
+
+
+
);
};
return (
-
-
- {`${id} — ${i18n('tablet.header')} — ${
- tenantName || queryClusterName || CLUSTER_DEFAULT_TITLE
- }`}
-
- {renderView()}
-
+
+ {renderHelmet()}
+ {renderPageMeta()}
+
+
+ {renderView()}
+ {renderTabs()}
+ {renderTabsContent()}
+
+
+
);
};
diff --git a/src/containers/Tablet/TabletInfo/TabletInfo.tsx b/src/containers/Tablet/TabletInfo/TabletInfo.tsx
deleted file mode 100644
index 9577cf95e..000000000
--- a/src/containers/Tablet/TabletInfo/TabletInfo.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import {Link as UIKitLink} from '@gravity-ui/uikit';
-import {Link} from 'react-router-dom';
-
-import type {InfoViewerItem} from '../../../components/InfoViewer';
-import {InfoViewer} from '../../../components/InfoViewer';
-import routes, {createHref} from '../../../routes';
-import {ETabletState} from '../../../types/api/tablet';
-import type {TTabletStateInfo} from '../../../types/api/tablet';
-import {calcUptime} from '../../../utils/dataFormatters/dataFormatters';
-import {getDefaultNodePath} from '../../Node/NodePages';
-import {b} from '../Tablet';
-
-interface TabletInfoProps {
- tablet: TTabletStateInfo;
- tenantPath?: string;
-}
-
-export const TabletInfo = ({tablet, tenantPath}: TabletInfoProps) => {
- const {
- ChangeTime,
- Generation,
- FollowerId,
- NodeId,
- HiveId,
- State,
- Type,
- TenantId: {SchemeShard} = {},
- } = tablet;
-
- const hasHiveId = HiveId && HiveId !== '0';
- const hasUptime = State === ETabletState.Active;
-
- const tabletInfo: InfoViewerItem[] = [{label: 'Database', value: tenantPath || '-'}];
-
- if (hasHiveId) {
- tabletInfo.push({
- label: 'HiveId',
- value: (
-
- {HiveId}
-
- ),
- });
- }
-
- if (SchemeShard) {
- tabletInfo.push({
- label: 'SchemeShard',
- value: (
-
- {SchemeShard}
-
- ),
- });
- }
-
- tabletInfo.push({label: 'Type', value: Type}, {label: 'State', value: State});
-
- if (hasUptime) {
- tabletInfo.push({label: 'Uptime', value: calcUptime(ChangeTime)});
- }
-
- tabletInfo.push(
- {label: 'Generation', value: Generation},
- {
- label: 'Node',
- value: (
-
- {NodeId}
-
- ),
- },
- );
-
- if (FollowerId) {
- tabletInfo.push({label: 'Follower', value: FollowerId});
- }
-
- return ;
-};
diff --git a/src/containers/Tablet/TabletControls/TabletControls.tsx b/src/containers/Tablet/components/TabletControls/TabletControls.tsx
similarity index 74%
rename from src/containers/Tablet/TabletControls/TabletControls.tsx
rename to src/containers/Tablet/components/TabletControls/TabletControls.tsx
index 20ceb6ab9..46f26418a 100644
--- a/src/containers/Tablet/TabletControls/TabletControls.tsx
+++ b/src/containers/Tablet/components/TabletControls/TabletControls.tsx
@@ -1,12 +1,15 @@
import React from 'react';
-import {ButtonWithConfirmDialog} from '../../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog';
-import {selectIsUserAllowedToMakeChanges} from '../../../store/reducers/authentication/authentication';
-import {ETabletState} from '../../../types/api/tablet';
-import type {TTabletStateInfo} from '../../../types/api/tablet';
-import {useTypedSelector} from '../../../utils/hooks';
-import {b} from '../Tablet';
-import i18n from '../i18n';
+import {ArrowRotateLeft, StopFill, TriangleRightFill} from '@gravity-ui/icons';
+import {Flex, Icon} from '@gravity-ui/uikit';
+
+import {ButtonWithConfirmDialog} from '../../../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog';
+import {selectIsUserAllowedToMakeChanges} from '../../../../store/reducers/authentication/authentication';
+import {ETabletState} from '../../../../types/api/tablet';
+import type {TTabletStateInfo} from '../../../../types/api/tablet';
+import {useTypedSelector} from '../../../../utils/hooks';
+import i18n from '../../i18n';
+import {hasHive} from '../../utils';
interface TabletControlsProps {
tablet: TTabletStateInfo;
@@ -28,9 +31,7 @@ export const TabletControls = ({tablet, fetchData}: TabletControlsProps) => {
return window.api.resumeTablet(TabletId, HiveId);
};
- const hasHiveId = () => {
- return HiveId && HiveId !== '0';
- };
+ const hasHiveId = hasHive(HiveId);
const isDisabledRestart = tablet.State === ETabletState.Stopped;
@@ -41,50 +42,53 @@ export const TabletControls = ({tablet, fetchData}: TabletControlsProps) => {
tablet.State === ETabletState.Stopped || tablet.State === ETabletState.Deleted;
return (
-
+
+
{i18n('controls.kill')}
- {hasHiveId() ? (
+ {hasHiveId && (
+
{i18n('controls.stop')}
+
{i18n('controls.resume')}
- ) : null}
-
+ )}
+
);
};
diff --git a/src/containers/Tablet/TabletControls/index.ts b/src/containers/Tablet/components/TabletControls/index.ts
similarity index 100%
rename from src/containers/Tablet/TabletControls/index.ts
rename to src/containers/Tablet/components/TabletControls/index.ts
diff --git a/src/containers/Tablet/components/TabletInfo/TabletInfo.scss b/src/containers/Tablet/components/TabletInfo/TabletInfo.scss
new file mode 100644
index 000000000..b464aa735
--- /dev/null
+++ b/src/containers/Tablet/components/TabletInfo/TabletInfo.scss
@@ -0,0 +1,12 @@
+@import '../../../../styles/mixins.scss';
+
+.ydb-tablet-info {
+ &__link {
+ @extend .link;
+ }
+
+ &__section-title {
+ margin: var(--g-spacing-1) 0 var(--g-spacing-3);
+ @include text-subheader-2();
+ }
+}
diff --git a/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx b/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx
new file mode 100644
index 000000000..1f1d491b9
--- /dev/null
+++ b/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx
@@ -0,0 +1,136 @@
+import {Flex} from '@gravity-ui/uikit';
+import {Link} from 'react-router-dom';
+
+import type {InfoViewerItem} from '../../../../components/InfoViewer';
+import {InfoViewer} from '../../../../components/InfoViewer';
+import {LinkWithIcon} from '../../../../components/LinkWithIcon/LinkWithIcon';
+import {TabletState} from '../../../../components/TabletState/TabletState';
+import {getTabletPagePath} from '../../../../routes';
+import {selectIsUserAllowedToMakeChanges} from '../../../../store/reducers/authentication/authentication';
+import {ETabletState} from '../../../../types/api/tablet';
+import type {TTabletStateInfo} from '../../../../types/api/tablet';
+import {cn} from '../../../../utils/cn';
+import {calcUptime} from '../../../../utils/dataFormatters/dataFormatters';
+import {createTabletDeveloperUIHref} from '../../../../utils/developerUI/developerUI';
+import {useTypedSelector} from '../../../../utils/hooks';
+import {getDefaultNodePath} from '../../../Node/NodePages';
+import {hasHive} from '../../utils';
+
+import {tabletInfoKeyset} from './i18n';
+
+const b = cn('ydb-tablet-info');
+
+import './TabletInfo.scss';
+
+interface TabletInfoProps {
+ tablet: TTabletStateInfo;
+}
+
+export const TabletInfo = ({tablet}: TabletInfoProps) => {
+ const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges);
+
+ const {
+ ChangeTime,
+ Generation,
+ FollowerId,
+ NodeId,
+ HiveId,
+ State,
+ TenantId: {SchemeShard} = {},
+ TabletId,
+ } = tablet;
+
+ const hasHiveId = hasHive(HiveId);
+ const hasUptime = State === ETabletState.Active;
+
+ const tabletInfo: InfoViewerItem[] = [];
+
+ if (hasHiveId) {
+ tabletInfo.push({
+ label: tabletInfoKeyset('field_hive'),
+ value: (
+
+ {HiveId}
+
+ ),
+ });
+ }
+
+ if (SchemeShard) {
+ tabletInfo.push({
+ label: tabletInfoKeyset('field_scheme-shard'),
+ value: (
+
+ {SchemeShard}
+
+ ),
+ });
+ }
+
+ tabletInfo.push({label: tabletInfoKeyset('field_state'), value: });
+
+ if (hasUptime) {
+ tabletInfo.push({label: tabletInfoKeyset('field_uptime'), value: calcUptime(ChangeTime)});
+ }
+
+ tabletInfo.push(
+ {label: tabletInfoKeyset('field_generation'), value: Generation},
+ {
+ label: tabletInfoKeyset('field_node'),
+ value: (
+
+ {NodeId}
+
+ ),
+ },
+ );
+
+ if (FollowerId) {
+ tabletInfo.push({label: tabletInfoKeyset('field_follower'), value: FollowerId});
+ }
+
+ const renderTabletInfo = () => {
+ return (
+
+
{tabletInfoKeyset('title_info')}
+
+
+ );
+ };
+
+ const renderLinks = () => {
+ if (!isUserAllowedToMakeChanges || !TabletId) {
+ return null;
+ }
+ return (
+
+
{tabletInfoKeyset('title_links')}
+
+
+
+
+
+
+
+ );
+ };
+
+ return (
+
+ {renderTabletInfo()}
+ {renderLinks()}
+
+ );
+};
diff --git a/src/containers/Tablet/components/TabletInfo/i18n/en.json b/src/containers/Tablet/components/TabletInfo/i18n/en.json
new file mode 100644
index 000000000..2f4b5bb44
--- /dev/null
+++ b/src/containers/Tablet/components/TabletInfo/i18n/en.json
@@ -0,0 +1,16 @@
+{
+ "field_scheme-shard": "SchemeShard",
+ "field_follower": "Follower",
+ "field_generation": "Generation",
+ "field_hive": "HiveId",
+ "field_state": "State",
+ "field_uptime": "Uptime",
+ "field_node": "Node",
+ "field_links": "Links",
+ "field_developer-ui-app": "App",
+ "field_developer-ui-counters": "Counters",
+ "field_developer-ui-executor": "Executor DB internals",
+ "field_developer-ui-state": "State Storage",
+ "title_info": "Info",
+ "title_links": "Links"
+}
diff --git a/src/containers/Tablet/components/TabletInfo/i18n/index.ts b/src/containers/Tablet/components/TabletInfo/i18n/index.ts
new file mode 100644
index 000000000..a60df0b98
--- /dev/null
+++ b/src/containers/Tablet/components/TabletInfo/i18n/index.ts
@@ -0,0 +1,7 @@
+import {registerKeysets} from '../../../../../utils/i18n';
+
+import en from './en.json';
+
+const COMPONENT = 'ydb-tablet-info';
+
+export const tabletInfoKeyset = registerKeysets(COMPONENT, {en});
diff --git a/src/containers/Tablet/TabletInfo/index.ts b/src/containers/Tablet/components/TabletInfo/index.ts
similarity index 100%
rename from src/containers/Tablet/TabletInfo/index.ts
rename to src/containers/Tablet/components/TabletInfo/index.ts
diff --git a/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss
new file mode 100644
index 000000000..48dd54865
--- /dev/null
+++ b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss
@@ -0,0 +1,78 @@
+@import '../../../../styles/mixins.scss';
+
+.ydb-tablet-storage-info {
+ $block: &;
+ &__table {
+ table-layout: fixed;
+ border-spacing: 0px;
+ border-collapse: collapse;
+ tr {
+ &:hover {
+ background-color: var(--g-color-base-generic-hover) !important;
+ }
+ }
+ tr:nth-of-type(2n + 1) {
+ background-color: var(--g-color-base-generic-ultralight);
+ }
+ :is(#{$block}__table-header-cell) {
+ height: 40px;
+ padding: 0;
+
+ text-align: left;
+
+ background-color: var(--g-color-base-background);
+ @include text-subheader-2();
+ }
+ :is(#{$block}__table-cell) {
+ height: 40px;
+ padding: 0;
+ @include text-body-2();
+ }
+ }
+
+ &__table-header-cell {
+ &_align_right {
+ #{$block}__table-header-content {
+ justify-content: flex-end;
+
+ text-align: right;
+ }
+ }
+ }
+
+ &__table-header-content {
+ display: flex;
+ align-items: center;
+
+ height: 100%;
+ padding: var(--g-spacing-1) var(--g-spacing-2);
+
+ vertical-align: middle;
+
+ border-bottom: 1px solid var(--g-color-line-generic);
+ }
+ :is(#{$block}__table-cell) {
+ height: 40px;
+ padding: 0;
+ @include text-body-2();
+ }
+ &__table-cell {
+ &_align_right {
+ text-align: right;
+ }
+ }
+ &__metrics-cell {
+ padding: var(--g-spacing-1) var(--g-spacing-2);
+
+ white-space: nowrap;
+ }
+ &__name-wrapper {
+ padding: var(--g-spacing-1) var(--g-spacing-2);
+ }
+ &__with-padding {
+ padding-left: calc(var(--g-spacing-2) + var(--g-spacing-6));
+ }
+ &__name-content_no-control {
+ padding-left: var(--g-spacing-6);
+ }
+}
diff --git a/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.tsx b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.tsx
new file mode 100644
index 000000000..719face4a
--- /dev/null
+++ b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+
+import {Table, useTable} from '@gravity-ui/table';
+import type {ExpandedState} from '@tanstack/react-table';
+
+import type {TTabletHiveResponse} from '../../../../types/api/tablet';
+
+import {getColumns} from './columns';
+import {b} from './shared';
+import {prepareData} from './utils';
+
+import './TabletStorageInfo.scss';
+
+interface TabletStorageInfoProps {
+ data?: TTabletHiveResponse | null;
+}
+
+export function TabletStorageInfo({data}: TabletStorageInfoProps) {
+ const [expanded, setExpanded] = React.useState({});
+ const tree = React.useMemo(() => prepareData(data), [data]);
+ const isExpandable = React.useMemo(() => tree.some((item) => item.children?.length), [tree]);
+ const columns = React.useMemo(() => getColumns(isExpandable), [isExpandable]);
+ const table = useTable({
+ columns,
+ data: tree,
+ getSubRows: (item) => item.children,
+ enableExpanding: true,
+ onExpandedChange: setExpanded,
+ state: {
+ expanded,
+ },
+ });
+ return (
+ //block wrapper for table
+
+
{
+ const align = column.columnDef.meta?.align;
+ return b('table-header-cell', {align});
+ }}
+ cellClassName={(cell) => {
+ const align = cell?.column.columnDef.meta?.align;
+ const verticalAlign = cell?.column.columnDef.meta?.verticalAlign;
+ return b('table-cell', {align, 'vertical-align': verticalAlign});
+ }}
+ className={b('table')}
+ />
+
+ );
+}
diff --git a/src/containers/Tablet/components/TabletStorageInfo/columns.tsx b/src/containers/Tablet/components/TabletStorageInfo/columns.tsx
new file mode 100644
index 000000000..54d9c9b0c
--- /dev/null
+++ b/src/containers/Tablet/components/TabletStorageInfo/columns.tsx
@@ -0,0 +1,100 @@
+import {ArrowToggle, Button, Flex} from '@gravity-ui/uikit';
+import type {CellContext, ColumnDef, Row} from '@tanstack/react-table';
+
+import {formatTimestamp} from '../../../../utils/dataFormatters/dataFormatters';
+
+import {tabletInfoKeyset} from './i18n';
+import {b} from './shared';
+import type {TabletStorageItem} from './types';
+
+interface ColumnHeaderProps {
+ name: string;
+ className?: string;
+}
+
+function ColumnHeader({name, className}: ColumnHeaderProps) {
+ return {name}
;
+}
+
+function metricsCell(
+ info: CellContext,
+ formatter?: (value: string | number) => string | number,
+) {
+ const value = info.getValue();
+ const formattedValue = typeof formatter === 'function' ? formatter(value) : value;
+
+ return {formattedValue}
;
+}
+
+interface GroupIdCellProps {
+ row: Row;
+ name?: string;
+ hasExpand?: boolean;
+}
+
+function GroupIdCell({row, name, hasExpand}: GroupIdCellProps) {
+ const isExpandable = row.getCanExpand();
+ return (
+
+ {isExpandable && (
+
+ )}
+
+ {name}
+
+
+ );
+}
+
+export function getColumns(hasExpand?: boolean) {
+ const columns: ColumnDef[] = [
+ {
+ accessorKey: 'channelIndex',
+ header: () => ,
+ size: 50,
+ cell: metricsCell,
+ meta: {align: 'right'},
+ },
+ {
+ accessorKey: 'storagePoolName',
+ header: () => ,
+ size: 200,
+ cell: metricsCell,
+ },
+ {
+ accessorKey: 'GroupID',
+ header: () => (
+
+ ),
+ size: 100,
+ cell: (info) => (
+ ()} hasExpand={hasExpand} />
+ ),
+ },
+ {
+ accessorKey: 'FromGeneration',
+ header: () => ,
+ size: 100,
+ cell: metricsCell,
+ meta: {align: 'right'},
+ },
+ {
+ accessorKey: 'Timestamp',
+ header: () => ,
+ size: 200,
+ cell: (info) => metricsCell(info, formatTimestamp),
+ meta: {align: 'right'},
+ },
+ ];
+ return columns;
+}
diff --git a/src/containers/Tablet/components/TabletStorageInfo/i18n/en.json b/src/containers/Tablet/components/TabletStorageInfo/i18n/en.json
new file mode 100644
index 000000000..12b9114bb
--- /dev/null
+++ b/src/containers/Tablet/components/TabletStorageInfo/i18n/en.json
@@ -0,0 +1,7 @@
+{
+ "label_channel-index": "Channel",
+ "label_storage-pool": "Storage Pool Name",
+ "label_group-id": "Group ID",
+ "label_generation": "From generation",
+ "label_timestamp": "Timestamp"
+}
diff --git a/src/containers/Tablet/components/TabletStorageInfo/i18n/index.ts b/src/containers/Tablet/components/TabletStorageInfo/i18n/index.ts
new file mode 100644
index 000000000..01ebbd1a5
--- /dev/null
+++ b/src/containers/Tablet/components/TabletStorageInfo/i18n/index.ts
@@ -0,0 +1,7 @@
+import {registerKeysets} from '../../../../../utils/i18n';
+
+import en from './en.json';
+
+const COMPONENT = 'ydb-tablet-storage-info';
+
+export const tabletInfoKeyset = registerKeysets(COMPONENT, {en});
diff --git a/src/containers/Tablet/components/TabletStorageInfo/shared.ts b/src/containers/Tablet/components/TabletStorageInfo/shared.ts
new file mode 100644
index 000000000..c219bf359
--- /dev/null
+++ b/src/containers/Tablet/components/TabletStorageInfo/shared.ts
@@ -0,0 +1,3 @@
+import {cn} from '../../../../utils/cn';
+
+export const b = cn('ydb-tablet-storage-info');
diff --git a/src/containers/Tablet/components/TabletStorageInfo/types.ts b/src/containers/Tablet/components/TabletStorageInfo/types.ts
new file mode 100644
index 000000000..f2358378f
--- /dev/null
+++ b/src/containers/Tablet/components/TabletStorageInfo/types.ts
@@ -0,0 +1,6 @@
+import type {TTabletStorageInfoChannelHistory} from '../../../../types/api/tablet';
+
+export interface TabletStorageItem extends TTabletStorageInfoChannelHistory {
+ storagePoolName?: string;
+ children?: TabletStorageItem[];
+}
diff --git a/src/containers/Tablet/components/TabletStorageInfo/utils.ts b/src/containers/Tablet/components/TabletStorageInfo/utils.ts
new file mode 100644
index 000000000..272362e5f
--- /dev/null
+++ b/src/containers/Tablet/components/TabletStorageInfo/utils.ts
@@ -0,0 +1,36 @@
+import type {TTabletHiveResponse} from '../../../../types/api/tablet';
+
+import type {TabletStorageItem} from './types';
+
+export function prepareData(data?: TTabletHiveResponse | null): TabletStorageItem[] {
+ if (!data) {
+ return [];
+ }
+ const {BoundChannels, TabletStorageInfo = {}} = data;
+
+ const Channels = TabletStorageInfo.Channels ?? [];
+
+ const result = [];
+
+ for (const channel of Channels) {
+ const channelIndex = channel.Channel;
+ const channelHistory = channel.History;
+ if (!channelIndex || !channelHistory || !channelHistory.length) {
+ continue;
+ }
+ const copiedChannelHistory = [...channelHistory];
+
+ copiedChannelHistory.reverse();
+ const [latest, ...rest] = copiedChannelHistory;
+ const storagePoolName = BoundChannels?.[channelIndex]?.StoragePoolName;
+
+ const params = {
+ ...latest,
+ storagePoolName,
+ channelIndex,
+ children: rest,
+ };
+ result.push(params);
+ }
+ return result;
+}
diff --git a/src/containers/Tablet/TabletTable/TabletTable.tsx b/src/containers/Tablet/components/TabletTable/TabletTable.tsx
similarity index 74%
rename from src/containers/Tablet/TabletTable/TabletTable.tsx
rename to src/containers/Tablet/components/TabletTable/TabletTable.tsx
index 5e096b1a7..588e472e9 100644
--- a/src/containers/Tablet/TabletTable/TabletTable.tsx
+++ b/src/containers/Tablet/components/TabletTable/TabletTable.tsx
@@ -1,12 +1,13 @@
import type {Column} from '@gravity-ui/react-data-table';
import DataTable from '@gravity-ui/react-data-table';
-import {EntityStatus} from '../../../components/EntityStatus/EntityStatus';
-import {InternalLink} from '../../../components/InternalLink/InternalLink';
-import {ResizeableDataTable} from '../../../components/ResizeableDataTable/ResizeableDataTable';
-import type {ITabletPreparedHistoryItem} from '../../../types/store/tablet';
-import {calcUptime} from '../../../utils/dataFormatters/dataFormatters';
-import {getDefaultNodePath} from '../../Node/NodePages';
+import {EntityStatus} from '../../../../components/EntityStatus/EntityStatus';
+import {InternalLink} from '../../../../components/InternalLink/InternalLink';
+import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable';
+import {TabletState} from '../../../../components/TabletState/TabletState';
+import type {ITabletPreparedHistoryItem} from '../../../../types/store/tablet';
+import {calcUptime} from '../../../../utils/dataFormatters/dataFormatters';
+import {getDefaultNodePath} from '../../../Node/NodePages';
const TABLET_COLUMNS_WIDTH_LS_KEY = 'tabletTableColumnsWidth';
@@ -25,7 +26,7 @@ const columns: Column[] = [
{
name: 'State',
sortable: false,
- render: ({row}) => row.state,
+ render: ({row}) => ,
},
{
name: 'Follower ID',
diff --git a/src/containers/Tablet/TabletTable/index.ts b/src/containers/Tablet/components/TabletTable/index.ts
similarity index 100%
rename from src/containers/Tablet/TabletTable/index.ts
rename to src/containers/Tablet/components/TabletTable/index.ts
diff --git a/src/containers/Tablet/i18n/en.json b/src/containers/Tablet/i18n/en.json
index 2bea6d463..d24385939 100644
--- a/src/containers/Tablet/i18n/en.json
+++ b/src/containers/Tablet/i18n/en.json
@@ -1,6 +1,9 @@
{
"tablet.header": "Tablet",
+ "tablet.meta-database": "Database",
+ "tablet.meta-follower": "Follower",
+
"controls.kill": "Restart",
"controls.stop": "Stop",
"controls.resume": "Resume",
@@ -13,5 +16,8 @@
"dialog.stop": "The tablet will be stopped. Do you want to proceed?",
"dialog.resume": "The tablet will be resumed. Do you want to proceed?",
- "emptyState": "The tablet was not found"
+ "emptyState": "The tablet was not found",
+
+ "label_tablet-history": "Tablets",
+ "label_tablet-channels": "Storage"
}
diff --git a/src/containers/Tablet/utils.ts b/src/containers/Tablet/utils.ts
new file mode 100644
index 000000000..d1c002eb6
--- /dev/null
+++ b/src/containers/Tablet/utils.ts
@@ -0,0 +1,3 @@
+export function hasHive(id?: string): id is string {
+ return Boolean(id && id !== '0');
+}
diff --git a/src/containers/Tablets/Tablets.tsx b/src/containers/Tablets/Tablets.tsx
index 7d6b2b194..270df62df 100644
--- a/src/containers/Tablets/Tablets.tsx
+++ b/src/containers/Tablets/Tablets.tsx
@@ -1,6 +1,6 @@
import {ArrowsRotateRight} from '@gravity-ui/icons';
import type {Column as DataTableColumn} from '@gravity-ui/react-data-table';
-import {Icon, Label, Text} from '@gravity-ui/uikit';
+import {Icon, Text} from '@gravity-ui/uikit';
import {skipToken} from '@reduxjs/toolkit/query';
import {ButtonWithConfirmDialog} from '../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog';
@@ -10,6 +10,7 @@ import {ResponseError} from '../../components/Errors/ResponseError';
import {InternalLink} from '../../components/InternalLink';
import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable';
import {TableSkeleton} from '../../components/TableSkeleton/TableSkeleton';
+import {TabletState} from '../../components/TabletState/TabletState';
import {getTabletPagePath} from '../../routes';
import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication';
import {selectTabletsWithFqdn, tabletsApi} from '../../store/reducers/tablets';
@@ -21,7 +22,6 @@ import {DEFAULT_TABLE_SETTINGS, EMPTY_DATA_PLACEHOLDER} from '../../utils/consta
import {calcUptime} from '../../utils/dataFormatters/dataFormatters';
import {createTabletDeveloperUIHref} from '../../utils/developerUI/developerUI';
import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../utils/hooks';
-import {mapTabletStateToLabelTheme} from '../../utils/tablet';
import {getDefaultNodePath} from '../Node/NodePages';
import i18n from './i18n';
@@ -78,7 +78,7 @@ const columns: DataTableColumn[] = [
return i18n('State');
},
render: ({row}) => {
- return ;
+ return ;
},
},
{
diff --git a/src/services/api.ts b/src/services/api.ts
index 1d3c5c1e0..d4a927fed 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -38,6 +38,7 @@ import type {TEvSystemStateResponse} from '../types/api/systemState';
import type {
TDomainKey,
TEvTabletStateResponse,
+ TTabletHiveResponse,
UnmergedTEvTabletStateResponse,
} from '../types/api/tablet';
import type {TTenantInfo, TTenants} from '../types/api/tenant';
@@ -389,11 +390,15 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
{concurrentId: concurrentId || 'getConsumer', requestConfig: {signal}},
);
}
- getTablet({id}: {id?: string}, {concurrentId, signal}: AxiosOptions = {}) {
+ getTablet(
+ {id, database}: {id?: string; database?: string},
+ {concurrentId, signal}: AxiosOptions = {},
+ ) {
return this.get(
this.getPath(`/viewer/json/tabletinfo?filter=(TabletId=${id})`),
{
enums: true,
+ database,
},
{
concurrentId,
@@ -401,12 +406,16 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
},
);
}
- getTabletHistory({id}: {id?: string}, {concurrentId, signal}: AxiosOptions = {}) {
+ getTabletHistory(
+ {id, database}: {id?: string; database?: string},
+ {concurrentId, signal}: AxiosOptions = {},
+ ) {
return this.get(
this.getPath(`/viewer/json/tabletinfo?filter=(TabletId=${id})`),
{
enums: true,
merge: false,
+ database,
},
{
concurrentId,
@@ -586,6 +595,23 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
{requestConfig: {'axios-retry': {retries: 0}}},
);
}
+ getTabletFromHive(
+ {id, hiveId}: {id?: string; hiveId?: string},
+ {concurrentId, signal}: AxiosOptions = {},
+ ) {
+ return this.get>(
+ this.getPath('/tablets/app'),
+ {
+ TabletID: hiveId,
+ page: 'TabletInfo',
+ tablet: id,
+ },
+ {
+ concurrentId,
+ requestConfig: {signal},
+ },
+ );
+ }
getTabletDescribe(tenantId: TDomainKey, {concurrentId, signal}: AxiosOptions = {}) {
return this.get>(
this.getPath('/viewer/json/describe'),
diff --git a/src/store/reducers/tablet.ts b/src/store/reducers/tablet.ts
index 046768fc6..faa6f5d2b 100644
--- a/src/store/reducers/tablet.ts
+++ b/src/store/reducers/tablet.ts
@@ -7,11 +7,11 @@ import {api} from './api';
export const tabletApi = api.injectEndpoints({
endpoints: (build) => ({
getTablet: build.query({
- queryFn: async ({id}: {id: string}, {signal}) => {
+ queryFn: async ({id, database}: {id: string; database?: string}, {signal}) => {
try {
const [tabletResponseData, historyResponseData, nodesList] = await Promise.all([
- window.api.getTablet({id}, {signal}),
- window.api.getTabletHistory({id}, {signal}),
+ window.api.getTablet({id, database}, {signal}),
+ window.api.getTabletHistory({id, database}, {signal}),
window.api.getNodesList({signal}),
]);
const nodesMap = prepareNodesMap(nodesList);
@@ -29,15 +29,17 @@ export const tabletApi = api.injectEndpoints({
const fqdn =
nodesMap && nodeId ? nodesMap.get(Number(nodeId)) : undefined;
- list.push({
- nodeId,
- generation: Generation,
- changeTime: ChangeTime,
- state: State,
- leader: Leader,
- followerId: FollowerId,
- fqdn,
- });
+ if (State !== 'Dead') {
+ list.push({
+ nodeId,
+ generation: Generation,
+ changeTime: ChangeTime,
+ state: State,
+ leader: Leader,
+ followerId: FollowerId,
+ fqdn,
+ });
+ }
}
return list;
}, []);
@@ -67,6 +69,21 @@ export const tabletApi = api.injectEndpoints({
},
providesTags: ['All'],
}),
+ getAdvancedTableInfo: build.query({
+ queryFn: async ({id, hiveId}: {id: string; hiveId?: string}, {signal}) => {
+ try {
+ const tabletResponseData = await window.api.getTabletFromHive(
+ {id, hiveId},
+ {signal},
+ );
+
+ return {data: tabletResponseData};
+ } catch (error) {
+ return {error};
+ }
+ },
+ providesTags: ['All'],
+ }),
}),
overrideExisting: 'throw',
});
diff --git a/src/types/api/tablet.ts b/src/types/api/tablet.ts
index 76dcf5498..7d5f49175 100644
--- a/src/types/api/tablet.ts
+++ b/src/types/api/tablet.ts
@@ -119,3 +119,27 @@ export enum ETabletState {
'Deleted' = 'Deleted',
'Stopped' = 'Stopped',
}
+
+interface TBoundChannel {
+ IOPS?: number;
+ StoragePoolName?: string;
+ Throughput?: number;
+ Size?: number;
+}
+
+export interface TTabletStorageInfoChannelHistory {
+ GroupID?: number;
+ FromGeneration?: number;
+ Timestamp?: string;
+}
+
+interface TTabletStorageInfoChannel {
+ Channel?: number;
+ History?: TTabletStorageInfoChannelHistory[];
+}
+
+//tablet data from hive
+export interface TTabletHiveResponse {
+ BoundChannels?: TBoundChannel[];
+ TabletStorageInfo?: {Channels?: TTabletStorageInfoChannel[]; Version?: number};
+}
diff --git a/src/types/store/tablets.ts b/src/types/store/tablets.ts
index fd74ef908..befbff9bb 100644
--- a/src/types/store/tablets.ts
+++ b/src/types/store/tablets.ts
@@ -8,6 +8,7 @@ export interface TabletsState {
export interface TabletsApiRequestParams {
nodes?: string[];
path?: string;
+ database?: string;
}
export interface TabletsRootStateSlice {
diff --git a/src/utils/dataFormatters/dataFormatters.ts b/src/utils/dataFormatters/dataFormatters.ts
index 9a6b64cbd..cea9b2cc4 100644
--- a/src/utils/dataFormatters/dataFormatters.ts
+++ b/src/utils/dataFormatters/dataFormatters.ts
@@ -136,6 +136,11 @@ export const formatDateTime = (value?: number | string, defaultValue = '') => {
return formattedData ?? defaultValue;
};
+export const formatTimestamp = (value?: string | number, defaultValue = '') => {
+ const formattedData = dateTimeParse(value)?.format('YYYY-MM-DD HH:mm:ss.SSS');
+
+ return formattedData ?? defaultValue;
+};
export const calcUptimeInSeconds = (milliseconds: number | string) => {
const currentDate = new Date();
diff --git a/src/utils/developerUI/developerUI.ts b/src/utils/developerUI/developerUI.ts
index 1a77e82ac..414fc4044 100644
--- a/src/utils/developerUI/developerUI.ts
+++ b/src/utils/developerUI/developerUI.ts
@@ -41,6 +41,12 @@ export const createVDiskDeveloperUILink = ({
return createDeveloperUILinkWithNodeId(nodeId, host) + vdiskPath;
};
-export function createTabletDeveloperUIHref(tabletId: number | string, host = backend) {
- return `${host}/tablets?TabletID=${tabletId}`;
+export function createTabletDeveloperUIHref(
+ tabletId: number | string,
+ tabletPage?: string,
+ searchParam = 'TabletID',
+ host = backend,
+) {
+ const subPage = tabletPage ? `/${tabletPage}` : '';
+ return `${host}/tablets${subPage}?${searchParam}=${tabletId}`;
}