ydb-embedded-ui 3.4.1 → 3.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +31 -10
  3. package/dist/components/CriticalActionDialog/CriticalActionDialog.scss +1 -1
  4. package/dist/components/CriticalActionDialog/{CriticalActionDialog.js → CriticalActionDialog.tsx} +21 -16
  5. package/dist/components/CriticalActionDialog/index.ts +1 -0
  6. package/dist/containers/App/Content.js +1 -1
  7. package/dist/containers/Nodes/Nodes.scss +4 -0
  8. package/dist/containers/Nodes/Nodes.tsx +2 -1
  9. package/dist/containers/Storage/PDisk/PDisk.scss +15 -0
  10. package/dist/containers/Storage/PDisk/PDisk.tsx +43 -13
  11. package/dist/containers/Storage/StorageGroups/StorageGroups.tsx +2 -2
  12. package/dist/containers/Storage/StorageNodes/StorageNodes.scss +8 -2
  13. package/dist/containers/Storage/StorageNodes/StorageNodes.tsx +3 -1
  14. package/dist/containers/Storage/VDisk/VDisk.tsx +2 -2
  15. package/dist/containers/Storage/VDiskPopup/VDiskPopup.tsx +2 -2
  16. package/dist/containers/Tablet/Tablet.tsx +121 -0
  17. package/dist/containers/Tablet/TabletControls/TabletControls.tsx +159 -0
  18. package/dist/containers/Tablet/TabletControls/index.ts +1 -0
  19. package/dist/containers/Tablet/TabletInfo/TabletInfo.tsx +80 -0
  20. package/dist/containers/Tablet/TabletInfo/index.ts +1 -0
  21. package/dist/containers/Tablet/TabletTable/TabletTable.tsx +59 -0
  22. package/dist/containers/Tablet/TabletTable/index.ts +1 -0
  23. package/dist/containers/Tablet/i18n/en.json +10 -0
  24. package/dist/containers/Tablet/i18n/index.ts +11 -0
  25. package/dist/containers/Tablet/i18n/ru.json +10 -0
  26. package/dist/containers/Tablet/index.ts +1 -0
  27. package/dist/containers/Tenant/Diagnostics/Partitions/Partitions.tsx +9 -15
  28. package/dist/containers/Tenant/Diagnostics/Partitions/PartitionsWrapper.tsx +2 -0
  29. package/dist/store/reducers/storage.js +1 -0
  30. package/dist/store/reducers/topic.ts +13 -0
  31. package/dist/types/api/nodes.ts +1 -1
  32. package/dist/types/store/topic.ts +3 -2
  33. package/dist/utils/nodes.ts +7 -0
  34. package/dist/utils/storage.ts +1 -1
  35. package/package.json +5 -2
  36. package/dist/containers/Tablet/Tablet.js +0 -448
