Skip to content

Commit aab4396

Browse files
committed
feat(TopShards): date range filter
1 parent 5a5014d commit aab4396

File tree

12 files changed

+305
-38
lines changed

12 files changed

+305
-38
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.top-shards {
2+
&__date-range {
3+
&-input {
4+
min-width: 190px;
5+
padding: 5px 8px;
6+
7+
color: var(--yc-color-text-primary);
8+
border: 1px solid var(--yc-color-line-generic);
9+
border-radius: var(--yc-border-radius-m);
10+
background: transparent;
11+
}
12+
}
13+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import cn from 'bem-cn-lite';
2+
import {ChangeEventHandler} from 'react';
3+
4+
import './DateRange.scss';
5+
6+
const b = cn('top-shards');
7+
8+
export interface DateRangeValues {
9+
/** ms from epoch */
10+
from?: number;
11+
/** ms from epoch */
12+
to?: number;
13+
}
14+
15+
interface DateRangeProps extends DateRangeValues {
16+
className?: string;
17+
onChange?: (value: DateRangeValues) => void;
18+
}
19+
20+
const toTimezonelessISOString = (timestamp?: number) => {
21+
if (!timestamp || isNaN(timestamp)) {
22+
return undefined;
23+
}
24+
25+
// shift by local offset to treat toISOString output as local time
26+
const shiftedTimestamp = timestamp - new Date().getTimezoneOffset() * 60 * 1000;
27+
return new Date(shiftedTimestamp).toISOString().substring(0, 'yyyy-MM-DDThh:mm'.length);
28+
};
29+
30+
export const DateRange = ({from, to, className, onChange}: DateRangeProps) => {
31+
const handleFromChange: ChangeEventHandler<HTMLInputElement> = ({target: {value}}) => {
32+
let newFrom = value ? new Date(value).getTime() : undefined;
33+
34+
// some browsers allow selecting time after the boundary specified in `max`
35+
if (newFrom && to && newFrom > to) {
36+
newFrom = to;
37+
}
38+
39+
onChange?.({from: newFrom, to});
40+
};
41+
42+
const handleToChange: ChangeEventHandler<HTMLInputElement> = ({target: {value}}) => {
43+
let newTo = value ? new Date(value).getTime() : undefined;
44+
45+
// some browsers allow selecting time before the boundary specified in `min`
46+
if (from && newTo && from > newTo) {
47+
newTo = from;
48+
}
49+
50+
onChange?.({from, to: newTo});
51+
};
52+
53+
const startISO = toTimezonelessISOString(from);
54+
const endISO = toTimezonelessISOString(to);
55+
56+
return (
57+
<div className={b('date-range', className)}>
58+
<input
59+
type="datetime-local"
60+
value={startISO}
61+
max={endISO}
62+
onChange={handleFromChange}
63+
className={b('date-range-input')}
64+
/>
65+
66+
<input
67+
type="datetime-local"
68+
min={startISO}
69+
value={endISO}
70+
onChange={handleToChange}
71+
className={b('date-range-input')}
72+
/>
73+
</div>
74+
);
75+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './DateRange';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
11
.top-shards {
2+
display: flex;
3+
flex-direction: column;
4+
5+
height: 100%;
6+
27
background-color: var(--yc-color-base-background);
8+
39
&__loader {
410
display: flex;
511
justify-content: center;
612
}
13+
14+
&__controls {
15+
display: flex;
16+
flex-wrap: wrap;
17+
align-items: baseline;
18+
gap: 16px;
19+
20+
margin-bottom: 10px;
21+
}
22+
23+
&__table {
24+
overflow: auto;
25+
flex-grow: 1;
26+
}
727
}

src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,28 @@ import HistoryContext from '../../../../contexts/HistoryContext';
1111

1212
import routes, {createHref} from '../../../../routes';
1313

14-
import {sendShardQuery, setShardQueryOptions} from '../../../../store/reducers/shardsWorkload';
14+
import {
15+
sendShardQuery,
16+
setShardQueryOptions,
17+
setTopShardFilters,
18+
} from '../../../../store/reducers/shardsWorkload';
1519
import {setCurrentSchemaPath, getSchema} from '../../../../store/reducers/schema';
20+
import type {IShardsWorkloadFilters} from '../../../../types/store/shardsWorkload';
1621

1722
import type {EPathType} from '../../../../types/api/schema';
1823

19-
import {DEFAULT_TABLE_SETTINGS} from '../../../../utils/constants';
24+
import {formatDateTime, formatNumber} from '../../../../utils';
25+
import {DEFAULT_TABLE_SETTINGS, HOUR_IN_SECONDS} from '../../../../utils/constants';
2026
import {useAutofetcher, useTypedSelector} from '../../../../utils/hooks';
21-
import {i18n} from '../../../../utils/i18n';
2227
import {prepareQueryError} from '../../../../utils/query';
2328

29+
import {getDefaultNodePath} from '../../../Node/NodePages';
30+
2431
import {isColumnEntityType} from '../../utils/schema';
2532

33+
import {DateRange, DateRangeValues} from './DateRange';
34+
35+
import i18n from './i18n';
2636
import './TopShards.scss';
2737

2838
const b = cn('top-shards');
@@ -41,16 +51,15 @@ const tableColumnsNames = {
4151
CPUCores: 'CPUCores',
4252
DataSize: 'DataSize',
4353
Path: 'Path',
54+
NodeId: 'NodeId',
55+
PeakTime: 'PeakTime',
56+
InFlightTxCount: 'InFlightTxCount',
4457
};
4558

4659
function prepareCPUWorkloadValue(value: string) {
4760
return `${(Number(value) * 100).toFixed(2)}%`;
4861
}
4962

50-
function prepareDateSizeValue(value: number) {
51-
return new Intl.NumberFormat(i18n.lang).format(value);
52-
}
53-
5463
function stringToDataTableSortOrder(value: string): SortOrder[] | undefined {
5564
return value
5665
? value.split(',').map((columnId) => ({
@@ -87,10 +96,24 @@ export const TopShards = ({tenantPath, type}: TopShardsProps) => {
8796
const {
8897
loading,
8998
data: {result: data = undefined} = {},
99+
filters: storeFilters,
90100
error,
91101
wasLoaded,
92102
} = useTypedSelector((state) => state.shardsWorkload);
93103

104+
// default date range should be the last hour, but shouldn't propagate into URL until user interacts with the control
105+
// redux initial value can't be used, as it synchronizes with URL
106+
const [filters, setFilters] = useState<IShardsWorkloadFilters>(() => {
107+
if (!storeFilters?.from && !storeFilters?.to) {
108+
return {
109+
from: Date.now() - HOUR_IN_SECONDS * 1000,
110+
to: Date.now(),
111+
};
112+
}
113+
114+
return storeFilters;
115+
});
116+
94117
const [sortOrder, setSortOrder] = useState(tableColumnsNames.CPUCores);
95118

96119
useAutofetcher(
@@ -100,10 +123,11 @@ export const TopShards = ({tenantPath, type}: TopShardsProps) => {
100123
database: tenantPath,
101124
path: currentSchemaPath,
102125
sortOrder: stringToQuerySortOrder(sortOrder),
126+
filters,
103127
}),
104128
);
105129
},
106-
[dispatch, currentSchemaPath, tenantPath, sortOrder],
130+
[dispatch, tenantPath, currentSchemaPath, sortOrder, filters],
107131
autorefresh,
108132
);
109133

@@ -115,7 +139,7 @@ export const TopShards = ({tenantPath, type}: TopShardsProps) => {
115139
data: undefined,
116140
}),
117141
);
118-
}, [dispatch, currentSchemaPath, tenantPath]);
142+
}, [dispatch, currentSchemaPath, tenantPath, filters]);
119143

