ydb-embedded-ui 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/assets/icons/question.svg +1 -0
  3. package/dist/components/ClusterInfo/ClusterInfo.tsx +8 -4
  4. package/dist/components/FullNodeViewer/FullNodeViewer.scss +4 -9
  5. package/dist/components/InfoViewer/InfoViewer.scss +3 -2
  6. package/dist/components/InternalLink/InternalLink.js +8 -0
  7. package/dist/components/Loader/Loader.scss +5 -0
  8. package/dist/components/Loader/Loader.tsx +16 -0
  9. package/dist/components/PDiskViewer/PDiskViewer.js +3 -4
  10. package/dist/containers/App/App.scss +4 -0
  11. package/dist/containers/App/Content.js +0 -2
  12. package/dist/containers/AppIcons/AppIcons.js +4 -0
  13. package/dist/containers/Authentication/Authentication.tsx +1 -1
  14. package/dist/containers/Cluster/Cluster.tsx +1 -1
  15. package/dist/containers/Header/Header.tsx +6 -1
  16. package/dist/containers/Heatmap/Heatmap.js +0 -1
  17. package/dist/containers/Node/Node.scss +12 -1
  18. package/dist/containers/Node/Node.tsx +187 -0
  19. package/dist/containers/Node/NodeOverview/NodeOverview.scss +0 -0
  20. package/dist/containers/Node/NodeOverview/NodeOverview.tsx +23 -0
  21. package/dist/containers/Node/NodePages.js +16 -0
  22. package/dist/containers/Node/NodeStructure/NodeStructure.scss +148 -0
  23. package/dist/containers/Node/NodeStructure/NodeStructure.tsx +153 -0
  24. package/dist/containers/Node/NodeStructure/Pdisk.tsx +299 -0
  25. package/dist/containers/Node/NodeStructure/Vdisk.tsx +153 -0
  26. package/dist/containers/Pdisk/Pdisk.js +2 -5
  27. package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.scss +10 -3
  28. package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.tsx +20 -15
  29. package/dist/containers/Storage/Pdisk/Pdisk.scss +1 -0
  30. package/dist/containers/Storage/Pdisk/Pdisk.tsx +7 -5
  31. package/dist/containers/Storage/Storage.js +12 -9
  32. package/dist/containers/Storage/StorageGroups/StorageGroups.scss +2 -1
  33. package/dist/containers/Storage/StorageGroups/StorageGroups.tsx +5 -2
  34. package/dist/containers/Storage/StorageNodes/StorageNodes.scss +3 -2
  35. package/dist/containers/Storage/StorageNodes/StorageNodes.tsx +7 -2
  36. package/dist/containers/Storage/Vdisk/Vdisk.js +7 -6
  37. package/dist/containers/Storage/Vdisk/Vdisk.scss +1 -0
  38. package/dist/containers/Tablet/Tablet.js +2 -7
  39. package/dist/containers/Tablets/Tablets.js +4 -12
  40. package/dist/containers/Tenant/Acl/Acl.js +0 -3
  41. package/dist/containers/Tenant/Diagnostics/Compute/Compute.js +1 -3
  42. package/dist/containers/Tenant/Diagnostics/Network/Network.js +3 -6
  43. package/dist/containers/Tenant/ObjectSummary/ObjectSummary.scss +1 -0
  44. package/dist/containers/Tenant/ObjectSummary/ObjectSummary.tsx +1 -1
  45. package/dist/containers/Tenant/QueryEditor/QueryEditor.js +26 -33
  46. package/dist/containers/Tenant/QueryEditor/QueryEditor.scss +4 -0
  47. package/dist/containers/Tenant/QueryEditor/QueryExplain/QueryExplain.js +29 -22
  48. package/dist/containers/Tenant/QueryEditor/QueryExplain/QueryExplain.scss +1 -0
  49. package/dist/containers/Tenant/QueryEditor/QueryResult/QueryResult.scss +2 -0
  50. package/dist/containers/Vdisk/Vdisk.js +2 -4
  51. package/dist/containers/VdiskPdiskNode/VdiskPdiskNode.js +4 -6
  52. package/dist/services/api.js +0 -1
  53. package/dist/store/reducers/executeQuery.js +5 -2
  54. package/dist/store/reducers/header.ts +1 -1
  55. package/dist/store/reducers/node.js +98 -3
  56. package/dist/store/reducers/nodes.js +0 -3
  57. package/dist/store/reducers/storage.js +6 -0
  58. package/dist/store/reducers/tablets.js +0 -3
  59. package/dist/utils/constants.js +0 -6
  60. package/dist/utils/getNodesColumns.js +2 -9
  61. package/dist/utils/utils.js +10 -1
  62. package/package.json +44 -33
  63. package/dist/containers/Node/Node.js +0 -184