@@ -0,0 +1,159 @@
1
+ import {useEffect, useState} from 'react';
2
+ import {Button} from '@gravity-ui/uikit';
3
+
4
+ import {ETabletState, TTabletStateInfo} from '../../../types/api/tablet';
5
+ import {CriticalActionDialog} from '../../../components/CriticalActionDialog';
6
+
7
+ import i18n from '../i18n';
8
+ import {b} from '../Tablet';
9
+
10
+ enum EVisibleDialogType {
11
+ 'kill' = 'kill',
12
+ 'stop' = 'kill',
13
+ 'resume' = 'kill',
14
+ }
15
+
16
+ type VisibleDialogType = EVisibleDialogType | null;
17
+
18
+ interface TabletControlsProps {
19
+ tablet: TTabletStateInfo;
20
+ }
21
+
22
+ export const TabletControls = ({tablet}: TabletControlsProps) => {
23
+ const {TabletId, HiveId} = tablet;
24
+
25
+ const [isDialogVisible, setIsDialogVisible] = useState(false);
26
+ const [visibleDialogType, setVisibleDialogType] = useState<VisibleDialogType>(null);
27
+ const [isTabletActionsDisabled, setIsTabletActionsDisabled] = useState(false);
28
+
29
+ // Enable controls after data update
30
+ useEffect(() => {
31
+ setIsTabletActionsDisabled(false);
32
+ }, [tablet]);
33
+
34
+ const makeShowDialog = (type: VisibleDialogType) => () => {
35
+ setIsDialogVisible(true);
36
+ setVisibleDialogType(type);
37
+ };
38
+
39
+ const showKillDialog = makeShowDialog(EVisibleDialogType.kill);
40
+ const showStopDialog = makeShowDialog(EVisibleDialogType.stop);
41
+ const showResumeDialog = makeShowDialog(EVisibleDialogType.resume);
42
+
43
+ const hideDialog = () => {
44
+ setIsDialogVisible(false);
45
+ setVisibleDialogType(null);
46
+ };
47
+
48
+ const _onKillClick = () => {
49
+ setIsTabletActionsDisabled(true);
50
+ return window.api.killTablet(TabletId);
51
+ };
52
+ const _onStopClick = () => {
53
+ setIsTabletActionsDisabled(true);
54
+ return window.api.stopTablet(TabletId, HiveId);
55
+ };
56
+ const _onResumeClick = () => {
57
+ setIsTabletActionsDisabled(true);
58
+ return window.api.resumeTablet(TabletId, HiveId);
59
+ };
60
+
61
+ const hasHiveId = () => {
62
+ return HiveId && HiveId !== '0';
63
+ };
64
+
65
+ const isDisabledResume = () => {
66
+ if (isTabletActionsDisabled) {
67
+ return true;
68
+ }
69
+
70
+ return tablet.State !== ETabletState.Stopped && tablet.State !== ETabletState.Dead;
71
+ };
72
+
73
+ const isDisabledKill = () => {
74
+ return isTabletActionsDisabled;
75
+ };
76
+
77
+ const isDisabledStop = () => {
78
+ if (isTabletActionsDisabled) {
79
+ return true;
80
+ }
81
+
82
+ return tablet.State === ETabletState.Stopped || tablet.State === ETabletState.Deleted;
83
+ };
84
+
85
+ const renderDialog = () => {
86
+ if (!isDialogVisible) {
87
+ return null;
88
+ }
89
+
90
+ switch (visibleDialogType) {
91
+ case EVisibleDialogType.kill: {
92
+ return (
93
+ <CriticalActionDialog
94
+ visible={isDialogVisible}
95
+ text={i18n('dialog.kill')}
96
+ onClose={hideDialog}
97
+ onConfirm={_onKillClick}
98
+ />
99
+ );
100
+ }
101
+ case EVisibleDialogType.stop: {
102
+ return (
103
+ <CriticalActionDialog
104
+ visible={isDialogVisible}
105
+ text={i18n('dialog.stop')}
106
+ onClose={hideDialog}
107
+ onConfirm={_onStopClick}
108
+ />
109
+ );
110
+ }
111
+ case EVisibleDialogType.resume: {
112
+ return (
113
+ <CriticalActionDialog
114
+ visible={isDialogVisible}
115
+ text={i18n('dialog.resume')}
116
+ onClose={hideDialog}
117
+ onConfirm={_onResumeClick}
118
+ />
119
+ );
120
+ }
121
+ default:
122
+ return null;
123
+ }
124
+ };
125
+
126
+ return (
127
+ <div className={b('controls')}>
128
+ <Button
129
+ onClick={showKillDialog}
130
+ view="action"
131
+ disabled={isDisabledKill()}
132
+ className={b('control')}
133
+ >
134
+ {i18n('controls.kill')}
135
+ </Button>
136
+ {hasHiveId() ? (
137
+ <>
138
+ <Button
139
+ onClick={showStopDialog}
140
+ view="action"
141
+ disabled={isDisabledStop()}
142
+ className={b('control')}
143
+ >
144
+ {i18n('controls.stop')}
145
+ </Button>
146
+ <Button
147
+ onClick={showResumeDialog}
148
+ view="action"
149
+ disabled={isDisabledResume()}
150
+ className={b('control')}
151
+ >
152
+ {i18n('controls.resume')}
153
+ </Button>
154
+ </>
155
+ ) : null}
156
+ {renderDialog()}
157
+ </div>
158
+ );
159
+ };
@@ -0,0 +1 @@
1
+ export * from './TabletControls';
@@ -0,0 +1,80 @@
1
+ import {Link} from 'react-router-dom';
2
+
3
+ import {Link as UIKitLink} from '@gravity-ui/uikit';
4
+
5
+ import {ETabletState, TTabletStateInfo} from '../../../types/api/tablet';
6
+ import {InfoViewer, InfoViewerItem} from '../../../components/InfoViewer';
7
+ import routes, {createHref} from '../../../routes';
8
+ import {calcUptime} from '../../../utils';
9
+ import {getDefaultNodePath} from '../../Node/NodePages';
10
+
11
+ import {b} from '../Tablet';
12
+
13
+ interface TabletInfoProps {
14
+ tablet: TTabletStateInfo;
15
+ tenantPath: string;
16
+ }
17
+
18
+ export const TabletInfo = ({tablet, tenantPath}: TabletInfoProps) => {
19
+ const {
20
+ ChangeTime,
21
+ Generation,
22
+ FollowerId,
23
+ NodeId,
24
+ HiveId,
25
+ State,
26
+ Type,
27
+ TenantId: {SchemeShard} = {},
28
+ } = tablet;
29
+
30
+ const hasHiveId = HiveId && HiveId !== '0';
31
+ const hasUptime = State === ETabletState.Active;
32
+
33
+ const tabletInfo: InfoViewerItem[] = [{label: 'Database', value: tenantPath}];
34
+
35
+ if (hasHiveId) {
36
+ tabletInfo.push({
37
+ label: 'HiveId',
38
+ value: (
39
+ <UIKitLink href={createHref(routes.tablet, {id: HiveId})} target="_blank">
40
+ {HiveId}
41
+ </UIKitLink>
42
+ ),
43
+ });
44
+ }
45
+
46
+ if (SchemeShard) {
47
+ tabletInfo.push({
48
+ label: 'SchemeShard',
49
+ value: (
50
+ <UIKitLink href={createHref(routes.tablet, {id: SchemeShard})} target="_blank">
51
+ {SchemeShard}
52
+ </UIKitLink>
53
+ ),
54
+ });
55
+ }
56
+
57
+ tabletInfo.push({label: 'Type', value: Type}, {label: 'State', value: State});
58
+
59
+ if (hasUptime) {
60
+ tabletInfo.push({label: 'Uptime', value: calcUptime(ChangeTime)});
61
+ }
62
+
63
+ tabletInfo.push(
64
+ {label: 'Generation', value: Generation},
65
+ {
66
+ label: 'Node',
67
+ value: (
68
+ <Link className={b('link')} to={getDefaultNodePath(String(NodeId))}>
69
+ {NodeId}
70
+ </Link>
71
+ ),
72
+ },
73
+ );
74
+
75
+ if (FollowerId) {
76
+ tabletInfo.push({label: 'Follower', value: FollowerId});
77
+ }
78
+
79
+ return <InfoViewer info={tabletInfo} />;
80
+ };
@@ -0,0 +1 @@
1
+ export * from './TabletInfo';
@@ -0,0 +1,59 @@
1
+ import DataTable, {Column} from '@gravity-ui/react-data-table';
2
+
3
+ import type {ITabletPreparedHistoryItem} from '../../../types/store/tablet';
4
+ import {calcUptime} from '../../../utils';
5
+
6
+ const columns: Column<ITabletPreparedHistoryItem>[] = [
7
+ {
8
+ name: 'Generation',
9
+ align: DataTable.RIGHT,
10
+ render: ({row}) => row.generation,
11
+ },
12
+ {
13
+ name: 'Node ID',
14
+ align: DataTable.RIGHT,
15
+ sortable: false,
16
+ render: ({row}) => row.nodeId,
17
+ },
18
+ {
19
+ name: 'Change time',
20
+ align: DataTable.RIGHT,
21
+ sortable: false,
22
+ render: ({row}) => calcUptime(row.changeTime),
23
+ },
24
+ {
25
+ name: 'State',
26
+ sortable: false,
27
+ render: ({row}) => row.state,
28
+ },
29
+ {
30
+ name: 'Follower ID',
31
+ sortable: false,
32
+ render: ({row}) => {
33
+ return row.leader ? 'leader' : row.followerId;
34
+ },
35
+ },
36
+ ];
37
+
38
+ const TABLE_SETTINGS = {
39
+ displayIndices: false,
40
+ };
41
+
42
+ interface TabletTableProps {
43
+ history: ITabletPreparedHistoryItem[];
44
+ }
45
+
46
+ export const TabletTable = ({history}: TabletTableProps) => {
47
+ return (
48
+ <DataTable
49
+ theme="yandex-cloud"
50
+ data={history}
51
+ columns={columns}
52
+ settings={TABLE_SETTINGS}
53
+ initialSortOrder={{
54
+ columnId: 'Generation',
55
+ order: DataTable.DESCENDING,
56
+ }}
57
+ />
58
+ );
59
+ };
@@ -0,0 +1 @@
1
+ export * from './TabletTable';
@@ -0,0 +1,10 @@
1
+ {
2
+ "tablet.header": "Tablet",
3
+ "controls.kill": "Restart",
4
+ "controls.stop": "Stop",
5
+ "controls.resume": "Resume",
6
+ "dialog.kill": "The tablet will be restarted. Do you want to proceed?",
7
+ "dialog.stop": "The tablet will be stopped. Do you want to proceed?",
8
+ "dialog.resume": "The tablet will be resumed. Do you want to proceed?",
9
+ "emptyState": "The tablet was not found"
10
+ }
@@ -0,0 +1,11 @@
1
+ import {i18n, Lang} from '../../../utils/i18n';
2
+
3
+ import en from './en.json';
4
+ import ru from './ru.json';
5
+
6
+ const COMPONENT = 'ydb-tablet-page';
7
+
8
+ i18n.registerKeyset(Lang.En, COMPONENT, en);
9
+ i18n.registerKeyset(Lang.Ru, COMPONENT, ru);
10
+
11
+ export default i18n.keyset(COMPONENT);
@@ -0,0 +1,10 @@
1
+ {
2
+ "tablet.header": "Таблетка",
3
+ "controls.kill": "Перезапустить",
4
+ "controls.stop": "Остановить",
5
+ "controls.resume": "Запустить",
6
+ "dialog.kill": "Таблетка будет перезапущена. Вы хотите продолжить?",
7
+ "dialog.stop": "Таблетка будет остановлена. Вы хотите продолжить?",
8
+ "dialog.resume": "Таблетка будет запущена. Вы хотите продолжить?",
9
+ "emptyState": "Таблетка не найдена"
10
+ }
@@ -0,0 +1 @@
1
+ export * from './Tablet';
@@ -1,5 +1,5 @@
1
1
  import block from 'bem-cn-lite';
