mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 18:40:01 -03:30
Merge pull request #9776 from AlexSCorey/9019-MultiChart
Observability Metrics SUMMARY This adds the chart for Observability Metrics (#9019). To see the chart you need to navigate to /metrics. Also, its best if you run a build that has multiple instances. This will do that for you COMPOSE_TAG=devel CLUSTER_NODE_COUNT=2 make docker-compose. When this feature loads the user has to select an instance (1, or all) and a metric to render data on the graph. Once they select those items, the chart appears and we start to make requests to the api every 3 seconds to get the data. (Currently the api does not support web sockets for this feature) If the user changes the values for either of the drop down items the chart resets. The chart also only show the last 50 data points. There is a "tooltip" that is rendered at the bottom left hand side. I decided to put it there, instead of on the chart itself because this chart could get quite crowded depending the number of data points rendered and the number of instances rendering lines. The X axis is sort of meaningless. The values below simply render the number of api requests. This isn't a value of time. Since the main goal of this feature is to show significant changes instead of tryin to pinpoint when the change occurs I felt that showing a time stamp on this axis would crowd the axis as well. ISSUE TYPE Feature Pull Request COMPONENT NAME UI AWX VERSION ADDITIONAL INFORMATION Reviewed-by: Kersom <None> Reviewed-by: Jake McDermott <yo@jakemcdermott.me> Reviewed-by: Alex Corey <Alex.swansboro@gmail.com> Reviewed-by: Mat Wilson <mawilson@redhat.com>
This commit is contained in:
commit
d53d41b84a
@ -20,6 +20,7 @@ import Login from './screens/Login';
|
||||
import { isAuthenticated } from './util/auth';
|
||||
import { getLanguageWithoutRegionCode } from './util/language';
|
||||
import { dynamicActivate, locales } from './i18nLoader';
|
||||
import Metrics from './screens/Metrics';
|
||||
|
||||
import getRouteConfig from './routeConfig';
|
||||
import SubscriptionEdit from './screens/Setting/Subscription/SubscriptionEdit';
|
||||
@ -58,6 +59,9 @@ const AuthorizedRoutes = ({ routeConfig }) => {
|
||||
</ProtectedRoute>
|
||||
))
|
||||
.concat(
|
||||
<ProtectedRoute key="metrics" path="/metrics">
|
||||
<Metrics />
|
||||
</ProtectedRoute>,
|
||||
<ProtectedRoute key="not-found" path="*">
|
||||
<NotFound />
|
||||
</ProtectedRoute>
|
||||
|
||||
@ -20,6 +20,7 @@ import JobTemplates from './models/JobTemplates';
|
||||
import Jobs from './models/Jobs';
|
||||
import Labels from './models/Labels';
|
||||
import Me from './models/Me';
|
||||
import Metrics from './models/Metrics';
|
||||
import NotificationTemplates from './models/NotificationTemplates';
|
||||
import Notifications from './models/Notifications';
|
||||
import Organizations from './models/Organizations';
|
||||
@ -64,6 +65,7 @@ const JobTemplatesAPI = new JobTemplates();
|
||||
const JobsAPI = new Jobs();
|
||||
const LabelsAPI = new Labels();
|
||||
const MeAPI = new Me();
|
||||
const MetricsAPI = new Metrics();
|
||||
const NotificationTemplatesAPI = new NotificationTemplates();
|
||||
const NotificationsAPI = new Notifications();
|
||||
const OrganizationsAPI = new Organizations();
|
||||
@ -109,6 +111,7 @@ export {
|
||||
JobsAPI,
|
||||
LabelsAPI,
|
||||
MeAPI,
|
||||
MetricsAPI,
|
||||
NotificationTemplatesAPI,
|
||||
NotificationsAPI,
|
||||
OrganizationsAPI,
|
||||
|
||||
@ -3,7 +3,7 @@ import Base from '../Base';
|
||||
class Metrics extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/inventories/';
|
||||
this.baseUrl = '/api/v2/metrics/';
|
||||
}
|
||||
}
|
||||
export default Metrics;
|
||||
|
||||
@ -164,6 +164,7 @@ describe('<InstanceGroupList />', () => {
|
||||
});
|
||||
|
||||
test('should render deletion error modal', async () => {
|
||||
jest.setTimeout(5000 * 4);
|
||||
InstanceGroupsAPI.destroy.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
|
||||
258
awx/ui_next/src/screens/Metrics/LineChart.jsx
Normal file
258
awx/ui_next/src/screens/Metrics/LineChart.jsx
Normal file
@ -0,0 +1,258 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import * as d3 from 'd3';
|
||||
|
||||
function LineChart({ data, helpText }) {
|
||||
const count = data[0]?.values.length;
|
||||
const draw = useCallback(() => {
|
||||
const margin = 80;
|
||||
const getWidth = () => {
|
||||
let width;
|
||||
// This is in an a try/catch due to an error from jest.
|
||||
// Even though the d3.select returns a valid selector with
|
||||
// style function, it says it is null in the test
|
||||
try {
|
||||
width =
|
||||
parseInt(d3.select(`#chart`).style('width'), 10) - margin || 700;
|
||||
} catch (error) {
|
||||
width = 700;
|
||||
}
|
||||
|
||||
return width;
|
||||
};
|
||||
const width = getWidth();
|
||||
const height = 500;
|
||||
const duration = 250;
|
||||
const circleRadius = 6;
|
||||
const circleRadiusHover = 8;
|
||||
|
||||
/* Scale */
|
||||
let smallestY;
|
||||
let largestY;
|
||||
data.map(line =>
|
||||
line.values.forEach(value => {
|
||||
if (smallestY === undefined) {
|
||||
smallestY = value.y;
|
||||
}
|
||||
if (value.y < smallestY) {
|
||||
smallestY = value.y;
|
||||
}
|
||||
if (largestY === undefined) {
|
||||
largestY = smallestY + 10;
|
||||
}
|
||||
if (value.y > largestY) {
|
||||
largestY = value.y;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const xScale = d3
|
||||
.scaleLinear()
|
||||
.domain(
|
||||
d3.max(data[0].values, d => d.x) > 49
|
||||
? d3.extent(data[0].values, d => d.x)
|
||||
: [0, 50]
|
||||
)
|
||||
.range([0, width - margin]);
|
||||
|
||||
const yScale = d3
|
||||
.scaleLinear()
|
||||
.domain([smallestY, largestY])
|
||||
.range([height - margin, 0]);
|
||||
|
||||
const color = d3.scaleOrdinal(d3.schemeCategory10);
|
||||
|
||||
/* Add SVG */
|
||||
d3.selectAll(`#chart > *`).remove();
|
||||
|
||||
const renderTooltip = d => {
|
||||
d3.selectAll(`.tooltip > *`).remove();
|
||||
|
||||
d3.select('#chart')
|
||||
.append('span')
|
||||
.attr('class', 'tooltip')
|
||||
.attr('stroke', 'black')
|
||||
.attr('fill', 'white')
|
||||
.style('padding-left', '50px');
|
||||
const tooltip = {};
|
||||
data.map(datum => {
|
||||
datum.values.forEach(value => {
|
||||
if (d.x === value.x) {
|
||||
tooltip[datum.name] = value.y;
|
||||
}
|
||||
});
|
||||
return tooltip;
|
||||
});
|
||||
Object.entries(tooltip).forEach(([key, value], i) => {
|
||||
d3.select('.tooltip')
|
||||
.append('span')
|
||||
.attr('class', 'tooltip-text-wrapper')
|
||||
.append('text')
|
||||
.attr('class', 'tooltip-text')
|
||||
.style('color', color(i))
|
||||
.style('padding-right', '20px')
|
||||
.text(`${key}: ${value}`);
|
||||
});
|
||||
};
|
||||
const removeTooltip = () => {
|
||||
d3.select('.tooltip')
|
||||
.style('cursor', 'none')
|
||||
.selectAll(`.tooltip > *`)
|
||||
.remove();
|
||||
};
|
||||
|
||||
// Add legend
|
||||
d3.selectAll(`.legend > *`).remove();
|
||||
const legendContainer = d3
|
||||
.select('#chart')
|
||||
.append('div')
|
||||
.style('display', 'flex')
|
||||
.attr('class', 'legend')
|
||||
.attr('height', '400px')
|
||||
.attr('width', '500px')
|
||||
.style('padding-left', '50px');
|
||||
|
||||
legendContainer
|
||||
.append('text')
|
||||
.attr('class', 'legend-title')
|
||||
.attr('x', '100')
|
||||
.attr('y', '50')
|
||||
.text('Legend');
|
||||
|
||||
legendContainer.data(data, (d, i) => {
|
||||
if (d?.name) {
|
||||
const legendItemContainer = legendContainer
|
||||
.append('div')
|
||||
.style('display', 'flex')
|
||||
.attr('id', 'legend-item-container')
|
||||
.style('padding-left', '20px');
|
||||
|
||||
legendItemContainer
|
||||
.append('div')
|
||||
.style('background-color', color(i))
|
||||
.style('height', '8px')
|
||||
.style('width', '8px')
|
||||
.style('border-radius', '50%')
|
||||
.style('padding', '5px')
|
||||
.style('margin-top', '6px');
|
||||
|
||||
legendItemContainer
|
||||
.append('text')
|
||||
.style('padding-left', '20px')
|
||||
.text(d.name);
|
||||
}
|
||||
});
|
||||
|
||||
// Add help text to top of chart
|
||||
|
||||
d3.select('#chart')
|
||||
.append('div')
|
||||
.attr('class', 'help-text')
|
||||
.style('padding-left', '50px')
|
||||
.style('padding-top', '20px')
|
||||
.text(helpText);
|
||||
|
||||
const svg = d3
|
||||
.select('#chart')
|
||||
.append('svg')
|
||||
.attr('width', `${width + margin}px`)
|
||||
.attr('height', `${height + margin}px`)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin}, ${margin})`);
|
||||
|
||||
/* Add line into SVG */
|
||||
const line = d3
|
||||
.line()
|
||||
.curve(d3.curveMonotoneX)
|
||||
.x(d => xScale(d.x))
|
||||
.y(d => yScale(d.y));
|
||||
|
||||
const lines = svg.append('g');
|
||||
|
||||
lines
|
||||
.selectAll('.line-group')
|
||||
.data(data)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'line-group')
|
||||
.append('path')
|
||||
.attr('class', 'line')
|
||||
.style('fill', 'none')
|
||||
.attr('d', d => line(d.values))
|
||||
.style('stroke', (d, i) => color(i))
|
||||
.style('stroke-width', '3px');
|
||||
|
||||
/* Add circles in the line */
|
||||
lines
|
||||
.selectAll('circle-group')
|
||||
.data(data)
|
||||
.enter()
|
||||
.append('g')
|
||||
.style('fill', (d, i) => color(i))
|
||||
.selectAll('circle')
|
||||
.data(d => d.values)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'circle')
|
||||
.on('mouseover', (d, i) => {
|
||||
if (data.length) {
|
||||
renderTooltip(d, i);
|
||||
}
|
||||
})
|
||||
.on('mouseout', () => {
|
||||
removeTooltip();
|
||||
})
|
||||
.append('circle')
|
||||
.attr('cx', d => xScale(d.x))
|
||||
.attr('cy', d => yScale(d.y))
|
||||
.attr('r', circleRadius)
|
||||
.on('mouseover', () => {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('r', circleRadiusHover);
|
||||
})
|
||||
.on('mouseout', () => {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('r', circleRadius);
|
||||
});
|
||||
|
||||
/* Add Axis into SVG */
|
||||
const xAxis = d3
|
||||
.axisBottom(xScale)
|
||||
.ticks(data[0].values.length > 5 ? data[0].values.length : 5);
|
||||
const yAxis = d3.axisLeft(yScale).ticks(5);
|
||||
|
||||
svg
|
||||
.append('g')
|
||||
.attr('class', 'x axis')
|
||||
.attr('transform', `translate(0, ${height - margin})`)
|
||||
.call(xAxis);
|
||||
|
||||
svg
|
||||
.append('g')
|
||||
.attr('class', 'y axis')
|
||||
.call(yAxis);
|
||||
}, [data, helpText]);
|
||||
|
||||
useEffect(() => {
|
||||
draw();
|
||||
}, [count, draw]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
draw();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
handleResize();
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [draw]);
|
||||
|
||||
return <div id="chart" />;
|
||||
}
|
||||
|
||||
export default LineChart;
|
||||
37
awx/ui_next/src/screens/Metrics/LineChart.test.jsx
Normal file
37
awx/ui_next/src/screens/Metrics/LineChart.test.jsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import LineChart from './LineChart';
|
||||
|
||||
describe('<LineChart/>', () => {
|
||||
test('should render properly', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<LineChart
|
||||
data={[
|
||||
{
|
||||
name: 'Instance 1',
|
||||
values: [
|
||||
{ x: 0, y: 10 },
|
||||
{ x: 1, y: 20 },
|
||||
{ x: 3, y: 30 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Instance 1',
|
||||
values: [
|
||||
{ x: 0, y: 40 },
|
||||
{ x: 1, y: 50 },
|
||||
{ x: 3, y: 60 },
|
||||
],
|
||||
},
|
||||
]}
|
||||
helpText="This is the help text"
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('LineChart').length).toBe(1);
|
||||
});
|
||||
});
|
||||
243
awx/ui_next/src/screens/Metrics/Metrics.jsx
Normal file
243
awx/ui_next/src/screens/Metrics/Metrics.jsx
Normal file
@ -0,0 +1,243 @@
|
||||
import React, { useEffect, useCallback, useState, useRef } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
PageSection,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Toolbar,
|
||||
ToolbarGroup,
|
||||
ToolbarContent,
|
||||
ToolbarItem,
|
||||
Select,
|
||||
SelectOption,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import LineChart from './LineChart';
|
||||
import { MetricsAPI, InstancesAPI } from '../../api';
|
||||
import useRequest from '../../util/useRequest';
|
||||
import ContentEmpty from '../../components/ContentEmpty';
|
||||
import ScreenHeader from '../../components/ScreenHeader/ScreenHeader';
|
||||
import ContentError from '../../components/ContentError';
|
||||
|
||||
let count = [0];
|
||||
|
||||
// hook thats calls api every 3 seconds to get data
|
||||
function useInterval(callback, delay, instance, metric) {
|
||||
const savedCallback = useRef();
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
count.push(count.length);
|
||||
if (instance && metric) {
|
||||
savedCallback.current();
|
||||
}
|
||||
}
|
||||
|
||||
const id = setInterval(tick, delay);
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
};
|
||||
}, [callback, delay, instance, metric]);
|
||||
return { count };
|
||||
}
|
||||
function Metrics({ i18n }) {
|
||||
const [instanceIsOpen, setInstanceIsOpen] = useState(false);
|
||||
const [instance, setInstance] = useState(null);
|
||||
const [metric, setMetric] = useState(null);
|
||||
const [metricIsOpen, setMetricIsOpen] = useState(false);
|
||||
const [renderedData, setRenderedData] = useState([]);
|
||||
const {
|
||||
result: { instances, metrics },
|
||||
error: fetchInitialError,
|
||||
request: fetchInstances,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const [
|
||||
{
|
||||
data: { results },
|
||||
},
|
||||
{ data: mets },
|
||||
] = await Promise.all([
|
||||
InstancesAPI.read(),
|
||||
MetricsAPI.read({
|
||||
subsystemonly: 1,
|
||||
format: 'json',
|
||||
}),
|
||||
]);
|
||||
|
||||
const metricOptions = Object.keys(mets);
|
||||
|
||||
return {
|
||||
instances: [...results.map(result => result.hostname), 'All'],
|
||||
metrics: metricOptions,
|
||||
};
|
||||
}, []),
|
||||
{ instances: [], metrics: [] }
|
||||
);
|
||||
|
||||
const {
|
||||
result: helpText,
|
||||
error: updateError,
|
||||
request: fetchData,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await MetricsAPI.read({
|
||||
subsystemonly: 1,
|
||||
format: 'json',
|
||||
node: instance === 'All' ? null : instance,
|
||||
metric,
|
||||
});
|
||||
|
||||
const rendered = renderedData;
|
||||
const instanceData = Object.values(data);
|
||||
instanceData.forEach(value => {
|
||||
value.samples.forEach(sample => {
|
||||
instances.forEach(i => {
|
||||
if (i === sample.labels.node) {
|
||||
const renderedIndex = renderedData.findIndex(rd => rd.name === i);
|
||||
|
||||
if (renderedIndex === -1) {
|
||||
rendered.push({
|
||||
name: i,
|
||||
values: [
|
||||
{
|
||||
y: sample.value,
|
||||
x: count.length - 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (
|
||||
rendered[renderedIndex].values?.length === 0 ||
|
||||
!rendered[renderedIndex].values
|
||||
) {
|
||||
rendered[renderedIndex].values = [
|
||||
{ y: sample.value, x: count.length - 1 },
|
||||
];
|
||||
} else {
|
||||
rendered[renderedIndex].values = [
|
||||
...rendered[renderedIndex].values,
|
||||
{ y: sample.value, x: count.length - 1 },
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
let countRestrictedData = rendered;
|
||||
if (count.length > 49) {
|
||||
countRestrictedData = rendered.map(({ values, name }) => ({
|
||||
name,
|
||||
values: values.slice(-50),
|
||||
}));
|
||||
}
|
||||
|
||||
setRenderedData(countRestrictedData);
|
||||
return data[metric].help_text;
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [instance, metric, instances]),
|
||||
''
|
||||
);
|
||||
|
||||
useInterval(fetchData, 3000, instance, metric);
|
||||
|
||||
useEffect(() => {
|
||||
if (instance && metric) {
|
||||
fetchData();
|
||||
}
|
||||
}, [fetchData, instance, metric]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInstances();
|
||||
}, [fetchInstances]);
|
||||
if (fetchInitialError || updateError) {
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<ContentError error={fetchInitialError || updateError} />;
|
||||
</CardBody>
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ScreenHeader breadcrumbConfig={{ '/metrics': i18n._(t`Metrics`) }} />
|
||||
|
||||
<PageSection>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem>{i18n._(t`Instance`)}</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Select
|
||||
ouiaId="Instance-select"
|
||||
onToggle={setInstanceIsOpen}
|
||||
isOpen={instanceIsOpen}
|
||||
onSelect={(e, value) => {
|
||||
count = [0];
|
||||
setInstance(value);
|
||||
setInstanceIsOpen(false);
|
||||
setRenderedData([]);
|
||||
}}
|
||||
selections={instance}
|
||||
placeholderText={i18n._(t`Select a instance`)}
|
||||
>
|
||||
{instances.map(inst => (
|
||||
<SelectOption value={inst} key={inst} />
|
||||
))}
|
||||
</Select>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>{i18n._(t`Metric`)}</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Select
|
||||
ouiaId="Metric-select"
|
||||
placeholderText={i18n._(t`Select a metric`)}
|
||||
isOpen={metricIsOpen}
|
||||
onSelect={(e, value) => {
|
||||
count = [0];
|
||||
setMetric(value);
|
||||
setRenderedData([]);
|
||||
setMetricIsOpen(false);
|
||||
}}
|
||||
onToggle={setMetricIsOpen}
|
||||
selections={metric}
|
||||
>
|
||||
{metrics.map(met => (
|
||||
<SelectOption value={met} key={met} />
|
||||
))}
|
||||
</Select>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{instance && metric ? (
|
||||
Object.keys(renderedData).length > 0 && (
|
||||
<LineChart
|
||||
data={renderedData}
|
||||
count={count}
|
||||
helpText={helpText}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<ContentEmpty
|
||||
title={i18n._(t`Select an instance and a metric to show chart`)}
|
||||
/>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(Metrics);
|
||||
72
awx/ui_next/src/screens/Metrics/Metrics.test.jsx
Normal file
72
awx/ui_next/src/screens/Metrics/Metrics.test.jsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import Metrics from './Metrics';
|
||||
import { MetricsAPI, InstancesAPI } from '../../api';
|
||||
|
||||
jest.mock('../../api/models/Instances');
|
||||
jest.mock('../../api/models/Metrics');
|
||||
InstancesAPI.read.mockResolvedValue({
|
||||
data: { results: [{ hostname: 'instance 1' }, { hostname: 'instance 2' }] },
|
||||
});
|
||||
MetricsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
metric1: {
|
||||
helptext: 'metric 1 help text',
|
||||
samples: [{ labels: { node: 'metric 1' }, value: 20 }],
|
||||
},
|
||||
metric2: {
|
||||
helptext: 'metric 2 help text',
|
||||
samples: [{ labels: { node: 'metric 2' }, value: 10 }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('<Metrics/>', () => {
|
||||
let wrapper;
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<Metrics />);
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('should mound properly', () => {
|
||||
expect(wrapper.find('Metrics').length).toBe(1);
|
||||
expect(wrapper.find('EmptyStateBody').length).toBe(1);
|
||||
expect(wrapper.find('ChartLine').length).toBe(0);
|
||||
});
|
||||
test('should render chart after selecting metric and instance', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Select[ouiaId="Instance-select"]').prop('onToggle')(true);
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('SelectOption[value="instance 1"]')
|
||||
.find('button')
|
||||
.prop('onClick')({}, 'instance 1');
|
||||
});
|
||||
wrapper.update();
|
||||
await act(() => {
|
||||
wrapper.find('Select[ouiaId="Metric-select"]').prop('onToggle')(true);
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('SelectOption[value="metric1"]')
|
||||
.find('button')
|
||||
.prop('onClick')({}, 'metric1');
|
||||
});
|
||||
wrapper.update();
|
||||
expect(MetricsAPI.read).toBeCalledWith({
|
||||
subsystemonly: 1,
|
||||
format: 'json',
|
||||
metric: 'metric1',
|
||||
node: 'instance 1',
|
||||
});
|
||||
});
|
||||
});
|
||||
1
awx/ui_next/src/screens/Metrics/index.js
Normal file
1
awx/ui_next/src/screens/Metrics/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Metrics';
|
||||
Loading…
x
Reference in New Issue
Block a user