diff --git a/awx/ui/src/api/index.js b/awx/ui/src/api/index.js
index 5876efc6f1..93631c2137 100644
--- a/awx/ui/src/api/index.js
+++ b/awx/ui/src/api/index.js
@@ -33,6 +33,7 @@ import Roles from './models/Roles';
import Root from './models/Root';
import Schedules from './models/Schedules';
import Settings from './models/Settings';
+import SubscriptionUsage from './models/SubscriptionUsage';
import SystemJobs from './models/SystemJobs';
import SystemJobTemplates from './models/SystemJobTemplates';
import Teams from './models/Teams';
@@ -82,6 +83,7 @@ const RolesAPI = new Roles();
const RootAPI = new Root();
const SchedulesAPI = new Schedules();
const SettingsAPI = new Settings();
+const SubscriptionUsageAPI = new SubscriptionUsage();
const SystemJobsAPI = new SystemJobs();
const SystemJobTemplatesAPI = new SystemJobTemplates();
const TeamsAPI = new Teams();
@@ -132,6 +134,7 @@ export {
RootAPI,
SchedulesAPI,
SettingsAPI,
+ SubscriptionUsageAPI,
SystemJobsAPI,
SystemJobTemplatesAPI,
TeamsAPI,
diff --git a/awx/ui/src/api/models/SubscriptionUsage.js b/awx/ui/src/api/models/SubscriptionUsage.js
new file mode 100644
index 0000000000..d5831e3a6c
--- /dev/null
+++ b/awx/ui/src/api/models/SubscriptionUsage.js
@@ -0,0 +1,16 @@
+import Base from '../Base';
+
+class SubscriptionUsage extends Base {
+ constructor(http) {
+ super(http);
+ this.baseUrl = 'api/v2/host_metric_summary_monthly/';
+ }
+
+ readSubscriptionUsageChart(dateRange) {
+ return this.http.get(
+ `${this.baseUrl}?date__gte=${dateRange}&order_by=date&page_size=100`
+ );
+ }
+}
+
+export default SubscriptionUsage;
diff --git a/awx/ui/src/routeConfig.js b/awx/ui/src/routeConfig.js
index 1c9b1a498d..f96fec5c84 100644
--- a/awx/ui/src/routeConfig.js
+++ b/awx/ui/src/routeConfig.js
@@ -17,6 +17,7 @@ import Organizations from 'screens/Organization';
import Projects from 'screens/Project';
import Schedules from 'screens/Schedule';
import Settings from 'screens/Setting';
+import SubscriptionUsage from 'screens/SubscriptionUsage/SubscriptionUsage';
import Teams from 'screens/Team';
import Templates from 'screens/Template';
import TopologyView from 'screens/TopologyView';
@@ -61,6 +62,11 @@ function getRouteConfig(userProfile = {}) {
path: '/host_metrics',
screen: HostMetrics,
},
+ {
+ title: Subscription Usage,
+ path: '/subscription_usage',
+ screen: SubscriptionUsage,
+ },
],
},
{
@@ -189,6 +195,7 @@ function getRouteConfig(userProfile = {}) {
'unique_managed_hosts'
) {
deleteRoute('host_metrics');
+ deleteRoute('subscription_usage');
}
if (userProfile?.isSuperUser || userProfile?.isSystemAuditor)
return routeConfig;
@@ -197,6 +204,7 @@ function getRouteConfig(userProfile = {}) {
deleteRoute('management_jobs');
deleteRoute('topology_view');
deleteRoute('instances');
+ deleteRoute('subscription_usage');
if (userProfile?.isOrgAdmin) return routeConfig;
if (!userProfile?.isNotificationAdmin) deleteRoute('notification_templates');
diff --git a/awx/ui/src/routeConfig.test.js b/awx/ui/src/routeConfig.test.js
index 7db858f08c..ffbd2587ee 100644
--- a/awx/ui/src/routeConfig.test.js
+++ b/awx/ui/src/routeConfig.test.js
@@ -31,6 +31,7 @@ describe('getRouteConfig', () => {
'/activity_stream',
'/workflow_approvals',
'/host_metrics',
+ '/subscription_usage',
'/templates',
'/credentials',
'/projects',
@@ -61,6 +62,7 @@ describe('getRouteConfig', () => {
'/activity_stream',
'/workflow_approvals',
'/host_metrics',
+ '/subscription_usage',
'/templates',
'/credentials',
'/projects',
diff --git a/awx/ui/src/screens/SubscriptionUsage/ChartComponents/UsageChart.js b/awx/ui/src/screens/SubscriptionUsage/ChartComponents/UsageChart.js
new file mode 100644
index 0000000000..38fdb51d6d
--- /dev/null
+++ b/awx/ui/src/screens/SubscriptionUsage/ChartComponents/UsageChart.js
@@ -0,0 +1,319 @@
+import React, { useEffect, useCallback } from 'react';
+import { string, number, shape, arrayOf } from 'prop-types';
+import * as d3 from 'd3';
+import { t } from '@lingui/macro';
+import { PageContextConsumer } from '@patternfly/react-core';
+import UsageChartTooltip from './UsageChartTooltip';
+
+function UsageChart({ id, data, height, pageContext }) {
+ const { isNavOpen } = pageContext;
+
+ // Methods
+ const draw = useCallback(() => {
+ const margin = { top: 15, right: 25, bottom: 105, left: 70 };
+
+ 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(`#${id}`).style('width'), 10) -
+ margin.left -
+ margin.right || 700;
+ } catch (error) {
+ width = 700;
+ }
+ return width;
+ };
+
+ // Clear our chart container element first
+ d3.selectAll(`#${id} > *`).remove();
+ const width = getWidth();
+
+ function transition(path) {
+ path.transition().duration(1000).attrTween('stroke-dasharray', tweenDash);
+ }
+
+ function tweenDash(...params) {
+ const l = params[2][params[1]].getTotalLength();
+ const i = d3.interpolateString(`0,${l}`, `${l},${l}`);
+ return (val) => i(val);
+ }
+
+ const x = d3.scaleTime().rangeRound([0, width]);
+ const y = d3.scaleLinear().range([height, 0]);
+
+ // [consumed, capacity]
+ const colors = d3.scaleOrdinal(['#06C', '#C9190B']);
+ const svg = d3
+ .select(`#${id}`)
+ .append('svg')
+ .attr('width', width + margin.left + margin.right)
+ .attr('height', height + margin.top + margin.bottom)
+ .attr('z', 100)
+ .append('g')
+ .attr('id', 'chart-container')
+ .attr('transform', `translate(${margin.left}, ${margin.top})`);
+ // Tooltip
+ const tooltip = new UsageChartTooltip({
+ svg: `#${id}`,
+ colors,
+ label: t`Hosts`,
+ });
+
+ const parseTime = d3.timeParse('%Y-%m-%d');
+
+ const formattedData = data?.reduce(
+ (formatted, { date, license_consumed, license_capacity }) => {
+ const MONTH = parseTime(date);
+ const CONSUMED = +license_consumed;
+ const CAPACITY = +license_capacity;
+ return formatted.concat({ MONTH, CONSUMED, CAPACITY });
+ },
+ []
+ );
+
+ // Scale the range of the data
+ const largestY = formattedData?.reduce((a_max, b) => {
+ const b_max = Math.max(b.CONSUMED > b.CAPACITY ? b.CONSUMED : b.CAPACITY);
+ return a_max > b_max ? a_max : b_max;
+ }, 0);
+ x.domain(d3.extent(formattedData, (d) => d.MONTH));
+ y.domain([
+ 0,
+ largestY > 4 ? largestY + Math.max(largestY / 10, 1) : 5,
+ ]).nice();
+
+ const capacityLine = d3
+ .line()
+ .curve(d3.curveMonotoneX)
+ .x((d) => x(d.MONTH))
+ .y((d) => y(d.CAPACITY));
+
+ const consumedLine = d3
+ .line()
+ .curve(d3.curveMonotoneX)
+ .x((d) => x(d.MONTH))
+ .y((d) => y(d.CONSUMED));
+
+ // Add the Y Axis
+ svg
+ .append('g')
+ .attr('class', 'y-axis')
+ .call(
+ d3
+ .axisLeft(y)
+ .ticks(
+ largestY > 3
+ ? Math.min(largestY + Math.max(largestY / 10, 1), 10)
+ : 5
+ )
+ .tickSize(-width)
+ .tickFormat(d3.format('d'))
+ )
+ .selectAll('line')
+ .attr('stroke', '#d7d7d7');
+ svg.selectAll('.y-axis .tick text').attr('x', -5).attr('font-size', '14');
+
+ // text label for the y axis
+ svg
+ .append('text')
+ .attr('transform', 'rotate(-90)')
+ .attr('y', 0 - margin.left)
+ .attr('x', 0 - height / 2)
+ .attr('dy', '1em')
+ .style('text-anchor', 'middle')
+ .text(t`Unique Hosts`);
+
+ // Add the X Axis
+ let ticks;
+ const maxTicks = Math.round(
+ formattedData.length / (formattedData.length / 2)
+ );
+ ticks = formattedData.map((d) => d.MONTH);
+ if (formattedData.length === 13) {
+ ticks = formattedData
+ .map((d, i) => (i % maxTicks === 0 ? d.MONTH : undefined))
+ .filter((item) => item);
+ }
+
+ svg.select('.domain').attr('stroke', '#d7d7d7');
+
+ svg
+ .append('g')
+ .attr('class', 'x-axis')
+ .attr('transform', `translate(0, ${height})`)
+ .call(
+ d3
+ .axisBottom(x)
+ .tickValues(ticks)
+ .tickSize(-height)
+ .tickFormat(d3.timeFormat('%m/%y'))
+ )
+ .selectAll('line')
+ .attr('stroke', '#d7d7d7');
+
+ svg
+ .selectAll('.x-axis .tick text')
+ .attr('x', -25)
+ .attr('font-size', '14')
+ .attr('transform', 'rotate(-65)');
+
+ // text label for the x axis
+ svg
+ .append('text')
+ .attr(
+ 'transform',
+ `translate(${width / 2} , ${height + margin.top + 50})`
+ )
+ .style('text-anchor', 'middle')
+ .text(t`Month`);
+ const vertical = svg
+ .append('path')
+ .attr('class', 'mouse-line')
+ .style('stroke', 'black')
+ .style('stroke-width', '3px')
+ .style('stroke-dasharray', '3, 3')
+ .style('opacity', '0');
+
+ const handleMouseOver = (event, d) => {
+ tooltip.handleMouseOver(event, d);
+ // show vertical line
+ vertical.transition().style('opacity', '1');
+ };
+ const handleMouseMove = function mouseMove(event) {
+ const [pointerX] = d3.pointer(event);
+ vertical.attr('d', () => `M${pointerX},${height} ${pointerX},${0}`);
+ };
+
+ const handleMouseOut = () => {
+ // hide tooltip
+ tooltip.handleMouseOut();
+ // hide vertical line
+ vertical.transition().style('opacity', 0);
+ };
+
+ const dateFormat = d3.timeFormat('%m/%y');
+
+ // Add the consumed line path
+ svg
+ .append('path')
+ .data([formattedData])
+ .attr('class', 'line')
+ .style('fill', 'none')
+ .style('stroke', () => colors(1))
+ .attr('stroke-width', 2)
+ .attr('d', consumedLine)
+ .call(transition);
+
+ // create our consumed line circles
+
+ svg
+ .selectAll('dot')
+ .data(formattedData)
+ .enter()
+ .append('circle')
+ .attr('r', 3)
+ .style('stroke', () => colors(1))
+ .style('fill', () => colors(1))
+ .attr('cx', (d) => x(d.MONTH))
+ .attr('cy', (d) => y(d.CONSUMED))
+ .attr('id', (d) => `consumed-dot-${dateFormat(d.MONTH)}`)
+ .on('mouseover', (event, d) => handleMouseOver(event, d))
+ .on('mousemove', handleMouseMove)
+ .on('mouseout', handleMouseOut);
+
+ // Add the capacity line path
+ svg
+ .append('path')
+ .data([formattedData])
+ .attr('class', 'line')
+ .style('fill', 'none')
+ .style('stroke', () => colors(0))
+ .attr('stroke-width', 2)
+ .attr('d', capacityLine)
+ .call(transition);
+
+ // create our capacity line circles
+
+ svg
+ .selectAll('dot')
+ .data(formattedData)
+ .enter()
+ .append('circle')
+ .attr('r', 3)
+ .style('stroke', () => colors(0))
+ .style('fill', () => colors(0))
+ .attr('cx', (d) => x(d.MONTH))
+ .attr('cy', (d) => y(d.CAPACITY))
+ .attr('id', (d) => `capacity-dot-${dateFormat(d.MONTH)}`)
+ .on('mouseover', handleMouseOver)
+ .on('mousemove', handleMouseMove)
+ .on('mouseout', handleMouseOut);
+
+ // Create legend
+ const legend_keys = [t`Subscriptions consumed`, t`Subscription capacity`];
+ let totalWidth = width / 2 - 175;
+
+ const lineLegend = svg
+ .selectAll('.lineLegend')
+ .data(legend_keys)
+ .enter()
+ .append('g')
+ .attr('class', 'lineLegend')
+ .each(function formatLegend() {
+ const current = d3.select(this);
+ current.attr('transform', `translate(${totalWidth}, ${height + 90})`);
+ totalWidth += 200;
+ });
+
+ lineLegend
+ .append('text')
+ .text((d) => d)
+ .attr('font-size', '14')
+ .attr('transform', 'translate(15,9)'); // align texts with boxes
+
+ lineLegend
+ .append('rect')
+ .attr('fill', (d) => colors(d))
+ .attr('width', 10)
+ .attr('height', 10);
+ }, [data, height, id]);
+
+ useEffect(() => {
+ draw();
+ }, [draw, isNavOpen]);
+
+ useEffect(() => {
+ function handleResize() {
+ draw();
+ }
+
+ window.addEventListener('resize', handleResize);
+
+ handleResize();
+
+ return () => window.removeEventListener('resize', handleResize);
+ }, [draw]);
+
+ return
;
+}
+
+UsageChart.propTypes = {
+ id: string.isRequired,
+ data: arrayOf(shape({})).isRequired,
+ height: number.isRequired,
+};
+
+const withPageContext = (Component) =>
+ function contextComponent(props) {
+ return (
+
+ {(pageContext) => }
+
+ );
+ };
+
+export default withPageContext(UsageChart);
diff --git a/awx/ui/src/screens/SubscriptionUsage/ChartComponents/UsageChartTooltip.js b/awx/ui/src/screens/SubscriptionUsage/ChartComponents/UsageChartTooltip.js
new file mode 100644
index 0000000000..7e8439c6a0
--- /dev/null
+++ b/awx/ui/src/screens/SubscriptionUsage/ChartComponents/UsageChartTooltip.js
@@ -0,0 +1,177 @@
+import * as d3 from 'd3';
+import { t } from '@lingui/macro';
+
+class UsageChartTooltip {
+ constructor(opts) {
+ this.label = opts.label;
+ this.svg = opts.svg;
+ this.colors = opts.colors;
+
+ this.draw();
+ }
+
+ draw() {
+ this.toolTipBase = d3.select(`${this.svg} > svg`).append('g');
+ this.toolTipBase.attr('id', 'chart-tooltip');
+ this.toolTipBase.attr('overflow', 'visible');
+ this.toolTipBase.style('opacity', 0);
+ this.toolTipBase.style('pointer-events', 'none');
+ this.toolTipBase.attr('transform', 'translate(100, 100)');
+ this.boxWidth = 200;
+ this.textWidthThreshold = 20;
+
+ this.toolTipPoint = this.toolTipBase
+ .append('rect')
+ .attr('transform', 'translate(10, -10) rotate(45)')
+ .attr('x', 0)
+ .attr('y', 0)
+ .attr('height', 20)
+ .attr('width', 20)
+ .attr('fill', '#393f44');
+ this.boundingBox = this.toolTipBase
+ .append('rect')
+ .attr('x', 10)
+ .attr('y', -41)
+ .attr('rx', 2)
+ .attr('height', 82)
+ .attr('width', this.boxWidth)
+ .attr('fill', '#393f44');
+ this.circleBlue = this.toolTipBase
+ .append('circle')
+ .attr('cx', 26)
+ .attr('cy', 0)
+ .attr('r', 7)
+ .attr('stroke', 'white')
+ .attr('fill', this.colors(1));
+ this.circleRed = this.toolTipBase
+ .append('circle')
+ .attr('cx', 26)
+ .attr('cy', 26)
+ .attr('r', 7)
+ .attr('stroke', 'white')
+ .attr('fill', this.colors(0));
+ this.consumedText = this.toolTipBase
+ .append('text')
+ .attr('x', 43)
+ .attr('y', 4)
+ .attr('font-size', 12)
+ .attr('fill', 'white')
+ .text(t`Subscriptions consumed`);
+ this.capacityText = this.toolTipBase
+ .append('text')
+ .attr('x', 43)
+ .attr('y', 28)
+ .attr('font-size', 12)
+ .attr('fill', 'white')
+ .text(t`Subscription capacity`);
+ this.icon = this.toolTipBase
+ .append('text')
+ .attr('fill', 'white')
+ .attr('stroke', 'white')
+ .attr('x', 24)
+ .attr('y', 30)
+ .attr('font-size', 12);
+ this.consumed = this.toolTipBase
+ .append('text')
+ .attr('fill', 'white')
+ .attr('font-size', 12)
+ .attr('x', 122)
+ .attr('y', 4)
+ .attr('id', 'consumed-count')
+ .text('0');
+ this.capacity = this.toolTipBase
+ .append('text')
+ .attr('fill', 'white')
+ .attr('font-size', 12)
+ .attr('x', 122)
+ .attr('y', 28)
+ .attr('id', 'capacity-count')
+ .text('0');
+ this.date = this.toolTipBase
+ .append('text')
+ .attr('fill', 'white')
+ .attr('stroke', 'white')
+ .attr('x', 20)
+ .attr('y', -21)
+ .attr('font-size', 12);
+ }
+
+ handleMouseOver = (event, data) => {
+ let consumed = 0;
+ let capacity = 0;
+ const [x, y] = d3.pointer(event);
+ const tooltipPointerX = x + 75;
+
+ const formatTooltipDate = d3.timeFormat('%m/%y');
+ if (!event) {
+ return;
+ }
+
+ const toolTipWidth = this.toolTipBase.node().getBoundingClientRect().width;
+ const chartWidth = d3
+ .select(`${this.svg}> svg`)
+ .node()
+ .getBoundingClientRect().width;
+ const overflow = 100 - (toolTipWidth / chartWidth) * 100;
+ const flipped = overflow < (tooltipPointerX / chartWidth) * 100;
+ if (data) {
+ consumed = data.CONSUMED || 0;
+ capacity = data.CAPACITY || 0;
+ this.date.text(formatTooltipDate(data.MONTH || null));
+ }
+
+ this.capacity.text(`${capacity}`);
+ this.consumed.text(`${consumed}`);
+ this.consumedTextWidth = this.consumed.node().getComputedTextLength();
+ this.capacityTextWidth = this.capacity.node().getComputedTextLength();
+
+ const maxTextPerc = (this.jobsWidth / this.boxWidth) * 100;
+ const threshold = 40;
+ const overage = maxTextPerc / threshold;
+ let adjustedWidth;
+ if (maxTextPerc > threshold) {
+ adjustedWidth = this.boxWidth * overage;
+ } else {
+ adjustedWidth = this.boxWidth;
+ }
+
+ this.boundingBox.attr('width', adjustedWidth);
+ this.toolTipBase.attr('transform', `translate(${tooltipPointerX}, ${y})`);
+ if (flipped) {
+ this.toolTipPoint.attr('transform', 'translate(-20, -10) rotate(45)');
+ this.boundingBox.attr('x', -adjustedWidth - 20);
+ this.circleBlue.attr('cx', -adjustedWidth);
+ this.circleRed.attr('cx', -adjustedWidth);
+ this.icon.attr('x', -adjustedWidth - 2);
+ this.consumedText.attr('x', -adjustedWidth + 17);
+ this.capacityText.attr('x', -adjustedWidth + 17);
+ this.consumed.attr('x', -this.consumedTextWidth - 20 - 12);
+ this.capacity.attr('x', -this.capacityTextWidth - 20 - 12);
+ this.date.attr('x', -adjustedWidth - 5);
+ } else {
+ this.toolTipPoint.attr('transform', 'translate(10, -10) rotate(45)');
+ this.boundingBox.attr('x', 10);
+ this.circleBlue.attr('cx', 26);
+ this.circleRed.attr('cx', 26);
+ this.icon.attr('x', 24);
+ this.consumedText.attr('x', 43);
+ this.capacityText.attr('x', 43);
+ this.consumed.attr('x', adjustedWidth - this.consumedTextWidth);
+ this.capacity.attr('x', adjustedWidth - this.capacityTextWidth);
+ this.date.attr('x', 20);
+ }
+
+ this.toolTipBase.style('opacity', 1);
+ this.toolTipBase.interrupt();
+ };
+
+ handleMouseOut = () => {
+ this.toolTipBase
+ .transition()
+ .delay(15)
+ .style('opacity', 0)
+ .style('pointer-events', 'none');
+ };
+}
+
+export default UsageChartTooltip;
diff --git a/awx/ui/src/screens/SubscriptionUsage/SubscriptionUsage.js b/awx/ui/src/screens/SubscriptionUsage/SubscriptionUsage.js
new file mode 100644
index 0000000000..a453424deb
--- /dev/null
+++ b/awx/ui/src/screens/SubscriptionUsage/SubscriptionUsage.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import styled from 'styled-components';
+
+import { t, Trans } from '@lingui/macro';
+import { Banner, Card, PageSection } from '@patternfly/react-core';
+import { InfoCircleIcon } from '@patternfly/react-icons';
+
+import { useConfig } from 'contexts/Config';
+import useBrandName from 'hooks/useBrandName';
+import ScreenHeader from 'components/ScreenHeader';
+import SubscriptionUsageChart from './SubscriptionUsageChart';
+
+const MainPageSection = styled(PageSection)`
+ padding-top: 24px;
+ padding-bottom: 0;
+
+ & .spacer {
+ margin-bottom: var(--pf-global--spacer--lg);
+ }
+`;
+
+function SubscriptionUsage() {
+ const config = useConfig();
+ const brandName = useBrandName();
+
+ return (
+ <>
+ {config?.ui_next && (
+
+
+
+ A tech preview of the new {brandName} user
+ interface can be found here.
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default SubscriptionUsage;
diff --git a/awx/ui/src/screens/SubscriptionUsage/SubscriptionUsageChart.js b/awx/ui/src/screens/SubscriptionUsage/SubscriptionUsageChart.js
new file mode 100644
index 0000000000..134447db7d
--- /dev/null
+++ b/awx/ui/src/screens/SubscriptionUsage/SubscriptionUsageChart.js
@@ -0,0 +1,167 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import styled from 'styled-components';
+
+import { t } from '@lingui/macro';
+import {
+ Card,
+ CardHeader,
+ CardActions,
+ CardBody,
+ CardTitle,
+ Flex,
+ FlexItem,
+ PageSection,
+ Select,
+ SelectVariant,
+ SelectOption,
+ Text,
+} from '@patternfly/react-core';
+
+import useRequest from 'hooks/useRequest';
+import { SubscriptionUsageAPI } from 'api';
+import { useUserProfile } from 'contexts/Config';
+import ContentLoading from 'components/ContentLoading';
+import UsageChart from './ChartComponents/UsageChart';
+
+const GraphCardHeader = styled(CardHeader)`
+ margin-bottom: var(--pf-global--spacer--lg);
+`;
+
+const ChartCardTitle = styled(CardTitle)`
+ padding-right: 24px;
+ font-size: 20px;
+ font-weight: var(--pf-c-title--m-xl--FontWeight);
+`;
+
+const CardText = styled(Text)`
+ padding-right: 24px;
+`;
+
+const GraphCardActions = styled(CardActions)`
+ margin-left: initial;
+ padding-left: 0;
+`;
+
+function SubscriptionUsageChart() {
+ const [isPeriodDropdownOpen, setIsPeriodDropdownOpen] = useState(false);
+ const [periodSelection, setPeriodSelection] = useState('year');
+ const userProfile = useUserProfile();
+
+ const calculateDateRange = () => {
+ const today = new Date();
+ let date = '';
+ switch (periodSelection) {
+ case 'year':
+ date =
+ today.getMonth() < 10
+ ? `${today.getFullYear() - 1}-0${today.getMonth() + 1}-01`
+ : `${today.getFullYear() - 1}-${today.getMonth() + 1}-01`;
+ break;
+ case 'two_years':
+ date =
+ today.getMonth() < 10
+ ? `${today.getFullYear() - 2}-0${today.getMonth() + 1}-01`
+ : `${today.getFullYear() - 2}-${today.getMonth() + 1}-01`;
+ break;
+ case 'three_years':
+ date =
+ today.getMonth() < 10
+ ? `${today.getFullYear() - 3}-0${today.getMonth() + 1}-01`
+ : `${today.getFullYear() - 3}-${today.getMonth() + 1}-01`;
+ break;
+ default:
+ date =
+ today.getMonth() < 10
+ ? `${today.getFullYear() - 1}-0${today.getMonth() + 1}-01`
+ : `${today.getFullYear() - 1}-${today.getMonth() + 1}-01`;
+ break;
+ }
+ return date;
+ };
+
+ const {
+ isLoading,
+ result: subscriptionUsageChartData,
+ request: fetchSubscriptionUsageChart,
+ } = useRequest(
+ useCallback(async () => {
+ const data = await SubscriptionUsageAPI.readSubscriptionUsageChart(
+ calculateDateRange()
+ );
+ return data.data.results;
+ }, [periodSelection]),
+ []
+ );
+
+ useEffect(() => {
+ fetchSubscriptionUsageChart();
+ }, [fetchSubscriptionUsageChart, periodSelection]);
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {t`Subscription Compliance`}
+
+
+
+ {t`Last recalculation date:`}{' '}
+ {userProfile.systemConfig.HOST_METRIC_SUMMARY_TASK_LAST_TS.slice(
+ 0,
+ 10
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+export default SubscriptionUsageChart;