- {pages.map(({id, title}) => {
- const linkPath = getDiagnosticsPageLink(id);
- return (
-
-
- {title}
-
-
- );
- })}
+ {pages.map(({id, title, badge}) => (
+
+ ))}
diff --git a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts
index 3e0b4ff8a5..2768cb85f6 100644
--- a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts
+++ b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts
@@ -1,5 +1,6 @@
import React from 'react';
+import type {LabelProps} from '@gravity-ui/uikit';
import {StringParam, useQueryParams} from 'use-query-params';
import {getTenantPath} from '../../../routes';
@@ -11,9 +12,16 @@ import type {TenantQuery} from '../TenantPages';
import {TenantTabsGroups} from '../TenantPages';
import {isDatabaseEntityType, isTopicEntityType} from '../utils/schema';
+interface Badge {
+ text: string;
+ theme?: LabelProps['theme'];
+ size?: LabelProps['size'];
+}
+
type Page = {
id: TenantDiagnosticsTab;
title: string;
+ badge?: Badge;
};
interface GetPagesOptions {
@@ -22,6 +30,7 @@ interface GetPagesOptions {
hasBackups?: boolean;
hasConfigs?: boolean;
hasAccess?: boolean;
+ hasMonitoring?: boolean;
databaseType?: ETenantType;
}
@@ -110,12 +119,23 @@ const operations = {
title: 'Operations',
};
+const monitoring = {
+ id: TENANT_DIAGNOSTICS_TABS_IDS.monitoring,
+ title: 'Monitoring',
+ badge: {
+ text: 'New',
+ theme: 'normal' as const,
+ size: 'xs' as const,
+ },
+};
+
const ASYNC_REPLICATION_PAGES = [overview, tablets, describe, access];
const TRANSFER_PAGES = [overview, tablets, describe, access];
const DATABASE_PAGES = [
overview,
+ monitoring,
topQueries,
topShards,
nodes,
@@ -131,6 +151,7 @@ const DATABASE_PAGES = [
const SERVERLESS_DATABASE_PAGES = [
overview,
+ monitoring,
topQueries,
topShards,
tablets,
@@ -220,6 +241,9 @@ function applyFilters(pages: Page[], type?: EPathType, options: GetPagesOptions
if (!options.hasAccess) {
removals.push(TENANT_DIAGNOSTICS_TABS_IDS.access);
}
+ if (!options.hasMonitoring) {
+ removals.push(TENANT_DIAGNOSTICS_TABS_IDS.monitoring);
+ }
return result.filter((p) => !removals.includes(p.id));
}
diff --git a/src/containers/Tenant/Diagnostics/DiagnosticsTabItem.tsx b/src/containers/Tenant/Diagnostics/DiagnosticsTabItem.tsx
new file mode 100644
index 0000000000..076ad92704
--- /dev/null
+++ b/src/containers/Tenant/Diagnostics/DiagnosticsTabItem.tsx
@@ -0,0 +1,33 @@
+import type {LabelProps} from '@gravity-ui/uikit';
+import {Label, Tab} from '@gravity-ui/uikit';
+
+import {InternalLink} from '../../../components/InternalLink';
+import {cn} from '../../../utils/cn';
+
+const b = cn('kv-tenant-diagnostics');
+
+interface DiagnosticsTabItemProps {
+ id: string;
+ title: string;
+ linkPath: string;
+ badge?: {
+ text: string;
+ theme?: LabelProps['theme'];
+ size?: LabelProps['size'];
+ };
+}
+
+export function DiagnosticsTabItem({id, title, linkPath, badge}: DiagnosticsTabItemProps) {
+ return (
+
+
+ {title}
+ {badge && (
+
+ )}
+
+
+ );
+}
diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx
index d2c2926cf3..c9601d46f6 100644
--- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx
+++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx
@@ -1,3 +1,4 @@
+import {DisplayPulse} from '@gravity-ui/icons';
import {Button, Flex, HelpMark, Icon, Label} from '@gravity-ui/uikit';
import {EntityStatus} from '../../../../components/EntityStatus/EntityStatus';
@@ -5,13 +6,22 @@ import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper'
import {QueriesActivityBar} from '../../../../components/QueriesActivityBar/QueriesActivityBar';
import {useDatabasesAvailable} from '../../../../store/reducers/capabilities/hooks';
import {overviewApi} from '../../../../store/reducers/overview/overview';
-import {TENANT_METRICS_TABS_IDS} from '../../../../store/reducers/tenant/constants';
-import {tenantApi} from '../../../../store/reducers/tenant/tenant';
+import {
+ TENANT_DIAGNOSTICS_TABS_IDS,
+ TENANT_METRICS_TABS_IDS,
+ TENANT_PAGES_IDS,
+} from '../../../../store/reducers/tenant/constants';
+import {
+ setDiagnosticsTab,
+ setTenantPage,
+ tenantApi,
+} from '../../../../store/reducers/tenant/tenant';
import {calculateTenantMetrics} from '../../../../store/reducers/tenants/utils';
import type {AdditionalTenantsProps} from '../../../../types/additionalProps';
-import {getDatabaseLinks} from '../../../../utils/additionalProps';
+import {uiFactory} from '../../../../uiFactory/uiFactory';
+import {getInfoTabLinks} from '../../../../utils/additionalProps';
import {TENANT_DEFAULT_TITLE} from '../../../../utils/constants';
-import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks';
+import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks';
import {useClusterNameFromQuery} from '../../../../utils/hooks/useDatabaseFromQuery';
import {mapDatabaseTypeToDBName} from '../../utils/schema';
@@ -40,6 +50,7 @@ export function TenantOverview({
const {metricsTab} = useTypedSelector((state) => state.tenant);
const [autoRefreshInterval] = useAutoRefreshInterval();
const clusterName = useClusterNameFromQuery();
+ const dispatch = useTypedDispatch();
const isMetaDatabasesAvailable = useDatabasesAvailable();
@@ -176,28 +187,44 @@ export function TenantOverview({
}
};
- const links = getDatabaseLinks(additionalTenantProps, Name, Type);
+ const links = getInfoTabLinks(additionalTenantProps, Name, Type);
+ const monitoringTabAvailable = Boolean(uiFactory.renderMonitoring);
+
+ const handleOpenMonitoring = () => {
+ dispatch(setTenantPage(TENANT_PAGES_IDS.diagnostics));
+ dispatch(setDiagnosticsTab(TENANT_DIAGNOSTICS_TABS_IDS.monitoring));
+ };
return (
-
{tenantType}
+
+ {tenantType}
+ {monitoringTabAvailable && (
+
+ )}
+
{renderName()}
-
- {links.map(({title, url, icon}) => (
-
- ))}
-
+ {links.length > 0 && (
+
+ {links.map(({title, url, icon}) => (
+
+ ))}
+
+ )}
{!isServerless && }
diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json
index 9ec4807cc0..c51694d47c 100644
--- a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json
+++ b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json
@@ -1,4 +1,5 @@
{
+ "action_open-monitoring": "Monitoring",
"top-nodes.empty-data": "No such nodes",
"title_top-nodes-load": "Top nodes by load",
"title_top-nodes-pool": "Top nodes by pools usage",
diff --git a/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx b/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx
index c481f81516..d3331f0c61 100644
--- a/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx
+++ b/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx
@@ -14,6 +14,7 @@ import {selectIsDirty, selectUserInput} from '../../../../store/reducers/query/q
import {schemaApi} from '../../../../store/reducers/schema/schema';
import {tableSchemaDataApi} from '../../../../store/reducers/tableSchemaData';
import type {EPathType, TEvDescribeSchemeResult} from '../../../../types/api/schema';
+import {uiFactory} from '../../../../uiFactory/uiFactory';
import {valueIsDefined} from '../../../../utils';
import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks';
import {getConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation';
@@ -144,6 +145,7 @@ export function SchemaTree(props: SchemaTreeProps) {
getConnectToDBDialog,
schemaData: actionsSchemaData,
isSchemaDataLoading: isActionsDataFetching,
+ hasMonitoring: typeof uiFactory.renderMonitoring === 'function',
},
databaseFullPath,
database,
diff --git a/src/containers/Tenant/i18n/en.json b/src/containers/Tenant/i18n/en.json
index 371eb83583..07d30799a9 100644
--- a/src/containers/Tenant/i18n/en.json
+++ b/src/containers/Tenant/i18n/en.json
@@ -28,6 +28,7 @@
"actions.connectToDB": "Connect to DB",
"actions.dropIndex": "Drop index",
"actions.openPreview": "Open preview",
+ "actions.openMonitoring": "Monitoring",
"actions.createTable": "Create table...",
"actions.createExternalTable": "Create external table...",
"actions.createTopic": "Create topic...",
diff --git a/src/containers/Tenant/utils/schemaActions.tsx b/src/containers/Tenant/utils/schemaActions.tsx
index 1ad121c38e..135b237092 100644
--- a/src/containers/Tenant/utils/schemaActions.tsx
+++ b/src/containers/Tenant/utils/schemaActions.tsx
@@ -1,12 +1,16 @@
-import {CirclePlus, Copy, PlugConnection} from '@gravity-ui/icons';
+import {CirclePlus, Copy, DisplayPulse, PlugConnection} from '@gravity-ui/icons';
import {Flex, Spin} from '@gravity-ui/uikit';
import copy from 'copy-to-clipboard';
import type {NavigationTreeNodeType} from 'ydb-ui-components';
import type {SnippetParams} from '../../../components/ConnectToDB/types';
import type {AppDispatch} from '../../../store';
-import {TENANT_PAGES_IDS, TENANT_QUERY_TABS_ID} from '../../../store/reducers/tenant/constants';
-import {setQueryTab, setTenantPage} from '../../../store/reducers/tenant/tenant';
+import {
+ TENANT_DIAGNOSTICS_TABS_IDS,
+ TENANT_PAGES_IDS,
+ TENANT_QUERY_TABS_ID,
+} from '../../../store/reducers/tenant/constants';
+import {setDiagnosticsTab, setQueryTab, setTenantPage} from '../../../store/reducers/tenant/tenant';
import createToast from '../../../utils/createToast';
import {insertSnippetToEditor} from '../../../utils/monaco/insertSnippet';
import {transformPath} from '../ObjectSummary/transformPath';
@@ -48,6 +52,7 @@ interface ActionsAdditionalParams {
getConnectToDBDialog?: (params: SnippetParams) => Promise;
schemaData?: SchemaData[];
isSchemaDataLoading?: boolean;
+ hasMonitoring?: boolean;
}
interface BindActionParams {
@@ -98,6 +103,11 @@ const bindActions = (
}
: undefined,
getConnectToDBDialog: () => getConnectToDBDialog?.({database: params.database}),
+ openMonitoring: () => {
+ dispatch(setTenantPage(TENANT_PAGES_IDS.diagnostics));
+ dispatch(setDiagnosticsTab(TENANT_DIAGNOSTICS_TABS_IDS.monitoring));
+ setActivePath(params.path);
+ },
createTable: inputQuery(createTableTemplate),
createColumnTable: inputQuery(createColumnTableTemplate),
createAsyncReplication: inputQuery(createAsyncReplicationTemplate),
@@ -190,6 +200,11 @@ export const getActions =
action: actions.getConnectToDBDialog,
iconStart: ,
};
+ const monitoringItem = {
+ text: i18n('actions.openMonitoring'),
+ action: actions.openMonitoring,
+ iconStart: ,
+ };
const createEntitiesSet = [
{text: i18n('actions.createTable'), action: actions.createTable},
@@ -216,10 +231,14 @@ export const getActions =
},
],
};
- const DB_SET: ActionsSet = [[copyItem, connectToDBItem], createEntitiesSet];
+ let DB_SET: ActionsSet = [[copyItem, connectToDBItem], createEntitiesSet];
const DIR_SET: ActionsSet = [[copyItem], createEntitiesSet];
+ if (additionalEffects.hasMonitoring) {
+ DB_SET = [[copyItem, connectToDBItem, monitoringItem], createEntitiesSet];
+ }
+
if (actions.createDirectory) {
const createDirectoryItem = {
text: i18n('actions.createDirectory'),
diff --git a/src/routes.ts b/src/routes.ts
index b94f0a7874..b18fc44ced 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -2,7 +2,7 @@ import * as React from 'react';
import type {Location} from 'history';
import isEmpty from 'lodash/isEmpty';
-import {compile} from 'path-to-regexp';
+import {compile, match} from 'path-to-regexp';
import qs from 'qs';
import type {QueryParamConfig} from 'use-query-params';
import {StringParam} from 'use-query-params';
@@ -210,9 +210,11 @@ export function useTabletPagePath() {
}
export function checkIsClustersPage(pathname: string) {
- return pathname.endsWith(routes.clusters);
+ const matchFn = match(routes.clusters);
+ return Boolean(matchFn(pathname));
}
export function checkIsTenantPage(pathname: string) {
- return pathname.endsWith(routes.tenant);
+ const matchFn = match(routes.tenant);
+ return Boolean(matchFn(pathname));
}
diff --git a/src/store/reducers/tenant/constants.ts b/src/store/reducers/tenant/constants.ts
index 9b3ae84100..2efa6be3b1 100644
--- a/src/store/reducers/tenant/constants.ts
+++ b/src/store/reducers/tenant/constants.ts
@@ -30,6 +30,7 @@ export const TENANT_DIAGNOSTICS_TABS_IDS = {
operations: 'operations',
access: 'access',
backups: 'backups',
+ monitoring: 'monitoring',
} as const;
export const TENANT_SUMMARY_TABS_IDS = {
diff --git a/src/uiFactory/types.ts b/src/uiFactory/types.ts
index afa4f586d7..ed04f72635 100644
--- a/src/uiFactory/types.ts
+++ b/src/uiFactory/types.ts
@@ -9,8 +9,9 @@ import type {
import type {ClusterInfo} from '../store/reducers/cluster/cluster';
import type {IssuesTree} from '../store/reducers/healthcheckInfo/types';
import type {PreparedTenant} from '../store/reducers/tenants/types';
-import type {ClusterLink, DatabaseLink} from '../types/additionalProps';
+import type {AdditionalTenantsProps, ClusterLink, DatabaseLink} from '../types/additionalProps';
import type {MetaBaseClusterInfo} from '../types/api/meta';
+import type {EPathSubType, EPathType} from '../types/api/schema/schema';
import type {ETenantType} from '../types/api/tenant';
import type {GetLogsLink} from '../utils/logs';
import type {GetMonitoringClusterLink, GetMonitoringLink} from '../utils/monitoring';
@@ -35,6 +36,7 @@ export interface UIFactory {
renderBackups?: RenderBackups;
renderEvents?: RenderEvents;
+ renderMonitoring?: RenderMonitoring;
clusterOrDatabaseAccessError?: Partial;
healthcheck: {
@@ -85,3 +87,13 @@ export type RenderBackups = (props: {
export type RenderEvents = (props: {
scrollContainerRef: React.RefObject;
}) => React.ReactNode;
+
+export type RenderMonitoring = (props: {
+ type?: EPathType;
+ subType?: EPathSubType;
+ database: string;
+ path: string;
+ databaseFullPath?: string;
+ additionalTenantProps?: AdditionalTenantsProps;
+ scrollContainerRef: React.RefObject;
+}) => React.ReactNode;
diff --git a/src/utils/additionalProps.ts b/src/utils/additionalProps.ts
index 71fc11edce..47f877fe02 100644
--- a/src/utils/additionalProps.ts
+++ b/src/utils/additionalProps.ts
@@ -36,3 +36,28 @@ export function getDatabaseLinks(
return links;
}
+
+export function getInfoTabLinks(
+ additionalProps?: AdditionalTenantsProps,
+ name?: string,
+ type?: ETenantType,
+) {
+ if (!additionalProps) {
+ return [];
+ }
+
+ const links: DatabaseLink[] = [];
+
+ if (additionalProps.getLogsLink) {
+ const link = additionalProps.getLogsLink(name);
+ if (link) {
+ links.push({title: i18n('field_logs-link'), url: link, icon: FileText});
+ }
+ }
+
+ if (additionalProps.getLinks) {
+ links.push(...additionalProps.getLinks(name, type));
+ }
+
+ return links;
+}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index a41e27e201..24f5567db7 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -106,6 +106,7 @@ export const QUERY_TECHNICAL_MARK = '/*UI-QUERY-EXCLUDE*/';
// ==== Titles ====
export const DEVELOPER_UI_TITLE = 'Developer UI';
+export const MONITORING_UI_TITLE = 'Monitoring';
export const CLUSTER_DEFAULT_TITLE = 'Cluster';
export const TENANT_DEFAULT_TITLE = 'Database';