diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..1f8c43606b --- /dev/null +++ b/.github/copilot-instructions.md @@ -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: `_` (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` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 891a34377d..e00cdb5be2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.scss b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.scss similarity index 96% rename from src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.scss rename to src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.scss index 42d78d9f7f..fe6b55b43c 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.scss +++ b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.scss @@ -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; @@ -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)); } diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx similarity index 97% rename from src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx rename to src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx index ef5fb992cc..d98331f3d1 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx @@ -21,11 +21,11 @@ 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[]; @@ -33,13 +33,13 @@ interface MetricsCardsProps { 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); diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.scss new file mode 100644 index 0000000000..048627b86e --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.scss @@ -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(); + } +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx index d9651f1da5..3a85c28655 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx @@ -1,13 +1,36 @@ 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; @@ -15,13 +38,88 @@ interface TenantCpuProps { } export function TenantCpu({tenantName, additionalNodesProps}: TenantCpuProps) { + const {cpuTab, nodesMode, handleCpuTabChange, handleNodesModeChange} = + useTenantCpuQueryParams(); + const getDiagnosticsPageLink = useDiagnosticsPageLinkGetter(); + + const renderNodesContent = () => { + const nodesModeControl = ( + + + {i18n('action_by-load')} + + + {i18n('action_by-pool-usage')} + + + ); + + const allNodesButton = ( + + {i18n('action_all-nodes')} + + + ); + + const nodesComponent = + nodesMode === TENANT_CPU_NODES_MODE_IDS.load ? ( + + ) : ( + + ); + + return ( + + + {nodesModeControl} + {allNodesButton} + + {nodesComponent} + + ); + }; + + const renderTabContent = () => { + switch (cpuTab) { + case TENANT_CPU_TABS_IDS.nodes: + return renderNodesContent(); + case TENANT_CPU_TABS_IDS.shards: + return ; + case TENANT_CPU_TABS_IDS.queries: + return ; + default: + return null; + } + }; + return ( - - - - + +
+ + + {cpuTabs.map(({id, title}) => { + return ( + handleCpuTabChange(id)}> + {title} + + ); + })} + + + +
{renderTabContent()}
+
); } diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx index 8f592c2791..49bb88f67e 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx @@ -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( @@ -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, @@ -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 ( - + + { + if (!queryParams.cpuTab) { + return TENANT_CPU_TABS_IDS.nodes; + } + const validTabs = Object.values(TENANT_CPU_TABS_IDS) as string[]; + return validTabs.includes(queryParams.cpuTab) + ? (queryParams.cpuTab as TenantCpuTab) + : TENANT_CPU_TABS_IDS.nodes; + })(); + + // Parse and validate nodesMode with fallback to load + const nodesMode: TenantNodesMode = (() => { + if (!queryParams.nodesMode) { + return TENANT_CPU_NODES_MODE_IDS.load; + } + const validModes = Object.values(TENANT_CPU_NODES_MODE_IDS) as string[]; + return validModes.includes(queryParams.nodesMode) + ? (queryParams.nodesMode as TenantNodesMode) + : TENANT_CPU_NODES_MODE_IDS.load; + })(); + + const handleCpuTabChange = (value: TenantCpuTab) => { + setQueryParams({cpuTab: value}, 'replaceIn'); + }; + + const handleNodesModeChange = (value: TenantNodesMode) => { + setQueryParams({nodesMode: value}, 'replaceIn'); + }; + + return { + cpuTab, + nodesMode, + handleCpuTabChange, + handleNodesModeChange, + }; +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss index e431272ff1..5f843dd47c 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss @@ -4,10 +4,9 @@ height: 100%; padding-bottom: 20px; - &__tenant-name-wrapper { - display: flex; - overflow: hidden; - align-items: center; + // Make header font size smaller using project typography + .data-table__th { + @include mixins.subheader-1-typography(); } &__top { @@ -35,9 +34,7 @@ &__title { margin-bottom: 10px; - font-weight: 700; - - @include mixins.body-2-typography(); + @include mixins.subheader-1-typography(); } &__table { diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx index 4e3075c536..2e375c84be 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 {HealthcheckPreview} from './Healthcheck/HealthcheckPreview'; -import {MetricsCards} from './MetricsCards/MetricsCards'; +import {MetricsTabs} from './MetricsTabs/MetricsTabs'; import {TenantCpu} from './TenantCpu/TenantCpu'; import {TenantMemory} from './TenantMemory/TenantMemory'; import {TenantStorage} from './TenantStorage/TenantStorage'; @@ -99,7 +99,7 @@ export function TenantOverview({ const renderName = () => { return ( -
+ -
+ ); }; @@ -163,7 +163,7 @@ export function TenantOverview({ - -
{title}
+ {title &&
{title}
} {error ? : null}
{renderContent()}
diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json index fba6578f12..b613553a2a 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json @@ -16,6 +16,7 @@ "by-memory": "by memory", "by-usage": "by usage", "by-size": "by size", + "action_all-nodes": "All Nodes", "cards.cpu-label": "CPU load", "cards.storage-label": "Storage", "cards.memory-label": "Memory used", @@ -39,5 +40,10 @@ "storage.db-storage-title": "Database storage", "storage.db-storage-description": "Size of data stored in distributed storage with all overheads for redundancy", "executed-last-hour": "executed in the last hour", - "column-header.process": "Process" + "column-header.process": "Process", + "title_top-nodes": "Top Nodes", + "title_top-shards": "Top Shards", + "title_top-queries": "Top Queries", + "action_by-load": "By Load", + "action_by-pool-usage": "By Pool Usage" } diff --git a/src/containers/Tenant/TenantPages.tsx b/src/containers/Tenant/TenantPages.tsx index 7b08a77390..f67117b4dc 100644 --- a/src/containers/Tenant/TenantPages.tsx +++ b/src/containers/Tenant/TenantPages.tsx @@ -26,6 +26,7 @@ export const TenantTabsGroups = { queryTab: 'queryTab', diagnosticsTab: 'diagnosticsTab', metricsTab: 'metricsTab', + cpuTab: 'cpuTab', } as const; export const TENANT_INFO_TABS = [ diff --git a/src/store/reducers/tenant/constants.ts b/src/store/reducers/tenant/constants.ts index d448a70731..6d1b7fe0d9 100644 --- a/src/store/reducers/tenant/constants.ts +++ b/src/store/reducers/tenant/constants.ts @@ -43,3 +43,14 @@ export const TENANT_METRICS_TABS_IDS = { storage: 'storage', memory: 'memory', } as const; + +export const TENANT_CPU_TABS_IDS = { + nodes: 'nodes', + shards: 'shards', + queries: 'queries', +} as const; + +export const TENANT_CPU_NODES_MODE_IDS = { + load: 'load', + pools: 'pools', +} as const; diff --git a/src/store/reducers/tenant/types.ts b/src/store/reducers/tenant/types.ts index 4ad67475aa..8915d1efa1 100644 --- a/src/store/reducers/tenant/types.ts +++ b/src/store/reducers/tenant/types.ts @@ -4,6 +4,8 @@ import type {ValueOf} from '../../../types/common'; import {TENANT_PAGES_IDS} from './constants'; import type { + TENANT_CPU_NODES_MODE_IDS, + TENANT_CPU_TABS_IDS, TENANT_DIAGNOSTICS_TABS_IDS, TENANT_METRICS_TABS_IDS, TENANT_QUERY_TABS_ID, @@ -17,6 +19,8 @@ export type TenantQueryTab = ValueOf; export type TenantDiagnosticsTab = ValueOf; export type TenantSummaryTab = ValueOf; export type TenantMetricsTab = ValueOf; +export type TenantCpuTab = ValueOf; +export type TenantNodesMode = ValueOf; export interface TenantState { tenantPage: TenantPage; diff --git a/src/utils/metrics.ts b/src/utils/metrics.ts index cdb1ea4df2..36bec6ed9e 100644 --- a/src/utils/metrics.ts +++ b/src/utils/metrics.ts @@ -79,7 +79,7 @@ export const getHighestSeverityStatus = (statuses: MetricStatus[]): MetricStatus /** * Calculate metric aggregates (sum of used/limit values) and determine status - * This replaces the repetitive calculation logic from MetricsCards.tsx + * This replaces the repetitive calculation logic from MetricsTabs.tsx */ export const calculateMetricAggregates = ( items: T[] = [], diff --git a/tests/suites/tenant/diagnostics/Diagnostics.ts b/tests/suites/tenant/diagnostics/Diagnostics.ts index 63cdd25ebf..54538fdada 100644 --- a/tests/suites/tenant/diagnostics/Diagnostics.ts +++ b/tests/suites/tenant/diagnostics/Diagnostics.ts @@ -304,11 +304,9 @@ export class Diagnostics { this.copyLinkButton = page.locator('.ydb-copy-link-button__icon'); // Info tab cards - this.cpuCard = page.locator('.tenant-metrics-cards__link-container:has-text("CPU")'); - this.storageCard = page.locator( - '.tenant-metrics-cards__link-container:has-text("Storage")', - ); - this.memoryCard = page.locator('.tenant-metrics-cards__link-container:has-text("Memory")'); + this.cpuCard = page.locator('.tenant-metrics-tabs__link-container:has-text("CPU")'); + this.storageCard = page.locator('.tenant-metrics-tabs__link-container:has-text("Storage")'); + this.memoryCard = page.locator('.tenant-metrics-tabs__link-container:has-text("Memory")'); this.healthcheckCard = page.locator('.ydb-healthcheck-preview'); this.ownerCard = page.locator('.ydb-access-rights__owner-card'); this.changeOwnerButton = page.locator('.ydb-access-rights__owner-card button');