ydb-embedded-ui 5.3.0 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,7 +3,6 @@ import { useCallback, useEffect, useReducer, useRef } from 'react';
3
3
  import { YagrPlugin } from '@gravity-ui/chartkit/yagr';
4
4
  import ChartKit, { settings } from '@gravity-ui/chartkit';
5
5
  import { useAutofetcher } from '../../utils/hooks';
6
- import { COLORS } from '../../utils/versions';
7
6
  import { cn } from '../../utils/cn';
8
7
  import { Loader } from '../Loader';
9
8
  import { ResponseError } from '../Errors/ResponseError';
@@ -11,6 +10,7 @@ import { convertResponse } from './convertReponse';
11
10
  import { getDefaultDataFormatter } from './getDefaultDataFormatter';
12
11
  import { getChartData } from './getChartData';
13
12
  import { chartReducer, initialChartState, setChartData, setChartDataLoading, setChartDataWasNotLoaded, setChartError, } from './reducer';
13
+ import { colorToRGBA, colors } from './colors';
14
14
  import i18n from './i18n';
15
15
  import './MetricChart.scss';
16
16
  const b = cn('ydb-metric-chart');
@@ -20,12 +20,16 @@ const prepareWidgetData = (data, options = {}) => {
20
20
  const defaultDataFormatter = getDefaultDataFormatter(dataType);
21
21
  const isDataEmpty = !data.metrics.length;
22
22
  const graphs = data.metrics.map((metric, index) => {
23
+ const lineColor = metric.color || colors[index];
24
+ const color = colorToRGBA(lineColor, 0.1);
23
25
  return {
24
26
  id: metric.target,
25
27
  name: metric.title || metric.target,
26
- color: metric.color || COLORS[index],
27
28
  data: metric.data,
28
29
  formatter: defaultDataFormatter,
30
+ lineColor,
31
+ color,
32
+ legendColorKey: 'lineColor',
29
33
  };
30
34
  });
31
35
  return {
@@ -41,7 +45,8 @@ const prepareWidgetData = (data, options = {}) => {
41
45
  padding: isDataEmpty ? [10, 0, 10, 0] : undefined,
42
46
  },
43
47
  series: {
44
- type: 'line',
48
+ type: 'area',
49
+ lineWidth: 1.5,
45
50
  },
46
51
  select: {
47
52
  zoom: false,
@@ -128,7 +133,7 @@ export const MetricChart = ({ database, title, metrics, timeFrame = '1h', autore
128
133
  }
129
134
  dispatch(setChartError(err));
130
135
  }
131
- }, [metrics, timeFrame, width]);
136
+ }, [database, metrics, timeFrame, width]);
132
137
  useAutofetcher(fetchChartData, [fetchChartData], autorefresh);
133
138
  const convertedData = prepareWidgetData(data, chartOptions);
134
139
  const renderContent = () => {
@@ -0,0 +1,2 @@
1
+ export declare const colors: string[];
2
+ export declare function colorToRGBA(initialColor: string, opacity: number): string;
@@ -0,0 +1,21 @@
1
+ import { colord } from 'colord';
2
+ // Grafana classic palette
3
+ export const colors = [
4
+ '#7EB26D',
5
+ '#EAB839',
6
+ '#6ED0E0',
7
+ '#EF843C',
8
+ '#E24D42',
9
+ '#1F78C1',
10
+ '#BA43A9',
11
+ '#705DA0',
12
+ '#508642',
13
+ '#CCA300', // 9: dark sand
14
+ ];
15
+ export function colorToRGBA(initialColor, opacity) {
16
+ const color = colord(initialColor);
17
+ if (!color.isValid()) {
18
+ throw new Error('Invalid color is passed');
19
+ }
20
+ return color.alpha(opacity).toRgbString();
21
+ }
@@ -11,6 +11,9 @@ export const getDefaultDataFormatter = (dataType) => {
11
11
  case 'size': {
12
12
  return formatChartValueToSize;
13
13
  }
14
+ case 'percent': {
15
+ return formatChartValueToPercent;
16
+ }
14
17
  default:
15
18
  return undefined;
16
19
  }
@@ -29,6 +32,12 @@ function formatChartValueToSize(value) {
29
32
  }
30
33
  return formatBytes({ value: convertToNumber(value), precision: 3 });
31
34
  }
