ydb-embedded-ui 1.0.4 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/assets/icons/question.svg +1 -0
  3. package/dist/components/AsideNavigation/AsideHeader.tsx +2 -1
  4. package/dist/components/ClusterInfo/ClusterInfo.tsx +8 -4
  5. package/dist/components/FullNodeViewer/FullNodeViewer.scss +4 -9
  6. package/dist/components/InfoViewer/InfoViewer.scss +3 -2
  7. package/dist/components/InternalLink/InternalLink.js +8 -0
  8. package/dist/components/Loader/Loader.scss +5 -0
  9. package/dist/components/Loader/Loader.tsx +16 -0
  10. package/dist/components/PDiskViewer/PDiskViewer.js +3 -4
  11. package/dist/containers/App/App.scss +4 -0
  12. package/dist/containers/App/Content.js +0 -2
  13. package/dist/containers/AppIcons/AppIcons.js +4 -0
  14. package/dist/containers/Authentication/Authentication.tsx +2 -2
  15. package/dist/containers/Cluster/Cluster.tsx +1 -1
  16. package/dist/containers/Header/Header.tsx +6 -1
  17. package/dist/containers/Heatmap/Heatmap.js +0 -1
  18. package/dist/containers/Node/Node.scss +12 -1
  19. package/dist/containers/Node/Node.tsx +174 -0
  20. package/dist/containers/Node/NodeOverview/NodeOverview.scss +0 -0
  21. package/dist/containers/Node/NodeOverview/NodeOverview.tsx +23 -0
  22. package/dist/containers/Node/NodePages.js +16 -0
  23. package/dist/containers/Node/NodeStructure/NodeStructure.scss +151 -0
  24. package/dist/containers/Node/NodeStructure/NodeStructure.tsx +155 -0
  25. package/dist/containers/Node/NodeStructure/Pdisk.tsx +299 -0
  26. package/dist/containers/Node/NodeStructure/Vdisk.tsx +153 -0
  27. package/dist/containers/Pdisk/Pdisk.js +2 -5
  28. package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.scss +10 -3
  29. package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.tsx +20 -15
  30. package/dist/containers/Storage/Pdisk/Pdisk.scss +1 -0
  31. package/dist/containers/Storage/Pdisk/Pdisk.tsx +7 -5
  32. package/dist/containers/Storage/Storage.js +12 -9
  33. package/dist/containers/Storage/StorageGroups/StorageGroups.scss +2 -1
  34. package/dist/containers/Storage/StorageGroups/StorageGroups.tsx +2 -2
  35. package/dist/containers/Storage/StorageNodes/StorageNodes.scss +3 -2
  36. package/dist/containers/Storage/StorageNodes/StorageNodes.tsx +2 -2
  37. package/dist/containers/Storage/Vdisk/Vdisk.js +7 -6
  38. package/dist/containers/Storage/Vdisk/Vdisk.scss +1 -0
  39. package/dist/containers/Tablet/Tablet.js +2 -7
  40. package/dist/containers/Tablets/Tablets.js +4 -12
  41. package/dist/containers/Tenant/Acl/Acl.js +0 -3
  42. package/dist/containers/Tenant/Diagnostics/Compute/Compute.js +1 -3
  43. package/dist/containers/Tenant/Diagnostics/Network/Network.js +3 -6
  44. package/dist/containers/Tenant/ObjectSummary/ObjectSummary.tsx +2 -2
  45. package/dist/containers/Tenant/QueryEditor/QueryEditor.js +24 -24
  46. package/dist/containers/Tenant/QueryEditor/QueryEditor.scss +4 -0
  47. package/dist/containers/Tenant/QueryEditor/QueryExplain/QueryExplain.js +4 -1
  48. package/dist/containers/Tenant/QueryEditor/QueryResult/QueryResult.scss +1 -0
  49. package/dist/containers/Vdisk/Vdisk.js +2 -4
  50. package/dist/containers/VdiskPdiskNode/VdiskPdiskNode.js +4 -6
  51. package/dist/services/api.js +0 -1
  52. package/dist/store/reducers/executeQuery.js +1 -1
  53. package/dist/store/reducers/header.ts +1 -1
  54. package/dist/store/reducers/node.js +98 -3
  55. package/dist/store/reducers/nodes.js +0 -3
  56. package/dist/store/reducers/storage.js +8 -2
  57. package/dist/store/reducers/tablets.js +0 -3
  58. package/dist/utils/constants.js +0 -6
  59. package/dist/utils/getNodesColumns.js +2 -9
  60. package/dist/utils/utils.js +10 -1
  61. package/package.json +43 -29
  62. package/dist/containers/Node/Node.js +0 -184
