Skip to content

Commit 91229e5

Browse files
Copilotadameat
andcommitted
Add Threads tab to Node page with mock data and UI components
Co-authored-by: adameat <[email protected]>
1 parent 0f27a16 commit 91229e5

File tree

12 files changed

+465
-0
lines changed

12 files changed

+465
-0
lines changed

src/containers/Node/Node.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {Tablets} from '../Tablets/Tablets';
2929
import type {NodeTab} from './NodePages';
3030
import {NODE_TABS, getDefaultNodePath, nodePageQueryParams, nodePageTabSchema} from './NodePages';
3131
import NodeStructure from './NodeStructure/NodeStructure';
32+
import {Threads} from './Threads/Threads';
3233
import i18n from './i18n';
3334

3435
import './Node.scss';
@@ -247,6 +248,10 @@ function NodePageContent({
247248
return <NodeStructure nodeId={nodeId} />;
248249
}
249250

251+
case 'threads': {
252+
return <Threads />;
253+
}
254+
250255
default:
251256
return false;
252257
}

src/containers/Node/NodePages.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const NODE_TABS_IDS = {
1111
storage: 'storage',
1212
tablets: 'tablets',
1313
structure: 'structure',
14+
threads: 'threads',
1415
} as const;
1516

1617
export type NodeTab = ValueOf<typeof NODE_TABS_IDS>;
@@ -34,6 +35,12 @@ export const NODE_TABS = [
3435
return i18n('tabs.tablets');
3536
},
3637
},
38+
{
39+
id: NODE_TABS_IDS.threads,
40+
get title() {
41+
return i18n('tabs.threads');
42+
},
43+
},
3744
];
3845