2
- import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
2
+ import {useCallback, useEffect, useMemo, useState} from 'react';
3
3
  import {useDispatch} from 'react-redux';
4
4
  import {escapeRegExp} from 'lodash/fp';
5
5
 
@@ -53,8 +53,6 @@ export const Partitions = ({path, type, nodes, consumers}: PartitionsProps) => {
53
53
 
54
54
  const dispatch = useDispatch();
55
55
 
56
- const isFirstRenderRef = useRef(true);
57
-
58
56
  const [generalSearchValue, setGeneralSearchValue] = useState('');
59
57
  const [partitionIdSearchValue, setPartitionIdSearchValue] = useState('');
60
58
 
@@ -74,14 +72,6 @@ export const Partitions = ({path, type, nodes, consumers}: PartitionsProps) => {
74
72
  useEffect(() => {
75
73
  // Manual path control to ensure it updates with other values so no request with wrong params will be sent
76
74
  setComponentCurrentPath(path);
77
-
78
- // Do not reset selected consumer on first effect call
79
- // To enable navigating to specific consumer
80
- if (isFirstRenderRef.current) {
81
- isFirstRenderRef.current = false;
82
- } else {
83
- dispatch(setSelectedConsumer(undefined));
84
- }
85
75
  }, [dispatch, path]);
86
76
 
87
77
  const fetchConsumerData = useCallback(
@@ -90,11 +80,11 @@ export const Partitions = ({path, type, nodes, consumers}: PartitionsProps) => {
90
80
  dispatch(setDataWasNotLoaded());
91
81
  }
92
82
 
93
- if (selectedConsumer) {
83
+ if (selectedConsumer && consumers && consumers.includes(selectedConsumer)) {
94
84
  dispatch(getConsumer(componentCurrentPath, selectedConsumer));
95
85
  }
96
86
  },
97
- [dispatch, selectedConsumer, componentCurrentPath],
87
+ [dispatch, selectedConsumer, componentCurrentPath, consumers],
98
88
  );
99
89
 
100
90
  useAutofetcher(fetchConsumerData, [fetchConsumerData], autorefresh);
@@ -111,10 +101,13 @@ export const Partitions = ({path, type, nodes, consumers}: PartitionsProps) => {
111
101
  );
112
102
 
113
103
  useEffect(() => {
114
- if (consumersToSelect && consumersToSelect.length && !selectedConsumer) {
104
+ const shouldUpdateSelectedConsumer =
105
+ !selectedConsumer || (consumers && !consumers.includes(selectedConsumer));
106
+
107
+ if (consumersToSelect && consumersToSelect.length && shouldUpdateSelectedConsumer) {
115
108
  dispatch(setSelectedConsumer(consumersToSelect[0].value));
116
109
  }
117
- }, [dispatch, consumersToSelect, selectedConsumer]);
110
+ }, [dispatch, consumersToSelect, selectedConsumer, consumers]);
118
111
 
119
112
  const selectedColumns: string[] = useMemo(
120
113
  () =>
@@ -222,6 +215,7 @@ export const Partitions = ({path, type, nodes, consumers}: PartitionsProps) => {
222
215
  options={consumersToSelect}
223
216
  value={[selectedConsumer || '']}
224
217
  onUpdate={handleConsumerSelectChange}
218
+ filterable={consumers && consumers.length > 5}
225
219
  />
226
220
  <Search
227
221
  onChange={handlePartitionIdSearchChange}
@@ -6,6 +6,7 @@ import type {EPathType} from '../../../../types/api/schema';
6
6
  import {useTypedSelector} from '../../../../utils/hooks';
7
7
 
8
8
  import {
9
+ cleanTopicData,
9
10
  getTopic,
10
11
  setDataWasNotLoaded as setTopicDataWasNotLoaded,
11
12
  } from '../../../../store/reducers/topic';
@@ -61,6 +62,7 @@ export const PartitionsWrapper = ({path, type}: PartitionsWrapperProps) => {
61
62
 
62
63
  useEffect(() => {
63
64
  dispatch(setTopicDataWasNotLoaded());
65
+ dispatch(cleanTopicData());
64
66
  dispatch(setNodesDataWasNotLoaded());
65
67
 
66
68
  dispatch(getTopic(path));
@@ -313,6 +313,7 @@ export const getFlatListStorageNodes = createSelector([getStorageNodes], (storag
313
313
  }).length;
314
314
  return {
315
315
  NodeId: node.NodeId,
316
+ SystemState: systemState.SystemState,
316
317
  FQDN: systemState.Host,
317
318
  DataCenter: systemState.DataCenter,
318
319
  Rack: systemState.Rack,
@@ -18,6 +18,7 @@ import {convertBytesObjectToSpeed} from '../../utils/bytesParsers';
18
18
  export const FETCH_TOPIC = createRequestActionTypes('topic', 'FETCH_TOPIC');
19
19
 
20
20
  const SET_DATA_WAS_NOT_LOADED = 'topic/SET_DATA_WAS_NOT_LOADED';
21
+ const CLEAN_TOPIC_DATA = 'topic/CLEAN_TOPIC_DATA';
21
22
 
22
23
  const initialState = {
23
24
  loading: true,
@@ -64,6 +65,12 @@ const topic: Reducer<ITopicState, ITopicAction> = (state = initialState, action)
64
65
  wasLoaded: false,
65
66
  };
66
67
  }
68
+ case CLEAN_TOPIC_DATA: {
69
+ return {
70
+ ...state,
71
+ data: undefined,
72
+ };
73
+ }
67
74
  default:
68
75
  return state;
69
76
  }
@@ -75,6 +82,12 @@ export const setDataWasNotLoaded = () => {
75
82
  } as const;
76
83
  };
77
84
 
85
+ export const cleanTopicData = () => {
86
+ return {
87
+ type: CLEAN_TOPIC_DATA,
88
+ } as const;
89
+ };
90
+
78
91
  export function getTopic(path?: string) {
79
92
  return createApiRequest({
80
93
  request: window.api.getTopic({path}),
@@ -7,7 +7,7 @@ import {TVDiskStateInfo} from './vdisk';
7
7
  // source: https://github.com/ydb-platform/ydb/blob/main/ydb/core/viewer/protos/viewer.proto
8
8
 
9
9
  export interface TNodesInfo {
10
- Overall: EFlag;
10
+ Overall?: EFlag;
11
11
  Nodes?: TNodeInfo[];
12
12
 
13
13
  /** uint64 */
@@ -1,4 +1,4 @@
1
- import {FETCH_TOPIC, setDataWasNotLoaded} from '../../store/reducers/topic';
1
+ import {FETCH_TOPIC, cleanTopicData, setDataWasNotLoaded} from '../../store/reducers/topic';
2
2
  import type {ApiRequestAction} from '../../store/utils';
3
3
  import type {IProcessSpeedStats} from '../../utils/bytesParsers';
4
4
  import type {IResponseError} from '../api/error';
@@ -31,7 +31,8 @@ export interface ITopicState {
31
31
 
32
32
  export type ITopicAction =
33
33
  | ApiRequestAction<typeof FETCH_TOPIC, DescribeTopicResult, IResponseError>
34
- | ReturnType<typeof setDataWasNotLoaded>;
34
+ | ReturnType<typeof setDataWasNotLoaded>
35
+ | ReturnType<typeof cleanTopicData>;
35
36
 
36
37
  export interface ITopicRootStateSlice {
37
38
  topic: ITopicState;
@@ -1,3 +1,7 @@
1
+ import type {TSystemStateInfo} from '../types/api/nodes';
2
+ import type {INodesPreparedEntity} from '../types/store/nodes';
3
+ import {EFlag} from '../types/api/enums';
4
+
1
5
  export enum NodesUptimeFilterValues {
2
6
  'All' = 'All',
3
7
  'SmallUptime' = 'SmallUptime',
@@ -7,3 +11,6 @@ export const NodesUptimeFilterTitles = {
7
11
  [NodesUptimeFilterValues.All]: 'All',
8
12
  [NodesUptimeFilterValues.SmallUptime]: 'Uptime < 1h',
9
13
  };
14
+
15
+ export const isUnavailableNode = (node: INodesPreparedEntity | TSystemStateInfo) =>
16
+ !node.SystemState || node.SystemState === EFlag.Grey;
@@ -1,7 +1,7 @@
1
1
  import type {TVSlotId, TVDiskStateInfo} from '../types/api/vdisk';
2
2
  import type {IStoragePoolGroup} from '../types/store/storage';
3
3
 
4
- export const isFullVDiksData = (disk: TVDiskStateInfo | TVSlotId): disk is TVDiskStateInfo =>
4
+ export const isFullVDiskData = (disk: TVDiskStateInfo | TVSlotId): disk is TVDiskStateInfo =>
5
5
  'VDiskId' in disk;
6
6
 
7
7
  export const getUsage = (data: IStoragePoolGroup, step = 1) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "3.4.1",
3
+ "version": "3.4.3",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -53,7 +53,9 @@
53
53
  "eject": "react-scripts eject",
54
54
  "prepublishOnly": "npm run package",
55
55
  "typecheck": "tsc --noEmit",
56
- "prepare": "husky install"
56
+ "prepare": "husky install",
57
+ "test:e2e:install": "npx playwright install --with-deps chromium",
58
+ "test:e2e": "npx playwright test --config=playwright.config.ts"
57
59
  },
58
60
  "lint-staged": {
59
61
  "*.{scss}": [
@@ -104,6 +106,7 @@
104
106
  "@gravity-ui/stylelint-config": "^1.0.1",
105
107
  "@gravity-ui/tsconfig": "^1.0.0",
106
108
  "@gravity-ui/uikit": "^3.20.2",
109
+ "@playwright/test": "^1.31.1",
107
110
  "@testing-library/jest-dom": "^5.15.0",
108
111
  "@testing-library/react": "^11.2.7",
109
112
  "@testing-library/user-event": "^12.8.3",