Skip to content

feat: redesign CPU section #2597

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# GitHub Copilot Instructions for YDB Embedded UI

> **Note**: This file contains project-specific instructions for GitHub Copilot code review and assistance.
> These instructions are derived from AGENTS.md but formatted specifically for Copilot's consumption.
> When updating project conventions, update both AGENTS.md (for human developers) and this file (for Copilot).

## Project Overview

This is a React-based monitoring and management interface for YDB clusters. The codebase follows specific patterns and conventions that must be maintained.

## Tech Stack Requirements

- Use React 18.3 with TypeScript 5.x
- Use Redux Toolkit 2.x with RTK Query for state management
- Use Gravity UI (@gravity-ui/uikit) 7.x for UI components
- Use React Router v5 (NOT v6) for routing
- Use Monaco Editor 0.52 for code editing features

## Critical Coding Rules

### API Architecture

- NEVER call APIs directly - always use `window.api.module.method()` pattern
- Use RTK Query's `injectEndpoints` pattern for API endpoints
- Wrap `window.api` calls in RTK Query for proper state management

### Component Patterns

- Use BEM naming with `cn()` utility: `const b = cn('component-name')`
- Use `PaginatedTable` component for all data tables
- Tables require: columns, fetchData function, and unique tableName
- Use virtual scrolling for large datasets

### Internationalization (MANDATORY)

- NEVER hardcode user-facing strings
- ALWAYS create i18n entries in component's `i18n/` folder
- Follow key format: `<context>_<content>` (e.g., `action_save`, `field_name`)
- Register keysets with `registerKeysets()` using unique component name

### State Management

- Use Redux Toolkit with domain-based organization
- NEVER mutate state in RTK Query - return new objects/arrays
- Clear errors on user input in forms
- Always handle loading states in UI

### UI Components

- Prefer Gravity UI components over custom implementations
- Use `createToast` for notifications
- Use `ResponseError` component for API errors
- Use `Loader` and `TableSkeleton` for loading states

### Form Handling

- Always use controlled components with validation
- Clear errors on user input
- Validate before submission
- Use Gravity UI form components with error states

### Dialog/Modal Patterns

- Use `@ebay/nice-modal-react` for complex modals
- Use Gravity UI `Dialog` for simple dialogs
- Always include loading states

### Type Conventions

- API types prefixed with 'T' (e.g., `TTenantInfo`, `TClusterInfo`)
- Types located in `src/types/api/` directory

### Performance Requirements

- Use React.memo for expensive renders
- Lazy load Monaco Editor
- Batch API requests when possible
- Use virtual scrolling for tables

### Testing Patterns

- Unit tests colocated in `__test__` directories
- E2E tests use Playwright with page objects pattern
- Use CSS class selectors for E2E element selection

### Navigation (React Router v5)

- Use React Router v5 hooks (`useHistory`, `useParams`)
- Always validate route params exist before use

## Common Utilities

- Formatters: `formatBytes()`, `formatDateTime()` from `src/utils/dataFormatters/`
- Time parsing: utilities in `src/utils/timeParsers/`
- Query utilities: `src/utils/query.ts` for SQL/YQL helpers

## Before Making Changes

- Run `npm run lint` and `npm run typecheck` before committing
- Follow conventional commit message format
- Ensure all user-facing text is internationalized
- Test with a local YDB instance when possible

## Debugging Helpers

- `window.api` - Access API methods in browser console
- `window.ydbEditor` - Monaco editor instance
- Enable request tracing with `DEV_ENABLE_TRACING_FOR_ALL_REQUESTS`
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ that cannot be merged.
To make a contribution you should submit a pull request. There will probably be discussion about the pull request and,
if any changes are needed, we would love to work with you to get your pull request merged.

## Development Guidelines

When making changes to coding conventions or project patterns:
- Update `AGENTS.md` - This is the primary documentation for human developers
- Update `.github/copilot-instructions.md` - This contains the same information formatted for GitHub Copilot

Both files should be kept in sync to ensure consistent code generation and review by both humans and AI assistants.

## Other questions

If you have any questions, please mail us at info@ydb.tech.
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,20 @@
*/