3946
export const nodePageTabSchema = z.nativeEnum(NODE_TABS_IDS).catch(NODE_TABS_IDS.tablets);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
.cpu-usage-bar {
2+
display: flex;
3+
align-items: center;
4+
gap: 8px;
5+
6+
min-width: 120px;
7+
8+
&__progress {
9+
flex: 1;
10+
11+
min-width: 60px;
12+
}
13+
14+
&__text {
15+
font-size: 12px;
16+
white-space: nowrap;
17+
}
18+
19+
&__total {
20+
font-weight: 500;
21+
}
22+
23+
&__breakdown {
24+
margin-left: 4px;
25+
26+
color: var(--g-color-text-secondary);
27+
}
28+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {Progress} from '@gravity-ui/uikit';
2+
3+
import {cn} from '../../../../utils/cn';
4+
5+
import './CpuUsageBar.scss';
6+
7+
const b = cn('cpu-usage-bar');
8+
9+
interface CpuUsageBarProps {
10+
systemUsage?: number;
11+
userUsage?: number;
12+
className?: string;
13+
}
14+
15+
/**
16+
* Component to display CPU usage as a progress bar showing both system and user usage
17+
*/
18+
export function CpuUsageBar({systemUsage = 0, userUsage = 0, className}: CpuUsageBarProps) {
19+
const totalUsage = systemUsage + userUsage;
20+
const systemPercent = Math.round(systemUsage * 100);
21+
const userPercent = Math.round(userUsage * 100);
22+
const totalPercent = Math.round(totalUsage * 100);
23+
24+
// Determine color based on total load
25+
const getProgressTheme = (): 'success' | 'warning' | 'danger' => {
26+
if (totalUsage >= 1.0) {
27+
return 'danger';
28+
} // 100% or more load
29+
if (totalUsage >= 0.8) {
30+
return 'warning';
31+
} // 80% or more load
32+
return 'success';
33+
};
34+
35+
return (
36+
<div className={b(null, className)}>
37+
<div className={b('progress')}>
38+
<Progress value={Math.min(totalPercent, 100)} theme={getProgressTheme()} size="s" />
39+
</div>
40+
<div className={b('text')}>
41+
<span className={b('total')}>{totalPercent}%</span>
42+
<span className={b('breakdown')}>
43+
(S: {systemPercent}%, U: {userPercent}%)
44+
</span>
45+
</div>
46+
</div>
47+
);
48+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
.thread-states-bar {
2+
&__bar {
3+
display: flex;
4+
overflow: hidden;
5+
6+
min-width: 80px;
7+
height: 16px;
8+
margin-bottom: 4px;
9+
10+
border: 1px solid var(--g-color-line-generic);
11+
border-radius: 4px;
12+
background-color: var(--g-color-base-generic);
13+
}
14+
15+
&__segment {
16+
transition: opacity 0.2s ease;
17+
18+
&:hover {
19+
opacity: 0.8;
20+
}
21+
}
22+
23+
&__legend {
24+
display: flex;
25+
flex-wrap: wrap;
26+
gap: 8px;
27+
28+
font-size: 11px;
29+
30+
color: var(--g-color-text-secondary);
31+
}
32+
33+
&__legend-item {
34+
display: flex;
35+
align-items: center;
36+
gap: 4px;
37+
38+
white-space: nowrap;
39+
}
40+
41+
&__legend-color {
42+
flex-shrink: 0;
43+
44+
width: 8px;
45+
height: 8px;
46+
47+
border-radius: 2px;
48+
}
49+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {cn} from '../../../../utils/cn';
2+
3+
import './ThreadStatesBar.scss';
4+
5+
const b = cn('thread-states-bar');
6+
7+
interface ThreadStatesBarProps {
8+
states?: Record<string, number>;
9+
totalThreads?: number;
10+
className?: string;
11+
}
12+
13+
/**
14+
* Thread state colors based on the state type
15+
*/
16+
const getStateColor = (state: string): string => {
17+
switch (state.toUpperCase()) {
18+
case 'R': // Running
19+
return 'var(--g-color-text-positive)';
20+
case 'S': // Sleeping
21+
return 'var(--g-color-text-secondary)';
22+
case 'D': // Uninterruptible sleep
23+
return 'var(--g-color-text-warning)';
24+
case 'Z': // Zombie
25+
case 'T': // Stopped
26+
case 'X': // Dead
27+
return 'var(--g-color-text-danger)';
28+
default:
29+
return 'var(--g-color-text-misc)';
30+
}
31+
};
32+
33+
/**
34+
* Component to display thread states as a horizontal bar chart
35+
*/
36+
export function ThreadStatesBar({states = {}, totalThreads, className}: ThreadStatesBarProps) {
37+
const total = totalThreads || Object.values(states).reduce((sum, count) => sum + count, 0);
38+
39+
if (total === 0) {
40+
return <div className={b(null, className)}>No threads</div>;
41+
}
42+
43+
const stateEntries = Object.entries(states).filter(([, count]) => count > 0);
44+
45+
return (
46+
<div className={b(null, className)}>
47+
<div className={b('bar')}>
48+
{stateEntries.map(([state, count]) => {
49+
const percentage = (count / total) * 100;
50+
return (
51+
<div
52+
key={state}
53+
className={b('segment')}
54+
style={{
55+
width: `${percentage}%`,
56+
backgroundColor: getStateColor(state),
57+
}}
58+
title={`${state}: ${count} threads (${Math.round(percentage)}%)`}
59+
/>
60+
);
61+
})}
62+
</div>
63+
<div className={b('legend')}>
64+
{stateEntries.map(([state, count]) => (
65+
<span key={state} className={b('legend-item')}>
66+
<span
67+
className={b('legend-color')}
68+
style={{backgroundColor: getStateColor(state)}}
69+
/>
70+
{state}: {count}
71+
</span>
72+
))}
73+
</div>
74+
</div>
75+
);
76+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.threads {
2+
&__error {
3+
margin-bottom: 16px;
4+
}
5+
6+
&__table {
7+
.g-table {
8+
--g-table-row-height: 56px;
9+
}
10+
}
11+
12+
&__empty {
13+
padding: 24px;
14+
15+
font-size: 14px;
16+
text-align: center;
17+
18+
color: var(--g-color-text-secondary);
19+
}
20+
}

0 commit comments

Comments
 (0)