35
+ function formatChartValueToPercent(value) {
36
+ if (value === null) {
37
+ return EMPTY_DATA_PLACEHOLDER;
38
+ }
39
+ return Math.round(convertToNumber(value) * 100) + '%';
40
+ }
32
41
  // Numeric values expected, not numeric value should be displayd as 0
33
42
  function convertToNumber(value) {
34
43
  if (isNumeric(value)) {
@@ -1,4 +1,8 @@
1
- export type Metric = 'queries.requests' | 'queries.latencies.p50' | 'queries.latencies.p75' | 'queries.latencies.p90' | 'queries.latencies.p99' | 'resources.cpu.usage' | 'resources.memory.used_bytes' | 'resources.storage.used_bytes';
1
+ import type { PoolName } from '../../types/api/nodes';
2
+ type Percentile = 'p50' | 'p75' | 'p90' | 'p99';
3
+ type QueriesLatenciesMetric = `queries.latencies.${Percentile}`;
4
+ type PoolUsageMetric = `resources.cpu.${PoolName}.usage`;
5
+ export type Metric = 'queries.requests' | 'resources.memory.used_bytes' | 'resources.storage.used_bytes' | 'resources.cpu.usage' | PoolUsageMetric | QueriesLatenciesMetric;
2
6
  export interface MetricDescription {
3
7
  target: Metric;
4
8
  title?: string;
@@ -12,9 +16,10 @@ export interface PreparedMetricsData {
12
16
  metrics: PreparedMetric[];
13
17
  }
14
18
  export type ChartValue = number | string | null;
15
- export type ChartDataType = 'ms' | 'size';
19
+ export type ChartDataType = 'ms' | 'size' | 'percent';
16
20
  export interface ChartOptions {
17
21
  dataType?: ChartDataType;
18
22
  }
19
23
  export type ChartDataStatus = 'loading' | 'success' | 'error';
20
24
  export type OnChartDataStatusChange = (newStatus: ChartDataStatus) => void;
25
+ export {};
@@ -2,6 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect } from 'react';
3
3
  import { StringParam, useQueryParams } from 'use-query-params';
4
4
  import { Helmet } from 'react-helmet-async';
5
+ import { Icon } from '@gravity-ui/uikit';
6
+ import ArrowRotateLeftIcon from '@gravity-ui/icons/svgs/arrow-rotate-left.svg';
5
7
  import { getPDiskData, getPDiskStorage, setPDiskDataWasNotLoaded, } from '../../store/reducers/pdisk/pdisk';
6
8
  import { setHeaderBreadcrumbs } from '../../store/reducers/header/header';
7
9
  import { getNodesList, selectNodesMap } from '../../store/reducers/nodesList';
@@ -11,6 +13,7 @@ import { PageMeta } from '../../components/PageMeta/PageMeta';
11
13
  import { StatusIcon } from '../../components/StatusIcon/StatusIcon';
12
14
  import { PDiskInfo } from '../../components/PDiskInfo/PDiskInfo';
13
15
  import { InfoViewerSkeleton } from '../../components/InfoViewerSkeleton/InfoViewerSkeleton';
16
+ import { ButtonWithConfirmDialog } from '../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog';
14
17
  import { PDiskGroups } from './PDiskGroups';
15
18
  import { pdiskPageCn } from './shared';
16
19
  import { pDiskPageKeyset } from './i18n';
@@ -30,16 +33,28 @@ export function PDisk() {
30
33
  useEffect(() => {
31
34
  dispatch(getNodesList());
32
35
  }, [dispatch]);
33
- const fetchData = useCallback((isBackground) => {
36
+ const fetchData = useCallback(async (isBackground) => {
34
37
  if (!isBackground) {
35
38
  dispatch(setPDiskDataWasNotLoaded());
36
39
  }
37
40
  if (nodeId && pDiskId) {
38
- dispatch(getPDiskData({ nodeId, pDiskId }));
39
- dispatch(getPDiskStorage({ nodeId, pDiskId }));
41
+ return Promise.all([
42
+ dispatch(getPDiskData({ nodeId, pDiskId })),
43
+ dispatch(getPDiskStorage({ nodeId, pDiskId })),
44
+ ]);
40
45
  }
46
+ return undefined;
41
47
  }, [dispatch, nodeId, pDiskId]);
42
48
  useAutofetcher(fetchData, [fetchData], true);
49
+ const handleRestart = async () => {
50
+ if (nodeId && pDiskId) {
51
+ return window.api.restartPDisk(nodeId, pDiskId);
52
+ }
53
+ return undefined;
54
+ };
55
+ const handleAfterRestart = async () => {
56
+ return fetchData(true);
57
+ };
43
58
  const renderHelmet = () => {
44
59
  const pDiskPagePart = pDiskId
45
60
  ? `${pDiskPageKeyset('pdisk')} ${pDiskId}`
@@ -55,6 +70,9 @@ export function PDisk() {
55
70
  const renderPageTitle = () => {
56
71
  return (_jsxs("div", Object.assign({ className: pdiskPageCn('title') }, { children: [_jsx("span", Object.assign({ className: pdiskPageCn('title__prefix') }, { children: pDiskPageKeyset('pdisk') })), _jsx(StatusIcon, { status: getSeverityColor(Severity), size: "s" }), pDiskId] })));
57
72
  };
73
+ const renderControls = () => {
74
+ return (_jsx("div", Object.assign({ className: pdiskPageCn('controls') }, { children: _jsxs(ButtonWithConfirmDialog, Object.assign({ onConfirmAction: handleRestart, onConfirmActionSuccess: handleAfterRestart, buttonDisabled: !nodeId || !pDiskId, buttonView: "normal", dialogContent: pDiskPageKeyset('restart-pdisk-dialog') }, { children: [_jsx(Icon, { data: ArrowRotateLeftIcon }), pDiskPageKeyset('restart-pdisk-button')] })) })));
75
+ };
58
76
  const renderInfo = () => {
59
77
  if (pDiskLoading && !pDiskWasLoaded) {
60
78
  return _jsx(InfoViewerSkeleton, { className: pdiskPageCn('info'), rows: 10 });
@@ -64,5 +82,5 @@ export function PDisk() {
64
82
  const renderGroupsTable = () => {
65
83
  return (_jsx(PDiskGroups, { data: groupsData, nodesMap: nodesMap, loading: groupsLoading && !groupsWasLoaded }));
66
84
  };
67
- return (_jsxs("div", Object.assign({ className: pdiskPageCn(null) }, { children: [renderHelmet(), renderPageMeta(), renderPageTitle(), renderInfo(), renderGroupsTable()] })));
85
+ return (_jsxs("div", Object.assign({ className: pdiskPageCn(null) }, { children: [renderHelmet(), renderPageMeta(), renderPageTitle(), renderControls(), renderInfo(), renderGroupsTable()] })));
68
86
  }
@@ -16,6 +16,7 @@
16
16
  &__meta,
17
17
  &__title,
18
18
  &__info,
19
+ &__controls,
19
20
  &__groups-title {
20
21
  position: sticky;
21
22
  left: 0;
@@ -2,5 +2,7 @@
2
2
  "fqdn": "FQDN",
3
3
  "pdisk": "PDisk",
4
4
  "groups": "Groups",
5
- "node": "Node"
5
+ "node": "Node",
6
+ "restart-pdisk-button": "Restart PDisk",
7
+ "restart-pdisk-dialog": "PDisk will be restarted. Do you want to proceed?"
6
8
  }
@@ -1 +1 @@
1
- export declare const pDiskPageKeyset: (key: "groups" | "node" | "pdisk" | "fqdn", params?: import("@gravity-ui/i18n").Params | undefined) => string;
1
+ export declare const pDiskPageKeyset: (key: "groups" | "node" | "pdisk" | "fqdn" | "restart-pdisk-button" | "restart-pdisk-dialog", params?: import("@gravity-ui/i18n").Params | undefined) => string;
@@ -1,12 +1,17 @@
1
1
  import i18n from '../i18n';
2
+ const pools = ['IC', 'IO', 'Batch', 'User', 'System'];
3
+ const getPoolMetricConfig = (poolName) => {
4
+ return {
5
+ target: `resources.cpu.${poolName}.usage`,
6
+ title: poolName,
7
+ };
8
+ };
2
9
  export const cpuDashboardConfig = [
3
10
  {
4
11
  title: i18n('charts.cpu-usage'),
5
- metrics: [
6
- {
7
- target: 'resources.cpu.usage',
8
- title: i18n('charts.cpu-usage'),
9
- },
10
- ],
12
+ metrics: pools.map(getPoolMetricConfig),
13
+ options: {
14
+ dataType: 'percent',
15
+ },
11
16
  },
12
17
  ];
@@ -18,7 +18,7 @@
18
18
  "by-size": "by size",
19
19
  "charts.queries-per-second": "Queries per second",
20
20
  "charts.transaction-latency": "Transactions latencies {{percentile}}",
21
- "charts.cpu-usage": "CPU cores used",
21
+ "charts.cpu-usage": "CPU usage by pool",
22
22
  "charts.storage-usage": "Tablet storage usage",
23
23
  "charts.memory-usage": "Memory usage",
24
24
  "storage.tablet-storage-title": "Tablet storage",
@@ -93,6 +93,7 @@ export declare class YdbEmbeddedAPI extends AxiosWrapper {
93
93
  getExplainQueryAst(query: string, database: string): Promise<import("../types/api/query").ExplainQueryResponse>;
94
94
  getHotKeys(path: string, enableSampling: boolean, { concurrentId }?: AxiosOptions): Promise<JsonHotKeysResponse>;
95
95
  getHealthcheckInfo(database: string, { concurrentId }?: AxiosOptions): Promise<HealthCheckAPIResponse>;
96
+ restartPDisk(nodeId: number | string, pDiskId: number | string): Promise<any>;
96
97
  killTablet(id?: string): Promise<string>;
97
98
  stopTablet(id?: string, hiveId?: string): Promise<string>;
98
99
  resumeTablet(id?: string, hiveId?: string): Promise<string>;
@@ -2,6 +2,7 @@ import { __rest } from "tslib";
2
2
  import AxiosWrapper from '@gravity-ui/axios-wrapper';
3
3
  import { backend as BACKEND, metaBackend as META_BACKEND } from '../store';
4
4
  import { prepareSortValue } from '../utils/filters';
5
+ import { createPDiskDeveloperUILink } from '../utils/developerUI/developerUI';
5
6
  import { BINARY_DATA_IN_PLAIN_TEXT_DISPLAY } from '../utils/constants';
6
7
  import { parseMetaCluster } from './parsers/parseMetaCluster';
7
8
  import { parseMetaTenants } from './parsers/parseMetaTenants';
@@ -207,6 +208,18 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
207
208
  getHealthcheckInfo(database, { concurrentId } = {}) {
208
209
  return this.get(this.getPath('/viewer/json/healthcheck?merge_records=true'), { tenant: database }, { concurrentId });
209
210
  }
211
+ restartPDisk(nodeId, pDiskId) {
212
+ const pDiskPath = createPDiskDeveloperUILink({
213
+ nodeId,
214
+ pDiskId,
215
+ host: this.getPath(''),
216
+ });
217
+ return this.post(pDiskPath, 'restartPDisk=', {}, {
218
+ headers: {
219
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
220
+ },
221
+ });
222
+ }
210
223
  killTablet(id) {
211
224
  return this.get(this.getPath(`/tablets?KillTabletID=${id}`), {});
212
225
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@gravity-ui/axios-wrapper": "^1.4.1",
14
- "@gravity-ui/chartkit": "^4.20.1",
14
+ "@gravity-ui/chartkit": "^4.23.0",
15
15
  "@gravity-ui/components": "^2.12.0",
16
16
  "@gravity-ui/date-utils": "^1.4.2",
17
17
  "@gravity-ui/i18n": "^1.2.0",
@@ -24,6 +24,7 @@
24
24
  "@reduxjs/toolkit": "^2.2.1",
25
25
  "axios": "^1.6.7",
26
26
  "bem-cn-lite": "^4.1.0",
27
+ "colord": "^2.9.3",
27
28
  "copy-to-clipboard": "^3.3.3",
28
29
  "crc-32": "^1.2.2",
29
30
  "history": "^4.10.1",