@@ -0,0 +1,148 @@
1
+ @import '../../../styles/mixins.scss';
2
+
3
+ .kv-node-structure {
4
+ display: flex;
5
+ overflow: auto;
6
+ flex-direction: column;
7
+ flex-shrink: 0;
8
+ @include body2-typography();
9
+
10
+ &__loader {
11
+ display: flex;
12
+ flex-grow: 1;
13
+ justify-content: center;
14
+ }
15
+
16
+ &__pdisk {
17
+ display: flex;
18
+ flex-direction: column;
19
+
20
+ width: 573px;
21
+ margin-bottom: 8px;
22
+ padding-left: 20px;
23
+
24
+ border: 1px solid var(--yc-color-line-generic);
25
+ border-radius: 5px;
26
+ }
27
+
28
+ &__pdisk-id {
29
+ display: flex;
30
+ align-items: flex-end;
31
+ }
32
+
33
+ &__pdisk-header {
34
+ display: flex;
35
+ justify-content: space-between;
36
+ align-items: center;
37
+
38
+ height: 48px;
39
+ }
40
+
41
+ &__pdisk-title-wrapper {
42
+ display: flex;
43
+ align-items: center;
44
+ gap: 8px;
45
+
46
+ font-weight: 600;
47
+ }
48
+
49
+ &__pdisk-details {
50
+ margin-bottom: 20px;
51
+ }
52
+
53
+ &__link {
54
+ text-decoration: none;
55
+
56
+ color: var(--yc-color-base-special);
57
+ }
58
+
59
+ &__vdisks-header {
60
+ font-weight: 600;
61
+ }
62
+
63
+ &__vdisks-container {
64
+ margin-bottom: 42px;
65
+ }
66
+
67
+ &__vdisk-details {
68
+ overflow: auto;
69
+
70
+ min-width: 200px;
71
+ max-height: 90vh;
72
+
73
+ .vdisk-pdisk-node__column {
74
+ margin-bottom: 0;
75
+ }
76
+ .vdisk-pdisk-node__section {
77
+ padding-bottom: 0;
78
+ }
79
+ }
80
+
81
+ &__vdisk-id {
82
+ display: flex;
83
+ align-items: center;
84
+
85
+ &_selected {
86
+ color: var(--yc-color-text-info);
87
+ }
88
+ }
89
+
90
+ &__vdisk-details-button {
91
+ &_selected {
92
+ color: var(--yc-color-text-info);
93
+ }
94
+ }
95
+
96
+ &__external-button {
97
+ display: inline-flex;
98
+ align-items: center;
99
+
100
+ margin-left: 4px;
101
+
102
+ transform: translateY(-1px);
103
+
104
+ .yc-button__text {
105
+ margin: 0 4px;
106
+ }
107
+
108
+ &_hidden {
109
+ visibility: hidden;
110
+ }
111
+ }
112
+
113
+ .data-table__row:hover {
114
+ .kv-node-structure__external-button_hidden {
115
+ visibility: visible;
116
+ }
117
+ }
118
+
119
+ &__selected-vdisk {
120
+ animation: onSelectedVdiskAnimation 4s;
121
+ }
122
+
123
+ &__row {
124
+ display: flex;
125
+ }
126
+
127
+ &__column {
128
+ display: flex;
129
+ flex-direction: column;
130
+
131
+ margin-bottom: 15px;
132
+ }
133
+
134
+ &__title {
135
+ margin-right: 16px;
136
+
137
+ font-size: var(--yc-text-body2-font-size);
138
+ font-weight: 500;
139
+ line-height: var(--yc-text-body2-line-height);
140
+ text-transform: uppercase;
141
+ }
142
+ }
143
+
144
+ @keyframes onSelectedVdiskAnimation {
145
+ 0% {
146
+ background-color: var(--yc-color-base-info-hover);
147
+ }
148
+ }
@@ -0,0 +1,153 @@
1
+ import {useEffect, useRef, useMemo} from 'react';
2
+ import {useDispatch, useSelector} from 'react-redux';
3
+ import url from 'url';
4
+ import _ from 'lodash';
5
+
6
+ import cn from 'bem-cn-lite';
7
+
8
+ import {PDisk} from './Pdisk';
9
+ import Loader from '../.././../components/Loader/Loader';
10
+
11
+ import {getNodeStructure, selectNodeStructure} from '../../../store/reducers/node';
12
+
13
+ import {AutoFetcher} from '../../../utils/autofetcher';
14
+
15
+ import './NodeStructure.scss';
16
+
17
+ const b = cn('kv-node-structure');
18
+
19
+ export function valueIsDefined(value: any) {
20
+ return value !== null && value !== undefined;
21
+ }
22
+
23
+ function generateId({type, id}: {type: 'pdisk' | 'vdisk'; id: string}) {
24
+ return `${type}-${id}`;
25
+ }
26
+
27
+ interface NodeStructureProps {
28
+ nodeId: string;
29
+ className?: string;
30
+ additionalNodesInfo?: any;
31
+ scrollContainer: Element | null;
32
+ }
33
+
34
+ const autofetcher = new AutoFetcher();
35
+
36
+ function NodeStructure(props: NodeStructureProps) {
37
+ const dispatch = useDispatch();
38
+
39
+ const nodeStructure: any = useSelector(selectNodeStructure);
40
+
41
+ const loadingStructure = useSelector((state: any) => state.node.loadingStructure);
42
+ const wasLoadedStructure = useSelector((state: any) => state.node.wasLoadedStructure);
43
+ const nodeData = useSelector(
44
+ (state: any) => state.node?.data?.SystemStateInfo?.[0]
45
+ );
46
+
47
+ const nodeHref = useMemo(() => {
48
+ return props.additionalNodesInfo?.getNodeRef
49
+ ? props.additionalNodesInfo.getNodeRef(nodeData)
50
+ : undefined;
51
+ }, [nodeData, props.additionalNodesInfo]);
52
+
53
+ const {pdiskId: pdiskIdFromUrl, vdiskId: vdiskIdFromUrl} = url.parse(
54
+ window.location.href,
55
+ true,
56
+ ).query;
57
+
58
+ const isReady = useRef(false);
59
+
60
+ const scrolled = useRef(false);
61
+
62
+ useEffect(() => {
63
+ return () => {
64
+ const {scrollContainer} = props;
65
+ if (scrollContainer) {
66
+ scrollContainer.scrollTo({
67
+ behavior: 'smooth',
68
+ top: 0,
69
+ });
70
+ }
71
+ };
72
+ }, []);
73
+
74
+ useEffect(() => {
75
+ dispatch(getNodeStructure(props.nodeId));
76
+ autofetcher.start();
77
+ autofetcher.fetch(() => dispatch(getNodeStructure(props.nodeId)));
78
+
79
+ return () => {
80
+ scrolled.current = false;
81
+ isReady.current = false;
82
+ autofetcher.stop();
83
+ };
84
+ }, [props.nodeId, dispatch]);
85
+
86
+ useEffect(() => {
87
+ if (!_.isEmpty(nodeStructure) && props.scrollContainer) {
88
+ isReady.current = true;
89
+ }
90
+ }, [nodeStructure, props.scrollContainer]);
91
+
92
+ useEffect(() => {
93
+ const {scrollContainer} = props;
94
+ if (isReady.current && !scrolled.current && scrollContainer) {
95
+ const element = document.getElementById(
96
+ generateId({type: 'pdisk', id: pdiskIdFromUrl as string}),
97
+ );
98
+
99
+ let scrollToVdisk = 0;
100
+
101
+ if (vdiskIdFromUrl) {
102
+ const vDisks = nodeStructure[pdiskIdFromUrl as string]?.vDisks;
103
+ const vDisk = vDisks?.find((el: any) => el.id === vdiskIdFromUrl);
104
+ const dataTable = vDisk ? document.querySelector('.data-table') : undefined;
105
+ const order = vDisk?.order;
106
+
107
+ if (dataTable) {
108
+ scrollToVdisk += (dataTable as HTMLElement).offsetTop + 40 * (order + 1);
109
+ }
110
+ }
111
+
112
+ if (element) {
113
+ scrollContainer.scrollTo({
114
+ behavior: 'smooth',
115
+ // should subtract 20 to avoid sticking the element to tabs
116
+ top: scrollToVdisk ? scrollToVdisk : element.offsetTop - 20,
117
+ });
118
+ scrolled.current = true;
119
+ }
120
+ }
121
+ }, [nodeStructure, props.scrollContainer, pdiskIdFromUrl, vdiskIdFromUrl]);
122
+
123
+ const renderStub = () => {
124
+ return 'There is no information about node structure.';
125
+ };
126
+
127
+ const renderStructure = () => {
128
+ const pDisksIds = Object.keys(nodeStructure);
129
+ return pDisksIds.length > 0
130
+ ? pDisksIds.map((pDiskId) => (
131
+ <PDisk
132
+ data={nodeStructure[pDiskId]}
133
+ key={pDiskId}
134
+ id={generateId({type: 'pdisk', id: pDiskId})}
135
+ unfolded={pdiskIdFromUrl === pDiskId}
136
+ selectedVdiskId={vdiskIdFromUrl as string}
137
+ nodeHref={nodeHref}
138
+ />
139
+ ))
140
+ : renderStub();
141
+ };
142
+
143
+ const renderContent = () => {
144
+ if (loadingStructure && !wasLoadedStructure) {
145
+ return <Loader size="m" />;
146
+ }
147
+ return renderStructure();
148
+ };
149
+
150
+ return <div className={b(null, props.className)}>{renderContent()}</div>;
151
+ }
152
+
153
+ export default NodeStructure;
@@ -0,0 +1,299 @@
1
+ import {useState} from 'react';
2
+ import cn from 'bem-cn-lite';
3
+ import _ from 'lodash';
4
+
5
+ import {ArrowToggle, Button, Tooltip} from '@yandex-cloud/uikit';
6
+
7
+ import DataTable, {Column, Settings} from '@yandex-cloud/react-data-table';
8
+
9
+ import EntityStatus from '../../../components/EntityStatus/EntityStatus';
10
+ import InfoViewer from '../../../components/InfoViewer/InfoViewer';
11
+ import ProgressViewer from '../../../components/ProgressViewer/ProgressViewer';
12
+ import Icon from '../../../components/Icon/Icon';
13
+ import {Vdisk} from './Vdisk';
14
+
15
+ import {bytesToGB, pad9} from '../../../utils/utils';
16
+ import {formatStorageValuesToGb} from '../../../utils';
17
+
18
+ import {DEFAULT_TABLE_SETTINGS} from '../../../utils/constants';
19
+ import {valueIsDefined} from './NodeStructure';
20
+
21
+ const b = cn('kv-node-structure');
22
+
23
+ interface PDiskProps {
24
+ data: Record<string, any>;
25
+ unfolded?: boolean;
26
+ id: string;
27
+ selectedVdiskId?: string;
28
+ nodeHref?: string;
29
+ }
30
+
31
+ enum VDiskTableColumnsIds {
32
+ slotId = 'VDiskSlotId',
33
+ VDiskState = 'VDiskState',
34
+ Size = 'Size',
35
+ Info = 'Info',
36
+ }
37
+
38
+ type VDiskTableColumnsIdsKeys = keyof typeof VDiskTableColumnsIds;
39
+ type VDiskTableColumnsIdsValues = typeof VDiskTableColumnsIds[VDiskTableColumnsIdsKeys];
40
+
41
+ const vDiskTableColumnsNames: Record<VDiskTableColumnsIdsValues, string> = {
42
+ VDiskSlotId: 'Slot id',
43
+ VDiskState: 'Status',
44
+ Size: 'Size',
45
+ Info: '',
46
+ };
47
+
48
+ interface RowType {
49
+ id: string;
50
+ [VDiskTableColumnsIds.slotId]: number;
51
+ [VDiskTableColumnsIds.VDiskState]: string;
52
+ AllocatedSize: string;
53
+ AvailableSize: string;
54
+ }
55
+
56
+ function getColumns({
57
+ pDiskId,
58
+ selectedVdiskId,
59
+ nodeHref,
60
+ }: {
61
+ pDiskId: number;
62
+ selectedVdiskId?: string;
63
+ nodeHref?: string;
64
+ }) {
65
+ const columns: Column<RowType>[] = [
66
+ {
67
+ name: VDiskTableColumnsIds.slotId as string,
68
+ header: vDiskTableColumnsNames[VDiskTableColumnsIds.slotId],
69
+ width: 100,
70
+ render: ({value, row}) => {
71
+ let vdiskInternalViewerLink: string | undefined;
72
+
73
+ if (nodeHref && value !== undefined) {
74
+ vdiskInternalViewerLink +=
75
+ nodeHref + '/actors/vdisks/vdisk' + pad9(pDiskId) + '_' + pad9(value);
76
+ }
77
+
78
+ return (
79
+ <div className={b('vdisk-id', {selected: row.id === selectedVdiskId})}>
80
+ <span>{value as number}</span>
81
+ {vdiskInternalViewerLink && (
82
+ <Button
83
+ size="s"
84
+ className={b('external-button', {hidden: true})}
85
+ href={vdiskInternalViewerLink}
86
+ target="_blank"
87
+ >
88
+ <Icon name="external" />
89
+ </Button>
90
+ )}
91
+ </div>
92
+ );
93
+ },
94
+ align: DataTable.LEFT,
95
+ },
96
+ {
97
+ name: VDiskTableColumnsIds.VDiskState as string,
98
+ header: vDiskTableColumnsNames[VDiskTableColumnsIds.VDiskState],
99
+ width: 70,
100
+ render: ({value}) => {
101
+ return <EntityStatus status={value === 'OK' ? 'green' : 'red'} />;
102
+ },
103
+ sortAccessor: (row) => (row[VDiskTableColumnsIds.VDiskState] === 'OK' ? 1 : 0),
104
+ align: DataTable.CENTER,
105
+ },
106
+ {
107
+ name: VDiskTableColumnsIds.Size as string,
108
+ header: vDiskTableColumnsNames[VDiskTableColumnsIds.Size],
109
+ width: 100,
110
+ render: ({row}) => {
111
+ return (
112
+ <ProgressViewer
113
+ value={row.AllocatedSize}
114
+ capacity={Number(row.AllocatedSize) + Number(row.AvailableSize)}
115
+ formatValues={formatStorageValuesToGb}
116
+ colorizeProgress={true}
117
+ />
118
+ );
119
+ },
120
+ sortAccessor: (row) => Number(row.AllocatedSize),
121
+ align: DataTable.CENTER,
122
+ },
123
+ {
124
+ name: VDiskTableColumnsIds.Info as string,
125
+ header: vDiskTableColumnsNames[VDiskTableColumnsIds.Info],
126
+ width: 70,
127
+ render: ({row}) => {
128
+ return (
129
+ <Tooltip
130
+ placement={['right']}
131
+ content={<Vdisk {...row} />}
132
+ contentClassName={b('vdisk-details')}
133
+ >
134
+ <Button
135
+ view="clear"
136
+ className={b('vdisk-details-button', {
137
+ selected: row.id === selectedVdiskId,
138
+ })}
139
+ >
140
+ <Icon name="information" viewBox="0 0 512 512" height={16} width={16} />
141
+ </Button>
142
+ </Tooltip>
143
+ );
144
+ },
145
+ sortable: false,
146
+ },
147
+ ];
148
+ return columns;
149
+ }
150
+
151
+ export function PDisk(props: PDiskProps) {
152
+ const [unfolded, setUnfolded] = useState(props.unfolded ?? false);
153
+
154
+ const data = props.data ?? {};
155
+
156
+ const onOpenPDiskDetails = () => {
157
+ setUnfolded(true);
158
+ };
159
+ const onClosePDiskDetails = () => {
160
+ setUnfolded(false);
161
+ };
162
+
163
+ const renderVDisks = () => {
164
+ const {selectedVdiskId, data, nodeHref} = props;
165
+ const {vDisks} = data;
166
+
167
+ return (
168
+ <DataTable
169
+ theme="yandex-cloud"
170
+ data={vDisks}
171
+ columns={getColumns({nodeHref, pDiskId: data.PDiskId, selectedVdiskId})}
172
+ settings={{...DEFAULT_TABLE_SETTINGS, dynamicRender: false} as Settings}
173
+ rowClassName={(row) => {
174
+ return row.id === selectedVdiskId ? b('selected-vdisk') : '';
175
+ }}
176
+ />
177
+ );
178
+ };
179
+
180
+ const renderPDiskDetails = () => {
181
+ if (_.isEmpty(data)) {
182
+ return <div>No information about PDisk</div>;
183
+ }
184
+ const {nodeHref} = props;
185
+ const {
186
+ TotalSize,
187
+ AvailableSize,
188
+ Device,
189
+ Guid,
190
+ PDiskId,
191
+ Path,
192
+ Realtime,
193
+ State,
194
+ Category,
195
+ SerialNumber,
196
+ } = data;
197
+
198
+ let pDiskInternalViewerLink: string | undefined;
199
+
200
+ if (nodeHref) {
201
+ pDiskInternalViewerLink += nodeHref + '/actors/pdisks/pdisk' + pad9(PDiskId);
202
+ }
203
+
204
+ const pdiskInfo: any = [
205
+ {
206
+ label: 'PDisk Id',
207
+ value: (
208
+ <div className={b('pdisk-id')}>
209
+ {PDiskId}
210
+ {pDiskInternalViewerLink && (
211
+ <Button
212
+ size="s"
213
+ className={b('external-button')}
214
+ href={pDiskInternalViewerLink}
215
+ target="_blank"
216
+ view="clear"
217
+ >
218
+ <Icon name="external" />
219
+ </Button>
220
+ )}
221
+ </div>
222
+ ),
223
+ },
224
+ ];
225
+ if (valueIsDefined(Path)) {
226
+ pdiskInfo.push({label: 'Path', value: Path});
227
+ }
228
+ if (valueIsDefined(Guid)) {
229
+ pdiskInfo.push({label: 'GUID', value: Guid});
230
+ }
231
+ if (valueIsDefined(Category)) {
232
+ pdiskInfo.push({label: 'Category', value: Category});
233
+ }
234
+ pdiskInfo.push({
235
+ label: 'Allocated Size',
236
+ value: bytesToGB(TotalSize - AvailableSize),
237
+ });
238
+ pdiskInfo.push({
239
+ label: 'Available Size',
240
+ value: bytesToGB(AvailableSize),
241
+ });
242
+ if (Number(TotalSize) >= 0 && Number(AvailableSize) >= 0) {
243
+ pdiskInfo.push({
244
+ label: 'Size',
245
+ value: (
246
+ <ProgressViewer
247
+ value={TotalSize - AvailableSize}
248
+ capacity={TotalSize}
249
+ formatValues={formatStorageValuesToGb}
250
+ colorizeProgress={true}
251
+ className={b('size')}
252
+ />
253
+ ),
254
+ });
255
+ }
256
+ if (valueIsDefined(State)) {
257
+ pdiskInfo.push({label: 'State', value: State});
258
+ }
259
+ if (valueIsDefined(Device)) {
260
+ pdiskInfo.push({
261
+ label: 'Device',
262
+ value: <EntityStatus status={Device} />,
263
+ });
264
+ }
265
+ if (valueIsDefined(Realtime)) {
266
+ pdiskInfo.push({
267
+ label: 'Realtime',
268
+ value: <EntityStatus status={Realtime} />,
269
+ });
270
+ }
271
+ if (valueIsDefined(SerialNumber)) {
272
+ pdiskInfo.push({label: 'SerialNumber', value: SerialNumber});
273
+ }
274
+ return (
275
+ <div>
276
+ <InfoViewer className={b('pdisk-details')} info={pdiskInfo} />
277
+ <div className={b('vdisks-container')}>
278
+ <div className={b('vdisks-header')}>VDisks</div>
279
+ {renderVDisks()}
280
+ </div>
281
+ </div>
282
+ );
283
+ };
284
+
285
+ return (
286
+ <div className={b('pdisk')} id={props.id}>
287
+ <div className={b('pdisk-header')}>
288
+ <div className={b('pdisk-title-wrapper')}>
289
+ <span>{data.Path}</span>
290
+ <EntityStatus status={data.Device} name={`${data.NodeId}-${data.PDiskId}`} />
291
+ </div>
292
+ <Button onClick={unfolded ? onClosePDiskDetails : onOpenPDiskDetails} view="clear">
293
+ <ArrowToggle direction={unfolded ? 'top' : 'bottom'} />
294
+ </Button>
295
+ </div>
296
+ {unfolded && renderPDiskDetails()}
297
+ </div>
298
+ );
299
+ }