ReactMigration: Migrate Loki and Elastic config pages to React (#19979)

This commit is contained in:
Andrej Ocenas
2019-10-25 16:43:20 +02:00
committed by GitHub
parent 771f21ed09
commit 551e24f9f8
26 changed files with 742 additions and 215 deletions

View File

@@ -1,54 +0,0 @@
import _ from 'lodash';
import { ElasticsearchOptions } from './types';
import { DataSourceInstanceSettings } from '@grafana/ui';
import { getMaxConcurrenShardRequestOrDefault } from './datasource';
export class ElasticConfigCtrl {
static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/config.html';
current: DataSourceInstanceSettings<ElasticsearchOptions>;
/** @ngInject */
constructor($scope: any) {
this.current.jsonData.timeField = this.current.jsonData.timeField || '@timestamp';
this.current.jsonData.esVersion = this.current.jsonData.esVersion || 5;
const defaultMaxConcurrentShardRequests = this.current.jsonData.esVersion >= 70 ? 5 : 256;
this.current.jsonData.maxConcurrentShardRequests =
this.current.jsonData.maxConcurrentShardRequests || defaultMaxConcurrentShardRequests;
this.current.jsonData.logMessageField = this.current.jsonData.logMessageField || '';
this.current.jsonData.logLevelField = this.current.jsonData.logLevelField || '';
}
indexPatternTypes: any = [
{ name: 'No pattern', value: undefined },
{ name: 'Hourly', value: 'Hourly', example: '[logstash-]YYYY.MM.DD.HH' },
{ name: 'Daily', value: 'Daily', example: '[logstash-]YYYY.MM.DD' },
{ name: 'Weekly', value: 'Weekly', example: '[logstash-]GGGG.WW' },
{ name: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM' },
{ name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY' },
];
esVersions = [
{ name: '2.x', value: 2 },
{ name: '5.x', value: 5 },
{ name: '5.6+', value: 56 },
{ name: '6.0+', value: 60 },
{ name: '7.0+', value: 70 },
];
indexPatternTypeChanged() {
if (
!this.current.database ||
this.current.database.length === 0 ||
this.current.database.startsWith('[logstash-]')
) {
const def: any = _.find(this.indexPatternTypes, {
value: this.current.jsonData.interval,
});
this.current.database = def.example || 'es-index-name';
}
}
versionChanged() {
this.current.jsonData.maxConcurrentShardRequests = getMaxConcurrenShardRequestOrDefault(this.current.jsonData);
}
}

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { ConfigEditor } from './ConfigEditor';
import { DataSourceHttpSettings } from '@grafana/ui';
import { ElasticDetails } from './ElasticDetails';
import { LogsConfig } from './LogsConfig';
import { createDefaultConfigOptions } from './mocks';
describe('ConfigEditor', () => {
it('should render without error', () => {
mount(<ConfigEditor onOptionsChange={() => {}} options={createDefaultConfigOptions()} />);
});
it('should render all parts of the config', () => {
const wrapper = shallow(<ConfigEditor onOptionsChange={() => {}} options={createDefaultConfigOptions()} />);
expect(wrapper.find(DataSourceHttpSettings).length).toBe(1);
expect(wrapper.find(ElasticDetails).length).toBe(1);
expect(wrapper.find(LogsConfig).length).toBe(1);
});
it('should set defaults', () => {
const options = createDefaultConfigOptions();
delete options.jsonData.esVersion;
delete options.jsonData.timeField;
delete options.jsonData.maxConcurrentShardRequests;
expect.assertions(3);
mount(
<ConfigEditor
onOptionsChange={options => {
expect(options.jsonData.esVersion).toBe(5);
expect(options.jsonData.timeField).toBe('@timestamp');
expect(options.jsonData.maxConcurrentShardRequests).toBe(256);
}}
options={options}
/>
);
});
it('should not apply default if values are set', () => {
expect.assertions(3);
mount(
<ConfigEditor
onOptionsChange={options => {
expect(options.jsonData.esVersion).toBe(70);
expect(options.jsonData.timeField).toBe('@time');
expect(options.jsonData.maxConcurrentShardRequests).toBe(300);
}}
options={createDefaultConfigOptions()}
/>
);
});
});

View File

@@ -0,0 +1,50 @@
import React, { useEffect } from 'react';
import { DataSourceHttpSettings, DataSourcePluginOptionsEditorProps } from '@grafana/ui';
import { ElasticsearchOptions } from '../types';
import { defaultMaxConcurrentShardRequests, ElasticDetails } from './ElasticDetails';
import { LogsConfig } from './LogsConfig';
export type Props = DataSourcePluginOptionsEditorProps<ElasticsearchOptions>;
export const ConfigEditor = (props: Props) => {
const { options, onOptionsChange } = props;
// Apply some defaults on initial render
useEffect(() => {
const esVersion = options.jsonData.esVersion || 5;
onOptionsChange({
...options,
jsonData: {
...options.jsonData,
timeField: options.jsonData.timeField || '@timestamp',
esVersion,
maxConcurrentShardRequests:
options.jsonData.maxConcurrentShardRequests || defaultMaxConcurrentShardRequests(esVersion),
logMessageField: options.jsonData.logMessageField || '',
logLevelField: options.jsonData.logLevelField || '',
},
});
}, []);
return (
<>
<DataSourceHttpSettings
defaultUrl={'http://localhost:3100'}
dataSourceConfig={options}
showAccessOptions={true}
onChange={onOptionsChange}
/>
<ElasticDetails value={options} onChange={onOptionsChange} />
<LogsConfig
value={options.jsonData}
onChange={newValue =>
onOptionsChange({
...options,
jsonData: newValue,
})
}
/>
</>
);
};

View File

@@ -0,0 +1,88 @@
import React from 'react';
import { last } from 'lodash';
import { mount } from 'enzyme';
import { ElasticDetails } from './ElasticDetails';
import { createDefaultConfigOptions } from './mocks';
import { Select } from '@grafana/ui';
describe('ElasticDetails', () => {
it('should render without error', () => {
mount(<ElasticDetails onChange={() => {}} value={createDefaultConfigOptions()} />);
});
it('should render "Max concurrent Shard Requests" if version high enough', () => {
const wrapper = mount(<ElasticDetails onChange={() => {}} value={createDefaultConfigOptions()} />);
expect(wrapper.find('input[aria-label="Max concurrent Shard Requests input"]').length).toBe(1);
});
it('should not render "Max concurrent Shard Requests" if version is low', () => {
const options = createDefaultConfigOptions();
options.jsonData.esVersion = 5;
const wrapper = mount(<ElasticDetails onChange={() => {}} value={options} />);
expect(wrapper.find('input[aria-label="Max concurrent Shard Requests input"]').length).toBe(0);
});
it('should change database on interval change when not set explicitly', () => {
const onChangeMock = jest.fn();
const wrapper = mount(<ElasticDetails onChange={onChangeMock} value={createDefaultConfigOptions()} />);
const selectEl = wrapper.find({ label: 'Pattern' }).find(Select);
selectEl.props().onChange({ value: 'Daily', label: 'Daily' });
expect(onChangeMock.mock.calls[0][0].jsonData.interval).toBe('Daily');
expect(onChangeMock.mock.calls[0][0].database).toBe('[logstash-]YYYY.MM.DD');
});
it('should change database on interval change if pattern is from example', () => {
const onChangeMock = jest.fn();
const options = createDefaultConfigOptions();
options.database = '[logstash-]YYYY.MM.DD.HH';
const wrapper = mount(<ElasticDetails onChange={onChangeMock} value={options} />);
const selectEl = wrapper.find({ label: 'Pattern' }).find(Select);
selectEl.props().onChange({ value: 'Monthly', label: 'Monthly' });
expect(onChangeMock.mock.calls[0][0].jsonData.interval).toBe('Monthly');
expect(onChangeMock.mock.calls[0][0].database).toBe('[logstash-]YYYY.MM');
});
describe('version change', () => {
const testCases = [
{ version: 50, expectedMaxConcurrentShardRequests: 256 },
{ version: 50, maxConcurrentShardRequests: 50, expectedMaxConcurrentShardRequests: 50 },
{ version: 56, expectedMaxConcurrentShardRequests: 256 },
{ version: 56, maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 256 },
{ version: 56, maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 256 },
{ version: 56, maxConcurrentShardRequests: 200, expectedMaxConcurrentShardRequests: 200 },
{ version: 70, expectedMaxConcurrentShardRequests: 5 },
{ version: 70, maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 5 },
{ version: 70, maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 5 },
{ version: 70, maxConcurrentShardRequests: 6, expectedMaxConcurrentShardRequests: 6 },
];
const onChangeMock = jest.fn();
const options = createDefaultConfigOptions();
const wrapper = mount(<ElasticDetails onChange={onChangeMock} value={options} />);
testCases.forEach(tc => {
it(`sets maxConcurrentShardRequests = ${tc.maxConcurrentShardRequests} if version = ${tc.version},`, () => {
wrapper.setProps({
onChange: onChangeMock,
value: {
...options,
jsonData: {
...options.jsonData,
maxConcurrentShardRequests: tc.maxConcurrentShardRequests,
},
},
});
const selectEl = wrapper.find({ label: 'Version' }).find(Select);
selectEl.props().onChange({ value: tc.version, label: tc.version.toString() });
expect(last(onChangeMock.mock.calls)[0].jsonData.maxConcurrentShardRequests).toBe(
tc.expectedMaxConcurrentShardRequests
);
});
});
});
});

View File

@@ -0,0 +1,221 @@
import React from 'react';
import { DataSourceSettings, EventsWithValidation, FormField, Input, regexValidation, Select } from '@grafana/ui';
import { ElasticsearchOptions } from '../types';
import { SelectableValue } from '@grafana/data';
const indexPatternTypes = [
{ label: 'No pattern', value: 'none' },
{ label: 'Hourly', value: 'Hourly', example: '[logstash-]YYYY.MM.DD.HH' },
{ label: 'Daily', value: 'Daily', example: '[logstash-]YYYY.MM.DD' },
{ label: 'Weekly', value: 'Weekly', example: '[logstash-]GGGG.WW' },
{ label: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM' },
{ label: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY' },
];
const esVersions = [
{ label: '2.x', value: 2 },
{ label: '5.x', value: 5 },
{ label: '5.6+', value: 56 },
{ label: '6.0+', value: 60 },
{ label: '7.0+', value: 70 },
];
type Props = {
value: DataSourceSettings<ElasticsearchOptions>;
onChange: (value: DataSourceSettings<ElasticsearchOptions>) => void;
};
export const ElasticDetails = (props: Props) => {
const { value, onChange } = props;
return (
<>
<h3 className="page-heading">Elasticsearch details</h3>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form max-width-25">
<FormField
labelWidth={10}
inputWidth={15}
label="Index name"
value={value.database || ''}
onChange={changeHandler('database', value, onChange)}
placeholder={'es-index-name'}
required
/>
</div>
<div className="gf-form width-14">
<FormField
labelWidth={10}
label="Pattern"
inputEl={
<Select
options={indexPatternTypes}
onChange={intervalHandler(value, onChange)}
value={indexPatternTypes.find(
pattern =>
pattern.value === (value.jsonData.interval === undefined ? 'none' : value.jsonData.interval)
)}
/>
}
/>
</div>
</div>
<div className="gf-form max-width-25">
<FormField
labelWidth={10}
inputWidth={15}
label="Time field name"
value={value.jsonData.timeField || ''}
onChange={jsonDataChangeHandler('timeField', value, onChange)}
required
/>
</div>
<div className="gf-form">
<span className="gf-form-select-wrapper">
<FormField
labelWidth={10}
label="Version"
inputEl={
<Select
options={esVersions}
onChange={option => {
const maxConcurrentShardRequests = getMaxConcurrenShardRequestOrDefault(
value.jsonData.maxConcurrentShardRequests,
option.value
);
onChange({
...value,
jsonData: {
...value.jsonData,
esVersion: option.value,
maxConcurrentShardRequests,
},
});
}}
value={esVersions.find(version => version.value === value.jsonData.esVersion)}
/>
}
/>
</span>
</div>
{value.jsonData.esVersion >= 56 && (
<div className="gf-form max-width-30">
<FormField
aria-label={'Max concurrent Shard Requests input'}
labelWidth={15}
label="Max concurrent Shard Requests"
value={value.jsonData.maxConcurrentShardRequests || ''}
onChange={jsonDataChangeHandler('maxConcurrentShardRequests', value, onChange)}
/>
</div>
)}
<div className="gf-form-inline">
<div className="gf-form">
<FormField
labelWidth={10}
label="Min time interval"
inputEl={
<Input
className={'width-6'}
value={value.jsonData.timeInterval || ''}
onChange={jsonDataChangeHandler('timeInterval', value, onChange)}
placeholder="10s"
validationEvents={{
[EventsWithValidation.onBlur]: [
regexValidation(
/^\d+(ms|[Mwdhmsy])$/,
'Value is not valid, you can use number with time unit specifier: y, M, w, d, h, m, s'
),
],
}}
/>
}
tooltip={
<>
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for
example <code>1m</code> if your data is written every minute.
</>
}
/>
</div>
</div>
</div>
</>
);
};
const changeHandler = (
key: keyof DataSourceSettings<ElasticsearchOptions>,
value: Props['value'],
onChange: Props['onChange']
) => (event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>) => {
onChange({
...value,
[key]: event.currentTarget.value,
});
};
const jsonDataChangeHandler = (key: keyof ElasticsearchOptions, value: Props['value'], onChange: Props['onChange']) => (
event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>
) => {
onChange({
...value,
jsonData: {
...value.jsonData,
[key]: event.currentTarget.value,
},
});
};
const intervalHandler = (value: Props['value'], onChange: Props['onChange']) => (option: SelectableValue<string>) => {
const { database } = value;
// If option value is undefined it will send its label instead so we have to convert made up value to undefined here.
const newInterval = option.value === 'none' ? undefined : option.value;
if (!database || database.length === 0 || database.startsWith('[logstash-]')) {
let newDatabase = '';
if (newInterval !== undefined) {
const pattern = indexPatternTypes.find(pattern => pattern.value === newInterval);
if (pattern) {
newDatabase = pattern.example;
}
}
onChange({
...value,
database: newDatabase,
jsonData: {
...value.jsonData,
interval: newInterval,
},
});
} else {
onChange({
...value,
jsonData: {
...value.jsonData,
interval: newInterval,
},
});
}
};
function getMaxConcurrenShardRequestOrDefault(maxConcurrentShardRequests: number, version: number): number {
if (maxConcurrentShardRequests === 5 && version < 70) {
return 256;
}
if (maxConcurrentShardRequests === 256 && version >= 70) {
return 5;
}
return maxConcurrentShardRequests || defaultMaxConcurrentShardRequests(version);
}
export function defaultMaxConcurrentShardRequests(version: number) {
return version >= 70 ? 5 : 256;
}

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { LogsConfig } from './LogsConfig';
import { createDefaultConfigOptions } from './mocks';
import { FormField } from '@grafana/ui';
describe('ElasticDetails', () => {
it('should render without error', () => {
mount(<LogsConfig onChange={() => {}} value={createDefaultConfigOptions().jsonData} />);
});
it('should render fields', () => {
const wrapper = shallow(<LogsConfig onChange={() => {}} value={createDefaultConfigOptions().jsonData} />);
expect(wrapper.find(FormField).length).toBe(2);
});
it('should pass correct data to onChange', () => {
const onChangeMock = jest.fn();
const wrapper = mount(<LogsConfig onChange={onChangeMock} value={createDefaultConfigOptions().jsonData} />);
const inputEl = wrapper
.find(FormField)
.at(0)
.find('input');
(inputEl.getDOMNode() as any).value = 'test_field';
inputEl.simulate('change');
expect(onChangeMock.mock.calls[0][0].logMessageField).toBe('test_field');
});
});

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { FormField } from '@grafana/ui';
import { ElasticsearchOptions } from '../types';
type Props = {
value: ElasticsearchOptions;
onChange: (value: ElasticsearchOptions) => void;
};
export const LogsConfig = (props: Props) => {
const { value, onChange } = props;
const changeHandler = (key: keyof ElasticsearchOptions) => (
event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>
) => {
onChange({
...value,
[key]: event.currentTarget.value,
});
};
return (
<>
<h3 className="page-heading">Logs</h3>
<div className="gf-form-group">
<div className="gf-form max-width-30">
<FormField
labelWidth={11}
label="Message field name"
value={value.logMessageField}
onChange={changeHandler('logMessageField')}
placeholder="_source"
/>
</div>
<div className="gf-form max-width-30">
<FormField
labelWidth={11}
label="Level field name"
value={value.logLevelField}
onChange={changeHandler('logLevelField')}
/>
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,15 @@
import { DataSourceSettings } from '@grafana/ui';
import { ElasticsearchOptions } from '../types';
import { createDatasourceSettings } from '../../../../features/datasources/mocks';
export function createDefaultConfigOptions(): DataSourceSettings<ElasticsearchOptions> {
return createDatasourceSettings<ElasticsearchOptions>({
timeField: '@time',
esVersion: 70,
interval: 'Hourly',
timeInterval: '10s',
maxConcurrentShardRequests: 300,
logMessageField: 'test.message',
logLevelField: 'test.level',
});
}

View File

@@ -583,16 +583,3 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
return false;
}
}
export function getMaxConcurrenShardRequestOrDefault(options: ElasticsearchOptions): number {
if (options.maxConcurrentShardRequests === 5 && options.esVersion < 70) {
return 256;
}
if (options.maxConcurrentShardRequests === 256 && options.esVersion >= 70) {
return 5;
}
const defaultMaxConcurrentShardRequests = options.esVersion >= 70 ? 5 : 256;
return options.maxConcurrentShardRequests || defaultMaxConcurrentShardRequests;
}

View File

@@ -1,8 +1,8 @@
import { DataSourcePlugin } from '@grafana/ui';
import { ElasticDatasource } from './datasource';
import { ElasticQueryCtrl } from './query_ctrl';
import { ElasticConfigCtrl } from './config_ctrl';
import ElasticsearchQueryField from './components/ElasticsearchQueryField';
import { ConfigEditor } from './configuration/ConfigEditor';
class ElasticAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
@@ -10,6 +10,6 @@ class ElasticAnnotationsQueryCtrl {
export const plugin = new DataSourcePlugin(ElasticDatasource)
.setQueryCtrl(ElasticQueryCtrl)
.setConfigCtrl(ElasticConfigCtrl)
.setConfigEditor(ConfigEditor)
.setExploreLogsQueryField(ElasticsearchQueryField)
.setAnnotationQueryCtrl(ElasticAnnotationsQueryCtrl);

View File

@@ -1,66 +0,0 @@
<datasource-http-settings current="ctrl.current" suggest-url="http://localhost:9200">
</datasource-http-settings>
<h3 class="page-heading">Elasticsearch details</h3>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form max-width-25">
<span class="gf-form-label width-9">Index name</span>
<input class="gf-form-input" type="text" ng-model='ctrl.current.database' placeholder="" required></input>
</div>
<div class="gf-form width-14">
<span class="gf-form-label width-9">Pattern</span>
<span class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="ctrl.current.jsonData.interval" ng-options="f.value as f.name for f in ctrl.indexPatternTypes" ng-change="ctrl.indexPatternTypeChanged()" ></select>
</span>
</div>
</div>
<div class="gf-form max-width-25">
<span class="gf-form-label width-9">Time field name</span>
<input class="gf-form-input" type="text" ng-model='ctrl.current.jsonData.timeField' placeholder="" required ng-init=""></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-9">Version</span>
<span class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="ctrl.current.jsonData.esVersion" ng-options="f.value as f.name for f in ctrl.esVersions" ng-change="ctrl.versionChanged()"></select>
</span>
</div>
<div class="gf-form max-width-30" ng-if="ctrl.current.jsonData.esVersion>=56">
<span class="gf-form-label width-15">Max concurrent Shard Requests</span>
<input class="gf-form-input" type="text" ng-model='ctrl.current.jsonData.maxConcurrentShardRequests' placeholder="" required></input>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Min time interval</span>
<input
type="text"
class="gf-form-input width-6 gf-form-input--has-help-icon"
ng-model="ctrl.current.jsonData.timeInterval"
spellcheck='false'
placeholder="10s"
ng-pattern="/^\d+(ms|[Mwdhmsy])$/"
></input>
<info-popover mode="right-absolute">
A lower limit for the auto group by time interval. Recommended to be set to write frequency,
for example <code>1m</code> if your data is written every minute.
</info-popover>
</div>
</div>
</div>
<b>Logs</b>
<div class="gf-form-group">
<div class="gf-form max-width-30">
<span class="gf-form-label width-11">Message field name</span>
<input class="gf-form-input" type="text" ng-model='ctrl.current.jsonData.logMessageField' placeholder="_source" />
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-11">Level field name</span>
<input class="gf-form-input" type="text" ng-model='ctrl.current.jsonData.logLevelField' placeholder="" />
</div>
</div>

View File

@@ -1,7 +1,7 @@
import angular, { IQService } from 'angular';
import { dateMath } from '@grafana/data';
import _ from 'lodash';
import { ElasticDatasource, getMaxConcurrenShardRequestOrDefault } from '../datasource';
import { ElasticDatasource } from '../datasource';
import { toUtc, dateTime } from '@grafana/data';
import { BackendSrv } from 'app/core/services/backend_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
@@ -646,27 +646,3 @@ describe('ElasticDatasource', function(this: any) {
});
});
});
describe('getMaxConcurrenShardRequestOrDefault', () => {
const testCases = [
{ version: 50, expectedMaxConcurrentShardRequests: 256 },
{ version: 50, maxConcurrentShardRequests: 50, expectedMaxConcurrentShardRequests: 50 },
{ version: 56, expectedMaxConcurrentShardRequests: 256 },
{ version: 56, maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 256 },
{ version: 56, maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 256 },
{ version: 56, maxConcurrentShardRequests: 200, expectedMaxConcurrentShardRequests: 200 },
{ version: 70, expectedMaxConcurrentShardRequests: 5 },
{ version: 70, maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 5 },
{ version: 70, maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 5 },
{ version: 70, maxConcurrentShardRequests: 6, expectedMaxConcurrentShardRequests: 6 },
];
testCases.forEach(tc => {
it(`version = ${tc.version}, maxConcurrentShardRequests = ${tc.maxConcurrentShardRequests}`, () => {
const options = { esVersion: tc.version, maxConcurrentShardRequests: tc.maxConcurrentShardRequests };
expect(getMaxConcurrenShardRequestOrDefault(options as ElasticsearchOptions)).toBe(
tc.expectedMaxConcurrentShardRequests
);
});
});
});