120144
const history = useContext(HistoryContext);
121145

@@ -126,6 +150,11 @@ export const TopShards = ({tenantPath, type}: TopShardsProps) => {
126150
setSortOrder(dataTableToStringSortOrder(newSortOrder));
127151
};
128152

153+
const handleDateRangeChange = (value: DateRangeValues) => {
154+
dispatch(setTopShardFilters(value));
155+
setFilters(value);
156+
};
157+
129158
const tableColumns: Column<any>[] = useMemo(() => {
130159
const onSchemaClick = (schemaPath: string) => {
131160
return () => {
@@ -161,7 +190,7 @@ export const TopShards = ({tenantPath, type}: TopShardsProps) => {
161190
name: tableColumnsNames.DataSize,
162191
header: 'DataSize (B)',
163192
render: ({value}) => {
164-
return prepareDateSizeValue(value as number);
193+
return formatNumber(value as number);
165194
},
166195
align: DataTable.RIGHT,
167196
},
@@ -176,6 +205,29 @@ export const TopShards = ({tenantPath, type}: TopShardsProps) => {
176205
},
177206
sortable: false,
178207
},
208+
{
209+
name: tableColumnsNames.NodeId,
210+
render: ({value: nodeId}) => {
211+
return (
212+
<InternalLink to={getDefaultNodePath(nodeId as string)}>
213+
{nodeId as string}
214+
</InternalLink>
215+
);
216+
},
217+
align: DataTable.RIGHT,
218+
sortable: false,
219+
},
220+
{
221+
name: tableColumnsNames.PeakTime,
222+
render: ({value}) => formatDateTime(new Date(value as string).valueOf()),
223+
sortable: false,
224+
},
225+
{
226+
name: tableColumnsNames.InFlightTxCount,
227+
render: ({value}) => formatNumber(value as number),
228+
align: DataTable.RIGHT,
229+
sortable: false,
230+
},
179231
];
180232
}, [dispatch, history, tenantPath]);
181233

@@ -192,12 +244,12 @@ export const TopShards = ({tenantPath, type}: TopShardsProps) => {
192244
return renderLoader();
193245
}
194246

195-
if (!data || data.length === 0 || isColumnEntityType(type)) {
196-
return 'No data';
247+
if (error && !error.isCancelled) {
248+
return <div className="error">{prepareQueryError(error)}</div>;
197249
}
198250

199-
if (error && !error.isCancelled) {
200-
return prepareQueryError(error);
251+
if (!data || isColumnEntityType(type)) {
252+
return i18n('no-data');
201253
}
202254

203255
return (
@@ -216,6 +268,10 @@ export const TopShards = ({tenantPath, type}: TopShardsProps) => {
216268

217269
return (
218270
<div className={b()}>
271+
<div className={b('controls')}>
272+
{i18n('description')}
273+
<DateRange from={filters.from} to={filters.to} onChange={handleDateRangeChange} />
274+
</div>
219275
{renderContent()}
220276
</div>
221277
);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"no-data": "No data",
3+
"description": "Shards with CPU load over 70% are listed"
4+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {i18n, Lang} from '../../../../../utils/i18n';
2+
3+
import en from './en.json';
4+
import ru from './ru.json';
5+
6+
const COMPONENT = 'ydb-diagnostics-top-shards';
7+
8+
i18n.registerKeyset(Lang.En, COMPONENT, en);
9+
i18n.registerKeyset(Lang.Ru, COMPONENT, ru);
10+
11+
export default i18n.keyset(COMPONENT);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"no-data": "Нет данных",
3+
"description": "Отображаются шарды с загрузкой CPU выше 70%"
4+
}

0 commit comments

Comments
 (0)