mirror of
https://github.com/ansible/awx.git
synced 2026-03-13 23:17:32 -02:30
Adds observability metrics chart
wip tooltip renders with colors and disappears scales y axis properly adds legend without buttlets adds legend data but needs styling adds legend, and cleans up code show help text
This commit is contained in:
@@ -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 ObservabilityMetrics from './screens/ObservabilityMetrics';
|
||||
|
||||
import getRouteConfig from './routeConfig';
|
||||
import SubscriptionEdit from './screens/Setting/Subscription/SubscriptionEdit';
|
||||
@@ -87,6 +88,7 @@ function App() {
|
||||
const { hash, search, pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<<<<<<< HEAD
|
||||
<I18nProvider i18n={i18n}>
|
||||
<Background>
|
||||
<Switch>
|
||||
@@ -108,6 +110,47 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
</Switch>
|
||||
</Background>
|
||||
=======
|
||||
<I18nProvider language={language} catalogs={catalogs}>
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Background>
|
||||
<Switch>
|
||||
<Route exact strict path="/*/">
|
||||
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
|
||||
</Route>
|
||||
<Route path="/login">
|
||||
<Login isAuthenticated={isAuthenticated} />
|
||||
</Route>
|
||||
<Route exact path="/">
|
||||
<Redirect to="/home" />
|
||||
</Route>
|
||||
<ProtectedRoute>
|
||||
<AppContainer navRouteConfig={getRouteConfig(i18n)}>
|
||||
<Switch>
|
||||
{getRouteConfig(i18n)
|
||||
.flatMap(({ routes }) => routes)
|
||||
.map(({ path, screen: Screen }) => (
|
||||
<ProtectedRoute key={path} path={path}>
|
||||
<Screen match={match} />
|
||||
</ProtectedRoute>
|
||||
))
|
||||
.concat(
|
||||
<Route exact path="/metrics">
|
||||
<ObservabilityMetrics />
|
||||
</Route>,
|
||||
<ProtectedRoute key="not-found" path="*">
|
||||
<NotFound />
|
||||
</ProtectedRoute>
|
||||
)}
|
||||
</Switch>
|
||||
</AppContainer>
|
||||
</ProtectedRoute>
|
||||
</Switch>
|
||||
</Background>
|
||||
)}
|
||||
</I18n>
|
||||
>>>>>>> Adds observability metrics chart
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
272
awx/ui_next/src/screens/ObservabilityMetrics/LineChart.jsx
Normal file
272
awx/ui_next/src/screens/ObservabilityMetrics/LineChart.jsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import * as d3 from 'd3';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { PageContextConsumer } from '@patternfly/react-core';
|
||||
|
||||
function LineChart({ data, i18n, helpText }) {
|
||||
console.log(data, 'data');
|
||||
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;
|
||||
data.map(line =>
|
||||
line.values.forEach(value => {
|
||||
if (smallestY === undefined) {
|
||||
smallestY = value.y;
|
||||
}
|
||||
if (value.y < smallestY) {
|
||||
smallestY = value.y;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const xScale = d3
|
||||
.scaleLinear()
|
||||
.domain(d3.extent(data[0].values, d => d.x))
|
||||
.range([0, width - margin]);
|
||||
|
||||
const yScale = d3
|
||||
.scaleLinear()
|
||||
.domain([smallestY, d3.max(data[0].values, d => d.y)])
|
||||
.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)
|
||||
.append('text')
|
||||
.attr('x', 30)
|
||||
.attr('y', 30)
|
||||
.attr('fill', '#000')
|
||||
.text(i18n._(t`Count`));
|
||||
|
||||
svg
|
||||
.append('g')
|
||||
.attr('class', 'y axis')
|
||||
.call(yAxis)
|
||||
.append('text')
|
||||
.attr('y', -30)
|
||||
.attr('x', 0)
|
||||
.attr('transform', 'rotate(-90)')
|
||||
.attr('fill', '#000')
|
||||
.text(i18n._(t`Values`));
|
||||
}, [data, i18n, helpText]);
|
||||
|
||||
useEffect(() => {
|
||||
draw();
|
||||
}, [count, draw]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
draw();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
handleResize();
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [draw]);
|
||||
|
||||
return <div id="chart" />;
|
||||
}
|
||||
|
||||
const withPageContext = Component => {
|
||||
return function contextComponent(props) {
|
||||
return (
|
||||
<PageContextConsumer>
|
||||
{pageContext => <Component {...props} pageContext={pageContext} />}
|
||||
</PageContextConsumer>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default withI18n()(withPageContext(LineChart));
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
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 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 ObservabilityMetrics({ 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) {
|
||||
console.log('here');
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<ContentError error={fetchInitialError || updateError} />;
|
||||
</CardBody>
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<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 an 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 an 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()(ObservabilityMetrics);
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import ObservabilityMetrics from './ObservabilityMetrics';
|
||||
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('<ObservabilityMetrics/>', () => {
|
||||
let wrapper;
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<ObservabilityMetrics />);
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('should mound properly', () => {
|
||||
expect(wrapper.find('ObservabilityMetrics').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/ObservabilityMetrics/index.js
Normal file
1
awx/ui_next/src/screens/ObservabilityMetrics/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './ObservabilityMetrics';
|
||||
Reference in New Issue
Block a user