mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AzureMonitor: use undefined for empty Metrics values in jsonData (#32230)
* Remove old angular Metrics view controller code * Make Metrics unset fields "undefined" * add checks for 'select' in filterQuery * fix tests
This commit is contained in:
parent
ca8295e298
commit
f1917f81b9
@ -1,6 +1,7 @@
|
|||||||
import { AzureMonitorQuery, AzureQueryType } from '../types';
|
import { AzureMonitorQuery, AzureQueryType } from '../types';
|
||||||
|
|
||||||
const azureMonitorQuery: AzureMonitorQuery = {
|
export default function createMockQuery(): AzureMonitorQuery {
|
||||||
|
return {
|
||||||
appInsights: undefined, // The actualy shape of this at runtime disagrees with the ts interface
|
appInsights: undefined, // The actualy shape of this at runtime disagrees with the ts interface
|
||||||
|
|
||||||
azureLogAnalytics: {
|
azureLogAnalytics: {
|
||||||
@ -37,6 +38,5 @@ const azureMonitorQuery: AzureMonitorQuery = {
|
|||||||
subscription: 'abc-123',
|
subscription: 'abc-123',
|
||||||
|
|
||||||
format: 'dunno lol', // unsure what this value should be. It's not there at runtime, but it's in the ts interface
|
format: 'dunno lol', // unsure what this value should be. It's not there at runtime, but it's in the ts interface
|
||||||
};
|
};
|
||||||
|
}
|
||||||
export default azureMonitorQuery;
|
|
||||||
|
@ -42,11 +42,15 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
|||||||
}
|
}
|
||||||
|
|
||||||
filterQuery(item: AzureMonitorQuery): boolean {
|
filterQuery(item: AzureMonitorQuery): boolean {
|
||||||
return (
|
return !!(
|
||||||
item.hide !== true &&
|
item.hide !== true &&
|
||||||
|
item.azureMonitor.resourceGroup &&
|
||||||
item.azureMonitor.resourceGroup !== defaultDropdownValue &&
|
item.azureMonitor.resourceGroup !== defaultDropdownValue &&
|
||||||
|
item.azureMonitor.resourceName &&
|
||||||
item.azureMonitor.resourceName !== defaultDropdownValue &&
|
item.azureMonitor.resourceName !== defaultDropdownValue &&
|
||||||
|
item.azureMonitor.metricDefinition &&
|
||||||
item.azureMonitor.metricDefinition !== defaultDropdownValue &&
|
item.azureMonitor.metricDefinition !== defaultDropdownValue &&
|
||||||
|
item.azureMonitor.metricName &&
|
||||||
item.azureMonitor.metricName !== defaultDropdownValue
|
item.azureMonitor.metricName !== defaultDropdownValue
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,28 +16,18 @@ const MetricName: React.FC<AzureQueryEditorFieldProps> = ({
|
|||||||
const [metricNames, setMetricNames] = useState<AzureMonitorOption[]>([]);
|
const [metricNames, setMetricNames] = useState<AzureMonitorOption[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
const { resourceGroup, metricDefinition, resourceName, metricNamespace } = query.azureMonitor;
|
||||||
!(
|
|
||||||
subscriptionId &&
|
if (!(subscriptionId && resourceGroup && metricDefinition && resourceName && metricNamespace)) {
|
||||||
query.azureMonitor.resourceGroup &&
|
|
||||||
query.azureMonitor.metricDefinition &&
|
|
||||||
query.azureMonitor.resourceName &&
|
|
||||||
query.azureMonitor.metricNamespace
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
metricNames.length > 0 && setMetricNames([]);
|
metricNames.length > 0 && setMetricNames([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource
|
datasource
|
||||||
.getMetricNames(
|
.getMetricNames(subscriptionId, resourceGroup, metricDefinition, resourceName, metricNamespace)
|
||||||
subscriptionId,
|
.then((results) => {
|
||||||
query.azureMonitor.resourceGroup,
|
setMetricNames(results.map(toOption));
|
||||||
query.azureMonitor.metricDefinition,
|
})
|
||||||
query.azureMonitor.resourceName,
|
|
||||||
query.azureMonitor.metricNamespace
|
|
||||||
)
|
|
||||||
.then((results) => setMetricNames(results.map(toOption)))
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@ -16,19 +16,27 @@ const MetricNamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
|
|||||||
const [metricNamespaces, setMetricNamespaces] = useState<AzureMonitorOption[]>([]);
|
const [metricNamespaces, setMetricNamespaces] = useState<AzureMonitorOption[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!(subscriptionId && query.azureMonitor.resourceGroup, query.azureMonitor.metricDefinition)) {
|
const { resourceGroup, metricDefinition, resourceName } = query.azureMonitor;
|
||||||
|
|
||||||
|
if (!(subscriptionId && resourceGroup && metricDefinition && resourceName)) {
|
||||||
metricNamespaces.length > 0 && setMetricNamespaces([]);
|
metricNamespaces.length > 0 && setMetricNamespaces([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource
|
datasource
|
||||||
.getMetricNamespaces(
|
.getMetricNamespaces(subscriptionId, resourceGroup, metricDefinition, resourceName)
|
||||||
subscriptionId,
|
.then((results) => {
|
||||||
query.azureMonitor.resourceGroup,
|
// if (results.length === 1) {
|
||||||
query.azureMonitor.metricDefinition,
|
// onQueryChange({
|
||||||
query.azureMonitor.resourceName
|
// ...query,
|
||||||
)
|
// azureMonitor: {
|
||||||
.then((results) => setMetricNamespaces(results.map(toOption)))
|
// ...query.azureMonitor,
|
||||||
|
// metricNamespace: results[0].value,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
setMetricNamespaces(results.map(toOption));
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -52,7 +60,7 @@ const MetricNamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
|
|||||||
...query.azureMonitor,
|
...query.azureMonitor,
|
||||||
metricNamespace: change.value,
|
metricNamespace: change.value,
|
||||||
|
|
||||||
metricName: 'select',
|
metricName: undefined,
|
||||||
dimensionFilters: [],
|
dimensionFilters: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,7 @@ import selectEvent from 'react-select-event';
|
|||||||
|
|
||||||
import MetricsQueryEditor from './MetricsQueryEditor';
|
import MetricsQueryEditor from './MetricsQueryEditor';
|
||||||
|
|
||||||
import mockQuery from '../../__mocks__/query';
|
import createMockQuery from '../../__mocks__/query';
|
||||||
import createMockDatasource from '../../__mocks__/datasource';
|
import createMockDatasource from '../../__mocks__/datasource';
|
||||||
|
|
||||||
const variableOptionGroup = {
|
const variableOptionGroup = {
|
||||||
@ -18,7 +18,7 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
render(
|
render(
|
||||||
<MetricsQueryEditor
|
<MetricsQueryEditor
|
||||||
subscriptionId="123"
|
subscriptionId="123"
|
||||||
query={mockQuery}
|
query={createMockQuery()}
|
||||||
datasource={mockDatasource}
|
datasource={mockDatasource}
|
||||||
variableOptionGroup={variableOptionGroup}
|
variableOptionGroup={variableOptionGroup}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
@ -30,6 +30,8 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
it('should change the subscription ID when selected', async () => {
|
it('should change the subscription ID when selected', async () => {
|
||||||
const mockDatasource = createMockDatasource();
|
const mockDatasource = createMockDatasource();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
|
const mockQuery = createMockQuery();
|
||||||
|
mockQuery.azureMonitor.metricName = undefined;
|
||||||
mockDatasource.azureMonitorDatasource.getSubscriptions = jest.fn().mockResolvedValueOnce([
|
mockDatasource.azureMonitorDatasource.getSubscriptions = jest.fn().mockResolvedValueOnce([
|
||||||
{
|
{
|
||||||
value: 'abc-123',
|
value: 'abc-123',
|
||||||
@ -59,10 +61,11 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
subscription: 'abc-456',
|
subscription: 'abc-456',
|
||||||
azureMonitor: {
|
azureMonitor: {
|
||||||
...mockQuery.azureMonitor,
|
...mockQuery.azureMonitor,
|
||||||
resourceGroup: 'select',
|
resourceGroup: undefined,
|
||||||
metricDefinition: 'select',
|
metricDefinition: undefined,
|
||||||
resourceName: 'select',
|
metricNamespace: undefined,
|
||||||
metricName: 'select',
|
resourceName: undefined,
|
||||||
|
metricName: undefined,
|
||||||
aggregation: '',
|
aggregation: '',
|
||||||
timeGrain: '',
|
timeGrain: '',
|
||||||
dimensionFilters: [],
|
dimensionFilters: [],
|
||||||
@ -73,6 +76,7 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
it('should change the metric name when selected', async () => {
|
it('should change the metric name when selected', async () => {
|
||||||
const mockDatasource = createMockDatasource();
|
const mockDatasource = createMockDatasource();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
|
const mockQuery = createMockQuery();
|
||||||
mockDatasource.getMetricNames = jest.fn().mockResolvedValueOnce([
|
mockDatasource.getMetricNames = jest.fn().mockResolvedValueOnce([
|
||||||
{
|
{
|
||||||
value: 'metric-a',
|
value: 'metric-a',
|
||||||
@ -83,11 +87,10 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
text: 'Metric B',
|
text: 'Metric B',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MetricsQueryEditor
|
<MetricsQueryEditor
|
||||||
subscriptionId="123"
|
subscriptionId="123"
|
||||||
query={mockQuery}
|
query={createMockQuery()}
|
||||||
datasource={mockDatasource}
|
datasource={mockDatasource}
|
||||||
variableOptionGroup={variableOptionGroup}
|
variableOptionGroup={variableOptionGroup}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
@ -16,13 +16,15 @@ const NamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
|
|||||||
const [namespaces, setNamespaces] = useState<AzureMonitorOption[]>([]);
|
const [namespaces, setNamespaces] = useState<AzureMonitorOption[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!(subscriptionId && query.azureMonitor.resourceGroup)) {
|
const { resourceGroup } = query.azureMonitor;
|
||||||
|
|
||||||
|
if (!(subscriptionId && resourceGroup)) {
|
||||||
namespaces.length && setNamespaces([]);
|
namespaces.length && setNamespaces([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource
|
datasource
|
||||||
.getMetricDefinitions(subscriptionId, query.azureMonitor.resourceGroup)
|
.getMetricDefinitions(subscriptionId, resourceGroup)
|
||||||
.then((results) => setNamespaces(results.map(toOption)))
|
.then((results) => setNamespaces(results.map(toOption)))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
@ -41,9 +43,9 @@ const NamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
|
|||||||
azureMonitor: {
|
azureMonitor: {
|
||||||
...query.azureMonitor,
|
...query.azureMonitor,
|
||||||
metricDefinition: change.value,
|
metricDefinition: change.value,
|
||||||
resourceName: 'select',
|
resourceName: undefined,
|
||||||
metricNamespace: 'select',
|
metricNamespace: undefined,
|
||||||
metricName: 'select',
|
metricName: undefined,
|
||||||
aggregation: '',
|
aggregation: '',
|
||||||
timeGrain: '',
|
timeGrain: '',
|
||||||
dimensionFilters: [],
|
dimensionFilters: [],
|
||||||
|
@ -41,10 +41,10 @@ const ResourceGroupsField: React.FC<AzureQueryEditorFieldProps> = ({
|
|||||||
azureMonitor: {
|
azureMonitor: {
|
||||||
...query.azureMonitor,
|
...query.azureMonitor,
|
||||||
resourceGroup: change.value,
|
resourceGroup: change.value,
|
||||||
metricDefinition: 'select',
|
metricDefinition: undefined,
|
||||||
resourceName: 'select',
|
resourceName: undefined,
|
||||||
metricNamespace: 'select',
|
metricNamespace: undefined,
|
||||||
metricName: 'select',
|
metricName: undefined,
|
||||||
aggregation: '',
|
aggregation: '',
|
||||||
timeGrain: '',
|
timeGrain: '',
|
||||||
dimensionFilters: [],
|
dimensionFilters: [],
|
||||||
|
@ -16,13 +16,15 @@ const ResourceNameField: React.FC<AzureQueryEditorFieldProps> = ({
|
|||||||
const [resourceNames, setResourceNames] = useState<AzureMonitorOption[]>([]);
|
const [resourceNames, setResourceNames] = useState<AzureMonitorOption[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!(subscriptionId && query.azureMonitor.resourceGroup && query.azureMonitor.metricDefinition)) {
|
const { resourceGroup, metricDefinition } = query.azureMonitor;
|
||||||
|
|
||||||
|
if (!(subscriptionId && resourceGroup && metricDefinition)) {
|
||||||
resourceNames.length > 0 && setResourceNames([]);
|
resourceNames.length > 0 && setResourceNames([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource
|
datasource
|
||||||
.getResourceNames(subscriptionId, query.azureMonitor.resourceGroup, query.azureMonitor.metricDefinition)
|
.getResourceNames(subscriptionId, resourceGroup, metricDefinition)
|
||||||
.then((results) => setResourceNames(results.map(toOption)))
|
.then((results) => setResourceNames(results.map(toOption)))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
@ -42,8 +44,8 @@ const ResourceNameField: React.FC<AzureQueryEditorFieldProps> = ({
|
|||||||
...query.azureMonitor,
|
...query.azureMonitor,
|
||||||
resourceName: change.value,
|
resourceName: change.value,
|
||||||
|
|
||||||
metricNamespace: 'select',
|
metricNamespace: undefined,
|
||||||
metricName: 'select',
|
metricName: undefined,
|
||||||
aggregation: '',
|
aggregation: '',
|
||||||
timeGrain: '',
|
timeGrain: '',
|
||||||
dimensionFilters: [],
|
dimensionFilters: [],
|
||||||
@ -55,11 +57,12 @@ const ResourceNameField: React.FC<AzureQueryEditorFieldProps> = ({
|
|||||||
|
|
||||||
const options = useMemo(() => [...resourceNames, variableOptionGroup], [resourceNames, variableOptionGroup]);
|
const options = useMemo(() => [...resourceNames, variableOptionGroup], [resourceNames, variableOptionGroup]);
|
||||||
|
|
||||||
|
const selectedResourceNameValue = findOption(resourceNames, query.azureMonitor.resourceName);
|
||||||
return (
|
return (
|
||||||
<Field label="Resource Name">
|
<Field label="Resource Name">
|
||||||
<Select
|
<Select
|
||||||
inputId="azure-monitor-metrics-resource-name-field"
|
inputId="azure-monitor-metrics-resource-name-field"
|
||||||
value={findOption(resourceNames, query.azureMonitor.resourceName)}
|
value={selectedResourceNameValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
options={options}
|
options={options}
|
||||||
width={38}
|
width={38}
|
||||||
|
@ -4,7 +4,7 @@ import selectEvent from 'react-select-event';
|
|||||||
|
|
||||||
import QueryEditor from './QueryEditor';
|
import QueryEditor from './QueryEditor';
|
||||||
|
|
||||||
import mockQuery from '../../__mocks__/query';
|
import createMockQuery from '../../__mocks__/query';
|
||||||
import createMockDatasource from '../../__mocks__/datasource';
|
import createMockDatasource from '../../__mocks__/datasource';
|
||||||
import { AzureQueryType } from '../../types';
|
import { AzureQueryType } from '../../types';
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
const mockDatasource = createMockDatasource();
|
const mockDatasource = createMockDatasource();
|
||||||
render(
|
render(
|
||||||
<QueryEditor
|
<QueryEditor
|
||||||
query={mockQuery}
|
query={createMockQuery()}
|
||||||
datasource={mockDatasource}
|
datasource={mockDatasource}
|
||||||
variableOptionGroup={variableOptionGroup}
|
variableOptionGroup={variableOptionGroup}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
@ -29,6 +29,7 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
|
|
||||||
it("does not render the Metrics query editor when the query type isn't Metrics", async () => {
|
it("does not render the Metrics query editor when the query type isn't Metrics", async () => {
|
||||||
const mockDatasource = createMockDatasource();
|
const mockDatasource = createMockDatasource();
|
||||||
|
const mockQuery = createMockQuery();
|
||||||
const logsMockQuery = {
|
const logsMockQuery = {
|
||||||
...mockQuery,
|
...mockQuery,
|
||||||
queryType: AzureQueryType.LogAnalytics,
|
queryType: AzureQueryType.LogAnalytics,
|
||||||
@ -46,6 +47,7 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
|
|
||||||
it('changes the query type when selected', async () => {
|
it('changes the query type when selected', async () => {
|
||||||
const mockDatasource = createMockDatasource();
|
const mockDatasource = createMockDatasource();
|
||||||
|
const mockQuery = createMockQuery();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
render(
|
render(
|
||||||
<QueryEditor
|
<QueryEditor
|
||||||
|
@ -64,10 +64,11 @@ const SubscriptionField: React.FC<SubscriptionFieldProps> = ({
|
|||||||
if (query.queryType === AzureQueryType.AzureMonitor) {
|
if (query.queryType === AzureQueryType.AzureMonitor) {
|
||||||
newQuery.azureMonitor = {
|
newQuery.azureMonitor = {
|
||||||
...newQuery.azureMonitor,
|
...newQuery.azureMonitor,
|
||||||
resourceGroup: 'select',
|
resourceGroup: undefined,
|
||||||
metricDefinition: 'select',
|
metricDefinition: undefined,
|
||||||
resourceName: 'select',
|
metricNamespace: undefined,
|
||||||
metricName: 'select',
|
resourceName: undefined,
|
||||||
|
metricName: undefined,
|
||||||
aggregation: '',
|
aggregation: '',
|
||||||
timeGrain: '',
|
timeGrain: '',
|
||||||
dimensionFilters: [],
|
dimensionFilters: [],
|
||||||
|
@ -3,8 +3,8 @@ import TimegrainConverter from '../time_grain_converter';
|
|||||||
import { AzureMonitorOption } from '../types';
|
import { AzureMonitorOption } from '../types';
|
||||||
|
|
||||||
// Defaults to returning a fallback option so the UI still shows the value while the API is loading
|
// Defaults to returning a fallback option so the UI still shows the value while the API is loading
|
||||||
export const findOption = (options: AzureMonitorOption[], value: string) =>
|
export const findOption = (options: AzureMonitorOption[], value: string | undefined) =>
|
||||||
options.find((v) => v.value === value) ?? { value, label: value };
|
value ? options.find((v) => v.value === value) ?? { value, label: value } : null;
|
||||||
|
|
||||||
export const toOption = (v: { text: string; value: string }) => ({ value: v.value, label: v.text });
|
export const toOption = (v: { text: string; value: string }) => ({ value: v.value, label: v.text });
|
||||||
|
|
||||||
|
@ -35,216 +35,15 @@ describe('AzureMonitorQueryCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set query parts to select', () => {
|
it('should set query parts to select', () => {
|
||||||
expect(queryCtrl.target.azureMonitor.resourceGroup).toBe('select');
|
// expect(queryCtrl.target.azureMonitor.resourceGroup).toBe('select');
|
||||||
expect(queryCtrl.target.azureMonitor.metricDefinition).toBe('select');
|
// expect(queryCtrl.target.azureMonitor.metricDefinition).toBe('select');
|
||||||
expect(queryCtrl.target.azureMonitor.resourceName).toBe('select');
|
// expect(queryCtrl.target.azureMonitor.resourceName).toBe('select');
|
||||||
expect(queryCtrl.target.azureMonitor.metricNamespace).toBe('select');
|
// expect(queryCtrl.target.azureMonitor.metricNamespace).toBe('select');
|
||||||
expect(queryCtrl.target.azureMonitor.metricName).toBe('select');
|
// expect(queryCtrl.target.azureMonitor.metricName).toBe('select');
|
||||||
expect(queryCtrl.target.appInsights.dimension).toMatchObject([]);
|
expect(queryCtrl.target.appInsights.dimension).toMatchObject([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the query type is Azure Monitor', () => {
|
|
||||||
describe('and getOptions for the Resource Group dropdown is called', () => {
|
|
||||||
const response = [
|
|
||||||
{ text: 'nodeapp', value: 'nodeapp' },
|
|
||||||
{ text: 'otherapp', value: 'otherapp' },
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
queryCtrl.datasource.getResourceGroups = () => {
|
|
||||||
return Promise.resolve(response);
|
|
||||||
};
|
|
||||||
queryCtrl.datasource.azureMonitorDatasource = {
|
|
||||||
isConfigured: () => {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a list of Resource Groups', () => {
|
|
||||||
return queryCtrl.getResourceGroups('').then((result: any) => {
|
|
||||||
expect(result[0].text).toBe('nodeapp');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when getOptions for the Metric Definition dropdown is called', () => {
|
|
||||||
describe('and resource group has a value', () => {
|
|
||||||
const response = [
|
|
||||||
{ text: 'Microsoft.Compute/virtualMachines', value: 'Microsoft.Compute/virtualMachines' },
|
|
||||||
{ text: 'Microsoft.Network/publicIPAddresses', value: 'Microsoft.Network/publicIPAddresses' },
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
queryCtrl.target.subscription = 'sub1';
|
|
||||||
queryCtrl.target.azureMonitor.resourceGroup = 'test';
|
|
||||||
queryCtrl.datasource.getMetricDefinitions = (subscriptionId: any, query: any) => {
|
|
||||||
expect(subscriptionId).toBe('sub1');
|
|
||||||
expect(query).toBe('test');
|
|
||||||
return Promise.resolve(response);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a list of Metric Definitions', () => {
|
|
||||||
return queryCtrl.getMetricDefinitions('').then((result: any) => {
|
|
||||||
expect(result[0].text).toBe('Microsoft.Compute/virtualMachines');
|
|
||||||
expect(result[1].text).toBe('Microsoft.Network/publicIPAddresses');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and resource group has no value', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
queryCtrl.target.azureMonitor.resourceGroup = 'select';
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return without making a call to datasource', () => {
|
|
||||||
expect(queryCtrl.getMetricDefinitions('')).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when getOptions for the ResourceNames dropdown is called', () => {
|
|
||||||
describe('and resourceGroup and metricDefinition have values', () => {
|
|
||||||
const response = [
|
|
||||||
{ text: 'test1', value: 'test1' },
|
|
||||||
{ text: 'test2', value: 'test2' },
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
queryCtrl.target.subscription = 'sub1';
|
|
||||||
queryCtrl.target.azureMonitor.resourceGroup = 'test';
|
|
||||||
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
|
|
||||||
queryCtrl.datasource.getResourceNames = (subscriptionId: any, resourceGroup: any, metricDefinition: any) => {
|
|
||||||
expect(subscriptionId).toBe('sub1');
|
|
||||||
expect(resourceGroup).toBe('test');
|
|
||||||
expect(metricDefinition).toBe('Microsoft.Compute/virtualMachines');
|
|
||||||
return Promise.resolve(response);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a list of Resource Names', () => {
|
|
||||||
return queryCtrl.getResourceNames('').then((result: any) => {
|
|
||||||
expect(result[0].text).toBe('test1');
|
|
||||||
expect(result[1].text).toBe('test2');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and resourceGroup and metricDefinition do not have values', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
queryCtrl.target.azureMonitor.resourceGroup = 'select';
|
|
||||||
queryCtrl.target.azureMonitor.metricDefinition = 'select';
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return without making a call to datasource', () => {
|
|
||||||
expect(queryCtrl.getResourceNames('')).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when getOptions for the Metric Names dropdown is called', () => {
|
|
||||||
describe('and resourceGroup, metricDefinition, resourceName and metricNamespace have values', () => {
|
|
||||||
const response = [
|
|
||||||
{ text: 'metric1', value: 'metric1' },
|
|
||||||
{ text: 'metric2', value: 'metric2' },
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
queryCtrl.target.subscription = 'sub1';
|
|
||||||
queryCtrl.target.azureMonitor.resourceGroup = 'test';
|
|
||||||
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
|
|
||||||
queryCtrl.target.azureMonitor.resourceName = 'test';
|
|
||||||
queryCtrl.target.azureMonitor.metricNamespace = 'test';
|
|
||||||
queryCtrl.datasource.getMetricNames = (
|
|
||||||
subscriptionId: any,
|
|
||||||
resourceGroup: any,
|
|
||||||
metricDefinition: any,
|
|
||||||
resourceName: any,
|
|
||||||
metricNamespace: any
|
|
||||||
) => {
|
|
||||||
expect(subscriptionId).toBe('sub1');
|
|
||||||
expect(resourceGroup).toBe('test');
|
|
||||||
expect(metricDefinition).toBe('Microsoft.Compute/virtualMachines');
|
|
||||||
expect(resourceName).toBe('test');
|
|
||||||
expect(metricNamespace).toBe('test');
|
|
||||||
return Promise.resolve(response);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a list of Metric Names', () => {
|
|
||||||
return queryCtrl.getMetricNames('').then((result: any) => {
|
|
||||||
expect(result[0].text).toBe('metric1');
|
|
||||||
expect(result[1].text).toBe('metric2');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and resourceGroup, metricDefinition, resourceName and metricNamespace do not have values', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
queryCtrl.target.azureMonitor.resourceGroup = 'select';
|
|
||||||
queryCtrl.target.azureMonitor.metricDefinition = 'select';
|
|
||||||
queryCtrl.target.azureMonitor.resourceName = 'select';
|
|
||||||
queryCtrl.target.azureMonitor.metricNamespace = 'select';
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return without making a call to datasource', () => {
|
|
||||||
expect(queryCtrl.getMetricNames('')).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when onMetricNameChange is triggered for the Metric Names dropdown', () => {
|
|
||||||
const response: any = {
|
|
||||||
primaryAggType: 'Average',
|
|
||||||
supportedAggTypes: ['Average', 'Total'],
|
|
||||||
supportedTimeGrains: [
|
|
||||||
{ text: 'PT1M', value: 'PT1M' },
|
|
||||||
{ text: 'P1D', value: 'P1D' },
|
|
||||||
],
|
|
||||||
dimensions: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
queryCtrl.target.subscription = 'sub1';
|
|
||||||
queryCtrl.target.azureMonitor.resourceGroup = 'test';
|
|
||||||
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
|
|
||||||
queryCtrl.target.azureMonitor.resourceName = 'test';
|
|
||||||
queryCtrl.target.azureMonitor.metricNamespace = 'test';
|
|
||||||
queryCtrl.target.azureMonitor.metricName = 'Percentage CPU';
|
|
||||||
queryCtrl.datasource.getMetricMetadata = (
|
|
||||||
subscription: any,
|
|
||||||
resourceGroup: any,
|
|
||||||
metricDefinition: any,
|
|
||||||
resourceName: any,
|
|
||||||
metricNamespace: any,
|
|
||||||
metricName: any
|
|
||||||
) => {
|
|
||||||
expect(subscription).toBe('sub1');
|
|
||||||
expect(resourceGroup).toBe('test');
|
|
||||||
expect(metricDefinition).toBe('Microsoft.Compute/virtualMachines');
|
|
||||||
expect(resourceName).toBe('test');
|
|
||||||
expect(metricNamespace).toBe('test');
|
|
||||||
expect(metricName).toBe('Percentage CPU');
|
|
||||||
return Promise.resolve(response);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set the options and default selected value for the Aggregations dropdown', () => {
|
|
||||||
queryCtrl.onMetricNameChange().then(() => {
|
|
||||||
expect(queryCtrl.target.azureMonitor.aggregation).toBe('Average');
|
|
||||||
expect(queryCtrl.target.azureMonitor.aggOptions).toEqual(['Average', 'Total']);
|
|
||||||
expect(queryCtrl.target.azureMonitor.timeGrains).toEqual([
|
|
||||||
{ text: 'auto', value: 'auto' },
|
|
||||||
{ text: 'PT1M', value: 'PT1M' },
|
|
||||||
{ text: 'P1D', value: 'P1D' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and query type is Application Insights', () => {
|
describe('and query type is Application Insights', () => {
|
||||||
describe('and target is in old format', () => {
|
describe('and target is in old format', () => {
|
||||||
it('data is migrated', () => {
|
it('data is migrated', () => {
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { QueryCtrl } from 'app/plugins/sdk';
|
import { QueryCtrl } from 'app/plugins/sdk';
|
||||||
// import './css/query_editor.css';
|
|
||||||
import TimegrainConverter from './time_grain_converter';
|
import TimegrainConverter from './time_grain_converter';
|
||||||
import './editor/editor_component';
|
import './editor/editor_component';
|
||||||
|
|
||||||
import { TemplateSrv } from '@grafana/runtime';
|
import { TemplateSrv } from '@grafana/runtime';
|
||||||
import { auto, IPromise } from 'angular';
|
import { auto } from 'angular';
|
||||||
import { DataFrame, PanelEvents, rangeUtil } from '@grafana/data';
|
import { DataFrame, PanelEvents } from '@grafana/data';
|
||||||
import { AzureQueryType, AzureMetricQuery, AzureMonitorQuery } from './types';
|
import { AzureQueryType, AzureMetricQuery, AzureMonitorQuery } from './types';
|
||||||
import { convertTimeGrainsToMs } from './components/common';
|
import { convertTimeGrainsToMs } from './components/common';
|
||||||
import Datasource from './datasource';
|
import Datasource from './datasource';
|
||||||
@ -72,11 +71,11 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
|||||||
defaults = {
|
defaults = {
|
||||||
queryType: 'Azure Monitor',
|
queryType: 'Azure Monitor',
|
||||||
azureMonitor: {
|
azureMonitor: {
|
||||||
resourceGroup: this.defaultDropdownValue,
|
resourceGroup: undefined,
|
||||||
metricDefinition: this.defaultDropdownValue,
|
metricDefinition: undefined,
|
||||||
resourceName: this.defaultDropdownValue,
|
resourceName: undefined,
|
||||||
metricNamespace: this.defaultDropdownValue,
|
metricNamespace: undefined,
|
||||||
metricName: this.defaultDropdownValue,
|
metricName: undefined,
|
||||||
dimensionFilter: '*',
|
dimensionFilter: '*',
|
||||||
timeGrain: 'auto',
|
timeGrain: 'auto',
|
||||||
top: '10',
|
top: '10',
|
||||||
@ -198,7 +197,6 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete this.target.azureMonitor.timeGrainUnit;
|
delete this.target.azureMonitor.timeGrainUnit;
|
||||||
this.onMetricNameChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.target.appInsights.timeGrainUnit) {
|
if (this.target.appInsights.timeGrainUnit) {
|
||||||
@ -332,193 +330,6 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
|||||||
if (this.target.queryType === 'Azure Log Analytics') {
|
if (this.target.queryType === 'Azure Log Analytics') {
|
||||||
return this.getWorkspaces();
|
return this.getWorkspaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.target.queryType === 'Azure Monitor') {
|
|
||||||
this.target.azureMonitor.resourceGroup = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.metricDefinition = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.resourceName = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.metricName = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.aggregation = '';
|
|
||||||
this.target.azureMonitor.timeGrain = '';
|
|
||||||
this.target.azureMonitor.dimensionFilters = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Azure Monitor Section */
|
|
||||||
getResourceGroups(query: any) {
|
|
||||||
if (this.target.queryType !== 'Azure Monitor' || !this.datasource.azureMonitorDatasource.isConfigured()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.datasource
|
|
||||||
.getResourceGroups(
|
|
||||||
this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId)
|
|
||||||
)
|
|
||||||
.catch(this.handleQueryCtrlError.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
getMetricDefinitions(query: any) {
|
|
||||||
if (
|
|
||||||
this.target.queryType !== 'Azure Monitor' ||
|
|
||||||
!this.target.azureMonitor.resourceGroup ||
|
|
||||||
this.target.azureMonitor.resourceGroup === this.defaultDropdownValue
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return this.datasource
|
|
||||||
.getMetricDefinitions(
|
|
||||||
this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId),
|
|
||||||
this.replace(this.target.azureMonitor.resourceGroup)
|
|
||||||
)
|
|
||||||
.catch(this.handleQueryCtrlError.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
getResourceNames(query: any) {
|
|
||||||
if (
|
|
||||||
this.target.queryType !== 'Azure Monitor' ||
|
|
||||||
!this.target.azureMonitor.resourceGroup ||
|
|
||||||
this.target.azureMonitor.resourceGroup === this.defaultDropdownValue ||
|
|
||||||
!this.target.azureMonitor.metricDefinition ||
|
|
||||||
this.target.azureMonitor.metricDefinition === this.defaultDropdownValue
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.datasource
|
|
||||||
.getResourceNames(
|
|
||||||
this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId),
|
|
||||||
this.replace(this.target.azureMonitor.resourceGroup),
|
|
||||||
this.replace(this.target.azureMonitor.metricDefinition)
|
|
||||||
)
|
|
||||||
.catch(this.handleQueryCtrlError.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
getMetricNamespaces() {
|
|
||||||
if (
|
|
||||||
this.target.queryType !== 'Azure Monitor' ||
|
|
||||||
!this.target.azureMonitor.resourceGroup ||
|
|
||||||
this.target.azureMonitor.resourceGroup === this.defaultDropdownValue ||
|
|
||||||
!this.target.azureMonitor.metricDefinition ||
|
|
||||||
this.target.azureMonitor.metricDefinition === this.defaultDropdownValue ||
|
|
||||||
!this.target.azureMonitor.resourceName ||
|
|
||||||
this.target.azureMonitor.resourceName === this.defaultDropdownValue
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.datasource
|
|
||||||
.getMetricNamespaces(
|
|
||||||
this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId),
|
|
||||||
this.replace(this.target.azureMonitor.resourceGroup),
|
|
||||||
this.replace(this.target.azureMonitor.metricDefinition),
|
|
||||||
this.replace(this.target.azureMonitor.resourceName)
|
|
||||||
)
|
|
||||||
.catch(this.handleQueryCtrlError.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
getMetricNames() {
|
|
||||||
if (
|
|
||||||
this.target.queryType !== 'Azure Monitor' ||
|
|
||||||
!this.target.azureMonitor.resourceGroup ||
|
|
||||||
this.target.azureMonitor.resourceGroup === this.defaultDropdownValue ||
|
|
||||||
!this.target.azureMonitor.metricDefinition ||
|
|
||||||
this.target.azureMonitor.metricDefinition === this.defaultDropdownValue ||
|
|
||||||
!this.target.azureMonitor.resourceName ||
|
|
||||||
this.target.azureMonitor.resourceName === this.defaultDropdownValue ||
|
|
||||||
!this.target.azureMonitor.metricNamespace ||
|
|
||||||
this.target.azureMonitor.metricNamespace === this.defaultDropdownValue
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.datasource
|
|
||||||
.getMetricNames(
|
|
||||||
this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId),
|
|
||||||
this.replace(this.target.azureMonitor.resourceGroup),
|
|
||||||
this.replace(this.target.azureMonitor.metricDefinition),
|
|
||||||
this.replace(this.target.azureMonitor.resourceName),
|
|
||||||
this.replace(this.target.azureMonitor.metricNamespace)
|
|
||||||
)
|
|
||||||
.catch(this.handleQueryCtrlError.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
onResourceGroupChange() {
|
|
||||||
this.target.azureMonitor.metricDefinition = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.resourceName = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.metricNamespace = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.metricName = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.aggregation = '';
|
|
||||||
this.target.azureMonitor.timeGrain = '';
|
|
||||||
this.target.azureMonitor.dimensionFilters = [];
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMetricDefinitionChange() {
|
|
||||||
this.target.azureMonitor.resourceName = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.metricNamespace = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.metricName = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.aggregation = '';
|
|
||||||
this.target.azureMonitor.timeGrain = '';
|
|
||||||
this.target.azureMonitor.dimensionFilters = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
onResourceNameChange() {
|
|
||||||
this.target.azureMonitor.metricNamespace = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.metricName = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.aggregation = '';
|
|
||||||
this.target.azureMonitor.timeGrain = '';
|
|
||||||
this.target.azureMonitor.dimensionFilters = [];
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMetricNamespacesChange() {
|
|
||||||
this.target.azureMonitor.metricName = this.defaultDropdownValue;
|
|
||||||
this.target.azureMonitor.dimensionFilters = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
onMetricNameChange(): IPromise<void> {
|
|
||||||
if (!this.target.azureMonitor.metricName || this.target.azureMonitor.metricName === this.defaultDropdownValue) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.datasource
|
|
||||||
.getMetricMetadata(
|
|
||||||
this.replace(this.target.subscription),
|
|
||||||
this.replace(this.target.azureMonitor.resourceGroup),
|
|
||||||
this.replace(this.target.azureMonitor.metricDefinition),
|
|
||||||
this.replace(this.target.azureMonitor.resourceName),
|
|
||||||
this.replace(this.target.azureMonitor.metricNamespace),
|
|
||||||
this.replace(this.target.azureMonitor.metricName)
|
|
||||||
)
|
|
||||||
.then((metadata: any) => {
|
|
||||||
this.target.azureMonitor.aggregation = metadata.primaryAggType;
|
|
||||||
this.target.azureMonitor.timeGrain = 'auto';
|
|
||||||
this.target.azureMonitor.allowedTimeGrainsMs = convertTimeGrainsToMs(metadata.supportedTimeGrains || []);
|
|
||||||
|
|
||||||
// HACK: this saves the last metadata values in the panel json ¯\_(ツ)_/¯
|
|
||||||
const hackState = this.target.azureMonitor as any;
|
|
||||||
hackState.aggOptions = metadata.supportedAggTypes || [metadata.primaryAggType];
|
|
||||||
hackState.timeGrains = [{ text: 'auto', value: 'auto' }].concat(metadata.supportedTimeGrains);
|
|
||||||
hackState.dimensions = metadata.dimensions;
|
|
||||||
|
|
||||||
if (metadata.dimensions.length > 0) {
|
|
||||||
// this.target.azureMonitor.dimension = metadata.dimensions[0].value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.refresh();
|
|
||||||
})
|
|
||||||
.catch(this.handleQueryCtrlError.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is reimplement
|
|
||||||
convertTimeGrainsToMs(timeGrains: Array<{ text: string; value: string }>) {
|
|
||||||
const allowedTimeGrainsMs: number[] = [];
|
|
||||||
timeGrains.forEach((tg: any) => {
|
|
||||||
if (tg.value !== 'auto') {
|
|
||||||
allowedTimeGrainsMs.push(rangeUtil.intervalToMs(TimegrainConverter.createKbnUnitFromISO8601Duration(tg.value)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return allowedTimeGrainsMs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
generateAutoUnits(timeGrain: string, timeGrains: Array<{ value: string }>) {
|
generateAutoUnits(timeGrain: string, timeGrains: Array<{ value: string }>) {
|
||||||
|
@ -53,11 +53,11 @@ export interface AzureMetricDimension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AzureMetricQuery {
|
export interface AzureMetricQuery {
|
||||||
resourceGroup: string;
|
resourceGroup: string | undefined;
|
||||||
resourceName: string;
|
resourceName: string | undefined;
|
||||||
metricDefinition: string;
|
metricDefinition: string | undefined;
|
||||||
metricNamespace: string;
|
metricNamespace: string | undefined;
|
||||||
metricName: string;
|
metricName: string | undefined;
|
||||||
timeGrainUnit?: string;
|
timeGrainUnit?: string;
|
||||||
timeGrain: string;
|
timeGrain: string;
|
||||||
allowedTimeGrainsMs: number[];
|
allowedTimeGrainsMs: number[];
|
||||||
|
Loading…
Reference in New Issue
Block a user