@mixin tab-edge-filler($side) {
&.tenant-metrics-cards__link-container_active::after {
&.tenant-metrics-tabs__link-container_active::after {
@include pseudo-active-filler($side);
}

&:not(.tenant-metrics-cards__link-container_active)::after {
&:not(.tenant-metrics-tabs__link-container_active)::after {
@include pseudo-inactive-filler($side);
}

&:not(.tenant-metrics-cards__link-container_active)::before {
&:not(.tenant-metrics-tabs__link-container_active)::before {
@include pseudo-background-fill($side);
}
}

.tenant-metrics-cards {
.tenant-metrics-tabs {
// CSS Variables for consistent design system
--tab-border-width: 1px;
--tab-filler-size: 10px;
Expand Down Expand Up @@ -198,7 +198,7 @@
}
}

.tenant-metrics-cards__link {
.tenant-metrics-tabs__link {
padding-bottom: var(--tab-border-compensation);
@include tab-border-base(var(--g-color-line-generic));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,25 @@ import {TenantTabsGroups, getTenantPath} from '../../../TenantPages';
import {TabCard} from '../TabCard/TabCard';
import i18n from '../i18n';

import './MetricsCards.scss';
import './MetricsTabs.scss';

const b = cn('tenant-metrics-cards');
const b = cn('tenant-metrics-tabs');

interface MetricsCardsProps {
interface MetricsTabsProps {
poolsCpuStats?: TenantPoolsStats[];
memoryStats?: TenantMetricStats[];
blobStorageStats?: TenantStorageStats[];
tabletStorageStats?: TenantStorageStats[];
networkStats?: TenantMetricStats[];
}

export function MetricsCards({
export function MetricsTabs({
poolsCpuStats,
memoryStats,
blobStorageStats,
tabletStorageStats,
networkStats,
}: MetricsCardsProps) {
}: MetricsTabsProps) {
const location = useLocation();
const {metricsTab} = useTypedSelector((state) => state.tenant);
const queryParams = parseQuery(location);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@use '../../../../../styles/mixins.scss';

.tenant-cpu {
&__tabs-container {
margin-top: var(--g-spacing-3);
}

&__tab-content {
margin-top: var(--g-spacing-3);
}

&__all-nodes-link {
display: flex;
align-items: center;
gap: var(--g-spacing-1);

margin-right: var(--g-spacing-2);

@include mixins.body-1-typography();
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,125 @@
import React from 'react';

import {ArrowRight} from '@gravity-ui/icons';
import {Flex, Icon, SegmentedRadioGroup, Tab, TabList, TabProvider} from '@gravity-ui/uikit';

import {InternalLink} from '../../../../../components/InternalLink';
import {
TENANT_CPU_NODES_MODE_IDS,
TENANT_CPU_TABS_IDS,
TENANT_DIAGNOSTICS_TABS_IDS,
} from '../../../../../store/reducers/tenant/constants';
import type {AdditionalNodesProps} from '../../../../../types/additionalProps';
import {cn} from '../../../../../utils/cn';
import {useDiagnosticsPageLinkGetter} from '../../../Diagnostics/DiagnosticsPages';
import {TenantDashboard} from '../TenantDashboard/TenantDashboard';
import i18n from '../i18n';

import {TopNodesByCpu} from './TopNodesByCpu';
import {TopNodesByLoad} from './TopNodesByLoad';
import {TopQueries} from './TopQueries';
import {TopShards} from './TopShards';
import {cpuDashboardConfig} from './cpuDashboardConfig';
import {useTenantCpuQueryParams} from './useTenantCpuQueryParams';

import './TenantCpu.scss';

const b = cn('tenant-cpu');

const cpuTabs = [
{id: TENANT_CPU_TABS_IDS.nodes, title: i18n('title_top-nodes')},
{id: TENANT_CPU_TABS_IDS.shards, title: i18n('title_top-shards')},
{id: TENANT_CPU_TABS_IDS.queries, title: i18n('title_top-queries')},
];

interface TenantCpuProps {
tenantName: string;
additionalNodesProps?: AdditionalNodesProps;
}

export function TenantCpu({tenantName, additionalNodesProps}: TenantCpuProps) {
const {cpuTab, nodesMode, handleCpuTabChange, handleNodesModeChange} =
useTenantCpuQueryParams();
const getDiagnosticsPageLink = useDiagnosticsPageLinkGetter();

const renderNodesContent = () => {
const nodesModeControl = (
<SegmentedRadioGroup value={nodesMode} onUpdate={handleNodesModeChange}>
<SegmentedRadioGroup.Option value={TENANT_CPU_NODES_MODE_IDS.load}>
{i18n('action_by-load')}
</SegmentedRadioGroup.Option>
<SegmentedRadioGroup.Option value={TENANT_CPU_NODES_MODE_IDS.pools}>
{i18n('action_by-pool-usage')}
</SegmentedRadioGroup.Option>
</SegmentedRadioGroup>
);

const allNodesButton = (
<InternalLink
className={b('all-nodes-link')}
to={getDiagnosticsPageLink(TENANT_DIAGNOSTICS_TABS_IDS.nodes)}
>
{i18n('action_all-nodes')}
<Icon data={ArrowRight} size={16} />
</InternalLink>
);

const nodesComponent =
nodesMode === TENANT_CPU_NODES_MODE_IDS.load ? (
<TopNodesByLoad
tenantName={tenantName}
additionalNodesProps={additionalNodesProps}
/>
) : (
<TopNodesByCpu
tenantName={tenantName}
additionalNodesProps={additionalNodesProps}
/>
);

return (
<Flex direction="column" gap={2}>
<Flex justifyContent="space-between" alignItems="center">
{nodesModeControl}
{allNodesButton}
</Flex>
{nodesComponent}
</Flex>
);
};

const renderTabContent = () => {
switch (cpuTab) {
case TENANT_CPU_TABS_IDS.nodes:
return renderNodesContent();
case TENANT_CPU_TABS_IDS.shards:
return <TopShards tenantName={tenantName} path={tenantName} />;
case TENANT_CPU_TABS_IDS.queries:
return <TopQueries tenantName={tenantName} />;
default:
return null;
}
};

return (
<React.Fragment>
<TenantDashboard database={tenantName} charts={cpuDashboardConfig} />
<TopNodesByLoad tenantName={tenantName} additionalNodesProps={additionalNodesProps} />
<TopNodesByCpu tenantName={tenantName} additionalNodesProps={additionalNodesProps} />
<TopShards tenantName={tenantName} path={tenantName} />
<TopQueries tenantName={tenantName} />

<div className={b('tabs-container')}>
<TabProvider value={cpuTab}>
<TabList size="m">
{cpuTabs.map(({id, title}) => {
return (
<Tab key={id} value={id} onClick={() => handleCpuTabChange(id)}>
{title}
</Tab>
);
})}
</TabList>
</TabProvider>

<div className={b('tab-content')}>{renderTabContent()}</div>
</div>
</React.Fragment>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,15 @@ import {
import type {GetNodesColumnsParams} from '../../../../../components/nodesColumns/types';
import {nodesApi} from '../../../../../store/reducers/nodes/nodes';
import type {NodesPreparedEntity} from '../../../../../store/reducers/nodes/types';
import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants';
import type {AdditionalNodesProps} from '../../../../../types/additionalProps';
import type {NodesRequiredField} from '../../../../../types/api/nodes';
import {
TENANT_OVERVIEW_TABLES_LIMIT,
TENANT_OVERVIEW_TABLES_SETTINGS,
} from '../../../../../utils/constants';
import {useAutoRefreshInterval, useSearchQuery} from '../../../../../utils/hooks';
import {useAutoRefreshInterval} from '../../../../../utils/hooks';
import {getRequiredDataFields} from '../../../../../utils/tableUtils/getRequiredDataFields';
import {TenantTabsGroups, getTenantPath} from '../../../TenantPages';
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
import {getSectionTitle} from '../getSectionTitle';
import i18n from '../i18n';

function getTopNodesByCpuColumns(
Expand All @@ -50,8 +47,6 @@ interface TopNodesByCpuProps {
}

export function TopNodesByCpu({tenantName, additionalNodesProps}: TopNodesByCpuProps) {
const query = useSearchQuery();

const [autoRefreshInterval] = useAutoRefreshInterval();
const [columns, fieldsRequired] = getTopNodesByCpuColumns({
getNodeRef: additionalNodesProps?.getNodeRef,
Expand All @@ -74,22 +69,8 @@ export function TopNodesByCpu({tenantName, additionalNodesProps}: TopNodesByCpuP

const topNodes = currentData?.Nodes || [];

const title = getSectionTitle({
entity: i18n('nodes'),
postfix: i18n('by-pools-usage'),
link: getTenantPath({
...query,
[TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.nodes,
}),
});

return (
<TenantOverviewTableLayout
title={title}
loading={loading}
error={error}
withData={Boolean(currentData)}
>
<TenantOverviewTableLayout loading={loading} error={error} withData={Boolean(currentData)}>
<ResizeableDataTable
columnsWidthLSKey={NODES_COLUMNS_WIDTH_LS_KEY}
data={topNodes}
Expand Down
Loading
Loading