ydb-embedded-ui 3.0.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,
|