Skip to content

Commit 3d81b36

Browse files
committed
test
1 parent 4db34be commit 3d81b36

File tree

8 files changed

+407
-9
lines changed

8 files changed

+407
-9
lines changed

public/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
window.systemSettings = {};
2424
window.userSettings = {};
2525
window.web_version = !'%REACT_APP_BACKEND%';
26-
window.custom_backend = '%NODE_ENV%' === 'development' && '%REACT_APP_BACKEND%';
26+
window.custom_backend = 'http://localhost:8765';
2727
window.meta_backend = '%REACT_APP_META_BACKEND%'
2828
</script>
2929
</head>

src/containers/App/Content.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {RawBreadcrumbItem} from '../Header/breadcrumbs';
2525
import {
2626
ClusterSlot,
2727
ClustersSlot,
28+
MultipartTestSlot,
2829
NodeSlot,
2930
PDiskPageSlot,
3031
RedirectSlot,
@@ -47,7 +48,14 @@ type RouteSlot = {
4748
wrapper?: React.ComponentType<any>;
4849
exact?: boolean;
4950
};
51+
5052
const routesSlots: RouteSlot[] = [
53+
{
54+
path: routes.multipartTest,
55+
slot: MultipartTestSlot,
56+
component: lazyComponent(() => import('../MultipartTest/MultipartTest'), 'MultipartTest'),
57+
exact: true,
58+
},
5159
{
5260
path: routes.cluster,
5361
slot: ClusterSlot,

src/containers/App/appSlots.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {RedirectProps, RouteComponentProps} from 'react-router-dom';
33
import {createSlot} from '../../components/slots';
44
import type {Cluster} from '../Cluster/Cluster';
55
import type {Clusters} from '../Clusters/Clusters';
6+
import type {MultipartTest} from '../MultipartTest/MultipartTest';
67
import type {Node} from '../Node/Node';
78
import type {PDiskPage} from '../PDiskPage/PDiskPage';
89
import type {StorageGroupPage} from '../StorageGroupPage/StorageGroupPage';
@@ -11,45 +12,58 @@ import type {Tenant} from '../Tenant/Tenant';
1112
import type {VDiskPage} from '../VDiskPage/VDiskPage';
1213

1314
export const ClustersSlot = createSlot<{
14-
children:
15+
children?:
1516
| React.ReactNode
1617
| ((props: {component: typeof Clusters} & RouteComponentProps) => React.ReactNode);
1718
}>('clusters');
19+
1820
export const ClusterSlot = createSlot<{
19-
children:
21+
children?:
2022
| React.ReactNode
2123
| ((props: {component: typeof Cluster} & RouteComponentProps) => React.ReactNode);
2224
}>('cluster');
25+
2326
export const TenantSlot = createSlot<{
24-
children:
27+
children?:
2528
| React.ReactNode
2629
| ((props: {component: typeof Tenant} & RouteComponentProps) => React.ReactNode);
2730
}>('tenant');
31+
2832
export const NodeSlot = createSlot<{
29-
children:
33+
children?:
3034
| React.ReactNode
3135
| ((props: {component: typeof Node} & RouteComponentProps) => React.ReactNode);
3236
}>('node');
37+
3338
export const PDiskPageSlot = createSlot<{
34-
children:
39+
children?:
3540
| React.ReactNode
3641
| ((props: {component: typeof PDiskPage} & RouteComponentProps) => React.ReactNode);
3742
}>('pDisk');
43+
3844
export const VDiskPageSlot = createSlot<{
39-
children:
45+
children?:
4046
| React.ReactNode
4147
| ((props: {component: typeof VDiskPage} & RouteComponentProps) => React.ReactNode);
4248
}>('vDisk');
49+
4350
export const StorageGroupSlot = createSlot<{
44-
children:
51+
children?:
4552
| React.ReactNode
4653
| ((props: {component: typeof StorageGroupPage} & RouteComponentProps) => React.ReactNode);
4754
}>('storageGroup');
55+
4856
export const TabletSlot = createSlot<{
49-
children:
57+
children?:
5058
| React.ReactNode
5159
| ((props: {component: typeof Tablet} & RouteComponentProps) => React.ReactNode);
5260
}>('tablet');
5361

62+
export const MultipartTestSlot = createSlot<{
63+
children?:
64+
| React.ReactNode
65+
| ((props: {component: typeof MultipartTest} & RouteComponentProps) => React.ReactNode);
66+
}>('multipartTest');
67+
5468
export const RoutesSlot = createSlot<{children: React.ReactNode}>('routes');
5569
export const RedirectSlot = createSlot<RedirectProps>('redirect');
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import React from 'react';
2+
3+
import {Button} from '@gravity-ui/uikit';
4+
5+
import type {MultipartChunk} from '../../store/reducers/multipart/multipart';
6+
import {useStreamMultipartQuery} from '../../store/reducers/multipart/multipart';
7+
8+
const ANIMATION_DURATION = 300;
9+
10+
export function MultipartTest() {
11+
const [startStream, setStartStream] = React.useState(false);
12+
const [receivedChunks, setReceivedChunks] = React.useState<MultipartChunk[]>([]);
13+
14+
const handleChunk = React.useCallback((chunk: MultipartChunk) => {
15+
console.log('handleChunk called in component:', chunk);
16+
console.log(chunk);
17+
setReceivedChunks((prevChunks: MultipartChunk[]) => {
18+
// Create a new array with the new chunk
19+
const newChunks = [chunk, ...prevChunks];
20+
// Sort by part_number in descending order to show newest first
21+
return newChunks;
22+
});
23+
}, []);
24+
25+
console.log('Component rendered, startStream:', startStream);
26+
27+
const {error, isLoading} = useStreamMultipartQuery(
28+
{
29+
url: '/viewer/json/query',
30+
onChunk: handleChunk,
31+
},
32+
{
33+
skip: !startStream,
34+
},
35+
);
36+
37+
const handleStart = React.useCallback(() => {
38+
console.log('Starting stream...');
39+
setReceivedChunks([]);
40+
setStartStream(true);
41+
}, []);
42+
43+
const handleStop = React.useCallback(() => {
44+
console.log('Stopping stream...');
45+
setStartStream(false);
46+
}, []);
47+
48+
const getErrorMessage = (err: unknown): string => {
49+
if (err instanceof Error) {
50+
return err.message;
51+
}
52+
if (typeof err === 'object') {
53+
return JSON.stringify(err);
54+
}
55+
return String(err);
56+
};
57+
58+
return (
59+
<div
60+
className="multipart-test"
61+
style={{padding: '32px', maxWidth: '800px', margin: '0 auto'}}
62+
>
63+
<h2
64+
style={{
65+
fontSize: '24px',
66+
fontWeight: 600,
67+
marginBottom: '24px',
68+
color: 'var(--g-color-text-primary)',
69+
}}
70+
>
71+
Multipart Streaming Test
72+
</h2>
73+
74+
<div style={{marginBottom: '24px', display: 'flex', gap: '12px', alignItems: 'center'}}>
75+
<Button
76+
view="action"
77+
size="l"
78+
onClick={startStream ? handleStop : handleStart}
79+
loading={isLoading}
80+
>
81+
{startStream ? 'Stop Streaming' : 'Start Streaming'}
82+
</Button>
83+
{receivedChunks.length > 0 && (
84+
<span
85+
style={{
86+
color: 'var(--g-color-text-secondary)',
87+
fontSize: '14px',
88+
}}
89+
>
90+
Received: {receivedChunks.length} chunks
91+
</span>
92+
)}
93+
</div>
94+
95+
{Boolean(error) && (
96+
<div
97+
style={{
98+
color: 'var(--g-color-text-danger)',
99+
marginBottom: '24px',
100+
padding: '12px',
101+
backgroundColor: 'var(--g-color-danger-light)',
102+
borderRadius: '6px',
103+
fontSize: '14px',
104+
}}
105+
>
106+
{`Error: ${getErrorMessage(error)}`}
107+
</div>
108+
)}
109+
110+
<div
111+
style={{
112+
display: 'flex',
113+
flexDirection: 'column',
114+
gap: '12px',
115+
}}
116+
>
117+
{receivedChunks.map((chunk: MultipartChunk) => {
118+
const content = chunk.content || {};
119+
const {color = '#e5e5e5', message = '', id} = content;
120+
121+
return (
122+
<div
123+
key={chunk.part_number}
124+
style={{
125+
padding: '16px',
126+
borderRadius: '8px',
127+
backgroundColor: color,
128+
color: isLightColor(color) ? '#000' : '#fff',
129+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
130+
animation: `fadeIn ${ANIMATION_DURATION}ms ease-out`,
131+
position: 'relative',
132+
overflow: 'hidden',
133+
}}
134+
>
135+
<div
136+
style={{
137+
display: 'flex',
138+
justifyContent: 'space-between',
139+
marginBottom: '8px',
140+
fontSize: '12px',
141+
opacity: 0.8,
142+
}}
143+
>
144+
<span>ID: {id}</span>
145+
<span>
146+
Part {chunk.part_number} of {chunk.total_parts}
147+
</span>
148+
</div>
149+
<div
150+
style={{
151+
fontSize: '16px',
152+
lineHeight: '1.5',
153+
wordBreak: 'break-word',
154+
}}
155+
>
156+
{message}
157+
</div>
158+
{chunk.result && (
159+
<div
160+
style={{
161+
marginTop: '8px',
162+
fontSize: '14px',
163+
opacity: 0.8,
164+
}}
165+
>
166+
Result: {chunk.result}
167+
</div>
168+
)}
169+
</div>
170+
);
171+
})}
172+
</div>
173+
174+
<style>{`
175+
@keyframes fadeIn {
176+
from {
177+
opacity: 0;
178+
transform: translateY(10px);
179+
}
180+
to {
181+
opacity: 1;
182+
transform: translateY(0);
183+
}
184+
}
185+
`}</style>
186+
</div>
187+
);
188+
}
189+
190+
// Helper function to determine if a color is light or dark
191+
function isLightColor(color: string) {
192+
const hex = color.replace('#', '');
193+
const r = parseInt(hex.substr(0, 2), 16);
194+
const g = parseInt(hex.substr(2, 2), 16);
195+
const b = parseInt(hex.substr(4, 2), 16);
196+
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
197+
return brightness > 128;
198+
}

src/routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const PDISK = 'pDisk';
1515
export const VDISK = 'vDisk';
1616
export const STORAGE_GROUP = 'storageGroup';
1717
export const TABLET = 'tablet';
18+
export const MULTIPART_TEST = 'multipart-test';
1819

1920
const routes = {
2021
clusters: `/${CLUSTERS}`,
@@ -27,6 +28,7 @@ const routes = {
2728
tablet: `/${TABLET}/:id`,
2829
tabletsFilters: `/tabletsFilters`,
2930
auth: `/auth`,
31+
multipartTest: `/${MULTIPART_TEST}`,
3032
} as const;
3133

3234
export default routes;

src/services/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {AxiosRequestConfig} from 'axios';
22

33
import {AuthAPI} from './auth';
44
import {MetaAPI} from './meta';
5+
import {MultipartAPI} from './multipart';
56
import {OperationAPI} from './operation';
67
import {PDiskAPI} from './pdisk';
78
import {SchemeAPI} from './scheme';
@@ -22,6 +23,7 @@ export class YdbEmbeddedAPI {
2223
vdisk: VDiskAPI;
2324
viewer: ViewerAPI;
2425
meta?: MetaAPI;
26+
multipart?: MultipartAPI;
2527

2628
constructor({config, webVersion}: {config: AxiosRequestConfig; webVersion?: boolean}) {
2729
this.auth = new AuthAPI({config});
@@ -36,6 +38,7 @@ export class YdbEmbeddedAPI {
3638
this.trace = new TraceAPI({config});
3739
this.vdisk = new VDiskAPI({config});
3840
this.viewer = new ViewerAPI({config});
41+
this.multipart = new MultipartAPI({config});
3942
}
4043
}
4144

0 commit comments

Comments
 (0)