Skip to content

Commit 6692015

Browse files
committed
feat(Preview): add preview for topics
1 parent f8c52a6 commit 6692015

File tree

15 files changed

+424
-180
lines changed

15 files changed

+424
-180
lines changed

src/containers/Tenant/Diagnostics/TopicData/columns/Columns.scss

-12
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,4 @@
11
.ydb-diagnostics-topic-data-columns {
2-
&__timestamp-ms {
3-
color: var(--g-color-text-secondary);
4-
}
5-
6-
&__ts-diff_danger {
7-
color: var(--g-color-text-danger);
8-
}
9-
&__message_clickable {
10-
cursor: pointer;
11-
12-
color: var(--g-color-text-info);
13-
}
142
&__offset_removed {
153
text-decoration: line-through;
164
}

src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx

+78-48
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import React from 'react';
2-
31
import DataTable from '@gravity-ui/react-data-table';
2+
import type {TextProps} from '@gravity-ui/uikit';
43
import {Text} from '@gravity-ui/uikit';
54
import {isNil} from 'lodash';
65

@@ -63,21 +62,14 @@ export function getAllColumns() {
6362
),
6463
align: DataTable.LEFT,
6564
render: ({row}) => <TopicDataTimestamp timestamp={row.WriteTimestamp} />,
66-
width: 220,
65+
width: 100,
6766
},
6867
{
6968
name: TOPIC_DATA_COLUMNS_IDS.TS_DIFF,
7069
header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.TS_DIFF],
7170
align: DataTable.RIGHT,
72-
render: ({row}) => {
73-
const numericValue = safeParseNumber(row.TimestampDiff);
74-
return (
75-
<span className={b('ts-diff', {danger: numericValue >= 100_000})}>
76-
{formatToMs(numericValue)}
77-
</span>
78-
);
79-
},
80-
width: 90,
71+
render: ({row: {TimestampDiff}}) => <TopicDataTsDiff value={TimestampDiff} />,
72+
width: 110,
8173
note: i18n('context_ts-diff'),
8274
},
8375
{
@@ -99,43 +91,16 @@ export function getAllColumns() {
9991
name: TOPIC_DATA_COLUMNS_IDS.MESSAGE,
10092
header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.MESSAGE],
10193
align: DataTable.LEFT,
102-
render: ({row: {Message, OriginalSize}}) => {
103-
if (isNil(Message)) {
104-
return EMPTY_DATA_PLACEHOLDER;
105-
}
106-
let encryptedMessage;
107-
let invalid = false;
108-
try {
109-
encryptedMessage = atob(Message);
110-
} catch {
111-
encryptedMessage = i18n('description_failed-decode');
112-
invalid = true;
113-
}
114-
115-
const truncated = safeParseNumber(OriginalSize) > TOPIC_MESSAGE_SIZE_LIMIT;
116-
return (
117-
<Text
118-
variant="body-2"
119-
color={invalid ? 'secondary' : 'primary'}
120-
className={b('message', {invalid})}
121-
>
122-
{encryptedMessage}
123-
{truncated && (
124-
<Text color="secondary" className={b('truncated')}>
125-
{' '}
126-
{i18n('description_truncated')}
127-
</Text>
128-
)}
129-
</Text>
130-
);
131-
},
94+
render: ({row: {Message, OriginalSize}}) => (
95+
<TopicDataMessage message={Message} size={OriginalSize} />
96+
),
13297
width: 500,
13398
},
13499
{
135100
name: TOPIC_DATA_COLUMNS_IDS.SIZE,
136101
header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.SIZE],
137102
align: DataTable.RIGHT,
138-
render: ({row}) => formatBytes(row.StorageSize),
103+
render: ({row: {StorageSize}}) => <TopicDataSize size={StorageSize} />,
139104
width: 100,
140105
},
141106
{
@@ -146,7 +111,7 @@ export function getAllColumns() {
146111
/>
147112
),
148113
align: DataTable.RIGHT,
149-
render: ({row}) => formatBytes(row.OriginalSize),
114+
render: ({row: {OriginalSize}}) => <TopicDataSize size={OriginalSize} />,
150115
width: 100,
151116
},
152117
{
@@ -193,21 +158,86 @@ export const REQUIRED_TOPIC_DATA_COLUMNS: TopicDataColumnId[] = ['offset'];
193158
interface TopicDataTimestampProps {
194159
timestamp?: string;
195160
}
196-
function TopicDataTimestamp({timestamp}: TopicDataTimestampProps) {
161+
export function TopicDataTimestamp({timestamp}: TopicDataTimestampProps) {
197162
if (!timestamp) {
198163
return EMPTY_DATA_PLACEHOLDER;
199164
}
200165
const formatted = formatTimestamp(timestamp);
201166
const splitted = formatted.split('.');
202167
const ms = splitted.pop();
203168
return (
204-
<React.Fragment>
169+
<Text variant="body-2">
205170
{splitted.join('.')}
206-
<span className={b('timestamp-ms')}>.{ms}</span>
207-
</React.Fragment>
171+
<Text variant="body-2" color="secondary">
172+
.{ms}
173+
</Text>
174+
</Text>
208175
);
209176
}
210177

178+
interface TopicDataTsDiffProps {
179+
value?: string;
180+
baseColor?: TextProps['color'];
181+
variant?: TextProps['variant'];
182+
}
183+
184+
export function TopicDataTsDiff({
185+
value,
186+
baseColor = 'primary',
187+
variant = 'body-2',
188+
}: TopicDataTsDiffProps) {
189+
const numericValue = safeParseNumber(value);
190+
return (
191+
<Text variant={variant} color={numericValue >= 100_000 ? 'danger' : baseColor}>
192+
{formatToMs(numericValue)}
193+
</Text>
194+
);
195+
}
196+
197+
interface TopicDataMessageProps {
198+
message?: string;
199+
size?: number;
200+
}
201+
202+
export function TopicDataMessage({message, size}: TopicDataMessageProps) {
203+
if (isNil(message)) {
204+
return EMPTY_DATA_PLACEHOLDER;
205+
}
206+
let encryptedMessage;
207+
let invalid = false;
208+
try {
209+
encryptedMessage = atob(message);
210+
} catch {
211+
encryptedMessage = i18n('description_failed-decode');
212+
invalid = true;
213+
}
214+
215+
const truncated = safeParseNumber(size) > TOPIC_MESSAGE_SIZE_LIMIT;
216+
return (
217+
<Text
218+
variant="body-2"
219+
color={invalid ? 'secondary' : 'primary'}
220+
className={b('message', {invalid})}
221+
>
222+
{encryptedMessage}
223+
{truncated && (
224+
<Text color="secondary" className={b('truncated')}>
225+
{' '}
226+
{i18n('description_truncated')}
227+
</Text>
228+
)}
229+
</Text>
230+
);
231+
}
232+
233+
interface TopicDataSizeProps {
234+
size?: number;
235+
}
236+
237+
export function TopicDataSize({size}: TopicDataSizeProps) {
238+
return formatBytes(size);
239+
}
240+
211241
function valueOrPlaceholder(
212242
value: string | number | undefined,
213243
placeholder = EMPTY_DATA_PLACEHOLDER,

src/containers/Tenant/Diagnostics/TopicData/i18n/en.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"label_offset": "Offset",
33
"label_timestamp-create": "Timestamp Create",
44
"label_timestamp-write": "Timestamp Write",
5-
"label_ts_diff": "TS Diff",
5+
"label_ts_diff": "Write Lag",
66
"label_key": "Key",
77
"label_metadata": "Metadata",
88
"label_message": "Message",
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,22 @@
11
@use '../../../../styles/mixins.scss';
22

3-
.kv-preview {
3+
.ydb-preview {
44
height: 100%;
55
@include mixins.flex-container();
66
@include mixins.query-data-table();
77
&__header {
88
position: sticky;
99
top: 0;
1010

11-
display: flex;
1211
flex-shrink: 0;
13-
justify-content: space-between;
14-
align-items: center;
1512

1613
height: 53px;
17-
padding: 0 20px;
14+
padding: 0 var(--g-spacing-6);
1815

1916
border-bottom: 1px solid var(--g-color-line-generic);
2017
background-color: var(--g-color-base-background);
2118
}
2219

23-
&__title {
24-
display: flex;
25-
gap: var(--g-spacing-1);
26-
}
27-
2820
&__table-name {
2921
margin-left: var(--g-spacing-1);
3022

@@ -39,18 +31,15 @@
3931
padding: 15px 20px;
4032
}
4133

42-
&__loader-container {
43-
display: flex;
44-
justify-content: center;
45-
align-items: center;
46-
47-
height: 100%;
48-
}
49-
5034
&__result {
5135
overflow: auto;
5236

5337
width: 100%;
54-
padding-left: 10px;
38+
padding-left: var(--g-spacing-5);
39+
}
40+
41+
&__partition-info {
42+
height: 36px;
43+
padding-left: var(--g-spacing-1);
5544
}
5645
}
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,29 @@
1-
import {Xmark} from '@gravity-ui/icons';
2-
import {Button, Icon, Loader, Text} from '@gravity-ui/uikit';
3-
4-
import EnableFullscreenButton from '../../../../components/EnableFullscreenButton/EnableFullscreenButton';
5-
import Fullscreen from '../../../../components/Fullscreen/Fullscreen';
6-
import {QueryResultTable} from '../../../../components/QueryResultTable';
7-
import {previewApi} from '../../../../store/reducers/preview';
8-
import {setShowPreview} from '../../../../store/reducers/schema/schema';
9-
import type {EPathType} from '../../../../types/api/schema';
10-
import {cn} from '../../../../utils/cn';
11-
import {useTypedDispatch} from '../../../../utils/hooks';
12-
import {parseQueryErrorToString} from '../../../../utils/query';
13-
import {isExternalTableType, isTableType} from '../../utils/schema';
1+
import {EPathType} from '../../../../types/api/schema';
2+
import {isTableType} from '../../utils/schema';
143
import i18n from '../i18n';
154

16-
import './Preview.scss';
17-
18-
const b = cn('kv-preview');
19-
20-
interface PreviewProps {
21-
database: string;
22-
path: string;
23-
type: EPathType | undefined;
24-
}
25-
26-
export const Preview = ({database, path, type}: PreviewProps) => {
27-
const dispatch = useTypedDispatch();
5+
import {Preview} from './components/PreviewView';
6+
import {TablePreview} from './components/TablePreview';
7+
import {TopicPreview} from './components/TopicPreview';
8+
import {b} from './shared';
9+
import type {PreviewContainerProps} from './types';
2810

29-
const isPreviewAvailable = isTableType(type);
30-
31-
const query = `select * from \`${path}\` limit 101`;
32-
const {currentData, isFetching, error} = previewApi.useSendQueryQuery(
33-
{
34-
database,
35-
query,
36-
action: isExternalTableType(type) ? 'execute-query' : 'execute-scan',
37-
limitRows: 100,
38-
},
39-
{
40-
skip: !isPreviewAvailable,
41-
refetchOnMountOrArgChange: true,
42-
},
43-
);
44-
const loading = isFetching && currentData === undefined;
45-
const data = currentData?.resultSets?.[0] ?? {};
46-
47-
const handleClosePreview = () => {
48-
dispatch(setShowPreview(false));
49-
};
11+
import './Preview.scss';
5012

51-
const renderHeader = () => {
52-
return (
53-
<div className={b('header')}>
54-
<div className={b('title')}>
55-
{i18n('preview.title')}
56-
<Text color="secondary" variant="body-2">
57-
{data.truncated ? `${i18n('preview.truncated')} ` : ''}(
58-
{data.result?.length ?? 0})
59-
</Text>
60-
<div className={b('table-name')}>{path}</div>
61-
</div>
62-
<div className={b('controls-left')}>
63-
<EnableFullscreenButton disabled={Boolean(error)} />
64-
<Button
65-
view="flat-secondary"
66-
onClick={handleClosePreview}
67-
title={i18n('preview.close')}
68-
>
69-
<Icon data={Xmark} size={18} />
70-
</Button>
71-
</div>
72-
</div>
73-
);
74-
};
13+
export function PreviewContainer(props: PreviewContainerProps) {
14+
const {type} = props;
15+
const isTable = isTableType(type);
16+
const isTopic = type === EPathType.EPathTypePersQueueGroup;
7517

76-
if (loading) {
77-
return (
78-
<div className={b('loader-container')}>
79-
<Loader size="m" />
80-
</div>
81-
);
18+
if (isTable) {
19+
return <TablePreview {...props} />;
8220
}
83-
84-
let message;
85-
86-
if (!isPreviewAvailable) {
87-
message = <div className={b('message-container')}>{i18n('preview.not-available')}</div>;
88-
} else if (error) {
89-
message = (
90-
<div className={b('message-container', 'error')}>{parseQueryErrorToString(error)}</div>
91-
);
21+
if (isTopic) {
22+
return <TopicPreview {...props} />;
9223
}
9324

94-
const content = message ?? (
95-
<div className={b('result')}>
96-
<QueryResultTable data={data.result} columns={data.columns} />
97-
</div>
25+
const renderContent = () => (
26+
<div className={b('message-container')}>{i18n('preview.not-available')}</div>
9827
);
99-
100-
return (
101-
<div className={b()}>
102-
{renderHeader()}
103-
<Fullscreen>{content}</Fullscreen>
104-
</div>
105-
);
106-
};
28+
return <Preview {...props} renderResult={renderContent} />;
29+
}

0 commit comments

Comments
 (0)