@@ -0,0 +1,151 @@
1
+ @import '../../../styles/mixins.scss';
2
+
3
+ .kv-node-structure {
4
+ position: relative;
5
+
6
+ display: flex;
7
+ overflow: auto;
8
+ flex-direction: column;
9
+ flex-shrink: 0;
10
+ @include flex-container();
11
+ @include body2-typography();
12
+
13
+ &__loader {
14
+ display: flex;
15
+ flex-grow: 1;
16
+ justify-content: center;
17
+ }
18
+
19
+ &__pdisk {
20
+ display: flex;
21
+ flex-direction: column;
22
+
23
+ width: 573px;
24
+ margin-bottom: 8px;
25
+ padding-left: 20px;
26
+
27
+ border: 1px solid var(--yc-color-line-generic);
28
+ border-radius: 5px;
29
+ }
30
+
31
+ &__pdisk-id {
32
+ display: flex;
33
+ align-items: flex-end;
34
+ }
35
+
36
+ &__pdisk-header {
37
+ display: flex;
38
+ justify-content: space-between;
39
+ align-items: center;
40
+
41
+ height: 48px;
42
+ }
43
+
44
+ &__pdisk-title-wrapper {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 8px;
48
+
49
+ font-weight: 600;
50
+ }
51
+
52
+ &__pdisk-details {
53
+ margin-bottom: 20px;
54
+ }
55
+
56
+ &__link {
57
+ text-decoration: none;
58
+
59
+ color: var(--yc-color-base-special);
60
+ }
61
+
62
+ &__vdisks-header {
63
+ font-weight: 600;
64
+ }
65
+
66
+ &__vdisks-container {
67
+ margin-bottom: 42px;
68
+ }
69
+
70
+ &__vdisk-details {
71
+ overflow: auto;
72
+
73
+ min-width: 200px;
74
+ max-height: 90vh;
75
+
76
+ .vdisk-pdisk-node__column {
77
+ margin-bottom: 0;
78
+ }
79
+ .vdisk-pdisk-node__section {
80
+ padding-bottom: 0;
81
+ }
82
+ }
83
+
84
+ &__vdisk-id {
85
+ display: flex;
86
+ align-items: center;
87
+
88
+ &_selected {
89
+ color: var(--yc-color-text-info);
90
+ }
91
+ }
92
+
93
+ &__vdisk-details-button {
94
+ &_selected {
95
+ color: var(--yc-color-text-info);
96
+ }
97
+ }
98
+
99
+ &__external-button {
100
+ display: inline-flex;
101
+ align-items: center;
102
+
103
+ margin-left: 4px;
104
+
105
+ transform: translateY(-1px);
106
+
107
+ .yc-button__text {
108
+ margin: 0 4px;
109
+ }
110
+
111
+ &_hidden {
112
+ visibility: hidden;
113
+ }
114
+ }
115
+
116
+ .data-table__row:hover {
117
+ .kv-node-structure__external-button_hidden {
118
+ visibility: visible;
119
+ }
120
+ }
121
+
122
+ &__selected-vdisk {
123
+ animation: onSelectedVdiskAnimation 4s;
124
+ }
125
+
126
+ &__row {
127
+ display: flex;
128
+ }
129
+
130
+ &__column {
131
+ display: flex;
132
+ flex-direction: column;
133
+
134
+ margin-bottom: 15px;
135
+ }
136
+
137
+ &__title {
138
+ margin-right: 16px;
139
+
140
+ font-size: var(--yc-text-body2-font-size);
141
+ font-weight: 500;
142
+ line-height: var(--yc-text-body2-line-height);
143
+ text-transform: uppercase;
144
+ }
145
+ }
146
+
147
+ @keyframes onSelectedVdiskAnimation {
148
+ 0% {
149
+ background-color: var(--yc-color-base-info-hover);
150
+ }
151
+ }
@@ -0,0 +1,155 @@
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
+ }
32
+
33
+ const autofetcher = new AutoFetcher();
34
+
35
+ function NodeStructure(props: NodeStructureProps) {
36
+ const dispatch = useDispatch();
37
+
38
+ const nodeStructure: any = useSelector(selectNodeStructure);
39
+
40
+ const loadingStructure = useSelector((state: any) => state.node.loadingStructure);
41
+ const wasLoadedStructure = useSelector((state: any) => state.node.wasLoadedStructure);
42
+ const nodeData = useSelector((state: any) => state.node?.data?.SystemStateInfo?.[0]);
43
+
44
+ const nodeHref = useMemo(() => {
45
+ return props.additionalNodesInfo?.getNodeRef
46
+ ? props.additionalNodesInfo.getNodeRef(nodeData)
47
+ : undefined;
48
+ }, [nodeData, props.additionalNodesInfo]);
49
+
50
+ const {pdiskId: pdiskIdFromUrl, vdiskId: vdiskIdFromUrl} = url.parse(
51
+ window.location.href,
52
+ true,
53
+ ).query;
54
+
55
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
56
+ const scrollContainer = scrollContainerRef.current;
57
+
58
+ const isReady = useRef(false);
59
+
60
+ const scrolled = useRef(false);
61
+
62
+ useEffect(() => {
63
+ return () => {
64
+ if (scrollContainer) {
65
+ scrollContainer.scrollTo({
66
+ behavior: 'smooth',
67
+ top: 0,
68
+ });
69
+ }
70
+ };
71
+ }, []);
72
+
73
+ useEffect(() => {
74
+ dispatch(getNodeStructure(props.nodeId));
75
+ autofetcher.start();
76
+ autofetcher.fetch(() => dispatch(getNodeStructure(props.nodeId)));
77
+
78
+ return () => {
79
+ scrolled.current = false;
80
+ isReady.current = false;
81
+ autofetcher.stop();
82
+ };
83
+ }, [props.nodeId, dispatch]);
84
+
85
+ useEffect(() => {
86
+ if (!_.isEmpty(nodeStructure) && scrollContainer) {
87
+ isReady.current = true;
88
+ }
89
+ }, [nodeStructure]);
90
+
91
+ useEffect(() => {
92
+ if (isReady.current && !scrolled.current && scrollContainer) {
93
+ const element = document.getElementById(
94
+ generateId({type: 'pdisk', id: pdiskIdFromUrl as string}),
95
+ );
96
+
97
+ let scrollToVdisk = 0;
98
+
99
+ if (vdiskIdFromUrl) {
100
+ const vDisks = nodeStructure[pdiskIdFromUrl as string]?.vDisks;
101
+ const vDisk = vDisks?.find((el: any) => el.id === vdiskIdFromUrl);
102
+ const dataTable = vDisk ? document.querySelector('.data-table') : undefined;
103
+ const order = vDisk?.order;
104
+
105
+ if (dataTable) {
106
+ scrollToVdisk += (dataTable as HTMLElement).offsetTop + 40 * order;
107
+ }
108
+ }
109
+
110
+ if (element) {
111
+ scrollContainer.scrollTo({
112
+ behavior: 'smooth',
113
+ // should subtract 20 to avoid sticking the element to tabs
114
+ top: scrollToVdisk ? scrollToVdisk : element.offsetTop,
115
+ });
116
+ scrolled.current = true;
117
+ }
118
+ }
119
+ }, [nodeStructure, pdiskIdFromUrl, vdiskIdFromUrl]);
120
+
121
+ const renderStub = () => {
122
+ return 'There is no information about node structure.';
123
+ };
124
+
125
+ const renderStructure = () => {
126
+ const pDisksIds = Object.keys(nodeStructure);
127
+ return pDisksIds.length > 0
128
+ ? pDisksIds.map((pDiskId) => (
129
+ <PDisk
130
+ data={nodeStructure[pDiskId]}
131
+ key={pDiskId}
132
+ id={generateId({type: 'pdisk', id: pDiskId})}
133
+ unfolded={pdiskIdFromUrl === pDiskId}
134
+ selectedVdiskId={vdiskIdFromUrl as string}
135
+ nodeHref={nodeHref}
136
+ />
137
+ ))
138
+ : renderStub();
139
+ };
140
+
141
+ const renderContent = () => {
142
+ if (loadingStructure && !wasLoadedStructure) {
143
+ return <Loader size="m" />;
144
+ }
145
+ return renderStructure();
146
+ };
147
+
148
+ return (
149
+ <div className={b()} ref={scrollContainerRef}>
150
+ <div className={props.className}>{renderContent()}</div>
151
+ </div>
152
+ );
153
+ }
154
+
155
+ 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
+ }