ydb-embedded-ui 3.0.1 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +26 -0
- package/README.md +2 -0
- package/dist/components/DateRange/DateRange.scss +11 -0
- package/dist/components/DateRange/DateRange.tsx +75 -0
- package/dist/components/DateRange/index.ts +1 -0
- package/dist/components/Illustration/Illustration.tsx +4 -11
- package/dist/components/InfoViewer/InfoViewer.scss +2 -0
- package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.tsx +1 -1
- package/dist/containers/Storage/StorageNodes/StorageNodes.tsx +16 -0
- package/dist/containers/Tenant/Diagnostics/Diagnostics.tsx +4 -5
- package/dist/containers/Tenant/Diagnostics/DiagnosticsPages.ts +7 -7
- package/dist/containers/Tenant/Diagnostics/OverloadedShards/OverloadedShards.scss +27 -0
- package/dist/containers/Tenant/Diagnostics/{TopShards/TopShards.tsx → OverloadedShards/OverloadedShards.tsx} +75 -20
- package/dist/containers/Tenant/Diagnostics/OverloadedShards/i18n/en.json +4 -0
- package/dist/containers/Tenant/Diagnostics/OverloadedShards/i18n/index.ts +11 -0
- package/dist/containers/Tenant/Diagnostics/OverloadedShards/i18n/ru.json +4 -0
- package/dist/containers/Tenant/Diagnostics/OverloadedShards/index.ts +1 -0
- package/dist/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss +16 -19
- package/dist/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx +202 -0
- package/dist/containers/Tenant/Diagnostics/TopQueries/i18n/en.json +4 -0
- package/dist/containers/Tenant/Diagnostics/TopQueries/i18n/index.ts +11 -0
- package/dist/containers/Tenant/Diagnostics/TopQueries/i18n/ru.json +4 -0
- package/dist/containers/Tenant/Diagnostics/TopQueries/index.ts +1 -0
- package/dist/containers/UserSettings/UserSettings.tsx +1 -1
- package/dist/services/api.d.ts +7 -0
- package/dist/store/reducers/describe.ts +4 -1
- package/dist/store/reducers/executeTopQueries.ts +170 -0
- package/dist/store/reducers/settings.js +1 -1
- package/dist/store/reducers/shardsWorkload.ts +91 -25
- package/dist/store/reducers/storage.js +2 -0
- package/dist/store/reducers/{tablets.js → tablets.ts} +30 -17
- package/dist/store/state-url-mapping.js +16 -0
- package/dist/types/api/compute.ts +52 -0
- package/dist/types/api/consumer.ts +257 -0
- package/dist/types/api/enums.ts +2 -2
- package/dist/types/api/nodes.ts +5 -2
- package/dist/types/api/pdisk.ts +3 -0
- package/dist/types/api/schema.ts +1 -0
- package/dist/types/api/storage.ts +31 -28
- package/dist/types/api/tablet.ts +18 -2
- package/dist/types/api/tenant.ts +4 -1
- package/dist/types/api/topic.ts +157 -0
- package/dist/types/api/vdisk.ts +3 -0
- package/dist/types/store/executeTopQueries.ts +29 -0
- package/dist/types/store/schema.ts +3 -3
- package/dist/types/store/shardsWorkload.ts +11 -2
- package/dist/types/store/tablets.ts +42 -0
- package/dist/utils/getNodesColumns.js +8 -1
- package/dist/utils/query.ts +1 -1
- package/package.json +3 -3
- package/dist/containers/Tenant/Diagnostics/TopQueries/TopQueries.js +0 -188
- package/dist/containers/Tenant/Diagnostics/TopShards/TopShards.scss +0 -7
- package/dist/containers/Tenant/Diagnostics/TopShards/index.ts +0 -1
- package/dist/store/reducers/executeTopQueries.js +0 -66
- package/dist/types/api/consumers.ts +0 -3
@@ -0,0 +1,202 @@
|
|
1
|
+
import {useCallback, useEffect, useRef, useState} from 'react';
|
2
|
+
import {useDispatch} from 'react-redux';
|
3
|
+
import cn from 'bem-cn-lite';
|
4
|
+
|
5
|
+
import DataTable, {Column, Settings} from '@yandex-cloud/react-data-table';
|
6
|
+
import {Loader} from '@gravity-ui/uikit';
|
7
|
+
|
8
|
+
import {DateRange, DateRangeValues} from '../../../../components/DateRange';
|
9
|
+
import {Search} from '../../../../components/Search';
|
10
|
+
import TruncatedQuery from '../../../../components/TruncatedQuery/TruncatedQuery';
|
11
|
+
|
12
|
+
import {changeUserInput} from '../../../../store/reducers/executeQuery';
|
13
|
+
import {
|
14
|
+
fetchTopQueries,
|
15
|
+
setTopQueriesFilters,
|
16
|
+
setTopQueriesState,
|
17
|
+
} from '../../../../store/reducers/executeTopQueries';
|
18
|
+
|
19
|
+
import type {KeyValueRow} from '../../../../types/api/query';
|
20
|
+
import type {EPathType} from '../../../../types/api/schema';
|
21
|
+
import type {ITopQueriesFilters} from '../../../../types/store/executeTopQueries';
|
22
|
+
import type {IQueryResult} from '../../../../types/store/query';
|
23
|
+
|
24
|
+
import {DEFAULT_TABLE_SETTINGS, HOUR_IN_SECONDS} from '../../../../utils/constants';
|
25
|
+
import {useAutofetcher, useTypedSelector} from '../../../../utils/hooks';
|
26
|
+
import {prepareQueryError} from '../../../../utils/query';
|
27
|
+
|
28
|
+
import {isColumnEntityType} from '../../utils/schema';
|
29
|
+
import {TenantGeneralTabsIds} from '../../TenantPages';
|
30
|
+
|
31
|
+
import i18n from './i18n';
|
32
|
+
import './TopQueries.scss';
|
33
|
+
|
34
|
+
const b = cn('kv-top-queries');
|
35
|
+
|
36
|
+
const TABLE_SETTINGS: Settings = {
|
37
|
+
...DEFAULT_TABLE_SETTINGS,
|
38
|
+
dynamicRenderType: 'variable',
|
39
|
+
};
|
40
|
+
|
41
|
+
const MAX_QUERY_HEIGHT = 10;
|
42
|
+
const COLUMNS: Column<KeyValueRow>[] = [
|
43
|
+
{
|
44
|
+
name: 'CPUTimeUs',
|
45
|
+
width: 140,
|
46
|
+
sortAccessor: (row) => Number(row['CPUTimeUs']),
|
47
|
+
},
|
48
|
+
{
|
49
|
+
name: 'QueryText',
|
50
|
+
width: 500,
|
51
|
+
sortable: false,
|
52
|
+
render: ({value}) => <TruncatedQuery value={value} maxQueryHeight={MAX_QUERY_HEIGHT} />,
|
53
|
+
},
|
54
|
+
];
|
55
|
+
|
56
|
+
interface TopQueriesProps {
|
57
|
+
path: string;
|
58
|
+
changeSchemaTab: (tab: TenantGeneralTabsIds) => void;
|
59
|
+
type?: EPathType;
|
60
|
+
}
|
61
|
+
|
62
|
+
export const TopQueries = ({path, type, changeSchemaTab}: TopQueriesProps) => {
|
63
|
+
const dispatch = useDispatch();
|
64
|
+
|
65
|
+
const {autorefresh} = useTypedSelector((state) => state.schema);
|
66
|
+
|
67
|
+
const {
|
68
|
+
loading,
|
69
|
+
wasLoaded,
|
70
|
+
error,
|
71
|
+
data: {result: data = undefined} = {},
|
72
|
+
filters: storeFilters,
|
73
|
+
} = useTypedSelector((state) => state.executeTopQueries);
|
74
|
+
|
75
|
+
const preventFetch = useRef(false);
|
76
|
+
|
77
|
+
// some filters sync between redux state and URL
|
78
|
+
// component state is for default values,
|
79
|
+
// default values are determined from the query response, and should not propagate to URL
|
80
|
+
const [filters, setFilters] = useState<ITopQueriesFilters>(storeFilters);
|
81
|
+
|
82
|
+
useEffect(() => {
|
83
|
+
dispatch(setTopQueriesFilters(filters));
|
84
|
+
}, [dispatch, filters]);
|
85
|
+
|
86
|
+
const setDefaultFiltersFromResponse = (responseData?: IQueryResult) => {
|
87
|
+
const intervalEnd = responseData?.result?.[0]?.IntervalEnd;
|
88
|
+
|
89
|
+
if (intervalEnd) {
|
90
|
+
const to = new Date(intervalEnd).getTime();
|
91
|
+
const from = new Date(to - HOUR_IN_SECONDS * 1000).getTime();
|
92
|
+
|
93
|
+
setFilters((currentFilters) => {
|
94
|
+
// request without filters returns the latest interval with data
|
95
|
+
// only in this case should update filters in ui
|
96
|
+
// also don't update if user already interacted with controls
|
97
|
+
const shouldUpdateFilters = !currentFilters.from && !currentFilters.to;
|
98
|
+
|
99
|
+
if (!shouldUpdateFilters) {
|
100
|
+
return currentFilters;
|
101
|
+
}
|
102
|
+
|
103
|
+
preventFetch.current = true;
|
104
|
+
|
105
|
+
return {...currentFilters, from, to};
|
106
|
+
});
|
107
|
+
}
|
108
|
+
};
|
109
|
+
|
110
|
+
useAutofetcher(
|
111
|
+
(isBackground) => {
|
112
|
+
if (preventFetch.current) {
|
113
|
+
preventFetch.current = false;
|
114
|
+
return;
|
115
|
+
}
|
116
|
+
|
117
|
+
if (!isBackground) {
|
118
|
+
dispatch(
|
119
|
+
setTopQueriesState({
|
120
|
+
wasLoaded: false,
|
121
|
+
data: undefined,
|
122
|
+
}),
|
123
|
+
);
|
124
|
+
}
|
125
|
+
|
126
|
+
// @ts-expect-error
|
127
|
+
// typed dispatch required, remove error expectation after adding it
|
128
|
+
dispatch(fetchTopQueries({database: path, filters})).then(
|
129
|
+
setDefaultFiltersFromResponse,
|
130
|
+
);
|
131
|
+
},
|
132
|
+
[dispatch, filters, path],
|
133
|
+
autorefresh,
|
134
|
+
);
|
135
|
+
|
136
|
+
const handleRowClick = useCallback(
|
137
|
+
(row) => {
|
138
|
+
const {QueryText: input} = row;
|
139
|
+
|
140
|
+
dispatch(changeUserInput({input}));
|
141
|
+
changeSchemaTab(TenantGeneralTabsIds.query);
|
142
|
+
},
|
143
|
+
[changeSchemaTab, dispatch],
|
144
|
+
);
|
145
|
+
|
146
|
+
const handleTextSearchUpdate = (text: string) => {
|
147
|
+
setFilters((currentFilters) => ({...currentFilters, text}));
|
148
|
+
};
|
149
|
+
|
150
|
+
const handleDateRangeChange = (value: DateRangeValues) => {
|
151
|
+
setFilters((currentFilters) => ({...currentFilters, ...value}));
|
152
|
+
};
|
153
|
+
|
154
|
+
const renderLoader = () => {
|
155
|
+
return (
|
156
|
+
<div className={b('loader')}>
|
157
|
+
<Loader size="m" />
|
158
|
+
</div>
|
159
|
+
);
|
160
|
+
};
|
161
|
+
|
162
|
+
const renderContent = () => {
|
163
|
+
if (loading && !wasLoaded) {
|
164
|
+
return renderLoader();
|
165
|
+
}
|
166
|
+
|
167
|
+
if (error && !error.isCancelled) {
|
168
|
+
return <div className="error">{prepareQueryError(error)}</div>;
|
169
|
+
}
|
170
|
+
|
171
|
+
if (!data || isColumnEntityType(type)) {
|
172
|
+
return i18n('no-data');
|
173
|
+
}
|
174
|
+
|
175
|
+
return (
|
176
|
+
<div className={b('result')}>
|
177
|
+
<DataTable
|
178
|
+
columns={COLUMNS}
|
179
|
+
data={data}
|
180
|
+
settings={TABLE_SETTINGS}
|
181
|
+
onRowClick={handleRowClick}
|
182
|
+
theme="yandex-cloud"
|
183
|
+
/>
|
184
|
+
</div>
|
185
|
+
);
|
186
|
+
};
|
187
|
+
|
188
|
+
return (
|
189
|
+
<div className={b()}>
|
190
|
+
<div className={b('controls')}>
|
191
|
+
<Search
|
192
|
+
value={filters.text}
|
193
|
+
onChange={handleTextSearchUpdate}
|
194
|
+
placeholder={i18n('filter.text.placeholder')}
|
195
|
+
className={b('search')}
|
196
|
+
/>
|
197
|
+
<DateRange from={filters.from} to={filters.to} onChange={handleDateRangeChange} />
|
198
|
+
</div>
|
199
|
+
{renderContent()}
|
200
|
+
</div>
|
201
|
+
);
|
202
|
+
};
|
@@ -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-queries';
|
7
|
+
|
8
|
+
i18n.registerKeyset(Lang.En, COMPONENT, en);
|
9
|
+
i18n.registerKeyset(Lang.Ru, COMPONENT, ru);
|
10
|
+
|
11
|
+
export default i18n.keyset(COMPONENT);
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './TopQueries';
|
package/dist/services/api.d.ts
CHANGED
@@ -48,6 +48,13 @@ interface Window {
|
|
48
48
|
getTenantInfo: (params: {
|
49
49
|
path: string;
|
50
50
|
}) => Promise<import('../types/api/tenant').TTenantInfo>;
|
51
|
+
getTabletsInfo: (params: {
|
52
|
+
nodes?: string[];
|
53
|
+
path?: string;
|
54
|
+
}) => Promise<import('../types/api/tablet').TEvTabletStateResponse>;
|
55
|
+
getHeatmapData: (params: {
|
56
|
+
path: string;
|
57
|
+
}) => Promise<import('../types/api/schema').TEvDescribeSchemeResult>;
|
51
58
|
[method: string]: Function;
|
52
59
|
};
|
53
60
|
}
|
@@ -2,7 +2,6 @@ import {createSelector, Selector} from 'reselect';
|
|
2
2
|
import {Reducer} from 'redux';
|
3
3
|
|
4
4
|
import '../../services/api';
|
5
|
-
import {IConsumer} from '../../types/api/consumers';
|
6
5
|
import {
|
7
6
|
IDescribeRootStateSlice,
|
8
7
|
IDescribeState,
|
@@ -100,6 +99,10 @@ const selectConsumersNames = (state: IDescribeRootStateSlice, path?: string) =>
|
|
100
99
|
? state.describe.data[path]?.PathDescription?.PersQueueGroup?.PQTabletConfig?.ReadRules
|
101
100
|
: undefined;
|
102
101
|
|
102
|
+
interface IConsumer {
|
103
|
+
name: string;
|
104
|
+
}
|
105
|
+
|
103
106
|
export const selectConsumers: Selector<IDescribeRootStateSlice, IConsumer[], [string | undefined]> =
|
104
107
|
createSelector(selectConsumersNames, (names = []) => names.map((name) => ({name})));
|
105
108
|
|
@@ -0,0 +1,170 @@
|
|
1
|
+
import type {AnyAction, Reducer} from 'redux';
|
2
|
+
import type {ThunkAction} from 'redux-thunk';
|
3
|
+
|
4
|
+
import '../../services/api';
|
5
|
+
import {
|
6
|
+
ITopQueriesAction,
|
7
|
+
ITopQueriesFilters,
|
8
|
+
ITopQueriesState,
|
9
|
+
} from '../../types/store/executeTopQueries';
|
10
|
+
import {IQueryResult} from '../../types/store/query';
|
11
|
+
|
12
|
+
import {parseQueryAPIExecuteResponse} from '../../utils/query';
|
13
|
+
|
14
|
+
import {createRequestActionTypes, createApiRequest} from '../utils';
|
15
|
+
|
16
|
+
import type {IRootState} from '.';
|
17
|
+
|
18
|
+
export const FETCH_TOP_QUERIES = createRequestActionTypes('top-queries', 'FETCH_TOP_QUERIES');
|
19
|
+
const SET_TOP_QUERIES_STATE = 'top-queries/SET_TOP_QUERIES_STATE';
|
20
|
+
const SET_TOP_QUERIES_FILTERS = 'top-queries/SET_TOP_QUERIES_FILTERS';
|
21
|
+
|
22
|
+
const initialState = {
|
23
|
+
loading: false,
|
24
|
+
wasLoaded: false,
|
25
|
+
filters: {},
|
26
|
+
};
|
27
|
+
|
28
|
+
const getMaxIntervalSubquery = (path: string) => `(
|
29
|
+
SELECT
|
30
|
+
MAX(IntervalEnd)
|
31
|
+
FROM \`${path}/.sys/top_queries_by_cpu_time_one_hour\`
|
32
|
+
)`;
|
33
|
+
|
34
|
+
function getFiltersConditions(path: string, filters?: ITopQueriesFilters) {
|
35
|
+
const conditions: string[] = [];
|
36
|
+
|
37
|
+
if (filters?.from && filters?.to && filters.from > filters.to) {
|
38
|
+
throw new Error('Invalid date range');
|
39
|
+
}
|
40
|
+
|
41
|
+
if (filters?.from) {
|
42
|
+
// matching `from` & `to` is an edge case
|
43
|
+
// other cases should not include the starting point, since intervals are stored using the ending time
|
44
|
+
const gt = filters.to === filters.from ? '>=' : '>';
|
45
|
+
conditions.push(`IntervalEnd ${gt} Timestamp('${new Date(filters.from).toISOString()}')`);
|
46
|
+
}
|
47
|
+
|
48
|
+
if (filters?.to) {
|
49
|
+
conditions.push(`IntervalEnd <= Timestamp('${new Date(filters.to).toISOString()}')`);
|
50
|
+
}
|
51
|
+
|
52
|
+
if (!filters?.from && !filters?.to) {
|
53
|
+
conditions.push(`IntervalEnd IN ${getMaxIntervalSubquery(path)}`);
|
54
|
+
}
|
55
|
+
|
56
|
+
if (filters?.text) {
|
57
|
+
conditions.push(`QueryText ILIKE '%${filters.text}%'`);
|
58
|
+
}
|
59
|
+
|
60
|
+
return conditions.join(' AND ');
|
61
|
+
}
|
62
|
+
|
63
|
+
const getQueryText = (path: string, filters?: ITopQueriesFilters) => {
|
64
|
+
const filterConditions = getFiltersConditions(path, filters);
|
65
|
+
return `
|
66
|
+
SELECT
|
67
|
+
CPUTime as CPUTimeUs,
|
68
|
+
QueryText,
|
69
|
+
IntervalEnd
|
70
|
+
FROM \`${path}/.sys/top_queries_by_cpu_time_one_hour\`
|
71
|
+
WHERE ${filterConditions || 'true'}
|
72
|
+
`;
|
73
|
+
};
|
74
|
+
|
75
|
+
const executeTopQueries: Reducer<ITopQueriesState, ITopQueriesAction> = (
|
76
|
+
state = initialState,
|
77
|
+
action,
|
78
|
+
) => {
|
79
|
+
switch (action.type) {
|
80
|
+
case FETCH_TOP_QUERIES.REQUEST: {
|
81
|
+
return {
|
82
|
+
...state,
|
83
|
+
loading: true,
|
84
|
+
error: undefined,
|
85
|
+
};
|
86
|
+
}
|
87
|
+
case FETCH_TOP_QUERIES.SUCCESS: {
|
88
|
+
return {
|
89
|
+
...state,
|
90
|
+
data: action.data,
|
91
|
+
loading: false,
|
92
|
+
error: undefined,
|
93
|
+
wasLoaded: true,
|
94
|
+
};
|
95
|
+
}
|
96
|
+
// 401 Unauthorized error is handled by GenericAPI
|
97
|
+
case FETCH_TOP_QUERIES.FAILURE: {
|
98
|
+
return {
|
99
|
+
...state,
|
100
|
+
error: action.error || 'Unauthorized',
|
101
|
+
loading: false,
|
102
|
+
};
|
103
|
+
}
|
104
|
+
case SET_TOP_QUERIES_STATE:
|
105
|
+
return {
|
106
|
+
...state,
|
107
|
+
...action.data,
|
108
|
+
};
|
109
|
+
case SET_TOP_QUERIES_FILTERS:
|
110
|
+
return {
|
111
|
+
...state,
|
112
|
+
filters: {
|
113
|
+
...state.filters,
|
114
|
+
...action.filters,
|
115
|
+
},
|
116
|
+
};
|
117
|
+
default:
|
118
|
+
return state;
|
119
|
+
}
|
120
|
+
};
|
121
|
+
|
122
|
+
type FetchTopQueries = (params: {
|
123
|
+
database: string;
|
124
|
+
filters?: ITopQueriesFilters;
|
125
|
+
}) => ThunkAction<Promise<IQueryResult | undefined>, IRootState, unknown, AnyAction>;
|
126
|
+
|
127
|
+
export const fetchTopQueries: FetchTopQueries =
|
128
|
+
({database, filters}) =>
|
129
|
+
async (dispatch, getState) => {
|
130
|
+
try {
|
131
|
+
return createApiRequest({
|
132
|
+
request: window.api.sendQuery(
|
133
|
+
{
|
134
|
+
schema: 'modern',
|
135
|
+
query: getQueryText(database, filters),
|
136
|
+
database,
|
137
|
+
action: 'execute-scan',
|
138
|
+
},
|
139
|
+
{
|
140
|
+
concurrentId: 'executeTopQueries',
|
141
|
+
},
|
142
|
+
),
|
143
|
+
actions: FETCH_TOP_QUERIES,
|
144
|
+
dataHandler: parseQueryAPIExecuteResponse,
|
145
|
+
})(dispatch, getState);
|
146
|
+
} catch (error) {
|
147
|
+
dispatch({
|
148
|
+
type: FETCH_TOP_QUERIES.FAILURE,
|
149
|
+
error,
|
150
|
+
});
|
151
|
+
|
152
|
+
throw error;
|
153
|
+
}
|
154
|
+
};
|
155
|
+
|
156
|
+
export function setTopQueriesState(state: Partial<ITopQueriesState>) {
|
157
|
+
return {
|
158
|
+
type: SET_TOP_QUERIES_STATE,
|
159
|
+
data: state,
|
160
|
+
} as const;
|
161
|
+
}
|
162
|
+
|
163
|
+
export function setTopQueriesFilters(filters: Partial<ITopQueriesFilters>) {
|
164
|
+
return {
|
165
|
+
type: SET_TOP_QUERIES_FILTERS,
|
166
|
+
filters,
|
167
|
+
} as const;
|
168
|
+
}
|
169
|
+
|
170
|
+
export default executeTopQueries;
|
@@ -30,7 +30,7 @@ export const initialState = {
|
|
30
30
|
...defaultUserSettings,
|
31
31
|
...userSettings,
|
32
32
|
[THEME_KEY]: readSavedSettingsValue(THEME_KEY, 'light'),
|
33
|
-
[INVERTED_DISKS_KEY]: readSavedSettingsValue(INVERTED_DISKS_KEY
|
33
|
+
[INVERTED_DISKS_KEY]: readSavedSettingsValue(INVERTED_DISKS_KEY, 'false'),
|
34
34
|
[SAVED_QUERIES_KEY]: readSavedSettingsValue(SAVED_QUERIES_KEY, '[]'),
|
35
35
|
[TENANT_INITIAL_TAB_KEY]: readSavedSettingsValue(TENANT_INITIAL_TAB_KEY),
|
36
36
|
[QUERY_INITIAL_RUN_ACTION_KEY]: readSavedSettingsValue(QUERY_INITIAL_RUN_ACTION_KEY),
|
@@ -1,18 +1,24 @@
|
|
1
1
|
import type {Reducer} from 'redux';
|
2
2
|
|
3
3
|
import '../../services/api';
|
4
|
-
import type {
|
4
|
+
import type {
|
5
|
+
IShardsWorkloadAction,
|
6
|
+
IShardsWorkloadFilters,
|
7
|
+
IShardsWorkloadState,
|
8
|
+
} from '../../types/store/shardsWorkload';
|
5
9
|
|
6
10
|
import {parseQueryAPIExecuteResponse} from '../../utils/query';
|
7
11
|
|
8
12
|
import {createRequestActionTypes, createApiRequest} from '../utils';
|
9
13
|
|
10
14
|
export const SEND_SHARD_QUERY = createRequestActionTypes('query', 'SEND_SHARD_QUERY');
|
11
|
-
const
|
15
|
+
const SET_SHARD_STATE = 'query/SET_SHARD_STATE';
|
16
|
+
const SET_SHARD_QUERY_FILTERS = 'shardsWorkload/SET_SHARD_QUERY_FILTERS';
|
12
17
|
|
13
18
|
const initialState = {
|
14
19
|
loading: false,
|
15
20
|
wasLoaded: false,
|
21
|
+
filters: {},
|
16
22
|
};
|
17
23
|
|
18
24
|
export interface SortOrder {
|
@@ -24,22 +30,56 @@ function formatSortOrder({columnId, order}: SortOrder) {
|
|
24
30
|
return `${columnId} ${order}`;
|
25
31
|
}
|
26
32
|
|
27
|
-
function
|
28
|
-
const
|
33
|
+
function getFiltersConditions(filters?: IShardsWorkloadFilters) {
|
34
|
+
const conditions: string[] = [];
|
35
|
+
|
36
|
+
if (filters?.from && filters?.to && filters.from > filters.to) {
|
37
|
+
throw new Error('Invalid date range');
|
38
|
+
}
|
29
39
|
|
40
|
+
if (filters?.from) {
|
41
|
+
// matching `from` & `to` is an edge case
|
42
|
+
// other cases should not include the starting point, since intervals are stored using the ending time
|
43
|
+
const gt = filters.to === filters.from ? '>=' : '>';
|
44
|
+
conditions.push(`IntervalEnd ${gt} Timestamp('${new Date(filters.from).toISOString()}')`);
|
45
|
+
}
|
46
|
+
|
47
|
+
if (filters?.to) {
|
48
|
+
conditions.push(`IntervalEnd <= Timestamp('${new Date(filters.to).toISOString()}')`);
|
49
|
+
}
|
50
|
+
|
51
|
+
return conditions.join(' AND ');
|
52
|
+
}
|
53
|
+
|
54
|
+
function createShardQuery(
|
55
|
+
path: string,
|
56
|
+
filters?: IShardsWorkloadFilters,
|
57
|
+
sortOrder?: SortOrder[],
|
58
|
+
tenantName?: string,
|
59
|
+
) {
|
30
60
|
const pathSelect = tenantName
|
31
61
|
? `CAST(SUBSTRING(CAST(Path AS String), ${tenantName.length}) AS Utf8) AS Path`
|
32
62
|
: 'Path';
|
33
63
|
|
64
|
+
let where = `Path='${path}' OR Path LIKE '${path}/%'`;
|
65
|
+
|
66
|
+
const filterConditions = getFiltersConditions(filters);
|
67
|
+
if (filterConditions.length) {
|
68
|
+
where = `(${where}) AND ${filterConditions}`;
|
69
|
+
}
|
70
|
+
|
71
|
+
const orderBy = sortOrder ? `ORDER BY ${sortOrder.map(formatSortOrder).join(', ')}` : '';
|
72
|
+
|
34
73
|
return `SELECT
|
35
74
|
${pathSelect},
|
36
75
|
TabletId,
|
37
76
|
CPUCores,
|
38
|
-
DataSize
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
77
|
+
DataSize,
|
78
|
+
NodeId,
|
79
|
+
PeakTime,
|
80
|
+
InFlightTxCount
|
81
|
+
FROM \`.sys/top_partitions_one_hour\`
|
82
|
+
WHERE ${where}
|
43
83
|
${orderBy}
|
44
84
|
LIMIT 20`;
|
45
85
|
}
|
@@ -75,11 +115,19 @@ const shardsWorkload: Reducer<IShardsWorkloadState, IShardsWorkloadAction> = (
|
|
75
115
|
loading: false,
|
76
116
|
};
|
77
117
|
}
|
78
|
-
case
|
118
|
+
case SET_SHARD_STATE:
|
79
119
|
return {
|
80
120
|
...state,
|
81
121
|
...action.data,
|
82
122
|
};
|
123
|
+
case SET_SHARD_QUERY_FILTERS:
|
124
|
+
return {
|
125
|
+
...state,
|
126
|
+
filters: {
|
127
|
+
...state.filters,
|
128
|
+
...action.filters,
|
129
|
+
},
|
130
|
+
};
|
83
131
|
default:
|
84
132
|
return state;
|
85
133
|
}
|
@@ -89,28 +137,46 @@ interface SendShardQueryParams {
|
|
89
137
|
database?: string;
|
90
138
|
path?: string;
|
91
139
|
sortOrder?: SortOrder[];
|
140
|
+
filters?: IShardsWorkloadFilters;
|
92
141
|
}
|
93
142
|
|
94
|
-
export const sendShardQuery = ({database, path = '', sortOrder}: SendShardQueryParams) => {
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
143
|
+
export const sendShardQuery = ({database, path = '', sortOrder, filters}: SendShardQueryParams) => {
|
144
|
+
try {
|
145
|
+
return createApiRequest({
|
146
|
+
request: window.api.sendQuery(
|
147
|
+
{
|
148
|
+
schema: 'modern',
|
149
|
+
query: createShardQuery(path, filters, sortOrder, database),
|
150
|
+
database,
|
151
|
+
action: queryAction,
|
152
|
+
},
|
153
|
+
{
|
154
|
+
concurrentId: 'shardsWorkload',
|
155
|
+
},
|
156
|
+
),
|
157
|
+
actions: SEND_SHARD_QUERY,
|
158
|
+
dataHandler: parseQueryAPIExecuteResponse,
|
159
|
+
});
|
160
|
+
} catch (error) {
|
161
|
+
return {
|
162
|
+
type: SEND_SHARD_QUERY.FAILURE,
|
163
|
+
error,
|
164
|
+
};
|
165
|
+
}
|
107
166
|
};
|
108
167
|
|
109
|
-
export function
|
168
|
+
export function setShardsState(options: Partial<IShardsWorkloadState>) {
|
110
169
|
return {
|
111
|
-
type:
|
170
|
+
type: SET_SHARD_STATE,
|
112
171
|
data: options,
|
113
172
|
} as const;
|
114
173
|
}
|
115
174
|
|
175
|
+
export function setShardsQueryFilters(filters: Partial<IShardsWorkloadFilters>) {
|
176
|
+
return {
|
177
|
+
type: SET_SHARD_QUERY_FILTERS,
|
178
|
+
filters,
|
179
|
+
} as const;
|
180
|
+
}
|
181
|
+
|
116
182
|
export default shardsWorkload;
|
@@ -307,6 +307,8 @@ export const getFlatListStorageNodes = createSelector([getStorageNodes], (storag
|
|
307
307
|
return {
|
308
308
|
NodeId: node.NodeId,
|
309
309
|
FQDN: systemState.Host,
|
310
|
+
DataCenter: systemState.DataCenter,
|
311
|
+
Rack: systemState.Rack,
|
310
312
|
uptime: calcUptime(systemState.StartTime),
|
311
313
|
StartTime: systemState.StartTime,
|
312
314
|
PDisks: node.PDisks,
|