From 503e6c4f2774fcee8721e819b510390ef2c50127 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Thu, 22 May 2025 16:56:38 +0300 Subject: [PATCH 01/14] fix: loader position for tenant --- .../TenantOverview/TenantOverview.scss | 4 -- .../TenantOverview/TenantOverview.tsx | 49 +++++++++---------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss index c12c761f0..d1d226d2d 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss @@ -6,10 +6,6 @@ height: 100%; padding-bottom: 20px; - &__loader { - display: flex; - justify-content: center; - } &__tenant-name-wrapper { display: flex; overflow: hidden; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx index 1a82e28b4..fb1085c41 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx @@ -1,6 +1,7 @@ -import {Flex, Loader} from '@gravity-ui/uikit'; +import {Flex} from '@gravity-ui/uikit'; import {EntityStatus} from '../../../../components/EntityStatus/EntityStatus'; +import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper'; import {LogsButton} from '../../../../components/LogsButton/LogsButton'; import {MonitoringButton} from '../../../../components/MonitoringButton/MonitoringButton'; import {overviewApi} from '../../../../store/reducers/overview/overview'; @@ -142,37 +143,31 @@ export function TenantOverview({ } }; - if (tenantLoading) { - return ( -
- -
- ); - } - const monitoringLink = additionalTenantProps?.getMonitoringLink?.(Name, Type); const logsLink = additionalTenantProps?.getLogsLink?.(Name); return ( -
-
-
{tenantType}
- - {renderName()} - - {monitoringLink && } - {logsLink && } + +
+
+
{tenantType}
+ + {renderName()} + + {monitoringLink && } + {logsLink && } + - - + +
+ {renderTabContent()}
- {renderTabContent()} -
+ ); } From 5aaf47acbc7f45c7f095039a21f0fa6ac8c6565e Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Thu, 29 May 2025 08:45:58 +0300 Subject: [PATCH 02/14] refactor: move EmptyFilter to components --- .../Storage => components}/EmptyFilter/EmptyFilter.tsx | 4 ++-- .../Storage => components}/EmptyFilter/i18n/en.json | 0 .../Storage => components}/EmptyFilter/i18n/index.ts | 2 +- .../Storage => components}/EmptyFilter/i18n/ru.json | 0 .../StorageGroupsEmptyDataMessage.tsx | 2 +- .../StorageNodesEmptyDataMessage.tsx | 2 +- src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename src/{containers/Storage => components}/EmptyFilter/EmptyFilter.tsx (85%) rename src/{containers/Storage => components}/EmptyFilter/i18n/en.json (100%) rename src/{containers/Storage => components}/EmptyFilter/i18n/index.ts (73%) rename src/{containers/Storage => components}/EmptyFilter/i18n/ru.json (100%) diff --git a/src/containers/Storage/EmptyFilter/EmptyFilter.tsx b/src/components/EmptyFilter/EmptyFilter.tsx similarity index 85% rename from src/containers/Storage/EmptyFilter/EmptyFilter.tsx rename to src/components/EmptyFilter/EmptyFilter.tsx index 223d81a2d..59ec31059 100644 --- a/src/containers/Storage/EmptyFilter/EmptyFilter.tsx +++ b/src/components/EmptyFilter/EmptyFilter.tsx @@ -1,7 +1,7 @@ import {Button} from '@gravity-ui/uikit'; -import {EmptyState} from '../../../components/EmptyState'; -import {Illustration} from '../../../components/Illustration'; +import {EmptyState} from '../EmptyState'; +import {Illustration} from '../Illustration'; import i18n from './i18n'; diff --git a/src/containers/Storage/EmptyFilter/i18n/en.json b/src/components/EmptyFilter/i18n/en.json similarity index 100% rename from src/containers/Storage/EmptyFilter/i18n/en.json rename to src/components/EmptyFilter/i18n/en.json diff --git a/src/containers/Storage/EmptyFilter/i18n/index.ts b/src/components/EmptyFilter/i18n/index.ts similarity index 73% rename from src/containers/Storage/EmptyFilter/i18n/index.ts rename to src/components/EmptyFilter/i18n/index.ts index fa42957d0..9a545b697 100644 --- a/src/containers/Storage/EmptyFilter/i18n/index.ts +++ b/src/components/EmptyFilter/i18n/index.ts @@ -1,4 +1,4 @@ -import {registerKeysets} from '../../../../utils/i18n'; +import {registerKeysets} from '../../../utils/i18n'; import en from './en.json'; import ru from './ru.json'; diff --git a/src/containers/Storage/EmptyFilter/i18n/ru.json b/src/components/EmptyFilter/i18n/ru.json similarity index 100% rename from src/containers/Storage/EmptyFilter/i18n/ru.json rename to src/components/EmptyFilter/i18n/ru.json diff --git a/src/containers/Storage/PaginatedStorageGroupsTable/StorageGroupsEmptyDataMessage.tsx b/src/containers/Storage/PaginatedStorageGroupsTable/StorageGroupsEmptyDataMessage.tsx index 623edd976..1abd94eef 100644 --- a/src/containers/Storage/PaginatedStorageGroupsTable/StorageGroupsEmptyDataMessage.tsx +++ b/src/containers/Storage/PaginatedStorageGroupsTable/StorageGroupsEmptyDataMessage.tsx @@ -1,6 +1,6 @@ +import {EmptyFilter} from '../../../components/EmptyFilter/EmptyFilter'; import {VISIBLE_ENTITIES} from '../../../store/reducers/storage/constants'; import type {VisibleEntities} from '../../../store/reducers/storage/types'; -import {EmptyFilter} from '../EmptyFilter/EmptyFilter'; import i18n from './i18n'; diff --git a/src/containers/Storage/PaginatedStorageNodesTable/StorageNodesEmptyDataMessage.tsx b/src/containers/Storage/PaginatedStorageNodesTable/StorageNodesEmptyDataMessage.tsx index 2091614c4..c866c951b 100644 --- a/src/containers/Storage/PaginatedStorageNodesTable/StorageNodesEmptyDataMessage.tsx +++ b/src/containers/Storage/PaginatedStorageNodesTable/StorageNodesEmptyDataMessage.tsx @@ -1,7 +1,7 @@ +import {EmptyFilter} from '../../../components/EmptyFilter/EmptyFilter'; import {VISIBLE_ENTITIES} from '../../../store/reducers/storage/constants'; import type {VisibleEntities} from '../../../store/reducers/storage/types'; import {NodesUptimeFilterValues} from '../../../utils/nodes'; -import {EmptyFilter} from '../EmptyFilter/EmptyFilter'; import i18n from './i18n'; diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx index 168088b9b..5bca9439e 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx @@ -5,6 +5,7 @@ import {skipToken} from '@reduxjs/toolkit/query'; import {isNil} from 'lodash'; import {DrawerWrapper} from '../../../../components/Drawer'; +import {EmptyFilter} from '../../../../components/EmptyFilter/EmptyFilter'; import EnableFullscreenButton from '../../../../components/EnableFullscreenButton/EnableFullscreenButton'; import Fullscreen from '../../../../components/Fullscreen/Fullscreen'; import { @@ -19,7 +20,6 @@ import {useAutoRefreshInterval} from '../../../../utils/hooks'; import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; import {renderPaginatedTableErrorMessage} from '../../../../utils/renderPaginatedTableErrorMessage'; import {safeParseNumber} from '../../../../utils/utils'; -import {EmptyFilter} from '../../../Storage/EmptyFilter/EmptyFilter'; import {TopicDataControls} from './TopicDataControls/TopicDataControls'; import {TopicMessageDetails} from './TopicMessageDetails/TopicMessageDetails'; From bca7007b126bf677c6024b2356014777d52264de Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 30 May 2025 17:51:31 +0300 Subject: [PATCH 03/14] feat(Healthcheck): redesign --- src/components/Drawer/Drawer.scss | 2 +- src/components/Drawer/Drawer.tsx | 7 +- src/components/EmptyState/EmptyState.scss | 9 + src/components/EmptyState/EmptyState.tsx | 1 + .../EnableFullscreenButton.tsx | 10 +- src/components/Fullscreen/Fullscreen.scss | 2 +- .../HealthcheckStatus/HealthcheckStatus.tsx | 69 +++++++ src/components/HealthcheckStatus/i18n/en.json | 7 + .../HealthcheckStatus/i18n/index.ts | 7 + .../Healthcheck/Healthcheck.scss | 106 ----------- .../Healthcheck/HealthcheckDetails.tsx | 56 ------ .../Healthcheck/HealthcheckPreview.scss | 41 +++++ .../Healthcheck/HealthcheckPreview.tsx | 138 +++++++------- .../Healthcheck/IssuesViewer/IssueTree.scss | 51 ------ .../Healthcheck/IssuesViewer/IssueTree.tsx | 85 --------- .../IssueTreeItem/IssueTreeItem.scss | 50 ------ .../IssueTreeItem/IssueTreeItem.tsx | 25 --- .../IssuesViewer/IssueTreeItem/index.ts | 1 - .../TenantOverview/Healthcheck/i18n/en.json | 14 +- .../TenantOverview/Healthcheck/i18n/index.ts | 3 +- .../TenantOverview/Healthcheck/i18n/ru.json | 8 - .../MetricsCards/MetricsCards.tsx | 13 -- .../TenantOverview/TenantOverview.scss | 2 +- .../TenantOverview/TenantOverview.tsx | 21 ++- .../Tenant/Healthcheck/Healthcheck.scss | 78 ++++++++ .../Tenant/Healthcheck/Healthcheck.tsx | 108 +++++++++++ .../components/HealthcheckFilter.tsx | 15 ++ .../components/HealthcheckIssue.tsx | 79 ++++++++ .../ComputeLocation.tsx | 120 +++++++++++++ .../HealthcheckIssueDetails.tsx | 132 ++++++++++++++ .../HealthcheckIssueDetails/NodeInfo.tsx | 34 ++++ .../HealthcheckIssueDetails/PoolInfo.tsx | 24 +++ .../StorageLocation.tsx | 168 ++++++++++++++++++ .../HealthcheckIssueDetails/utils.tsx | 65 +++++++ .../components/HealthcheckIssueTabs.tsx | 77 ++++++++ .../components/HealthcheckIssues.tsx | 67 +++++++ .../components/HealthcheckRefresh.tsx | 75 ++++++++ .../components/HealthcheckView.tsx | 71 ++++++++ .../Tenant/Healthcheck/i18n/en.json | 52 ++++++ .../Tenant/Healthcheck/i18n/index.ts | 7 + src/containers/Tenant/Healthcheck/shared.ts | 50 ++++++ .../useHealthcheck.ts | 25 ++- src/containers/Tenant/Tenant.tsx | 89 ++++------ .../Tenant/TenantDrawerWrappers.tsx | 109 ++++++++++++ src/containers/Tenant/TenantPages.tsx | 2 + src/containers/Tenant/constants.ts | 28 +++ src/containers/Tenant/i18n/en.json | 9 +- src/containers/Tenant/useTenantQueryParams.ts | 59 ++++++ .../healthcheckInfo/healthcheckInfo.ts | 66 ++----- src/store/reducers/healthcheckInfo/types.ts | 2 + src/store/reducers/healthcheckInfo/utils.ts | 41 +++++ src/store/reducers/tenant/constants.ts | 1 - src/styles/index.scss | 3 + src/types/api/healthcheck.ts | 106 +++++++++-- src/uiFactory/types.ts | 12 ++ src/uiFactory/uiFactory.ts | 8 + 56 files changed, 1892 insertions(+), 618 deletions(-) create mode 100644 src/components/HealthcheckStatus/HealthcheckStatus.tsx create mode 100644 src/components/HealthcheckStatus/i18n/en.json create mode 100644 src/components/HealthcheckStatus/i18n/index.ts delete mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/Healthcheck.scss delete mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/HealthcheckDetails.tsx create mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/HealthcheckPreview.scss delete mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.scss delete mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.tsx delete mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTreeItem/IssueTreeItem.scss delete mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTreeItem/IssueTreeItem.tsx delete mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTreeItem/index.ts delete mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/ru.json create mode 100644 src/containers/Tenant/Healthcheck/Healthcheck.scss create mode 100644 src/containers/Tenant/Healthcheck/Healthcheck.tsx create mode 100644 src/containers/Tenant/Healthcheck/components/HealthcheckFilter.tsx create mode 100644 src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx create mode 100644 src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/ComputeLocation.tsx create mode 100644 src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/HealthcheckIssueDetails.tsx create mode 100644 src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.tsx create mode 100644 src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/PoolInfo.tsx create mode 100644 src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx create mode 100644 src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/utils.tsx create mode 100644 src/containers/Tenant/Healthcheck/components/HealthcheckIssueTabs.tsx create mode 100644 src/containers/Tenant/Healthcheck/components/HealthcheckIssues.tsx create mode 100644 src/containers/Tenant/Healthcheck/components/HealthcheckRefresh.tsx create mode 100644 src/containers/Tenant/Healthcheck/components/HealthcheckView.tsx create mode 100644 src/containers/Tenant/Healthcheck/i18n/en.json create mode 100644 src/containers/Tenant/Healthcheck/i18n/index.ts create mode 100644 src/containers/Tenant/Healthcheck/shared.ts rename src/containers/Tenant/{Diagnostics/TenantOverview => Healthcheck}/useHealthcheck.ts (51%) create mode 100644 src/containers/Tenant/TenantDrawerWrappers.tsx create mode 100644 src/containers/Tenant/constants.ts create mode 100644 src/containers/Tenant/useTenantQueryParams.ts diff --git a/src/components/Drawer/Drawer.scss b/src/components/Drawer/Drawer.scss index bbeff174d..10595ece6 100644 --- a/src/components/Drawer/Drawer.scss +++ b/src/components/Drawer/Drawer.scss @@ -8,7 +8,7 @@ } &__item { - z-index: 4; + z-index: calc(var(--ydb-drawer-veil-z-index) + 1); height: 100%; } diff --git a/src/components/Drawer/Drawer.tsx b/src/components/Drawer/Drawer.tsx index 95defbbcf..fcc558ede 100644 --- a/src/components/Drawer/Drawer.tsx +++ b/src/components/Drawer/Drawer.tsx @@ -32,6 +32,7 @@ interface DrawerPaneContentWrapperProps { detectClickOutside?: boolean; defaultWidth?: number; isPercentageWidth?: boolean; + showVeil?: boolean; } const DrawerPaneContentWrapper = ({ @@ -45,6 +46,7 @@ const DrawerPaneContentWrapper = ({ className, detectClickOutside = false, isPercentageWidth, + showVeil, }: DrawerPaneContentWrapperProps) => { const [drawerWidth, setDrawerWidth] = React.useState(() => { const savedWidth = localStorage.getItem(storageKey); @@ -113,7 +115,7 @@ const DrawerPaneContentWrapper = ({ { React.useEffect(() => { return () => { @@ -220,6 +224,7 @@ export const DrawerWrapper = ({ {children} - - + + + ); } diff --git a/src/components/Fullscreen/Fullscreen.scss b/src/components/Fullscreen/Fullscreen.scss index 75882ab41..efe27e699 100644 --- a/src/components/Fullscreen/Fullscreen.scss +++ b/src/components/Fullscreen/Fullscreen.scss @@ -6,7 +6,7 @@ // should expand to fill the content area, keeping aside navigation visible // counts on .gn-aside-header__content to have position: relative, it is set in App.scss position: absolute; - z-index: 10; + z-index: calc(var(--ydb-drawer-veil-z-index) + 3); inset: 0; background-color: var(--g-color-base-background); diff --git a/src/components/HealthcheckStatus/HealthcheckStatus.tsx b/src/components/HealthcheckStatus/HealthcheckStatus.tsx new file mode 100644 index 000000000..69a2eea45 --- /dev/null +++ b/src/components/HealthcheckStatus/HealthcheckStatus.tsx @@ -0,0 +1,69 @@ +import { + CircleCheck, + CircleExclamation, + CircleInfo, + PlugConnection, + TriangleExclamation, +} from '@gravity-ui/icons'; +import type {LabelProps} from '@gravity-ui/uikit'; +import {Icon, Label} from '@gravity-ui/uikit'; + +import {SelfCheckResult} from '../../types/api/healthcheck'; + +import i18n from './i18n'; + +const SelfCheckResultToLabelTheme: Record = { + [SelfCheckResult.GOOD]: 'success', + [SelfCheckResult.DEGRADED]: 'info', + [SelfCheckResult.MAINTENANCE_REQUIRED]: 'warning', + [SelfCheckResult.EMERGENCY]: 'danger', + [SelfCheckResult.UNSPECIFIED]: 'normal', +}; + +const SelfCheckResultToIcon: Record< + SelfCheckResult, + (props: React.SVGProps) => React.JSX.Element +> = { + [SelfCheckResult.GOOD]: CircleCheck, + [SelfCheckResult.DEGRADED]: CircleInfo, + [SelfCheckResult.MAINTENANCE_REQUIRED]: TriangleExclamation, + [SelfCheckResult.EMERGENCY]: CircleExclamation, + [SelfCheckResult.UNSPECIFIED]: PlugConnection, +}; + +const SelfCheckResultToText: Record = { + get [SelfCheckResult.GOOD]() { + return i18n('title_good'); + }, + get [SelfCheckResult.DEGRADED]() { + return i18n('title_degraded'); + }, + get [SelfCheckResult.MAINTENANCE_REQUIRED]() { + return i18n('title_maintenance'); + }, + get [SelfCheckResult.EMERGENCY]() { + return i18n('title_emergency'); + }, + get [SelfCheckResult.UNSPECIFIED]() { + return i18n('title_unspecified'); + }, +}; + +interface HealthcheckStatusProps { + status: SelfCheckResult; + size?: LabelProps['size']; +} + +export function HealthcheckStatus({status, size = 'm'}: HealthcheckStatusProps) { + const theme = SelfCheckResultToLabelTheme[status]; + + return ( + + ); +} diff --git a/src/components/HealthcheckStatus/i18n/en.json b/src/components/HealthcheckStatus/i18n/en.json new file mode 100644 index 000000000..716d60dcd --- /dev/null +++ b/src/components/HealthcheckStatus/i18n/en.json @@ -0,0 +1,7 @@ +{ + "title_degraded": "Degraded", + "title_emergency": "Emergency", + "title_maintenance": "Maintenance required", + "title_good": "Good", + "title_unspecified": "Unspecified" +} diff --git a/src/components/HealthcheckStatus/i18n/index.ts b/src/components/HealthcheckStatus/i18n/index.ts new file mode 100644 index 000000000..a44218ead --- /dev/null +++ b/src/components/HealthcheckStatus/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-healthcheck-status'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/Healthcheck.scss b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/Healthcheck.scss deleted file mode 100644 index f9bb6b523..000000000 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/Healthcheck.scss +++ /dev/null @@ -1,106 +0,0 @@ -@use '../../../../../styles/mixins.scss'; - -.healthcheck { - $block: &; - - &__details { - width: 872px; - } - - &__details-content-wrapper { - overflow-x: hidden; - } - - &__preview { - display: flex; - flex-direction: column; - - height: 100%; - } - - &__preview-title { - font-weight: 600; - - color: var(--g-color-text-link); - - @include mixins.lead-typography(); - } - - &__preview-content { - margin: auto; - - line-height: 24px; - } - - &__preview-status-icon { - width: 64px; - height: 64px; - } - - &__preview-title-wrapper { - display: flex; - align-items: center; - gap: 8px; - - margin-bottom: 4px; - } - - &__preview-issue { - position: relative; - top: -8px; - - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - - &_good { - color: var(--g-color-text-positive); - & #{$block}__self-check-status-indicator { - background-color: var(--g-color-base-positive-light); - } - } - &_degraded { - color: var(--g-color-text-info); - & #{$block}__self-check-status-indicator { - background-color: var(--g-color-base-info-light); - } - } - &_emergency { - color: var(--g-color-text-danger); - & #{$block}__self-check-status-indicator { - background-color: var(--g-color-base-danger-light); - } - } - &_unspecified { - color: var(--g-color-text-misc); - & #{$block}__self-check-status-indicator { - background-color: var(--g-color-base-misc-light); - } - } - &_maintenance_required { - color: var(--g-color-text-warning-heavy); - & #{$block}__self-check-status-indicator { - background-color: var(--g-color-base-warning-light); - } - } - } - - &__self-check-status-indicator { - display: inline-block; - - padding: 0 8px; - - font-size: 13px; - line-height: 24px; - text-wrap: nowrap; - - border-radius: 4px; - } - &__icon-warn { - color: var(--g-color-text-warning); - } - &__icon-wrapper { - display: flex; - } -} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/HealthcheckDetails.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/HealthcheckDetails.tsx deleted file mode 100644 index e6bc168d3..000000000 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/HealthcheckDetails.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; - -import {ResponseError} from '../../../../../components/Errors/ResponseError'; -import {Loader} from '../../../../../components/Loader'; -import {useClusterBaseInfo} from '../../../../../store/reducers/cluster/cluster'; -import {cn} from '../../../../../utils/cn'; -import {useAutoRefreshInterval} from '../../../../../utils/hooks'; -import {useHealthcheck} from '../useHealthcheck'; - -import IssueTree from './IssuesViewer/IssueTree'; -import i18n from './i18n'; - -import './Healthcheck.scss'; - -const b = cn('healthcheck'); - -interface HealthcheckDetailsProps { - tenantName: string; -} - -export function HealthcheckDetails({tenantName}: HealthcheckDetailsProps) { - const [autoRefreshInterval] = useAutoRefreshInterval(); - const {name} = useClusterBaseInfo(); - const {issueTrees, loading, error} = useHealthcheck(tenantName, { - //FIXME https://github.com/ydb-platform/ydb-embedded-ui/issues/1889 - autorefresh: name === 'ydb_ru' ? undefined : autoRefreshInterval, - }); - - const renderContent = () => { - if (error) { - return ; - } - - if (loading) { - return ; - } - - if (!issueTrees || !issueTrees.length) { - return i18n('status_message.ok'); - } - - return ( - - {issueTrees.map((issueTree) => ( - - ))} - - ); - }; - - return ( -
-
{renderContent()}
-
- ); -} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/HealthcheckPreview.scss b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/HealthcheckPreview.scss new file mode 100644 index 000000000..f5b75ce77 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/HealthcheckPreview.scss @@ -0,0 +1,41 @@ +.ydb-healthcheck-preview { + $block: &; + + &__icon-wrapper { + display: inline-flex; + + color: var(--g-color-text-warning); + } + + &__skeleton { + width: 100%; + height: 60px; + } + + &__alert-message { + height: 30px; + } + + &__icon { + display: flex; + + height: 100%; + margin: auto; + + &_good { + color: var(--g-color-text-positive); + } + &_degraded { + color: var(--g-color-text-info); + } + &_emergency { + color: var(--g-color-text-danger); + } + &_unspecified { + color: var(--g-color-text-misc); + } + &_maintenance_required { + color: var(--g-color-text-warning-heavy); + } + } +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/HealthcheckPreview.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/HealthcheckPreview.tsx index a5ea1043e..ad18e975a 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/HealthcheckPreview.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/HealthcheckPreview.tsx @@ -1,53 +1,46 @@ import React from 'react'; -import { - CircleCheck, - CircleInfo, - CircleQuestion, - CircleXmark, - TriangleExclamationFill, -} from '@gravity-ui/icons'; -import type {IconData} from '@gravity-ui/uikit'; -import {Icon, Popover} from '@gravity-ui/uikit'; - -import {DiagnosticCard} from '../../../../../components/DiagnosticCard/DiagnosticCard'; +import type {AlertProps} from '@gravity-ui/uikit'; +import {Alert, Button, Flex, Icon, Popover, Skeleton} from '@gravity-ui/uikit'; + import {ResponseError} from '../../../../../components/Errors/ResponseError'; -import {Loader} from '../../../../../components/Loader'; import {useClusterBaseInfo} from '../../../../../store/reducers/cluster/cluster'; import {healthcheckApi} from '../../../../../store/reducers/healthcheckInfo/healthcheckInfo'; import {SelfCheckResult} from '../../../../../types/api/healthcheck'; import {cn} from '../../../../../utils/cn'; -import {useAutoRefreshInterval, useTypedSelector} from '../../../../../utils/hooks'; +import {useAutoRefreshInterval} from '../../../../../utils/hooks'; +import {HEALTHCHECK_RESULT_TO_ICON, HEALTHCHECK_RESULT_TO_TEXT} from '../../../constants'; +import {useTenantQueryParams} from '../../../useTenantQueryParams'; import i18n from './i18n'; import CircleExclamationIcon from '@gravity-ui/icons/svgs/circle-exclamation.svg'; -import './Healthcheck.scss'; +import './HealthcheckPreview.scss'; -const b = cn('healthcheck'); +const b = cn('ydb-healthcheck-preview'); interface HealthcheckPreviewProps { tenantName: string; active?: boolean; } -const icons: Record = { - [SelfCheckResult.UNSPECIFIED]: CircleQuestion, - [SelfCheckResult.GOOD]: CircleCheck, - [SelfCheckResult.DEGRADED]: CircleInfo, - [SelfCheckResult.MAINTENANCE_REQUIRED]: CircleXmark, - [SelfCheckResult.EMERGENCY]: TriangleExclamationFill, +const checkResultToAlertTheme: Record = { + [SelfCheckResult.UNSPECIFIED]: 'normal', + [SelfCheckResult.GOOD]: 'success', + [SelfCheckResult.DEGRADED]: 'info', + [SelfCheckResult.MAINTENANCE_REQUIRED]: 'warning', + [SelfCheckResult.EMERGENCY]: 'danger', }; export function HealthcheckPreview(props: HealthcheckPreviewProps) { - const {tenantName, active} = props; + const {tenantName} = props; const [autoRefreshInterval] = useAutoRefreshInterval(); - const {metricsTab} = useTypedSelector((state) => state.tenant); - const {name} = useClusterBaseInfo(); + const {handleShowHealthcheckChange} = useTenantQueryParams(); + const healthcheckPreviewDisabled = name === 'ydb_ru'; const { @@ -67,10 +60,10 @@ export function HealthcheckPreview(props: HealthcheckPreviewProps) { healthcheckApi.useLazyGetHealthcheckInfoQuery(); React.useEffect(() => { - if (metricsTab === 'healthcheck' && healthcheckPreviewDisabled) { + if (healthcheckPreviewDisabled) { getHealthcheckQuery({database: tenantName}); } - }, [metricsTab, healthcheckPreviewDisabled, tenantName, getHealthcheckQuery]); + }, [healthcheckPreviewDisabled, tenantName, getHealthcheckQuery]); React.useEffect(() => { const fetchHealthcheck = () => { @@ -87,60 +80,69 @@ export function HealthcheckPreview(props: HealthcheckPreviewProps) { const loading = (isFetching && data === undefined) || (isFetchingManually && manualData === undefined); - const renderHeader = () => { + const selfCheckResult: SelfCheckResult = + data?.self_check_result || manualData?.self_check_result || SelfCheckResult.UNSPECIFIED; + + const modifier = selfCheckResult.toLowerCase(); + + if (loading) { + return ; + } + + const issuesCount = data?.issue_log?.filter((issue) => !issue.reason).length; + + const issuesText = issuesCount ? i18n('description_problems', {count: issuesCount}) : ''; + + const renderAlertMessage = () => { + if (error) { + return ; + } return ( -
-
-
{i18n('title.healthcheck')}
- {/* FIXME https://github.com/ydb-platform/ydb-embedded-ui/issues/1889 */} + + + {HEALTHCHECK_RESULT_TO_TEXT[selfCheckResult]} + {issuesText ? ` ${issuesText}` : ''} {healthcheckPreviewDisabled ? ( - {() => ( - - )} + {() => } ) : null} -
-
- ); - }; - - const renderContent = () => { - if (error) { - return ; - } - - if (loading) { - return ; - } - - const selfCheckResult = - data?.self_check_result || manualData?.self_check_result || SelfCheckResult.UNSPECIFIED; - const modifier = selfCheckResult.toLowerCase(); - return ( -
-
- -
- {selfCheckResult.replace(/_/g, ' ')} -
-
-
+ + {issuesCount && ( + + )} + ); }; return ( - - {renderHeader()} - {renderContent()} - + + } + /> ); } diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.scss b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.scss deleted file mode 100644 index ec576b391..000000000 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.scss +++ /dev/null @@ -1,51 +0,0 @@ -@use '../../../../../../styles/mixins.scss'; - -.issue-tree { - display: flex; - - &__block { - width: 100%; - } - - &__checkbox { - margin: 5px 0 10px; - } - - &__info-panel { - position: sticky; - - height: 100%; - margin: 11px 0; - padding: 8px 20px; - - border-radius: 4px; - background: var(--g-color-base-generic); - - .ydb-json-viewer { - --toolbar-background-color: var(--g-color-base-simple-hover-solid); - } - } - - .ydb-tree-view { - $calculated-margin: calc(24px * var(--ydb-tree-view-level)); - - &__item { - height: 40px; - } - - .tree-view_arrow { - width: 40px; - height: 40px; - } - - // Without !important this class does not have enough weight compared to styles set in TreeView - .ydb-tree-view__item { - margin-left: $calculated-margin !important; - padding-left: 0 !important; - } - - .issue-tree__info-panel { - margin-left: $calculated-margin; - } - } -} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.tsx deleted file mode 100644 index 94ab34b9c..000000000 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; - -import _omit from 'lodash/omit'; -import {TreeView} from 'ydb-ui-components'; - -import {JsonViewer} from '../../../../../../components/JsonViewer/JsonViewer'; -import {unipikaConvert} from '../../../../../../components/JsonViewer/unipika/unipika'; -import type {IssuesTree} from '../../../../../../store/reducers/healthcheckInfo/types'; -import {hcStatusToColorFlag} from '../../../../../../store/reducers/healthcheckInfo/utils'; -import {cn} from '../../../../../../utils/cn'; - -import {IssueTreeItem} from './IssueTreeItem'; - -import './IssueTree.scss'; - -const b = cn('issue-tree'); - -interface IssuesViewerProps { - issueTree: IssuesTree; -} - -const IssueTree = ({issueTree}: IssuesViewerProps) => { - const [collapsedIssues, setCollapsedIssues] = React.useState>({}); - - const renderInfoPanel = React.useCallback((info?: object) => { - if (!info) { - return null; - } - - return ( -
- -
- ); - }, []); - - const renderTree = React.useCallback( - (data: IssuesTree[]) => { - return data.map((item) => { - const {id} = item; - const {status, message, type, reasonsItems, level, ...rest} = item; - - const isCollapsed = - typeof collapsedIssues[id] === 'undefined' || collapsedIssues[id]; - - const toggleCollapsed = () => { - setCollapsedIssues((collapsedIssues) => ({ - ...collapsedIssues, - [id]: !isCollapsed, - })); - }; - - return ( - - } - collapsed={isCollapsed} - hasArrow={true} - onClick={toggleCollapsed} - onArrowClick={toggleCollapsed} - level={level - 1} - > - {renderInfoPanel(_omit(rest, ['reason']))} - {renderTree(reasonsItems || [])} - - ); - }); - }, - [collapsedIssues, renderInfoPanel], - ); - - return ( -
-
{renderTree([issueTree])}
-
- ); -}; - -export default IssueTree; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTreeItem/IssueTreeItem.scss b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTreeItem/IssueTreeItem.scss deleted file mode 100644 index b6c58d107..000000000 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTreeItem/IssueTreeItem.scss +++ /dev/null @@ -1,50 +0,0 @@ -.issue-tree-item { - display: flex; - justify-content: space-between; - align-items: center; - - height: 40px; - - cursor: pointer; - - &__field { - display: flex; - overflow: hidden; - - &_status { - display: flex; - - white-space: nowrap; - } - &_additional { - width: max-content; - - cursor: pointer; - - color: var(--g-color-text-link); - - &:hover { - color: var(--g-color-text-link-hover); - } - } - &_message { - overflow: hidden; - flex-shrink: 0; - - width: 300px; - - white-space: normal; - } - } - - &__field-tooltip { - &#{&} { - min-width: 500px; - max-width: 500px; - } - } - - &__field-label { - color: var(--g-color-text-secondary); - } -} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTreeItem/IssueTreeItem.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTreeItem/IssueTreeItem.tsx deleted file mode 100644 index 1dac61e59..000000000 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTreeItem/IssueTreeItem.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import {EntityStatus} from '../../../../../../../components/EntityStatus/EntityStatus'; -import type {EFlag} from '../../../../../../../types/api/enums'; -import {cn} from '../../../../../../../utils/cn'; - -import './IssueTreeItem.scss'; - -const b = cn('issue-tree-item'); - -interface IssueRowProps { - status: EFlag; - message: string; - type: string; - onClick?: VoidFunction; -} - -export const IssueTreeItem = ({status, message, type, onClick}: IssueRowProps) => { - return ( -
-
- -
-
{message}
-
- ); -}; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTreeItem/index.ts b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTreeItem/index.ts deleted file mode 100644 index 070f94e44..000000000 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTreeItem/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './IssueTreeItem'; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/en.json b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/en.json index 440ae011a..880d2fc70 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/en.json @@ -1,8 +1,10 @@ { - "title.healthcheck": "Healthcheck", - "label.update": "Update", - "label.show-details": "Show details", - "label.issues": "Issues:", - "status_message.ok": "No issues", - "no-data": "no healthcheck data" + "description_problems": [ + "There is {{count}} issues.", + "There are {{count}} issues.", + "There are {{count}} issues.", + "There are {{count}} issues." + ], + "action_review-issues": "Review issues", + "description_no-data": "No healthcheck data" } diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/index.ts b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/index.ts index b0fa69fde..2902318e6 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/index.ts +++ b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/index.ts @@ -1,8 +1,7 @@ import {registerKeysets} from '../../../../../../utils/i18n'; import en from './en.json'; -import ru from './ru.json'; const COMPONENT = 'ydb-diagnostics-healthcheck'; -export default registerKeysets(COMPONENT, {ru, en}); +export default registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/ru.json b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/ru.json deleted file mode 100644 index b3c3de530..000000000 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/ru.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "title.healthcheck": "Healthcheck", - "label.update": "Обновить", - "label.show-details": "Посмотреть подробности", - "label.issues": "Проблемы:", - "status_message.ok": "Нет проблем", - "no-data": "нет данных healthcheck" -} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx index 609cdc31e..ba8904cc4 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx @@ -18,7 +18,6 @@ import {cn} from '../../../../../utils/cn'; import {formatStorageValues} from '../../../../../utils/dataFormatters/dataFormatters'; import {useTypedSelector} from '../../../../../utils/hooks'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; -import {HealthcheckPreview} from '../Healthcheck/HealthcheckPreview'; import i18n from '../i18n'; import type {DiagnosticsCardMetric} from './MetricCard/MetricCard'; @@ -42,7 +41,6 @@ interface MetricsCardsProps { memoryStats?: TenantMetricStats[]; blobStorageStats?: TenantStorageStats[]; tabletStorageStats?: TenantStorageStats[]; - tenantName: string; } export function MetricsCards({ @@ -50,7 +48,6 @@ export function MetricsCards({ memoryStats, blobStorageStats, tabletStorageStats, - tenantName, }: MetricsCardsProps) { const location = useLocation(); @@ -80,10 +77,6 @@ export function MetricsCards({ ...queryParams, [TenantTabsGroups.metricsTab]: getTabIfNotActive(TENANT_METRICS_TABS_IDS.memory), }), - [TENANT_METRICS_TABS_IDS.healthcheck]: getTenantPath({ - ...queryParams, - [TenantTabsGroups.metricsTab]: getTabIfNotActive(TENANT_METRICS_TABS_IDS.healthcheck), - }), }; return ( @@ -107,12 +100,6 @@ export function MetricsCards({ active={metricsTab === TENANT_METRICS_TABS_IDS.memory} /> - - -
); } diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss index d1d226d2d..3faa57b27 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss @@ -31,7 +31,7 @@ position: sticky; left: 0; - width: max-content; + width: 100%; } &__title { diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx index fb1085c41..75f80acf3 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx @@ -15,7 +15,7 @@ import {useClusterNameFromQuery} from '../../../../utils/hooks/useDatabaseFromQu import {mapDatabaseTypeToDBName} from '../../utils/schema'; import {DefaultOverviewContent} from './DefaultOverviewContent/DefaultOverviewContent'; -import {HealthcheckDetails} from './Healthcheck/HealthcheckDetails'; +import {HealthcheckPreview} from './Healthcheck/HealthcheckPreview'; import {MetricsCards} from './MetricsCards/MetricsCards'; import {TenantCpu} from './TenantCpu/TenantCpu'; import {TenantMemory} from './TenantMemory/TenantMemory'; @@ -134,9 +134,6 @@ export function TenantOverview({ /> ); } - case TENANT_METRICS_TABS_IDS.healthcheck: { - return ; - } default: { return ; } @@ -158,13 +155,15 @@ export function TenantOverview({ {logsLink && } - + + + + {renderTabContent()} diff --git a/src/containers/Tenant/Healthcheck/Healthcheck.scss b/src/containers/Tenant/Healthcheck/Healthcheck.scss new file mode 100644 index 000000000..89e622cd3 --- /dev/null +++ b/src/containers/Tenant/Healthcheck/Healthcheck.scss @@ -0,0 +1,78 @@ +.ydb-healthcheck { + &__stub-wrapper { + margin: auto; + } + &__control-wrapper { + width: max-content; + } + &__controls { + position: sticky; + z-index: 1; + top: 0px; + left: 0; + + padding: var(--g-spacing-4); + + background-color: var(--g-color-base-background); + } + &__controls_fullscreen { + padding-right: calc(var(--g-spacing-7) * 2); + } + &__issues { + padding: 0 var(--g-spacing-4) var(--g-spacing-4) var(--g-spacing-4); + } + &__issue-wrapper { + border: 1px solid var(--g-color-line-generic); + border-radius: var(--g-border-radius-s); + } + &__issue-summary { + padding: var(--g-spacing-4); + + cursor: pointer; + + border-radius: var(--g-border-radius-s); + + &:hover { + background-color: var(--g-color-base-generic-ultralight); + } + } + &__issue-content { + --g-definition-list-item-gap: var(--g-spacing-2); + width: 100%; + } + &__issue-divider { + height: 28px; + } + &__issue-tab { + cursor: pointer; + &_active { + color: var(--g-color-text-primary); + } + } + + &__tab-status { + height: 6px; + + border-radius: 50%; + background-color: var(--g-color-text-misc); + aspect-ratio: 1; + } + &__tab-status_green { + background-color: var(--g-color-text-positive); + } + &__tab-status_blue { + background-color: var(--g-color-text-info); + } + &__tab-status_yellow { + background-color: var(--g-color-text-warning); + } + &__tab-status_orange { + background-color: var(--g-color-text-warning-heavy); + } + &__tab-status_red { + background-color: var(--g-color-text-danger); + } + &__issue-details { + padding: 0 var(--g-spacing-4) var(--g-spacing-4) var(--g-spacing-4); + } +} diff --git a/src/containers/Tenant/Healthcheck/Healthcheck.tsx b/src/containers/Tenant/Healthcheck/Healthcheck.tsx new file mode 100644 index 000000000..294e939f2 --- /dev/null +++ b/src/containers/Tenant/Healthcheck/Healthcheck.tsx @@ -0,0 +1,108 @@ +import React from 'react'; + +import {Flex, Icon} from '@gravity-ui/uikit'; + +import {ResponseError} from '../../../components/Errors/ResponseError'; +import Fullscreen from '../../../components/Fullscreen/Fullscreen'; +import {HealthcheckStatus} from '../../../components/HealthcheckStatus/HealthcheckStatus'; +import {Illustration} from '../../../components/Illustration'; +import {Loader} from '../../../components/Loader'; +import {useClusterBaseInfo} from '../../../store/reducers/cluster/cluster'; +import type {IssuesTree} from '../../../store/reducers/healthcheckInfo/types'; +import {SelfCheckResult} from '../../../types/api/healthcheck'; +import {uiFactory} from '../../../uiFactory/uiFactory'; +import {useAutoRefreshInterval, useTypedSelector} from '../../../utils/hooks'; +import {HEALTHCHECK_RESULT_TO_TEXT} from '../constants'; + +import {HealthcheckFilter} from './components/HealthcheckFilter'; +import {Issues} from './components/HealthcheckIssues'; +import {HealthcheckRefresh} from './components/HealthcheckRefresh'; +import {HealthcheckView} from './components/HealthcheckView'; +import i18n from './i18n'; +import type {CommonIssueType} from './shared'; +import {b} from './shared'; +import {useHealthcheck} from './useHealthcheck'; + +import cryCatIcon from '../../../assets/icons/cry-cat.svg'; + +import './Healthcheck.scss'; + +interface HealthcheckDetailsProps { + tenantName: string; + countIssueTypes?: ( + issueTrees: IssuesTree[], + ) => Record & Record; +} + +export function Healthcheck({ + tenantName, + countIssueTypes = uiFactory.countHealthcheckIssuesByType, +}: HealthcheckDetailsProps) { + const [autoRefreshInterval] = useAutoRefreshInterval(); + const fullscreen = useTypedSelector((state) => state.fullscreen); + const {name} = useClusterBaseInfo(); + const {loading, error, selfCheckResult, fulfilledTimeStamp, leavesIssues, refetch} = + useHealthcheck(tenantName, { + //FIXME https://github.com/ydb-platform/ydb-embedded-ui/issues/1889 + autorefresh: name === 'ydb_ru' ? undefined : autoRefreshInterval, + }); + + const issuesCount = React.useMemo( + () => countIssueTypes(leavesIssues), + [leavesIssues, countIssueTypes], + ); + + const renderControls = () => { + return ( + + + + + + + + + ); + }; + + const renderContent = () => { + if (error) { + return ( + + + + + ); + } + + if (loading) { + return ; + } + + if (selfCheckResult === SelfCheckResult.GOOD && (!leavesIssues || !leavesIssues.length)) { + return ( + + + {HEALTHCHECK_RESULT_TO_TEXT[selfCheckResult]} + + ); + } + + return ( + + {renderControls()} + + + + + ); + }; + + return ( + + + {renderContent()} + + + ); +} diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckFilter.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckFilter.tsx new file mode 100644 index 000000000..454a100b8 --- /dev/null +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckFilter.tsx @@ -0,0 +1,15 @@ +import {Search} from '../../../../components/Search'; +import {useTenantQueryParams} from '../../useTenantQueryParams'; +import i18n from '../i18n'; + +export function HealthcheckFilter() { + const {issuesFilter, handleIssuesFilterChange} = useTenantQueryParams(); + return ( + + ); +} diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx new file mode 100644 index 000000000..ae59f1fcd --- /dev/null +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx @@ -0,0 +1,79 @@ +import React from 'react'; + +import {ArrowToggle, Disclosure, Divider, Flex, Text} from '@gravity-ui/uikit'; + +import {EntityStatus} from '../../../../components/EntityStatusNew/EntityStatus'; +import type {IssuesTree} from '../../../../store/reducers/healthcheckInfo/types'; +import {hcStatusToColorFlag} from '../../../../store/reducers/healthcheckInfo/utils'; +import {b} from '../shared'; + +import {IssueDetails} from './HealthcheckIssueDetails/HealthcheckIssueDetails'; +import {HealthcheckIssueTabs} from './HealthcheckIssueTabs'; + +interface HealthcheckIssueProps { + issue: IssuesTree; +} + +export function HealthcheckIssue({issue}: HealthcheckIssueProps) { + const [selectedTab, setSelectedTab] = React.useState(issue.id); + const parents = React.useMemo(() => { + const parents = []; + let current: IssuesTree | undefined = issue; + while (current) { + parents.push(current); + current = current.parent; + } + return parents.reverse(); + }, [issue]); + + const currentIssue = React.useMemo(() => { + return parents.find((parent) => parent.id === selectedTab); + }, [parents, selectedTab]); + + return ( + + + + {(props) => ( +
+ + + {issue.message} + + {issue.status && ( +
+ +
+ )} +
+ + + + +
+
+ )} +
+ + + {currentIssue && } + +
+
+ ); +} diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/ComputeLocation.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/ComputeLocation.tsx new file mode 100644 index 000000000..64f8fee7a --- /dev/null +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/ComputeLocation.tsx @@ -0,0 +1,120 @@ +import React from 'react'; + +import {Flex} from '@gravity-ui/uikit'; +import {isEmpty} from 'lodash'; + +import {InternalLink} from '../../../../../components/InternalLink'; +import {getTabletPagePath} from '../../../../../routes'; +import type {Location} from '../../../../../types/api/healthcheck'; +import {useTenantQueryParams} from '../../../useTenantQueryParams'; +import i18n from '../../i18n'; + +import {NodeInfo} from './NodeInfo'; +import {PoolInfo} from './PoolInfo'; +import {IdList, LocationDetails, SectionWithTitle} from './utils'; + +export type LocationFieldCompute = 'tablet' | 'schema' | 'node' | 'pool'; + +type ComputeLocationType = Location['compute']; + +const LocationFieldRenderer: Record< + LocationFieldCompute, + (location: ComputeLocationType) => React.ReactNode +> = { + node: (location: ComputeLocationType) => , + pool: (location: ComputeLocationType) => , + tablet: (location: ComputeLocationType) => , + schema: (location: ComputeLocationType) => , +}; + +interface ComputeLocationProps { + location: ComputeLocationType; + hiddenFields?: LocationFieldCompute[]; +} + +export function ComputeLocation({location, hiddenFields = []}: ComputeLocationProps) { + const {node, tablet, schema, pool} = location ?? {}; + + const fields = React.useMemo(() => { + const fields: LocationFieldCompute[] = []; + if (node) { + fields.push('node'); + } + if (pool) { + fields.push('pool'); + } + if (tablet) { + fields.push('tablet'); + } + if (schema) { + fields.push('schema'); + } + return fields.filter((field) => !hiddenFields.includes(field)); + }, [node, pool, tablet, schema, hiddenFields]); + + if (!location || isEmpty(location) || fields.length === 0) { + return null; + } + + return ( + + + {fields.map((field) => LocationFieldRenderer[field](location))} + + + ); +} + +interface ComputeSectionProps { + location?: ComputeLocationType; +} + +function TabletInfo({location}: ComputeSectionProps) { + const {tablet} = location ?? {}; + const {database} = useTenantQueryParams(); + + if (!tablet) { + return null; + } + + return ( + ( + + {id} + + )} + /> + ) : undefined, + title: i18n('label_tablet-id'), + }, + {value: tablet.type, title: i18n('label_tablet-type')}, + {value: tablet.count, title: i18n('label_tablet-count')}, + ]} + /> + ); +} + +function SchemaInfo({location}: ComputeSectionProps) { + const {schema} = location ?? {}; + + if (!schema) { + return null; + } + + return ( + + ); +} diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/HealthcheckIssueDetails.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/HealthcheckIssueDetails.tsx new file mode 100644 index 000000000..a2a0b2df5 --- /dev/null +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/HealthcheckIssueDetails.tsx @@ -0,0 +1,132 @@ +import React from 'react'; + +import {Flex} from '@gravity-ui/uikit'; +import {isEmpty} from 'lodash'; + +import {InternalLink} from '../../../../../components/InternalLink'; +import {getTabletPagePath} from '../../../../../routes'; +import type {IssuesTree} from '../../../../../store/reducers/healthcheckInfo/types'; +import type {Location} from '../../../../../types/api/healthcheck'; +import {useTenantQueryParams} from '../../../useTenantQueryParams'; +import i18n from '../../i18n'; + +import {ComputeLocation} from './ComputeLocation'; +import type {LocationFieldCompute} from './ComputeLocation'; +import {NodeInfo} from './NodeInfo'; +import type {LocationFieldStorage} from './StorageLocation'; +import {StorageLocation} from './StorageLocation'; +import {IdList, LocationDetails, SectionWithTitle} from './utils'; + +interface HealthcheckIssueDetailsProps { + issue: IssuesTree; +} + +export function IssueDetails({issue}: HealthcheckIssueDetailsProps) { + const {database} = useTenantQueryParams(); + const {location} = issue; + + const {detailsFields, hiddenStorageFields, hiddenComputeFields} = React.useMemo(() => { + const hiddenStorageFields: LocationFieldStorage[] = []; + const hiddenComputeFields: LocationFieldCompute[] = []; + const fields: {value?: React.ReactNode; title: string}[] = [ + {value: issue.message, title: i18n('label_description')}, + ]; + if (issue.type === 'STORAGE_POOL') { + fields.push({ + value: issue.location?.storage?.pool?.name, + title: `${i18n('label_pool')} ${i18n('label_pool-name')}`, + }); + hiddenStorageFields.push('pool'); + } + if (issue.type === 'TABLET') { + const tablet = issue.location?.compute?.tablet; + fields.push( + { + value: tablet?.id?.length ? ( + ( + + {id} + + )} + /> + ) : undefined, + title: `${i18n('label_tablet')} ${i18n('label_tablet-id')}`, + }, + { + value: tablet?.type, + title: `${i18n('label_tablet')} ${i18n('label_tablet-type')}`, + }, + { + value: tablet?.count, + title: `${i18n('label_tablet')} ${i18n('label_tablet-count')}`, + }, + ); + hiddenComputeFields.push('tablet'); + } + return {detailsFields: fields, hiddenComputeFields, hiddenStorageFields}; + }, [issue, database]); + + return ( + + + + + + + + + + + ); +} + +interface DatabaseLocationProps { + location: Location['database']; +} + +function DatabaseLocation({location}: DatabaseLocationProps) { + if (!location || !location.name) { + return null; + } + + return ( + + ); +} + +interface NodeLocationProps { + location: Location['node']; +} + +function NodeLocation({location}: NodeLocationProps) { + if (!location || isEmpty(location)) { + return null; + } + + return ( + + + + ); +} + +interface PeerLocationProps { + location: Location['peer']; +} + +function PeerLocation({location}: PeerLocationProps) { + if (!location || isEmpty(location)) { + return null; + } + + return ( + + + + ); +} diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.tsx new file mode 100644 index 000000000..e5a0041b5 --- /dev/null +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.tsx @@ -0,0 +1,34 @@ +import {InternalLink} from '../../../../../components/InternalLink'; +import type {Location} from '../../../../../types/api/healthcheck'; +import {getDefaultNodePath} from '../../../../Node/NodePages'; +import {useTenantQueryParams} from '../../../useTenantQueryParams'; +import i18n from '../../i18n'; + +import {LocationDetails} from './utils'; + +interface NodeInfoProps { + node?: Location['node']; + title?: string; +} + +export function NodeInfo({node, title = i18n('label_node')}: NodeInfoProps) { + const {database} = useTenantQueryParams(); + if (!node) { + return null; + } + + const nodeLink = node.id ? ( + {node.id} + ) : undefined; + + return ( + + ); +} diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/PoolInfo.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/PoolInfo.tsx new file mode 100644 index 000000000..5d6dfba89 --- /dev/null +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/PoolInfo.tsx @@ -0,0 +1,24 @@ +import type {Location} from '../../../../../types/api/healthcheck'; +import i18n from '../../i18n'; + +import {LocationDetails} from './utils'; + +interface PoolInfoProps { + location?: Location['storage'] | Location['compute']; +} + +export function PoolInfo({location}: PoolInfoProps) { + const {pool} = location ?? {}; + const {name} = pool ?? {}; + + if (!name) { + return null; + } + + return ( + + ); +} diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx new file mode 100644 index 000000000..48b16c82c --- /dev/null +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx @@ -0,0 +1,168 @@ +import React from 'react'; + +import {Flex} from '@gravity-ui/uikit'; +import {isEmpty} from 'lodash'; + +import {InternalLink} from '../../../../../components/InternalLink'; +import {getPDiskPagePath, getVDiskPagePath} from '../../../../../routes'; +import type {Location} from '../../../../../types/api/healthcheck'; +import i18n from '../../i18n'; + +import {NodeInfo} from './NodeInfo'; +import {PoolInfo} from './PoolInfo'; +import {IdList, LocationDetails, SectionWithTitle} from './utils'; + +export type LocationFieldStorage = 'pool' | 'node' | 'pdisk' | 'vdisk' | 'group'; + +type StorageLocationType = Location['storage']; + +const LocationFieldRenderer: Record< + LocationFieldStorage, + (location: StorageLocationType) => React.ReactNode +> = { + node: (location: StorageLocationType) => , + pool: (location: StorageLocationType) => , + group: (location: StorageLocationType) => , + vdisk: (location: StorageLocationType) => , + pdisk: (location: StorageLocationType) => , +}; + +interface StorageLocationProps { + location: StorageLocationType; + hiddenFields?: LocationFieldStorage[]; +} + +export function StorageLocation({location, hiddenFields = []}: StorageLocationProps) { + const {node, pool} = location ?? {}; + const {group} = pool ?? {}; + const {vdisk} = group ?? {}; + const {pdisk} = vdisk ?? {}; + + const fields = React.useMemo(() => { + const fields: LocationFieldStorage[] = []; + if (node) { + fields.push('node'); + } + if (pool) { + fields.push('pool'); + } + if (group) { + fields.push('group'); + } + if (vdisk) { + fields.push('vdisk'); + } + if (pdisk) { + fields.push('pdisk'); + } + return fields.filter((field) => !hiddenFields.includes(field)); + }, [node, pool, group, vdisk, pdisk, hiddenFields]); + + if (!location || isEmpty(location) || fields.length === 0) { + return null; + } + + return ( + + + {fields.map((field) => LocationFieldRenderer[field](location))} + + + ); +} + +interface StorageSectionProps { + location?: Location['storage']; +} + +function GroupInfo({location}: StorageSectionProps) { + const {pool} = location ?? {}; + const {group} = pool ?? {}; + + const ids = group?.id; + + if (!ids?.length) { + return null; + } + + return ( + : undefined, + title: i18n('label_group-id'), + }, + ]} + /> + ); +} + +function VDiskInfo({location}: StorageSectionProps) { + const {node, pool} = location ?? {}; + const {group} = pool ?? {}; + const {vdisk} = group ?? {}; + + const ids = vdisk?.id; + + if (!ids?.length) { + return null; + } + + return ( + ( + + {id} + + )} + /> + ) : undefined, + title: i18n('label_vdisk-id'), + }, + ]} + /> + ); +} +function PDiskInfo({location}: StorageSectionProps) { + const {node, pool} = location ?? {}; + const {group} = pool ?? {}; + const {vdisk} = group ?? {}; + const {pdisk} = vdisk ?? {}; + + if (!pdisk?.length) { + return null; + } + + return pdisk.map((el: {id: string; path: string}) => ( + + {el.id} + + ) : ( + el.id + ), + title: i18n('label_pdisk-id'), + }, + {value: el.path, title: i18n('label_pdisk-path')}, + ]} + /> + )); +} diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/utils.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/utils.tsx new file mode 100644 index 000000000..bb011348a --- /dev/null +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/utils.tsx @@ -0,0 +1,65 @@ +import React from 'react'; + +import type {FlexProps, TextProps} from '@gravity-ui/uikit'; +import {DefinitionList, Flex, Text} from '@gravity-ui/uikit'; + +interface SectionWithTitleProps { + title?: string; + children: React.ReactNode; + titleVariant?: TextProps['variant']; + gap?: FlexProps['gap']; +} + +export function SectionWithTitle({ + title, + children, + titleVariant = 'body-2', + gap = 2, +}: SectionWithTitleProps) { + return ( + + {title && {title}} + {children} + + ); +} + +interface LocationDetailsProps { + title?: string; + fields: {value?: React.ReactNode; title: string; copy?: string}[]; +} + +export function LocationDetails({title, fields}: LocationDetailsProps) { + const filteredFields = fields.filter((f) => f.value); + + if (filteredFields.length === 0) { + return null; + } + + return ( + + + {filteredFields.map((field) => ( + + {field.value} + + ))} + + + ); +} + +interface IdListProps { + ids: string[]; + renderItem?: (id: string) => React.ReactNode; +} + +export function IdList({ids, renderItem}: IdListProps) { + return ( + + {ids.map((id) => ( + {renderItem ? renderItem(id) : id} + ))} + + ); +} diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueTabs.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueTabs.tsx new file mode 100644 index 000000000..24ab7c356 --- /dev/null +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueTabs.tsx @@ -0,0 +1,77 @@ +import React from 'react'; + +import {Flex, Text} from '@gravity-ui/uikit'; + +import type {IssuesTree} from '../../../../store/reducers/healthcheckInfo/types'; +import type {StatusFlag} from '../../../../types/api/healthcheck'; +import {b} from '../shared'; + +function getTypeText(type: string) { + const normalizedType = type.split('_').join(' '); + let result = normalizedType.charAt(0).toUpperCase() + normalizedType.slice(1).toLowerCase(); + result = result.replace(/\bvdisk\b/gi, 'VDisk').replace(/\bpdisk\b/gi, 'PDisk'); + return result; +} + +interface HealthcheckIssueTabsProps { + parents: IssuesTree[]; + selectedTab: string; + setSelectedTab: (tab: string) => void; +} + +export function HealthcheckIssueTabs({ + parents, + selectedTab, + setSelectedTab, +}: HealthcheckIssueTabsProps) { + //parent.length === 1 means that it's only issue itself, no need to render tabs + if (parents.length <= 1) { + return null; + } + + return ( + + {parents.map((parent, index) => ( + + setSelectedTab(parent.id)} + > + + + {getTypeText(parent.type)} + + + {index !== parents.length - 1 && /} + + ))} + + ); +} + +interface HealthcheckIssueTabProps { + children: React.ReactNode; + active?: boolean; + onClick: VoidFunction; +} + +function HealthcjeckIssueTab({children, active, onClick}: HealthcheckIssueTabProps) { + return ( + + {children} + + ); +} + +interface TabStatusProps { + status: StatusFlag; +} + +function TabStatus({status}: TabStatusProps) { + return
; +} diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssues.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssues.tsx new file mode 100644 index 000000000..df18921bd --- /dev/null +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssues.tsx @@ -0,0 +1,67 @@ +import React from 'react'; + +import {NoSearchResults} from '@gravity-ui/illustrations'; +import {Flex} from '@gravity-ui/uikit'; + +import {EmptyState} from '../../../../components/EmptyState'; +import type {IssuesTree} from '../../../../store/reducers/healthcheckInfo/types'; +import {useTenantQueryParams} from '../../useTenantQueryParams'; +import i18n from '../i18n'; + +import {HealthcheckIssue} from './HealthcheckIssue'; + +interface IssuesProps { + issues: IssuesTree[]; +} + +export function Issues({issues}: IssuesProps) { + const {view, issuesFilter} = useTenantQueryParams(); + + const filteredIssues = React.useMemo(() => { + const normalizedFilter = issuesFilter?.toLowerCase().trim(); + if (!normalizedFilter) { + return issues; + } + return issues.filter((issue) => { + const stack = Object.values(issue); + while (stack.length) { + const value = stack.pop(); + if (typeof value === 'object') { + stack.push(...Object.values(value)); + } else if (String(value).toLowerCase().includes(normalizedFilter)) { + return true; + } + } + return false; + }); + }, [issues, issuesFilter]); + + const filteredIssuesCurrentView = React.useMemo( + () => + view + ? filteredIssues.filter((issue) => { + const type = issue.upperType || issue.type; + return type.toLowerCase().startsWith(view); + }) + : [], + [filteredIssues, view], + ); + + if (filteredIssuesCurrentView.length === 0) { + return ( + + } + position="center" + size="xs" + title={i18n('label_no-issues')} + description={i18n('description_no-issues')} + /> + + ); + } + + return filteredIssuesCurrentView.map((issue) => ( + + )); +} diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckRefresh.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckRefresh.tsx new file mode 100644 index 000000000..541688a93 --- /dev/null +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckRefresh.tsx @@ -0,0 +1,75 @@ +import React from 'react'; + +import {duration} from '@gravity-ui/date-utils'; +import {ArrowsRotateLeft} from '@gravity-ui/icons'; +import {ActionTooltip, Button, Flex, Text} from '@gravity-ui/uikit'; +import {isNil} from 'lodash'; + +import {DAY_IN_SECONDS, HOUR_IN_SECONDS, SECOND_IN_MS} from '../../../../utils/constants'; +import i18n from '../i18n'; + +function getPassedMilliseconds(lastFullfiled = 0) { + return Date.now() - lastFullfiled; +} + +interface HealthcheckRefreshProps { + lastFullfiled?: number; + refresh: () => void; +} + +export function HealthcheckRefresh({lastFullfiled, refresh}: HealthcheckRefreshProps) { + const [passedFromLastFullfiled, setPassedFromLastFullfiled] = React.useState( + getPassedMilliseconds(lastFullfiled), + ); + + React.useEffect(() => { + setPassedFromLastFullfiled(getPassedMilliseconds(lastFullfiled)); + const interval = setInterval(() => { + setPassedFromLastFullfiled(getPassedMilliseconds(lastFullfiled)); + }, 60 * SECOND_IN_MS); + return () => clearInterval(interval); + }, [lastFullfiled]); + + const renderPassedFromLastFullfiled = () => { + if (isNil(lastFullfiled)) { + return null; + } + const showHours = passedFromLastFullfiled > HOUR_IN_SECONDS * SECOND_IN_MS; + const showDays = passedFromLastFullfiled > DAY_IN_SECONDS * SECOND_IN_MS; + + const preparedDuration = duration(passedFromLastFullfiled); + + let durationText = ''; + + if (showDays) { + const days = preparedDuration.asDays(); + durationText = i18n('description_days', {count: Math.round(days)}); + } else if (showHours) { + const hours = preparedDuration.asHours(); + durationText = i18n('description_hours', {count: Math.round(hours)}); + } else { + const minutes = preparedDuration.asMinutes(); + durationText = i18n('description_minutes', {count: Math.round(minutes)}); + } + + return {durationText}; + }; + + return ( + + {renderPassedFromLastFullfiled()} + + + + + ); +} diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckView.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckView.tsx new file mode 100644 index 000000000..5987a2996 --- /dev/null +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckView.tsx @@ -0,0 +1,71 @@ +import React from 'react'; + +import {RadioButton, Text} from '@gravity-ui/uikit'; + +import {uiFactory} from '../../../../uiFactory/uiFactory'; +import {useTenantQueryParams} from '../../useTenantQueryParams'; +import type {CommonIssueType} from '../shared'; +import {b} from '../shared'; + +const HealthcheckViewValues: Record = { + storage: 'storage', + compute: 'compute', +}; + +interface HealthcheckViewProps { + issuesCount: Record; + viewTitles?: Record; + sortOrder?: T[]; +} + +export function HealthcheckView({ + issuesCount, + viewTitles = uiFactory.getHealthckechViewTitles(), + sortOrder = uiFactory.getHealthcheckViewsOrder(), +}: HealthcheckViewProps) { + const {view, handleHealthcheckViewChange, handleIssuesFilterChange} = useTenantQueryParams(); + + const issuesTypes = React.useMemo(() => Object.keys(issuesCount), [issuesCount]); + + React.useEffect(() => { + if (view) { + return; + } + if (issuesCount[HealthcheckViewValues.storage]) { + handleHealthcheckViewChange(HealthcheckViewValues.storage); + } else if (issuesCount[HealthcheckViewValues.compute]) { + handleHealthcheckViewChange(HealthcheckViewValues.compute); + } else { + const firstIssueTypeWithIssues = sortOrder.find( + (issueType) => issuesCount[issueType] > 0, + ); + handleHealthcheckViewChange(firstIssueTypeWithIssues); + } + }, [view, handleHealthcheckViewChange, issuesCount, issuesTypes, sortOrder]); + + const renderCount = (view: (typeof sortOrder)[number]) => { + return {issuesCount[view] ?? 0}; + }; + + const renderHealthcheckViewOption = (view: (typeof sortOrder)[number]) => { + return ( + + {viewTitles[view] ?? view}  + {renderCount(view)} + + ); + }; + + return ( + { + handleHealthcheckViewChange(newView); + handleIssuesFilterChange(''); + }} + className={b('control-wrapper')} + > + {sortOrder.map((type) => renderHealthcheckViewOption(type))} + + ); +} diff --git a/src/containers/Tenant/Healthcheck/i18n/en.json b/src/containers/Tenant/Healthcheck/i18n/en.json new file mode 100644 index 000000000..a737c94f0 --- /dev/null +++ b/src/containers/Tenant/Healthcheck/i18n/en.json @@ -0,0 +1,52 @@ +{ + "description_no-data": "No healthcheck data", + "label_storage": "Storage", + "label_compute": "Compute", + "description_search-issue": "Search issue...", + "action_refresh": "Refresh", + "description_minutes": "{{count}} min ago", + "description_hours": [ + "{{count}} hour ago", + "{{count}} hours ago", + "{{count}} hours ago", + "{{count}} hours ago" + ], + "description_days": [ + "{{count}} day ago", + "{{count}} days ago", + "{{count}} days ago", + "{{count}} days ago" + ], + "label_no-issues": "No issues", + "description_no-issues": "Here you will see issues that require your attention", + "label_details": "Details", + "label_location": "Location", + "label_storage_location": "Storage Location", + "label_compute_location": "Compute Location", + "label_database_location": "Database Location", + "label_node": "Node", + "label_pool": "Pool", + "label_group": "Group", + "label_vdisk": "VDisk", + "label_pdisk": "PDisk", + "label_tablet": "Tablet", + "label_schema": "Schema", + "label_database": "Database", + "label_description": "Description", + "label_node-id": "ID", + "label_node-host": "Host", + "label_node-port": "Port", + "label_pool-name": "Name", + "label_tablet-id": "ID", + "label_tablet-type": "Type", + "label_tablet-count": "Count", + "label_schema-type": "Type", + "label_schema-path": "Path", + "label_group-id": "ID", + "label_vdisk-id": "ID", + "label_pdisk-id": "ID", + "label_pdisk-path": "Path", + "label_peer": "Peer Node", + "label_node_location": "Node Location", + "label_peer_location": "Peer Node Location" +} diff --git a/src/containers/Tenant/Healthcheck/i18n/index.ts b/src/containers/Tenant/Healthcheck/i18n/index.ts new file mode 100644 index 000000000..ce86ce422 --- /dev/null +++ b/src/containers/Tenant/Healthcheck/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-healthcheck-details'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tenant/Healthcheck/shared.ts b/src/containers/Tenant/Healthcheck/shared.ts new file mode 100644 index 000000000..52f1893dc --- /dev/null +++ b/src/containers/Tenant/Healthcheck/shared.ts @@ -0,0 +1,50 @@ +import type {IssuesTree} from '../../../store/reducers/healthcheckInfo/types'; +import {cn} from '../../../utils/cn'; + +import i18n from './i18n'; + +export const b = cn('ydb-healthcheck'); + +export type CommonIssueType = 'compute' | 'storage'; + +const HealthcheckViewTitles = { + get storage() { + return i18n('label_storage'); + }, + get compute() { + return i18n('label_compute'); + }, +}; + +const DefaultSortOrder: CommonIssueType[] = ['storage', 'compute']; + +export function getHealthckechViewTitles() { + return HealthcheckViewTitles; +} + +export function getHealthcheckViewsOrder() { + return DefaultSortOrder; +} + +export function countHealthcheckIssuesByType( + issueTrees: IssuesTree[], +): Record { + const result: Record = { + storage: 0, + compute: 0, + }; + + for (const issue of issueTrees) { + const type = issue.upperType ?? issue.type; + if (type.startsWith('STORAGE')) { + result.storage++; + } else if (type.startsWith('COMPUTE')) { + result.compute++; + } + } + return result; +} + +export type GetHealthcheckViewTitles = () => Record; + +export type GetHealthcheckViewsOrder = () => T[]; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/useHealthcheck.ts b/src/containers/Tenant/Healthcheck/useHealthcheck.ts similarity index 51% rename from src/containers/Tenant/Diagnostics/TenantOverview/useHealthcheck.ts rename to src/containers/Tenant/Healthcheck/useHealthcheck.ts index b0ae2a42a..e52a2c9bb 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/useHealthcheck.ts +++ b/src/containers/Tenant/Healthcheck/useHealthcheck.ts @@ -1,20 +1,18 @@ import { healthcheckApi, - selectIssuesStatistics, - selectIssuesTrees, -} from '../../../../store/reducers/healthcheckInfo/healthcheckInfo'; -import type {IssuesTree} from '../../../../store/reducers/healthcheckInfo/types'; -import {SelfCheckResult} from '../../../../types/api/healthcheck'; -import type {StatusFlag} from '../../../../types/api/healthcheck'; -import {useTypedSelector} from '../../../../utils/hooks'; + selectLeavesIssues, +} from '../../../store/reducers/healthcheckInfo/healthcheckInfo'; +import type {IssuesTree} from '../../../store/reducers/healthcheckInfo/types'; +import {SelfCheckResult} from '../../../types/api/healthcheck'; +import {useTypedSelector} from '../../../utils/hooks'; interface HealthcheckParams { - issueTrees: IssuesTree[]; - issuesStatistics: [StatusFlag, number][]; + leavesIssues: IssuesTree[]; loading: boolean; error?: unknown; refetch: () => void; selfCheckResult: SelfCheckResult; + fulfilledTimeStamp?: number; } export const useHealthcheck = ( @@ -26,22 +24,23 @@ export const useHealthcheck = ( isFetching, error, refetch, + fulfilledTimeStamp, } = healthcheckApi.useGetHealthcheckInfoQuery( {database: tenantName}, { pollingInterval: autorefresh, }, ); + const selfCheckResult = data?.self_check_result || SelfCheckResult.UNSPECIFIED; - const issuesStatistics = useTypedSelector((state) => selectIssuesStatistics(state, tenantName)); - const issueTrees = useTypedSelector((state) => selectIssuesTrees(state, tenantName)); + const leavesIssues = useTypedSelector((state) => selectLeavesIssues(state, tenantName)); return { - issueTrees, - issuesStatistics, loading: data === undefined && isFetching, error, refetch, selfCheckResult, + fulfilledTimeStamp, + leavesIssues, }; }; diff --git a/src/containers/Tenant/Tenant.tsx b/src/containers/Tenant/Tenant.tsx index b03b3e294..15117085b 100644 --- a/src/containers/Tenant/Tenant.tsx +++ b/src/containers/Tenant/Tenant.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {Helmet} from 'react-helmet-async'; -import {StringParam, useQueryParams} from 'use-query-params'; import {PageError} from '../../components/Errors/PageError/PageError'; import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper'; @@ -17,7 +16,9 @@ import {isAccessError} from '../../utils/response'; import ObjectGeneral from './ObjectGeneral/ObjectGeneral'; import {ObjectSummary} from './ObjectSummary/ObjectSummary'; +import {TenantDrawerWrapper} from './TenantDrawerWrappers'; import i18n from './i18n'; +import {useTenantQueryParams} from './useTenantQueryParams'; import { PaneVisibilityActionTypes, paneVisibilityToggleReducerCreator, @@ -49,55 +50,41 @@ export function Tenant(props: TenantProps) { getTenantSummaryState, ); - // TODO: name is used together with database to keep old links valid - // Remove it after some time - 1-2 weeks - const [{database, name, schema}, setQuery] = useQueryParams({ - database: StringParam, - name: StringParam, - schema: StringParam, - }); + const {database, schema} = useTenantQueryParams(); - React.useEffect(() => { - if (name && !database) { - setQuery({database: name, name: undefined}, 'replaceIn'); - } - }, [database, name, setQuery]); - - const tenantName = database ?? name; - - if (!tenantName) { + if (!database) { throw new Error('Tenant name is not defined'); } const previousTenant = React.useRef(); React.useEffect(() => { - if (previousTenant.current !== tenantName) { + if (previousTenant.current !== database) { const register = async () => { const {registerYQLCompletionItemProvider} = await import( '../../utils/monaco/yql/yql.completionItemProvider' ); - registerYQLCompletionItemProvider(tenantName); + registerYQLCompletionItemProvider(database); }; register().catch(console.error); - previousTenant.current = tenantName; + previousTenant.current = database; } - }, [tenantName]); + }, [database]); const dispatch = useTypedDispatch(); React.useEffect(() => { - dispatch(setHeaderBreadcrumbs('tenant', {tenantName})); - }, [tenantName, dispatch]); + dispatch(setHeaderBreadcrumbs('tenant', {tenantName: database})); + }, [database, dispatch]); - const path = schema ?? tenantName; + const path = schema ?? database; const { currentData: currentItem, error, isLoading, - } = overviewApi.useGetOverviewQuery({path, database: tenantName}); + } = overviewApi.useGetOverviewQuery({path, database: database}); const preloadedData = useTypedSelector((state) => - selectSchemaObjectData(state, path, tenantName), + selectSchemaObjectData(state, path, database), ); // Use preloaded data if there is no current item data yet @@ -135,34 +122,36 @@ export function Tenant(props: TenantProps) { /> - - -
- + + -
-
+
+ +
+ +
diff --git a/src/containers/Tenant/TenantDrawerWrappers.tsx b/src/containers/Tenant/TenantDrawerWrappers.tsx new file mode 100644 index 000000000..63ed53746 --- /dev/null +++ b/src/containers/Tenant/TenantDrawerWrappers.tsx @@ -0,0 +1,109 @@ +import React from 'react'; + +import {ArrowDownToLine} from '@gravity-ui/icons'; +import {ActionTooltip, Button, Flex, Icon, Text} from '@gravity-ui/uikit'; + +import {DrawerWrapper} from '../../components/Drawer'; +import {DrawerContextProvider} from '../../components/Drawer/DrawerContext'; +import EnableFullscreenButton from '../../components/EnableFullscreenButton/EnableFullscreenButton'; +import { + selectAllHealthcheckInfo, + selectCheckStatus, +} from '../../store/reducers/healthcheckInfo/healthcheckInfo'; +import type {SelfCheckResult} from '../../types/api/healthcheck'; +import {createAndDownloadJsonFile} from '../../utils/downloadFile'; +import {useTypedSelector} from '../../utils/hooks'; + +import {Healthcheck} from './Healthcheck/Healthcheck'; +import {HEALTHCHECK_RESULT_TO_TEXT} from './constants'; +import i18n from './i18n'; +import {useTenantQueryParams} from './useTenantQueryParams'; + +interface TenantDrawerWrapperProps { + children: React.ReactNode; + database: string; +} + +export function TenantDrawerWrapper({children, database}: TenantDrawerWrapperProps) { + const { + handleShowHealthcheckChange, + showHealthcheck, + handleIssuesFilterChange, + handleHealthcheckViewChange, + } = useTenantQueryParams(); + + const healthcheckStatus = useTypedSelector((state) => selectCheckStatus(state, database || '')); + + const healthcheckData = useTypedSelector((state) => + selectAllHealthcheckInfo(state, database || ''), + ); + + const handleCloseDrawer = React.useCallback(() => { + handleShowHealthcheckChange(false); + handleIssuesFilterChange(undefined); + handleHealthcheckViewChange(undefined); + }, [handleShowHealthcheckChange, handleIssuesFilterChange, handleHealthcheckViewChange]); + + const renderDrawerContent = React.useCallback(() => { + return ; + }, []); + + return ( + + + + + ), + }, + { + type: 'custom', + node: , + key: 'fullscreen', + }, + {type: 'close'}, + ]} + title={} + > + {children} + + + ); +} + +interface DrawerTitleProps { + status?: SelfCheckResult; +} + +function DrawerTitle({status}: DrawerTitleProps) { + return ( + + {i18n('label_healthcheck-dashboard')} + {status && HEALTHCHECK_RESULT_TO_TEXT[status]} + + ); +} diff --git a/src/containers/Tenant/TenantPages.tsx b/src/containers/Tenant/TenantPages.tsx index 81bd20850..c357730e5 100644 --- a/src/containers/Tenant/TenantPages.tsx +++ b/src/containers/Tenant/TenantPages.tsx @@ -14,6 +14,8 @@ type AdditionalQueryParams = { backend?: string; selectedPartition?: string; activeOffset?: string; + metricsTab?: string; + showPreview?: boolean; }; export type TenantQuery = TenantQueryParams | AdditionalQueryParams; diff --git a/src/containers/Tenant/constants.ts b/src/containers/Tenant/constants.ts new file mode 100644 index 000000000..a6f4a0008 --- /dev/null +++ b/src/containers/Tenant/constants.ts @@ -0,0 +1,28 @@ +import { + CircleCheckFill, + CircleInfoFill, + CircleQuestionFill, + CircleXmarkFill, + TriangleExclamationFill, +} from '@gravity-ui/icons'; +import type {IconData} from '@gravity-ui/uikit'; + +import {SelfCheckResult} from '../../types/api/healthcheck'; + +import i18n from './i18n'; + +export const HEALTHCHECK_RESULT_TO_TEXT: Record = { + [SelfCheckResult.UNSPECIFIED]: '', + [SelfCheckResult.GOOD]: i18n('description_ok'), + [SelfCheckResult.DEGRADED]: i18n('description_degraded'), + [SelfCheckResult.MAINTENANCE_REQUIRED]: i18n('description_maintenance'), + [SelfCheckResult.EMERGENCY]: i18n('description_emergency'), +}; + +export const HEALTHCHECK_RESULT_TO_ICON: Record = { + [SelfCheckResult.UNSPECIFIED]: CircleQuestionFill, + [SelfCheckResult.GOOD]: CircleCheckFill, + [SelfCheckResult.DEGRADED]: CircleInfoFill, + [SelfCheckResult.MAINTENANCE_REQUIRED]: CircleXmarkFill, + [SelfCheckResult.EMERGENCY]: TriangleExclamationFill, +}; diff --git a/src/containers/Tenant/i18n/en.json b/src/containers/Tenant/i18n/en.json index a7d9313e4..1f588674a 100644 --- a/src/containers/Tenant/i18n/en.json +++ b/src/containers/Tenant/i18n/en.json @@ -58,5 +58,12 @@ "schema.tree.dialog.header": "Create directory", "schema.tree.dialog.description": "Inside", "schema.tree.dialog.buttonCancel": "Cancel", - "schema.tree.dialog.buttonApply": "Create" + "schema.tree.dialog.buttonApply": "Create", + "label_healthcheck-dashboard": "Healthcheck Dashboard", + + "description_ok": "Everything is working fine.", + "description_degraded": "Some issues, but still working.", + "description_maintenance": "Working, but needs urgent attention to avoid failure.", + "description_emergency": "Something is broken and not working.", + "label_download": "Download healthcheck data" } diff --git a/src/containers/Tenant/useTenantQueryParams.ts b/src/containers/Tenant/useTenantQueryParams.ts new file mode 100644 index 000000000..04f620424 --- /dev/null +++ b/src/containers/Tenant/useTenantQueryParams.ts @@ -0,0 +1,59 @@ +import React from 'react'; + +import {BooleanParam, StringParam, useQueryParams} from 'use-query-params'; + +export function useTenantQueryParams() { + const [{showHealthcheck, database, schema, view, issuesFilter}, setQueryParams] = + useQueryParams({ + showHealthcheck: BooleanParam, + database: StringParam, + schema: StringParam, + view: StringParam, + issuesFilter: StringParam, + }); + const handleShowHealthcheckChange = React.useCallback( + (value?: boolean) => { + setQueryParams({showHealthcheck: value}, 'replaceIn'); + }, + [setQueryParams], + ); + + const handleDatabaseChange = React.useCallback( + (value?: string) => { + setQueryParams({database: value}, 'replaceIn'); + }, + [setQueryParams], + ); + + const handleSchemaChange = React.useCallback( + (value?: string) => { + setQueryParams({schema: value}, 'replaceIn'); + }, + [setQueryParams], + ); + const handleIssuesFilterChange = React.useCallback( + (value?: string) => { + setQueryParams({issuesFilter: value}, 'replaceIn'); + }, + [setQueryParams], + ); + const handleHealthcheckViewChange = React.useCallback( + (value?: string) => { + setQueryParams({view: value}, 'replaceIn'); + }, + [setQueryParams], + ); + + return { + showHealthcheck, + handleShowHealthcheckChange, + database, + handleDatabaseChange, + schema, + handleSchemaChange, + view, + handleHealthcheckViewChange, + issuesFilter, + handleIssuesFilterChange, + }; +} diff --git a/src/store/reducers/healthcheckInfo/healthcheckInfo.ts b/src/store/reducers/healthcheckInfo/healthcheckInfo.ts index 402f74ff9..f94a1f544 100644 --- a/src/store/reducers/healthcheckInfo/healthcheckInfo.ts +++ b/src/store/reducers/healthcheckInfo/healthcheckInfo.ts @@ -4,7 +4,7 @@ import type {IssueLog, StatusFlag} from '../../../types/api/healthcheck'; import type {RootState} from '../../defaultStore'; import {api} from '../api'; -import type {IssuesTree} from './types'; +import {getLeavesFromTree} from './utils'; export const healthcheckApi = api.injectEndpoints({ endpoints: (builder) => ({ @@ -18,7 +18,9 @@ export const healthcheckApi = api.injectEndpoints({ {database, maxLevel}, {signal}, ); - return {data}; + return { + data, + }; } catch (error) { return {error}; } @@ -46,10 +48,6 @@ const sortIssues = (data: IssueLog[]): IssueLog[] => { }); }; -const getReasonsForIssue = ({issue, data}: {issue: IssueLog; data: IssueLog[]}) => { - return sortIssues(data.filter((item) => issue.reason && issue.reason.indexOf(item.id) !== -1)); -}; - const getRoots = (data: IssueLog[]): IssueLog[] => { return sortIssues( data.filter((item) => { @@ -58,51 +56,17 @@ const getRoots = (data: IssueLog[]): IssueLog[] => { ); }; -const getInvertedConsequencesTree = ({ - data, - roots, -}: { - data: IssueLog[]; - roots?: IssueLog[]; -}): IssuesTree[] => { - return roots - ? roots.map((issue) => { - const reasonsItems = getInvertedConsequencesTree({ - roots: getReasonsForIssue({issue, data}), - data, - }); - - return { - ...issue, - reasonsItems, - }; - }) - : []; -}; - -const getIssuesStatistics = (data: IssueLog[]): [StatusFlag, number][] => { - const issuesMap = {} as Record; - - for (const issue of data) { - if (!issuesMap[issue.status]) { - issuesMap[issue.status] = 0; - } - issuesMap[issue.status]++; - } - - return (Object.entries(issuesMap) as [StatusFlag, number][]).sort(([aStatus], [bStatus]) => { - const bPriority = mapStatusToPriority[bStatus] || 0; - const aPriority = mapStatusToPriority[aStatus] || 0; - - return aPriority - bPriority; - }); -}; - const createGetHealthcheckInfoSelector = createSelector( (database: string) => database, (database) => healthcheckApi.endpoints.getHealthcheckInfo.select({database}), ); +export const selectCheckStatus = createSelector( + (state: RootState) => state, + (_state: RootState, database: string) => createGetHealthcheckInfoSelector(database), + (state: RootState, selectGetPost) => selectGetPost(state).data?.self_check_result, +); + const getIssuesLog = createSelector( (state: RootState) => state, (_state: RootState, database: string) => createGetHealthcheckInfoSelector(database), @@ -111,13 +75,15 @@ const getIssuesLog = createSelector( const selectIssuesTreesRoots = createSelector(getIssuesLog, (issues = []) => getRoots(issues)); -export const selectIssuesTrees = createSelector( +export const selectLeavesIssues = createSelector( [getIssuesLog, selectIssuesTreesRoots], (data = [], roots = []) => { - return getInvertedConsequencesTree({data, roots}); + return roots.map((root) => getLeavesFromTree(data, root)).flat(); }, ); -export const selectIssuesStatistics = createSelector(getIssuesLog, (issues = []) => - getIssuesStatistics(issues), +export const selectAllHealthcheckInfo = createSelector( + (state: RootState) => state, + (_state: RootState, database: string) => createGetHealthcheckInfoSelector(database), + (state: RootState, selectGetPost) => selectGetPost(state).data, ); diff --git a/src/store/reducers/healthcheckInfo/types.ts b/src/store/reducers/healthcheckInfo/types.ts index ad0db05dd..8afd3b86c 100644 --- a/src/store/reducers/healthcheckInfo/types.ts +++ b/src/store/reducers/healthcheckInfo/types.ts @@ -2,4 +2,6 @@ import type {IssueLog} from '../../../types/api/healthcheck'; export interface IssuesTree extends IssueLog { reasonsItems?: IssuesTree[]; + parent?: IssuesTree; + upperType?: string; } diff --git a/src/store/reducers/healthcheckInfo/utils.ts b/src/store/reducers/healthcheckInfo/utils.ts index 0657b409f..764b5199b 100644 --- a/src/store/reducers/healthcheckInfo/utils.ts +++ b/src/store/reducers/healthcheckInfo/utils.ts @@ -1,6 +1,9 @@ import {EFlag} from '../../../types/api/enums'; +import type {IssueLog} from '../../../types/api/healthcheck'; import {StatusFlag} from '../../../types/api/healthcheck'; +import type {IssuesTree} from './types'; + export const hcStatusToColorFlag: Record = { [StatusFlag.UNSPECIFIED]: EFlag.Grey, [StatusFlag.GREY]: EFlag.Grey, @@ -10,3 +13,41 @@ export const hcStatusToColorFlag: Record = { [StatusFlag.ORANGE]: EFlag.Orange, [StatusFlag.RED]: EFlag.Red, }; + +export function getLeavesFromTree(issues: IssueLog[], root: IssueLog): IssuesTree[] { + const result: IssuesTree[] = []; + + if (!root.reason || root.reason.length === 0) { + return [root]; + } + + for (const issueId of root.reason) { + const directChild: IssuesTree | undefined = issues.find((issue) => issue.id === issueId); + if (!directChild) { + continue; + } + const stack: IssuesTree[] = [directChild]; + + const directChildType = directChild.type; + + while (stack.length > 0) { + const currentNode = stack.pop()!; + + if (!currentNode.reason || currentNode.reason.length === 0) { + result.push(currentNode); + continue; + } + + for (const reason of currentNode.reason) { + const child: IssuesTree | undefined = issues.find((issue) => issue.id === reason); + if (!child) { + continue; + } + const extendedChild = {...child, parent: currentNode, upperType: directChildType}; + stack.push(extendedChild); + } + } + } + + return result; +} diff --git a/src/store/reducers/tenant/constants.ts b/src/store/reducers/tenant/constants.ts index 2d40249e1..e90ae507e 100644 --- a/src/store/reducers/tenant/constants.ts +++ b/src/store/reducers/tenant/constants.ts @@ -40,5 +40,4 @@ export const TENANT_METRICS_TABS_IDS = { cpu: 'cpu', storage: 'storage', memory: 'memory', - healthcheck: 'healthcheck', } as const; diff --git a/src/styles/index.scss b/src/styles/index.scss index b941caa91..81e5c31a1 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -4,6 +4,9 @@ @forward './illustrations.scss'; body { + --ydb-drawer-veil-z-index: 11; + + --gn-drawer-veil-z-index: var(--ydb-drawer-veil-z-index); margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', diff --git a/src/types/api/healthcheck.ts b/src/types/api/healthcheck.ts index b0a9e0648..5f55b94f4 100644 --- a/src/types/api/healthcheck.ts +++ b/src/types/api/healthcheck.ts @@ -1,3 +1,4 @@ +// Matches proto3 definition from Ydb.Monitoring package export enum SelfCheckResult { UNSPECIFIED = 'UNSPECIFIED', GOOD = 'GOOD', @@ -28,12 +29,12 @@ interface LocationStoragePDisk { } interface LocationStorageVDisk { - id: string; - pdisk: LocationStoragePDisk; + id: string[]; + pdisk: LocationStoragePDisk[]; } interface LocationStorageGroup { - id: string; + id: string[]; vdisk: LocationStorageVDisk; } @@ -53,30 +54,32 @@ interface LocationComputePool { interface LocationComputeTablet { type: string; - id?: string[]; + id: string[]; count: number; } interface LocationComputeSchema { - type?: string; - path?: string; + type: string; + path: string; } interface LocationCompute { - node?: LocationNode; - pool?: LocationComputePool; + node: LocationNode; + pool: LocationComputePool; tablet: LocationComputeTablet; - schema?: LocationComputeSchema; + schema: LocationComputeSchema; } interface LocationDatabase { name: string; } -interface Location { - storage: LocationStorage; - compute: LocationCompute; - database: LocationDatabase; +export interface Location { + storage?: LocationStorage; + compute?: LocationCompute; + database?: LocationDatabase; + node?: LocationNode; + peer?: LocationNode; } export interface IssueLog { @@ -87,6 +90,80 @@ export interface IssueLog { reason?: string[]; type: string; level: number; + listed?: number; + count?: number; +} + +interface StoragePDiskStatus { + id: string; + overall: StatusFlag; +} + +interface StorageVDiskStatus { + id: string; + overall: StatusFlag; + vdisk_status: StatusFlag; + pdisk: StoragePDiskStatus; +} + +interface StorageGroupStatus { + id: string; + overall: StatusFlag; + vdisks: StorageVDiskStatus[]; +} + +interface StoragePoolStatus { + id: string; + overall: StatusFlag; + groups: StorageGroupStatus[]; +} + +interface StorageStatus { + overall: StatusFlag; + pools: StoragePoolStatus[]; +} + +interface ComputeTabletStatus { + overall: StatusFlag; + type: string; + state: string; + count: number; + id: string[]; +} + +interface ThreadPoolStatus { + overall: StatusFlag; + name: string; + usage: number; +} + +interface LoadAverageStatus { + overall: StatusFlag; + load: number; + cores: number; +} + +interface ComputeNodeStatus { + id: string; + overall: StatusFlag; + tablets: ComputeTabletStatus[]; + pools: ThreadPoolStatus[]; + load: LoadAverageStatus; +} + +interface ComputeStatus { + overall: StatusFlag; + nodes: ComputeNodeStatus[]; + tablets: ComputeTabletStatus[]; + paths_quota_usage: number; + shards_quota_usage: number; +} + +interface DatabaseStatus { + name: string; + overall: StatusFlag; + storage: StorageStatus; + compute: ComputeStatus; } export interface HealthCheckAPIResponse { @@ -94,4 +171,7 @@ export interface HealthCheckAPIResponse { self_check_result: SelfCheckResult; // eslint-disable-next-line camelcase issue_log?: IssueLog[]; + // eslint-disable-next-line camelcase + database_status?: DatabaseStatus[]; + location?: LocationNode; } diff --git a/src/uiFactory/types.ts b/src/uiFactory/types.ts index 366889ce1..60da23712 100644 --- a/src/uiFactory/types.ts +++ b/src/uiFactory/types.ts @@ -1,3 +1,9 @@ +import type { + CommonIssueType, + GetHealthcheckViewTitles, + GetHealthcheckViewsOrder, +} from '../containers/Tenant/Healthcheck/shared'; +import type {IssuesTree} from '../store/reducers/healthcheckInfo/types'; import type {PreparedTenant} from '../store/reducers/tenants/types'; import type {GetLogsLink} from '../utils/logs'; import type {GetMonitoringClusterLink, GetMonitoringLink} from '../utils/monitoring'; @@ -10,6 +16,12 @@ export interface UIFactory { getLogsLink?: GetLogsLink; getMonitoringLink?: GetMonitoringLink; getMonitoringClusterLink?: GetMonitoringClusterLink; + + getHealthckechViewTitles: GetHealthcheckViewTitles; + getHealthcheckViewsOrder: GetHealthcheckViewsOrder; + countHealthcheckIssuesByType: ( + issueTrees: IssuesTree[], + ) => Record & Record; } export type HandleCreateDB = (params: {clusterName: string}) => Promise; diff --git a/src/uiFactory/uiFactory.ts b/src/uiFactory/uiFactory.ts index 6dae3aceb..e7f5c8eee 100644 --- a/src/uiFactory/uiFactory.ts +++ b/src/uiFactory/uiFactory.ts @@ -1,3 +1,8 @@ +import { + countHealthcheckIssuesByType, + getHealthcheckViewsOrder, + getHealthckechViewTitles, +} from '../containers/Tenant/Healthcheck/shared'; import { getMonitoringClusterLink as getMonitoringClusterLinkDefault, getMonitoringLink as getMonitoringLinkDefault, @@ -8,6 +13,9 @@ import type {UIFactory} from './types'; const uiFactoryBase: UIFactory = { getMonitoringLink: getMonitoringLinkDefault, getMonitoringClusterLink: getMonitoringClusterLinkDefault, + getHealthckechViewTitles: getHealthckechViewTitles, + getHealthcheckViewsOrder: getHealthcheckViewsOrder, + countHealthcheckIssuesByType: countHealthcheckIssuesByType, }; export function configureUIFactory(overrides: UIFactory) { From 85d4064792fe1c775c89d543cf0cf481ce5c2814 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Sat, 31 May 2025 11:01:12 +0300 Subject: [PATCH 04/14] fix: tests --- src/components/Drawer/Drawer.scss | 2 +- src/components/Fullscreen/Fullscreen.scss | 2 +- src/styles/index.scss | 2 +- tests/suites/tenant/diagnostics/Diagnostics.ts | 6 ++---- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/Drawer/Drawer.scss b/src/components/Drawer/Drawer.scss index 10595ece6..5b7907c1e 100644 --- a/src/components/Drawer/Drawer.scss +++ b/src/components/Drawer/Drawer.scss @@ -8,7 +8,7 @@ } &__item { - z-index: calc(var(--ydb-drawer-veil-z-index) + 1); + z-index: 11; height: 100%; } diff --git a/src/components/Fullscreen/Fullscreen.scss b/src/components/Fullscreen/Fullscreen.scss index efe27e699..75882ab41 100644 --- a/src/components/Fullscreen/Fullscreen.scss +++ b/src/components/Fullscreen/Fullscreen.scss @@ -6,7 +6,7 @@ // should expand to fill the content area, keeping aside navigation visible // counts on .gn-aside-header__content to have position: relative, it is set in App.scss position: absolute; - z-index: calc(var(--ydb-drawer-veil-z-index) + 3); + z-index: 10; inset: 0; background-color: var(--g-color-base-background); diff --git a/src/styles/index.scss b/src/styles/index.scss index 81e5c31a1..7c9b1e3b6 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -4,7 +4,7 @@ @forward './illustrations.scss'; body { - --ydb-drawer-veil-z-index: 11; + --ydb-drawer-veil-z-index: 0; --gn-drawer-veil-z-index: var(--ydb-drawer-veil-z-index); margin: 0; diff --git a/tests/suites/tenant/diagnostics/Diagnostics.ts b/tests/suites/tenant/diagnostics/Diagnostics.ts index f17cda1f5..83a68f7c6 100644 --- a/tests/suites/tenant/diagnostics/Diagnostics.ts +++ b/tests/suites/tenant/diagnostics/Diagnostics.ts @@ -243,7 +243,7 @@ export class Diagnostics { this.cpuCard = page.locator('.metrics-cards__tab:has-text("CPU")'); this.storageCard = page.locator('.metrics-cards__tab:has-text("Storage")'); this.memoryCard = page.locator('.metrics-cards__tab:has-text("Memory")'); - this.healthcheckCard = page.locator('.metrics-cards__tab:has-text("Healthcheck")'); + this.healthcheckCard = page.locator('.ydb-healthcheck-preview'); } async isSchemaViewerVisible() { @@ -334,9 +334,7 @@ export class Diagnostics { } async getHealthcheckStatus() { - const statusElement = this.healthcheckCard.locator( - '.healthcheck__self-check-status-indicator', - ); + const statusElement = this.healthcheckCard.locator('.ydb-healthcheck-preview__icon'); return (await statusElement.textContent())?.trim() || ''; } From 0b2b0a21c2c2510261346529783f24c72268d62a Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Sat, 31 May 2025 11:41:43 +0300 Subject: [PATCH 05/14] fix: tests --- tests/suites/tenant/diagnostics/Diagnostics.ts | 6 ++++++ tests/suites/tenant/diagnostics/tabs/info.test.ts | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/suites/tenant/diagnostics/Diagnostics.ts b/tests/suites/tenant/diagnostics/Diagnostics.ts index 83a68f7c6..d6c8f37b2 100644 --- a/tests/suites/tenant/diagnostics/Diagnostics.ts +++ b/tests/suites/tenant/diagnostics/Diagnostics.ts @@ -338,6 +338,12 @@ export class Diagnostics { return (await statusElement.textContent())?.trim() || ''; } + async hasHealthcheckStatusClass(className: string) { + const statusElement = this.healthcheckCard.locator('.ydb-healthcheck-preview__icon'); + const classList = await statusElement.evaluate((el) => Array.from(el.classList)); + return classList.includes(className); + } + async selectTopShardsMode(mode: TopShardsMode): Promise { const option = this.tableRadioButton.locator(`.g-radio-button__option:has-text("${mode}")`); await option.evaluate((el) => (el as HTMLElement).click()); diff --git a/tests/suites/tenant/diagnostics/tabs/info.test.ts b/tests/suites/tenant/diagnostics/tabs/info.test.ts index 75918464d..6c8f79763 100644 --- a/tests/suites/tenant/diagnostics/tabs/info.test.ts +++ b/tests/suites/tenant/diagnostics/tabs/info.test.ts @@ -52,6 +52,11 @@ test.describe('Diagnostics Info tab', async () => { await diagnostics.clickTab(DiagnosticsTab.Info); const status = await diagnostics.getHealthcheckStatus(); - expect(status).toBe('GOOD'); + expect(status).toBeTruthy(); + + const isGood = await diagnostics.hasHealthcheckStatusClass( + 'ydb-healthcheck-preview__icon_good', + ); + expect(isGood).toBe(true); }); }); From 41341d6e7a7e94a8dc72377417e6f1413d0312da Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Sat, 31 May 2025 12:17:35 +0300 Subject: [PATCH 06/14] fix: tests --- tests/suites/tenant/diagnostics/Diagnostics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/suites/tenant/diagnostics/Diagnostics.ts b/tests/suites/tenant/diagnostics/Diagnostics.ts index d6c8f37b2..fdd1e9820 100644 --- a/tests/suites/tenant/diagnostics/Diagnostics.ts +++ b/tests/suites/tenant/diagnostics/Diagnostics.ts @@ -335,7 +335,7 @@ export class Diagnostics { async getHealthcheckStatus() { const statusElement = this.healthcheckCard.locator('.ydb-healthcheck-preview__icon'); - return (await statusElement.textContent())?.trim() || ''; + return await statusElement.isVisible(); } async hasHealthcheckStatusClass(className: string) { From 4ef00d6907ce3494a6e7c86bd642bd94df2f438a Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Sun, 1 Jun 2025 10:51:28 +0300 Subject: [PATCH 07/14] fix(SplitPane): gutter z-index 0 --- src/components/SplitPane/SplitPane.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/SplitPane/SplitPane.scss b/src/components/SplitPane/SplitPane.scss index e05af1df4..83063853f 100644 --- a/src/components/SplitPane/SplitPane.scss +++ b/src/components/SplitPane/SplitPane.scss @@ -22,7 +22,6 @@ .gutter { position: relative; - z-index: 10; background: var(--g-color-base-background); From e3087ca45cb757229f4b90fb0d3247b031d91e2c Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Sun, 1 Jun 2025 11:38:50 +0300 Subject: [PATCH 08/14] feat(MetricCards): add network utilization --- .../ProgressViewer/ProgressViewer.tsx | 6 ++- .../MetricsCards/MetricCard/MetricCard.scss | 7 +++ .../MetricsCards/MetricCard/MetricCard.tsx | 25 +++++++++-- .../MetricsCards/MetricsCards.tsx | 45 ++++++++++++++++++- .../TenantOverview/TenantOverview.tsx | 2 + .../Diagnostics/TenantOverview/i18n/en.json | 2 + src/containers/UserSettings/i18n/en.json | 2 +- src/store/reducers/tenants/utils.ts | 21 ++++++++- src/types/api/tenant.ts | 5 +++ 9 files changed, 107 insertions(+), 8 deletions(-) diff --git a/src/components/ProgressViewer/ProgressViewer.tsx b/src/components/ProgressViewer/ProgressViewer.tsx index 94ec1f3b9..c6749f881 100644 --- a/src/components/ProgressViewer/ProgressViewer.tsx +++ b/src/components/ProgressViewer/ProgressViewer.tsx @@ -35,6 +35,7 @@ Props description: 6) inverseColorize - invert the colors of the progress bar 7) warningThreshold - the percentage of fullness at which the color of the progress bar changes to yellow 8) dangerThreshold - the percentage of fullness at which the color of the progress bar changes to red +9) overflow - parcents may be more that 100% */ export interface ProgressViewerProps { @@ -49,6 +50,7 @@ export interface ProgressViewerProps { warningThreshold?: number; dangerThreshold?: number; hideCapacity?: boolean; + overflow?: boolean; } export function ProgressViewer({ @@ -56,6 +58,7 @@ export function ProgressViewer({ capacity, formatValues = defaultFormatValues, percents, + overflow, className, size = 'xs', colorizeProgress, @@ -68,12 +71,13 @@ export function ProgressViewer({ let fillWidth = Math.floor((parseFloat(String(value)) / parseFloat(String(capacity))) * 100) || 0; + const rawFillWidth = fillWidth; fillWidth = fillWidth > 100 ? 100 : fillWidth; let valueText: number | string | undefined = value, capacityText: number | string | undefined = capacity, divider = '/'; if (percents) { - valueText = fillWidth + '%'; + valueText = (overflow ? rawFillWidth : fillWidth) + '%'; capacityText = ''; divider = ''; } else if (formatValues) { diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricCard/MetricCard.scss b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricCard/MetricCard.scss index dc863b062..acf60cb51 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricCard/MetricCard.scss +++ b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricCard/MetricCard.scss @@ -14,6 +14,13 @@ margin-bottom: 10px; } + &__note { + display: flex; + .g-help-mark__button { + display: flex; + } + } + &__label { font-weight: 600; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricCard/MetricCard.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricCard/MetricCard.tsx index cdd419b45..f43595cda 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricCard/MetricCard.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricCard/MetricCard.tsx @@ -1,3 +1,5 @@ +import {Flex, HelpMark} from '@gravity-ui/uikit'; + import {DiagnosticCard} from '../../../../../../components/DiagnosticCard/DiagnosticCard'; import {ProgressViewer} from '../../../../../../components/ProgressViewer/ProgressViewer'; import type {ProgressViewerProps} from '../../../../../../components/ProgressViewer/ProgressViewer'; @@ -36,9 +38,11 @@ interface MetricCardProps { label?: string; status?: MetricStatus; metrics: DiagnosticsCardMetric[]; + interactive?: boolean; + note?: string; } -export function MetricCard({active, label, status, metrics}: MetricCardProps) { +export function MetricCard({active, label, status, metrics, interactive, note}: MetricCardProps) { const renderContent = () => { return metrics.map(({title, ...progressViewerProps}, index) => { return ( @@ -49,10 +53,25 @@ export function MetricCard({active, label, status, metrics}: MetricCardProps) { ); }); }; + const renderNote = () => { + if (!note) { + return null; + } + return ( + + {note} + + ); + }; return ( - +
- {label &&
{label}
} + {label && ( + + {label} + {renderNote()} + + )} {getStatusIcon(status)}
{renderContent()}
diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx index ba8904cc4..2e740619d 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx @@ -14,9 +14,11 @@ import type { TenantStorageStats, } from '../../../../../store/reducers/tenants/utils'; import {getMetricStatusFromUsage} from '../../../../../store/reducers/tenants/utils'; +import {formatBytes} from '../../../../../utils/bytesParsers'; import {cn} from '../../../../../utils/cn'; +import {SHOW_NETWORK_UTILIZATION} from '../../../../../utils/constants'; import {formatStorageValues} from '../../../../../utils/dataFormatters/dataFormatters'; -import {useTypedSelector} from '../../../../../utils/hooks'; +import {useSetting, useTypedSelector} from '../../../../../utils/hooks'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; import i18n from '../i18n'; @@ -41,6 +43,7 @@ interface MetricsCardsProps { memoryStats?: TenantMetricStats[]; blobStorageStats?: TenantStorageStats[]; tabletStorageStats?: TenantStorageStats[]; + networkStats?: TenantMetricStats[]; } export function MetricsCards({ @@ -48,6 +51,7 @@ export function MetricsCards({ memoryStats, blobStorageStats, tabletStorageStats, + networkStats, }: MetricsCardsProps) { const location = useLocation(); @@ -100,6 +104,7 @@ export function MetricsCards({ active={metricsTab === TENANT_METRICS_TABS_IDS.memory} /> + ); } @@ -210,3 +215,41 @@ function MemoryCard({active, memoryStats = []}: MemoryCardProps) { /> ); } +interface NetworkCardProps { + networkStats?: TenantMetricStats[]; +} + +function NetworkCard({networkStats}: NetworkCardProps) { + const [showNetworkUtilization] = useSetting(SHOW_NETWORK_UTILIZATION); + if (!showNetworkUtilization || !networkStats) { + return null; + } + let status: MetricStatus = METRIC_STATUS.Unspecified; + + const metrics: DiagnosticsCardMetric[] = networkStats.map((metric) => { + const {used, limit, usage} = metric; + + const metricStatus = getMetricStatusFromUsage(usage); + if (MetricStatusToSeverity[metricStatus] > MetricStatusToSeverity[status]) { + status = metricStatus; + } + + return { + title: formatBytes({value: limit, withSpeedLabel: true}), + value: used, + capacity: limit, + percents: true, + overflow: true, + }; + }); + + return ( + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx index 75f80acf3..192e22e3a 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx @@ -86,6 +86,7 @@ export function TenantOverview({ poolsStats, memoryStats, + networkStats, blobStorageStats, tabletStorageStats, } = calculateTenantMetrics(tenantData); @@ -162,6 +163,7 @@ export function TenantOverview({ memoryStats={memoryStats} blobStorageStats={blobStorageStats} tabletStorageStats={tabletStorageStats} + networkStats={networkStats} /> diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json index 996c1722f..51bc2fa59 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json @@ -24,6 +24,8 @@ "cards.cpu-label": "CPU", "cards.storage-label": "Storage", "cards.memory-label": "Memory", + "cards.network-label": "Network", + "cards.network-note": "Network usage is the average outgoing bandwidth usage across all nodes in the database", "charts.queries-per-second": "Queries per second", "charts.transaction-latency": "Transactions latencies {{percentile}}", diff --git a/src/containers/UserSettings/i18n/en.json b/src/containers/UserSettings/i18n/en.json index 638c0b82d..f8788754e 100644 --- a/src/containers/UserSettings/i18n/en.json +++ b/src/containers/UserSettings/i18n/en.json @@ -38,7 +38,7 @@ "settings.enableNetworkTable.title": "Enable network table", - "settings.showNetworkUtilization.title": "Show cluster network utilization", + "settings.showNetworkUtilization.title": "Show network utilization", "settings.useShowPlanToSvg.title": "Execution plan", "settings.useShowPlanToSvg.description": " Show \"Execution plan\" button in query result widow. Opens svg with execution plan in a new window.", diff --git a/src/store/reducers/tenants/utils.ts b/src/store/reducers/tenants/utils.ts index e596bc3ad..c9ec581ad 100644 --- a/src/store/reducers/tenants/utils.ts +++ b/src/store/reducers/tenants/utils.ts @@ -1,8 +1,10 @@ +import {isNil} from 'lodash'; + import type {PoolName, TPoolStats} from '../../../types/api/nodes'; import type {TTenant} from '../../../types/api/tenant'; import {EType} from '../../../types/api/tenant'; import {DEFAULT_DANGER_THRESHOLD, DEFAULT_WARNING_THRESHOLD} from '../../../utils/constants'; -import {isNumeric} from '../../../utils/utils'; +import {isNumeric, safeParseNumber} from '../../../utils/utils'; import {METRIC_STATUS} from './contants'; import type {PreparedTenant} from './types'; @@ -16,7 +18,7 @@ const getControlPlaneValue = (tenant: TTenant) => { }; export interface TenantMetricStats { - name: T; + name?: T; usage?: number; limit?: number; used?: number; @@ -60,6 +62,8 @@ export const calculateTenantMetrics = (tenant: TTenant = {}) => { DatabaseQuotas = {}, StorageUsage, QuotaUsage, + NetworkUtilization, + NetworkWriteThroughput, } = tenant; const cpu = Number(CoresUsed) * 1_000_000 || 0; @@ -140,6 +144,18 @@ export const calculateTenantMetrics = (tenant: TTenant = {}) => { }, ]; + const isNetworkStatsAvailabe = !isNil(NetworkUtilization) && !isNil(NetworkWriteThroughput); + const networkThroughput = safeParseNumber(NetworkWriteThroughput); + const usedNetwork = safeParseNumber(NetworkUtilization) * networkThroughput; + const networkStats: TenantMetricStats[] | undefined = isNetworkStatsAvailabe + ? [ + { + used: usedNetwork, + limit: networkThroughput, + }, + ] + : undefined; + return { memory, blobStorage, @@ -153,6 +169,7 @@ export const calculateTenantMetrics = (tenant: TTenant = {}) => { memoryStats, blobStorageStats, tabletStorageStats, + networkStats, }; }; diff --git a/src/types/api/tenant.ts b/src/types/api/tenant.ts index 4fb38fb2f..a9b6aebd3 100644 --- a/src/types/api/tenant.ts +++ b/src/types/api/tenant.ts @@ -62,6 +62,11 @@ export interface TTenant { StorageUsage?: TStorageUsage[]; QuotaUsage?: TStorageUsage[]; + + /** value is float */ + NetworkUtilization?: number; + /** value is uint64 */ + NetworkWriteThroughput?: string; } export interface THiveDomainStatsStateCount { From f531ff7fe56b069e091d9ffa89bcbcc3e6e4f430 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Tue, 3 Jun 2025 15:11:27 +0300 Subject: [PATCH 09/14] fix: design review --- .../components/HealthcheckIssue.tsx | 7 ++- .../ComputeLocation.tsx | 4 +- .../HealthcheckIssueDetails.tsx | 51 ++++++++----------- .../HealthcheckIssueDetails/NodeInfo.tsx | 2 +- .../HealthcheckIssueDetails/PoolInfo.tsx | 7 +-- .../StorageLocation.tsx | 5 +- .../HealthcheckIssueDetails/utils.tsx | 5 +- .../Tenant/Healthcheck/i18n/en.json | 37 +++++--------- 8 files changed, 47 insertions(+), 71 deletions(-) diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx index ae59f1fcd..af4b611de 100644 --- a/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx @@ -54,7 +54,12 @@ export function HealthcheckIssue({issue}: HealthcheckIssueProps) { )} - + + {fields.map((field) => LocationFieldRenderer[field](location))} @@ -79,7 +79,6 @@ function TabletInfo({location}: ComputeSectionProps) { return ( ) : undefined, - title: `${i18n('label_tablet')} ${i18n('label_tablet-id')}`, + title: i18n('label_tablet-id'), }, { value: tablet?.type, - title: `${i18n('label_tablet')} ${i18n('label_tablet-type')}`, + title: i18n('label_tablet-type'), }, { value: tablet?.count, - title: `${i18n('label_tablet')} ${i18n('label_tablet-count')}`, + title: i18n('label_tablet-count'), }, ); hiddenComputeFields.push('tablet'); @@ -70,35 +77,19 @@ export function IssueDetails({issue}: HealthcheckIssueDetailsProps) { return ( - - - - - - - - + + + + + ); } -interface DatabaseLocationProps { - location: Location['database']; -} - -function DatabaseLocation({location}: DatabaseLocationProps) { - if (!location || !location.name) { - return null; - } - - return ( - - ); -} - interface NodeLocationProps { location: Location['node']; } @@ -126,7 +117,7 @@ function PeerLocation({location}: PeerLocationProps) { return ( - + ); } diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.tsx index e5a0041b5..1d61e7a21 100644 --- a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.tsx +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.tsx @@ -11,7 +11,7 @@ interface NodeInfoProps { title?: string; } -export function NodeInfo({node, title = i18n('label_node')}: NodeInfoProps) { +export function NodeInfo({node, title}: NodeInfoProps) { const {database} = useTenantQueryParams(); if (!node) { return null; diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/PoolInfo.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/PoolInfo.tsx index 5d6dfba89..d5dd378d5 100644 --- a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/PoolInfo.tsx +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/PoolInfo.tsx @@ -15,10 +15,5 @@ export function PoolInfo({location}: PoolInfoProps) { return null; } - return ( - - ); + return ; } diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx index 48b16c82c..999bae49b 100644 --- a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx @@ -63,7 +63,7 @@ export function StorageLocation({location, hiddenFields = []}: StorageLocationPr } return ( - + {fields.map((field) => LocationFieldRenderer[field](location))} @@ -87,7 +87,6 @@ function GroupInfo({location}: StorageSectionProps) { return ( : undefined, @@ -111,7 +110,6 @@ function VDiskInfo({location}: StorageSectionProps) { return ( ( f.value); if (filteredFields.length === 0) { @@ -37,7 +38,7 @@ export function LocationDetails({title, fields}: LocationDetailsProps) { } return ( - + {filteredFields.map((field) => ( diff --git a/src/containers/Tenant/Healthcheck/i18n/en.json b/src/containers/Tenant/Healthcheck/i18n/en.json index a737c94f0..caef14688 100644 --- a/src/containers/Tenant/Healthcheck/i18n/en.json +++ b/src/containers/Tenant/Healthcheck/i18n/en.json @@ -20,33 +20,22 @@ "label_no-issues": "No issues", "description_no-issues": "Here you will see issues that require your attention", "label_details": "Details", - "label_location": "Location", "label_storage_location": "Storage Location", "label_compute_location": "Compute Location", - "label_database_location": "Database Location", - "label_node": "Node", - "label_pool": "Pool", - "label_group": "Group", - "label_vdisk": "VDisk", - "label_pdisk": "PDisk", - "label_tablet": "Tablet", - "label_schema": "Schema", - "label_database": "Database", "label_description": "Description", - "label_node-id": "ID", - "label_node-host": "Host", - "label_node-port": "Port", - "label_pool-name": "Name", - "label_tablet-id": "ID", - "label_tablet-type": "Type", - "label_tablet-count": "Count", - "label_schema-type": "Type", - "label_schema-path": "Path", - "label_group-id": "ID", - "label_vdisk-id": "ID", - "label_pdisk-id": "ID", - "label_pdisk-path": "Path", - "label_peer": "Peer Node", + "label_node-id": "Node ID", + "label_node-host": "Node Host", + "label_node-port": "Node Port", + "label_pool-name": "Pool Name", + "label_tablet-id": "Tablet ID", + "label_tablet-type": "Tablet Type", + "label_tablet-count": "Tablets Count", + "label_schema-type": "Schema Type", + "label_schema-path": "Schema Path", + "label_group-id": "Group ID", + "label_vdisk-id": "VDisk ID", + "label_pdisk-id": "Pdisk ID", + "label_pdisk-path": "PDisk Path", "label_node_location": "Node Location", "label_peer_location": "Peer Node Location" } From 996c7010556141798e55f77defc50be450a1548f Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Wed, 4 Jun 2025 13:14:01 +0300 Subject: [PATCH 10/14] fix: add animation --- .../Tenant/Healthcheck/Healthcheck.scss | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/containers/Tenant/Healthcheck/Healthcheck.scss b/src/containers/Tenant/Healthcheck/Healthcheck.scss index 89e622cd3..3dce8b788 100644 --- a/src/containers/Tenant/Healthcheck/Healthcheck.scss +++ b/src/containers/Tenant/Healthcheck/Healthcheck.scss @@ -24,6 +24,19 @@ &__issue-wrapper { border: 1px solid var(--g-color-line-generic); border-radius: var(--g-border-radius-s); + + .g-disclosure_enter_active { + display: grid; + + animation-name: disclosure-expanded; + animation-duration: 0.2s; + } + .g-disclosure_exit_active { + display: grid; + + animation-name: disclosure-collapsed; + animation-duration: 0.1s; + } } &__issue-summary { padding: var(--g-spacing-4); @@ -76,3 +89,21 @@ padding: 0 var(--g-spacing-4) var(--g-spacing-4) var(--g-spacing-4); } } + +@keyframes disclosure-expanded { + 0% { + grid-template-rows: 0fr; + } + 100% { + grid-template-rows: 1fr; + } +} + +@keyframes disclosure-collapsed { + 0% { + grid-template-rows: 1fr; + } + 100% { + grid-template-rows: 0fr; + } +} From 4eeb23dcc07f832220a3ba8c584df9004fa01b0b Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Wed, 4 Jun 2025 13:17:31 +0300 Subject: [PATCH 11/14] fix: add animation --- src/containers/Tenant/Healthcheck/Healthcheck.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/containers/Tenant/Healthcheck/Healthcheck.scss b/src/containers/Tenant/Healthcheck/Healthcheck.scss index 3dce8b788..cbdd6dd88 100644 --- a/src/containers/Tenant/Healthcheck/Healthcheck.scss +++ b/src/containers/Tenant/Healthcheck/Healthcheck.scss @@ -86,6 +86,8 @@ background-color: var(--g-color-text-danger); } &__issue-details { + overflow: hidden; + padding: 0 var(--g-spacing-4) var(--g-spacing-4) var(--g-spacing-4); } } From 56713a286d0f3fdd60f0d3e1daabe4c6c32fdc80 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Wed, 4 Jun 2025 16:03:12 +0300 Subject: [PATCH 12/14] fix: review --- src/components/Drawer/Drawer.scss | 2 +- src/components/Drawer/Drawer.tsx | 12 ++++++------ src/components/EmptyState/EmptyState.scss | 2 +- .../EnableFullscreenButton.tsx | 4 +++- src/components/EnableFullscreenButton/i18n/en.json | 3 +++ .../EnableFullscreenButton/i18n/index.ts | 7 +++++++ src/components/ProgressViewer/ProgressViewer.tsx | 8 ++++---- .../TenantOverview/Healthcheck/i18n/en.json | 2 +- .../TenantOverview/MetricsCards/MetricsCards.tsx | 2 +- src/containers/Tenant/Healthcheck/Healthcheck.scss | 2 +- .../Healthcheck/components/HealthcheckIssue.tsx | 5 +++-- .../HealthcheckIssueDetails/StorageLocation.tsx | 14 +++++++------- .../Healthcheck/components/HealthcheckIssues.tsx | 8 ++++++-- .../Healthcheck/components/HealthcheckView.tsx | 4 ++-- src/containers/Tenant/Healthcheck/shared.ts | 2 +- src/store/reducers/healthcheckInfo/types.ts | 2 +- src/store/reducers/healthcheckInfo/utils.ts | 6 +++++- src/uiFactory/types.ts | 6 ++++-- src/uiFactory/uiFactory.ts | 6 ++++-- 19 files changed, 61 insertions(+), 36 deletions(-) create mode 100644 src/components/EnableFullscreenButton/i18n/en.json create mode 100644 src/components/EnableFullscreenButton/i18n/index.ts diff --git a/src/components/Drawer/Drawer.scss b/src/components/Drawer/Drawer.scss index 5b7907c1e..bbeff174d 100644 --- a/src/components/Drawer/Drawer.scss +++ b/src/components/Drawer/Drawer.scss @@ -8,7 +8,7 @@ } &__item { - z-index: 11; + z-index: 4; height: 100%; } diff --git a/src/components/Drawer/Drawer.tsx b/src/components/Drawer/Drawer.tsx index fcc558ede..96f0dad33 100644 --- a/src/components/Drawer/Drawer.tsx +++ b/src/components/Drawer/Drawer.tsx @@ -32,7 +32,7 @@ interface DrawerPaneContentWrapperProps { detectClickOutside?: boolean; defaultWidth?: number; isPercentageWidth?: boolean; - showVeil?: boolean; + hideVeil?: boolean; } const DrawerPaneContentWrapper = ({ @@ -46,7 +46,7 @@ const DrawerPaneContentWrapper = ({ className, detectClickOutside = false, isPercentageWidth, - showVeil, + hideVeil = true, }: DrawerPaneContentWrapperProps) => { const [drawerWidth, setDrawerWidth] = React.useState(() => { const savedWidth = localStorage.getItem(storageKey); @@ -115,7 +115,7 @@ const DrawerPaneContentWrapper = ({ { React.useEffect(() => { return () => { @@ -224,7 +224,7 @@ export const DrawerWrapper = ({ {children} + diff --git a/src/components/EnableFullscreenButton/i18n/en.json b/src/components/EnableFullscreenButton/i18n/en.json new file mode 100644 index 000000000..fb05e75f2 --- /dev/null +++ b/src/components/EnableFullscreenButton/i18n/en.json @@ -0,0 +1,3 @@ +{ + "title_fullscreen": "Fullscreen" +} diff --git a/src/components/EnableFullscreenButton/i18n/index.ts b/src/components/EnableFullscreenButton/i18n/index.ts new file mode 100644 index 000000000..7d147fc39 --- /dev/null +++ b/src/components/EnableFullscreenButton/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-fullscreen-button'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/components/ProgressViewer/ProgressViewer.tsx b/src/components/ProgressViewer/ProgressViewer.tsx index c6749f881..f7ef0001b 100644 --- a/src/components/ProgressViewer/ProgressViewer.tsx +++ b/src/components/ProgressViewer/ProgressViewer.tsx @@ -35,7 +35,7 @@ Props description: 6) inverseColorize - invert the colors of the progress bar 7) warningThreshold - the percentage of fullness at which the color of the progress bar changes to yellow 8) dangerThreshold - the percentage of fullness at which the color of the progress bar changes to red -9) overflow - parcents may be more that 100% +9) withOverflow - percents may be more that 100% */ export interface ProgressViewerProps { @@ -50,7 +50,7 @@ export interface ProgressViewerProps { warningThreshold?: number; dangerThreshold?: number; hideCapacity?: boolean; - overflow?: boolean; + withOverflow?: boolean; } export function ProgressViewer({ @@ -58,7 +58,7 @@ export function ProgressViewer({ capacity, formatValues = defaultFormatValues, percents, - overflow, + withOverflow, className, size = 'xs', colorizeProgress, @@ -77,7 +77,7 @@ export function ProgressViewer({ capacityText: number | string | undefined = capacity, divider = '/'; if (percents) { - valueText = (overflow ? rawFillWidth : fillWidth) + '%'; + valueText = (withOverflow ? rawFillWidth : fillWidth) + '%'; capacityText = ''; divider = ''; } else if (formatValues) { diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/en.json b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/en.json index 880d2fc70..1d572c4cf 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/i18n/en.json @@ -1,6 +1,6 @@ { "description_problems": [ - "There is {{count}} issues.", + "There is {{count}} issue.", "There are {{count}} issues.", "There are {{count}} issues.", "There are {{count}} issues." diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx index 2e740619d..84290700d 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx @@ -239,7 +239,7 @@ function NetworkCard({networkStats}: NetworkCardProps) { value: used, capacity: limit, percents: true, - overflow: true, + withOverflow: true, }; }); diff --git a/src/containers/Tenant/Healthcheck/Healthcheck.scss b/src/containers/Tenant/Healthcheck/Healthcheck.scss index cbdd6dd88..59b8d7205 100644 --- a/src/containers/Tenant/Healthcheck/Healthcheck.scss +++ b/src/containers/Tenant/Healthcheck/Healthcheck.scss @@ -8,7 +8,7 @@ &__controls { position: sticky; z-index: 1; - top: 0px; + top: 0; left: 0; padding: var(--g-spacing-4); diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx index af4b611de..416b5228e 100644 --- a/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx @@ -12,9 +12,10 @@ import {HealthcheckIssueTabs} from './HealthcheckIssueTabs'; interface HealthcheckIssueProps { issue: IssuesTree; + expanded?: boolean; } -export function HealthcheckIssue({issue}: HealthcheckIssueProps) { +export function HealthcheckIssue({issue, expanded}: HealthcheckIssueProps) { const [selectedTab, setSelectedTab] = React.useState(issue.id); const parents = React.useMemo(() => { const parents = []; @@ -32,7 +33,7 @@ export function HealthcheckIssue({issue}: HealthcheckIssueProps) { return ( - + {(props) => (
diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx index 999bae49b..b745a099a 100644 --- a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx @@ -143,22 +143,22 @@ function PDiskInfo({location}: StorageSectionProps) { return null; } - return pdisk.map((el: {id: string; path: string}) => ( + return pdisk.map((disk: {id: string; path: string}) => ( - {el.id} + disk.id && node?.id ? ( + + {disk.id} ) : ( - el.id + disk.id ), title: i18n('label_pdisk-id'), }, - {value: el.path, title: i18n('label_pdisk-path')}, + {value: disk.path, title: i18n('label_pdisk-path')}, ]} /> )); diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssues.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssues.tsx index df18921bd..208dbd1b5 100644 --- a/src/containers/Tenant/Healthcheck/components/HealthcheckIssues.tsx +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssues.tsx @@ -40,7 +40,7 @@ export function Issues({issues}: IssuesProps) { () => view ? filteredIssues.filter((issue) => { - const type = issue.upperType || issue.type; + const type = issue.firstParentType || issue.type; return type.toLowerCase().startsWith(view); }) : [], @@ -62,6 +62,10 @@ export function Issues({issues}: IssuesProps) { } return filteredIssuesCurrentView.map((issue) => ( - + )); } diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckView.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckView.tsx index 5987a2996..cf3a4f431 100644 --- a/src/containers/Tenant/Healthcheck/components/HealthcheckView.tsx +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckView.tsx @@ -20,8 +20,8 @@ interface HealthcheckViewProps { export function HealthcheckView({ issuesCount, - viewTitles = uiFactory.getHealthckechViewTitles(), - sortOrder = uiFactory.getHealthcheckViewsOrder(), + viewTitles = uiFactory.healthcheck.getHealthckechViewTitles(), + sortOrder = uiFactory.healthcheck.getHealthcheckViewsOrder(), }: HealthcheckViewProps) { const {view, handleHealthcheckViewChange, handleIssuesFilterChange} = useTenantQueryParams(); diff --git a/src/containers/Tenant/Healthcheck/shared.ts b/src/containers/Tenant/Healthcheck/shared.ts index 52f1893dc..49786d48b 100644 --- a/src/containers/Tenant/Healthcheck/shared.ts +++ b/src/containers/Tenant/Healthcheck/shared.ts @@ -35,7 +35,7 @@ export function countHealthcheckIssuesByType( }; for (const issue of issueTrees) { - const type = issue.upperType ?? issue.type; + const type = issue.firstParentType ?? issue.type; if (type.startsWith('STORAGE')) { result.storage++; } else if (type.startsWith('COMPUTE')) { diff --git a/src/store/reducers/healthcheckInfo/types.ts b/src/store/reducers/healthcheckInfo/types.ts index 8afd3b86c..055dcfa5f 100644 --- a/src/store/reducers/healthcheckInfo/types.ts +++ b/src/store/reducers/healthcheckInfo/types.ts @@ -3,5 +3,5 @@ import type {IssueLog} from '../../../types/api/healthcheck'; export interface IssuesTree extends IssueLog { reasonsItems?: IssuesTree[]; parent?: IssuesTree; - upperType?: string; + firstParentType?: string; } diff --git a/src/store/reducers/healthcheckInfo/utils.ts b/src/store/reducers/healthcheckInfo/utils.ts index 764b5199b..d70c2bcb2 100644 --- a/src/store/reducers/healthcheckInfo/utils.ts +++ b/src/store/reducers/healthcheckInfo/utils.ts @@ -43,7 +43,11 @@ export function getLeavesFromTree(issues: IssueLog[], root: IssueLog): IssuesTre if (!child) { continue; } - const extendedChild = {...child, parent: currentNode, upperType: directChildType}; + const extendedChild = { + ...child, + parent: currentNode, + firstParentType: directChildType, + }; stack.push(extendedChild); } } diff --git a/src/uiFactory/types.ts b/src/uiFactory/types.ts index 60da23712..98c349c79 100644 --- a/src/uiFactory/types.ts +++ b/src/uiFactory/types.ts @@ -17,8 +17,10 @@ export interface UIFactory { getMonitoringLink?: GetMonitoringLink; getMonitoringClusterLink?: GetMonitoringClusterLink; - getHealthckechViewTitles: GetHealthcheckViewTitles; - getHealthcheckViewsOrder: GetHealthcheckViewsOrder; + healthcheck: { + getHealthckechViewTitles: GetHealthcheckViewTitles; + getHealthcheckViewsOrder: GetHealthcheckViewsOrder; + }; countHealthcheckIssuesByType: ( issueTrees: IssuesTree[], ) => Record & Record; diff --git a/src/uiFactory/uiFactory.ts b/src/uiFactory/uiFactory.ts index e7f5c8eee..6894f23f3 100644 --- a/src/uiFactory/uiFactory.ts +++ b/src/uiFactory/uiFactory.ts @@ -13,8 +13,10 @@ import type {UIFactory} from './types'; const uiFactoryBase: UIFactory = { getMonitoringLink: getMonitoringLinkDefault, getMonitoringClusterLink: getMonitoringClusterLinkDefault, - getHealthckechViewTitles: getHealthckechViewTitles, - getHealthcheckViewsOrder: getHealthcheckViewsOrder, + healthcheck: { + getHealthckechViewTitles, + getHealthcheckViewsOrder, + }, countHealthcheckIssuesByType: countHealthcheckIssuesByType, }; From 2ce27ab08e8e7e0d070d6b63fc4a34504b0b835d Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Wed, 4 Jun 2025 17:53:48 +0300 Subject: [PATCH 13/14] fix: review --- .../Tenant/Healthcheck/Healthcheck.scss | 15 ++++++++------- .../components/HealthcheckIssue.tsx | 18 ++++++++++-------- src/containers/Tenant/TenantDrawerWrappers.tsx | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/containers/Tenant/Healthcheck/Healthcheck.scss b/src/containers/Tenant/Healthcheck/Healthcheck.scss index 59b8d7205..1ed557c5f 100644 --- a/src/containers/Tenant/Healthcheck/Healthcheck.scss +++ b/src/containers/Tenant/Healthcheck/Healthcheck.scss @@ -21,21 +21,24 @@ &__issues { padding: 0 var(--g-spacing-4) var(--g-spacing-4) var(--g-spacing-4); } + + &__animation-container { + overflow: hidden; + + min-height: 0; + } &__issue-wrapper { border: 1px solid var(--g-color-line-generic); border-radius: var(--g-border-radius-s); - .g-disclosure_enter_active { display: grid; - animation-name: disclosure-expanded; - animation-duration: 0.2s; + animation: disclosure-expanded 0.4s cubic-bezier(0.23, 1, 0.32, 1) forwards; } .g-disclosure_exit_active { display: grid; - animation-name: disclosure-collapsed; - animation-duration: 0.1s; + animation: disclosure-collapsed 0.4s cubic-bezier(0.23, 1, 0.32, 1) forwards; } } &__issue-summary { @@ -86,8 +89,6 @@ background-color: var(--g-color-text-danger); } &__issue-details { - overflow: hidden; - padding: 0 var(--g-spacing-4) var(--g-spacing-4) var(--g-spacing-4); } } diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx index 416b5228e..6d2ed4873 100644 --- a/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx @@ -71,14 +71,16 @@ export function HealthcheckIssue({issue, expanded}: HealthcheckIssueProps) {
)}
- - - {currentIssue && } - +
+ + + {currentIssue && } + +
); diff --git a/src/containers/Tenant/TenantDrawerWrappers.tsx b/src/containers/Tenant/TenantDrawerWrappers.tsx index 63ed53746..0d4c1a9b5 100644 --- a/src/containers/Tenant/TenantDrawerWrappers.tsx +++ b/src/containers/Tenant/TenantDrawerWrappers.tsx @@ -57,7 +57,7 @@ export function TenantDrawerWrapper({children, database}: TenantDrawerWrapperPro drawerId="tenant-healthcheck-details" storageKey="tenant-healthcheck-details-drawer-width" detectClickOutside - showVeil + hideVeil={false} isPercentageWidth drawerControls={[ { From 7281810737eb9f11934a51be22f2466bbf70c9bd Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Wed, 4 Jun 2025 17:55:39 +0300 Subject: [PATCH 14/14] fix: animation --- src/containers/Tenant/Healthcheck/Healthcheck.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containers/Tenant/Healthcheck/Healthcheck.scss b/src/containers/Tenant/Healthcheck/Healthcheck.scss index 1ed557c5f..3b5b58f9f 100644 --- a/src/containers/Tenant/Healthcheck/Healthcheck.scss +++ b/src/containers/Tenant/Healthcheck/Healthcheck.scss @@ -33,12 +33,12 @@ .g-disclosure_enter_active { display: grid; - animation: disclosure-expanded 0.4s cubic-bezier(0.23, 1, 0.32, 1) forwards; + animation: disclosure-expanded 0.2s cubic-bezier(0.23, 1, 0.32, 1) forwards; } .g-disclosure_exit_active { display: grid; - animation: disclosure-collapsed 0.4s cubic-bezier(0.23, 1, 0.32, 1) forwards; + animation: disclosure-collapsed 0.2s cubic-bezier(0.23, 1, 0.32, 1) forwards; } } &__issue-summary {