feat: [SIG-557]: added Billing usage graph - daily and weekly (#4686)

* feat: added Billing usage graph - daily and weekly

* feat: removed mocked response

* feat: removed weekly chart and fixed data transformations

* feat: added loading states

* feat: moved function to util file

* feat: fixed review comments

* feat: fixed JEST test case

* feat: test fix - commit

* feat: test fix - commit

* feat: test fix - commit

* feat: edited title conditionally

* feat: edited tooltip content

* feat: removed time from tooltip content and skeleton for cycleInfo Alert

---------

Co-authored-by: Sagar Rajput <sagarrajput@192.168.1.2>
This commit is contained in:
SagarRajput-7 2024-03-13 14:30:49 +05:30 committed by GitHub
parent 506448fe61
commit c6080ca02e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 498 additions and 123 deletions

View File

@ -1,13 +1,29 @@
.billing-container {
padding: 16px 0;
width: 100%;
padding-top: 36px;
width: 65%;
.billing-summary {
margin: 24px 8px;
}
.billing-details {
margin: 36px 8px;
margin: 24px 0px;
.ant-table-title {
color: var(--bg-vanilla-400);
background-color: rgb(27, 28, 32);
}
.ant-table-cell {
background-color: var(--bg-ink-400);
border-color: var(--bg-slate-500);
}
.ant-table-tbody {
td {
border-color: var(--bg-slate-500);
}
}
}
.upgrade-plan-benefits {
@ -24,6 +40,15 @@
}
}
}
.empty-graph-card {
.ant-card-body {
height: 40vh;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.ant-skeleton.ant-skeleton-element.ant-skeleton-active {
@ -34,3 +59,20 @@
.ant-skeleton.ant-skeleton-element .ant-skeleton-input {
min-width: 100% !important;
}
.lightMode {
.billing-container {
.billing-details {
.ant-table-cell {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-200);
}
.ant-table-tbody {
td {
border-color: var(--bg-vanilla-200);
}
}
}
}
}

View File

@ -12,13 +12,36 @@ import BillingContainer from './BillingContainer';
const lisenceUrl = 'http://localhost/api/v2/licenses';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
describe('BillingContainer', () => {
test('Component should render', async () => {
act(() => {
render(<BillingContainer />);
});
const unit = screen.getAllByText(/unit/i);
expect(unit[1]).toBeInTheDocument();
const dataInjection = screen.getByRole('columnheader', {
name: /data ingested/i,
});
@ -32,24 +55,15 @@ describe('BillingContainer', () => {
});
expect(cost).toBeInTheDocument();
const total = screen.getByRole('cell', {
name: /total/i,
});
expect(total).toBeInTheDocument();
const manageBilling = screen.getByRole('button', {
name: /manage billing/i,
});
expect(manageBilling).toBeInTheDocument();
const dollar = screen.getByRole('cell', {
name: /\$0/i,
});
const dollar = screen.getByText(/\$0/i);
expect(dollar).toBeInTheDocument();
const currentBill = screen.getByRole('heading', {
name: /current bill total/i,
});
const currentBill = screen.getByText('Billing');
expect(currentBill).toBeInTheDocument();
});
@ -61,9 +75,7 @@ describe('BillingContainer', () => {
const freeTrailText = await screen.findByText('Free Trial');
expect(freeTrailText).toBeInTheDocument();
const currentBill = await screen.findByRole('heading', {
name: /current bill total/i,
});
const currentBill = screen.getByText('Billing');
expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findByText(/\$0/i);
@ -102,9 +114,7 @@ describe('BillingContainer', () => {
render(<BillingContainer />);
});
const currentBill = await screen.findByRole('heading', {
name: /current bill total/i,
});
const currentBill = screen.getByText('Billing');
expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findByText(/\$0/i);
@ -137,45 +147,30 @@ describe('BillingContainer', () => {
res(ctx.status(200), ctx.json(notOfTrailResponse)),
),
);
render(<BillingContainer />);
const { findByText } = render(<BillingContainer />);
const billingPeriodText = `Your current billing period is from ${getFormattedDate(
billingSuccessResponse.data.billingPeriodStart,
)} to ${getFormattedDate(billingSuccessResponse.data.billingPeriodEnd)}`;
const billingPeriod = await screen.findByRole('heading', {
name: new RegExp(billingPeriodText, 'i'),
});
const billingPeriod = await findByText(billingPeriodText);
expect(billingPeriod).toBeInTheDocument();
const currentBill = await screen.findByRole('heading', {
name: /current bill total/i,
});
const currentBill = screen.getByText('Billing');
expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findAllByText(/\$1278.3/i);
expect(dollar0[0]).toBeInTheDocument();
expect(dollar0.length).toBe(2);
const dollar0 = await screen.findByText(/\$1,278.3/i);
expect(dollar0).toBeInTheDocument();
const metricsRow = await screen.findByRole('row', {
name: /metrics Million 4012 0.1 \$ 401.2/i,
name: /metrics 4012 Million 0.1 \$ 401.2/i,
});
expect(metricsRow).toBeInTheDocument();
const logRow = await screen.findByRole('row', {
name: /Logs GB 497 0.4 \$ 198.8/i,
name: /Logs 497 GB 0.4 \$ 198.8/i,
});
expect(logRow).toBeInTheDocument();
const totalBill = await screen.findByRole('cell', {
name: /\$1278/i,
});
expect(totalBill).toBeInTheDocument();
const totalBillRow = await screen.findByRole('row', {
name: /total \$1278/i,
});
expect(totalBillRow).toBeInTheDocument();
});
test('Should render corrent day remaining in billing period', async () => {

View File

@ -2,11 +2,24 @@
import './BillingContainer.styles.scss';
import { CheckCircleOutlined } from '@ant-design/icons';
import { Button, Col, Row, Skeleton, Table, Tag, Typography } from 'antd';
import { Color } from '@signozhq/design-tokens';
import {
Alert,
Button,
Card,
Col,
Flex,
Row,
Skeleton,
Table,
Tag,
Typography,
} from 'antd';
import { ColumnsType } from 'antd/es/table';
import updateCreditCardApi from 'api/billing/checkout';
import getUsage from 'api/billing/getUsage';
import manageCreditCardApi from 'api/billing/manage';
import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useAnalytics from 'hooks/analytics/useAnalytics';
@ -22,8 +35,11 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
interface DataType {
key: string;
name: string;
@ -104,12 +120,11 @@ export default function BillingContainer(): JSX.Element {
const daysRemainingStr = 'days remaining in your billing period.';
const [headerText, setHeaderText] = useState('');
const [billAmount, setBillAmount] = useState(0);
const [totalBillAmount, setTotalBillAmount] = useState(0);
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [daysRemaining, setDaysRemaining] = useState(0);
const [isFreeTrial, setIsFreeTrial] = useState(false);
const [data, setData] = useState<any[]>([]);
const billCurrency = '$';
const [apiResponse, setApiResponse] = useState<any>({});
const { trackEvent } = useAnalytics();
@ -120,10 +135,12 @@ export default function BillingContainer(): JSX.Element {
const handleError = useAxiosError();
const isCloudUserVal = isCloudUser();
const processUsageData = useCallback(
(data: any): void => {
const {
details: { breakdown = [], total, billTotal },
details: { breakdown = [], billTotal },
billingPeriodStart,
billingPeriodEnd,
} = data?.payload || {};
@ -141,8 +158,7 @@ export default function BillingContainer(): JSX.Element {
formattedUsageData.push({
key: `${index}${i}`,
name: i === 0 ? element?.type : '',
unit: element?.unit,
dataIngested: tier.quantity,
dataIngested: `${tier.quantity} ${element?.unit}`,
pricePerUnit: tier.unitPrice,
cost: `$ ${tier.tierCost}`,
});
@ -152,7 +168,6 @@ export default function BillingContainer(): JSX.Element {
}
setData(formattedUsageData);
setTotalBillAmount(total);
if (!licensesData?.payload?.onTrial) {
const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
@ -165,6 +180,8 @@ export default function BillingContainer(): JSX.Element {
setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
setBillAmount(billTotal);
}
setApiResponse(data?.payload || {});
},
[licensesData?.payload?.onTrial],
);
@ -208,11 +225,6 @@ export default function BillingContainer(): JSX.Element {
key: 'name',
render: (text): JSX.Element => <div>{text}</div>,
},
{
title: 'Unit',
dataIndex: 'unit',
key: 'unit',
},
{
title: 'Data Ingested',
dataIndex: 'dataIngested',
@ -230,24 +242,6 @@ export default function BillingContainer(): JSX.Element {
},
];
const renderSummary = (): JSX.Element => (
<Table.Summary.Row>
<Table.Summary.Cell index={0}>
<Typography.Title level={3} style={{ fontWeight: 500, margin: ' 0px' }}>
Total
</Typography.Title>
</Table.Summary.Cell>
<Table.Summary.Cell index={1}> &nbsp; </Table.Summary.Cell>
<Table.Summary.Cell index={2}> &nbsp;</Table.Summary.Cell>
<Table.Summary.Cell index={3}> &nbsp; </Table.Summary.Cell>
<Table.Summary.Cell index={4}>
<Typography.Title level={3} style={{ fontWeight: 500, margin: ' 0px' }}>
${totalBillAmount}
</Typography.Title>
</Table.Summary.Cell>
</Table.Summary.Row>
);
const renderTableSkeleton = (): JSX.Element => (
<Table
dataSource={dummyData}
@ -336,66 +330,81 @@ export default function BillingContainer(): JSX.Element {
updateCreditCard,
]);
const BillingUsageGraphCallback = useCallback(
() =>
!isLoading ? (
<BillingUsageGraph data={apiResponse} billAmount={billAmount} />
) : (
<Card className="empty-graph-card" bordered={false}>
<Spinner size="large" tip="Loading..." height="35vh" />
</Card>
),
[apiResponse, billAmount, isLoading],
);
return (
<div className="billing-container">
<Row
justify="space-between"
align="middle"
gutter={[16, 16]}
style={{
margin: 0,
}}
<Flex vertical style={{ marginBottom: 16 }}>
<Typography.Text style={{ fontWeight: 500, fontSize: 18 }}>
Billing
</Typography.Text>
<Typography.Text color={Color.BG_VANILLA_400}>
Manage your billing information, invoices, and monitor costs.
</Typography.Text>
</Flex>
<Card
bordered={false}
style={{ minHeight: 150, marginBottom: 16 }}
className="page-info"
>
<Col span={20}>
<Typography.Title level={4} ellipsis style={{ fontWeight: '300' }}>
{headerText}
</Typography.Title>
{licensesData?.payload?.onTrial &&
licensesData?.payload?.trialConvertedToSubscription && (
<Typography.Title
level={5}
ellipsis
style={{ fontWeight: '300', color: '#49aa19' }}
>
We have received your card details, your billing will only start after
the end of your free trial period.
</Typography.Title>
)}
</Col>
<Col span={4} style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Flex justify="space-between" align="center">
<Flex vertical>
<Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
{isCloudUserVal ? 'Enterprise Cloud' : 'Enterprise'}{' '}
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
</Typography.Title>
<Typography.Text style={{ fontSize: 12, color: Color.BG_VANILLA_400 }}>
{daysRemaining} {daysRemainingStr}
</Typography.Text>
</Flex>
<Button
type="primary"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading}
onClick={handleBilling}
>
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
? 'Upgrade Plan'
: 'Manage Billing'}
</Button>
</Col>
</Row>
</Flex>
<div className="billing-summary">
<Typography.Title level={4} style={{ margin: '16px 0' }}>
Current bill total
</Typography.Title>
{licensesData?.payload?.onTrial &&
licensesData?.payload?.trialConvertedToSubscription && (
<Typography.Text
ellipsis
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
>
We have received your card details, your billing will only start after
the end of your free trial period.
</Typography.Text>
)}
<Typography.Title
level={3}
style={{ margin: '16px 0', display: 'flex', alignItems: 'center' }}
>
{billCurrency}
{billAmount} &nbsp;
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
</Typography.Title>
{!isLoading ? (
<Alert
message={headerText}
type="info"
showIcon
style={{ marginTop: 12 }}
/>
) : (
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
)}
</Card>
<Typography.Paragraph style={{ margin: '16px 0' }}>
{daysRemaining} {daysRemainingStr}
</Typography.Paragraph>
</div>
<BillingUsageGraphCallback />
<div className="billing-details">
{!isLoading && (
@ -403,7 +412,7 @@ export default function BillingContainer(): JSX.Element {
columns={columns}
dataSource={data}
pagination={false}
summary={renderSummary}
bordered={false}
/>
)}

View File

@ -0,0 +1,29 @@
.billing-graph-card {
.ant-card-body {
height: 40vh;
.uplot-graph-container {
padding: 8px;
}
}
.total-spent {
font-family: 'SF Mono' monospace;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 24px;
}
.total-spent-title {
font-size: 12px;
font-weight: 500;
line-height: 22px;
letter-spacing: 0.48px;
color: rgba(255, 255, 255, 0.5);
}
}
.lightMode {
.total-spent-title {
color: var(--bg-ink-100);
}
}

View File

@ -0,0 +1,190 @@
import './BillingUsageGraph.styles.scss';
import '../../../lib/uPlotLib/uPlotLib.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Card, Flex, Typography } from 'antd';
import { getComponentForPanelType } from 'constants/panelTypes';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PropsTypePropsMap } from 'container/GridPanelSwitch/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import tooltipPlugin from 'lib/uPlotLib/plugins/tooltipPlugin';
import getAxes from 'lib/uPlotLib/utils/getAxes';
import getRenderer from 'lib/uPlotLib/utils/getRenderer';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { getXAxisScale } from 'lib/uPlotLib/utils/getXAxisScale';
import { getYAxisScale } from 'lib/uPlotLib/utils/getYAxisScale';
import { FC, useMemo, useRef } from 'react';
import uPlot from 'uplot';
import {
convertDataToMetricRangePayload,
fillMissingValuesForQuantities,
} from './utils';
interface BillingUsageGraphProps {
data: any;
billAmount: number;
}
const paths = (
u: any,
seriesIdx: number,
idx0: number,
idx1: number,
extendGap: boolean,
buildClip: boolean,
): uPlot.Series.PathBuilder => {
const s = u.series[seriesIdx];
const style = s.drawStyle;
const interp = s.lineInterpolation;
const renderer = getRenderer(style, interp);
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
};
export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
const { data, billAmount } = props;
const graphCompatibleData = useMemo(
() => convertDataToMetricRangePayload(data),
[data],
);
const chartData = getUPlotChartData(graphCompatibleData);
const graphRef = useRef<HTMLDivElement>(null);
const isDarkMode = useIsDarkMode();
const containerDimensions = useResizeObserver(graphRef);
const { billingPeriodStart: startTime, billingPeriodEnd: endTime } = data;
const Component = getComponentForPanelType(PANEL_TYPES.BAR) as FC<
PropsTypePropsMap[PANEL_TYPES]
>;
const getGraphSeries = (color: string, label: string): any => ({
drawStyle: 'bars',
paths,
lineInterpolation: 'spline',
show: true,
label,
fill: color,
stroke: color,
width: 2,
spanGaps: true,
points: {
size: 5,
show: false,
stroke: color,
},
});
const uPlotSeries: any = useMemo(
() => [
{ label: 'Timestamp', stroke: 'purple' },
getGraphSeries(
'#DECCBC',
graphCompatibleData.data.result[0]?.legend as string,
),
getGraphSeries(
'#4E74F8',
graphCompatibleData.data.result[1]?.legend as string,
),
getGraphSeries(
'#F24769',
graphCompatibleData.data.result[2]?.legend as string,
),
],
[graphCompatibleData.data.result],
);
const axesOptions = getAxes(isDarkMode, '');
const optionsForChart: uPlot.Options = useMemo(
() => ({
id: 'billing-usage-breakdown',
series: uPlotSeries,
width: containerDimensions.width,
height: containerDimensions.height - 30,
axes: [
{
...axesOptions[0],
grid: {
...axesOptions.grid,
show: false,
stroke: isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400,
},
},
{
...axesOptions[1],
stroke: isDarkMode ? Color.BG_SLATE_200 : Color.BG_INK_400,
},
],
scales: {
x: {
...getXAxisScale(startTime - 86400, endTime), // Minus 86400 from startTime to decrease a day to have a buffer start
},
y: {
...getYAxisScale({
series: graphCompatibleData?.data.newResult.data.result,
yAxisUnit: '',
softMax: null,
softMin: null,
}),
},
},
legend: {
show: true,
live: false,
isolate: true,
},
cursor: {
lock: false,
focus: {
prox: 1e6,
bias: 1,
},
},
focus: {
alpha: 0.3,
},
padding: [32, 32, 16, 16],
plugins: [
tooltipPlugin(
fillMissingValuesForQuantities(graphCompatibleData, chartData[0]),
'',
true,
),
],
}),
[
axesOptions,
chartData,
containerDimensions.height,
containerDimensions.width,
endTime,
graphCompatibleData,
isDarkMode,
startTime,
uPlotSeries,
],
);
const numberFormatter = new Intl.NumberFormat('en-US');
return (
<Card bordered={false} className="billing-graph-card">
<Flex justify="space-between">
<Flex vertical gap={6}>
<Typography.Text className="total-spent-title">
TOTAL SPENT
</Typography.Text>
<Typography.Text color={Color.BG_VANILLA_100} className="total-spent">
${numberFormatter.format(billAmount)}
</Typography.Text>
</Flex>
</Flex>
<div ref={graphRef} style={{ height: '100%', paddingBottom: 48 }}>
<Component data={chartData} options={optionsForChart} />
</div>
</Card>
);
}

View File

@ -0,0 +1,87 @@
import { isEmpty, isNull } from 'lodash-es';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export const convertDataToMetricRangePayload = (
data: any,
): MetricRangePayloadProps => {
const emptyStateData = {
data: {
newResult: { data: { result: [], resultType: '' } },
result: [],
resultType: '',
},
};
if (isEmpty(data)) {
return emptyStateData;
}
const {
details: { breakdown = [] },
} = data || {};
if (isNull(breakdown) || breakdown.length === 0) {
return emptyStateData;
}
const payload = breakdown.map((info: any) => {
const metric = info.type;
const sortedBreakdownData = (info?.dayWiseBreakdown?.breakdown || []).sort(
(a: any, b: any) => a.timestamp - b.timestamp,
);
const values = (sortedBreakdownData || []).map((categoryInfo: any) => [
categoryInfo.timestamp,
categoryInfo.total,
]);
const queryName = info.type;
const legend = info.type;
const { unit } = info;
const quantity = sortedBreakdownData.map(
(categoryInfo: any) => categoryInfo.quantity,
);
return { metric, values, queryName, legend, quantity, unit };
});
const sortedData = payload.sort((a: any, b: any) => {
const sumA = a.values.reduce((acc: any, val: any) => acc + val[1], 0);
const avgA = a.values.length ? sumA / a.values.length : 0;
const sumB = b.values.reduce((acc: any, val: any) => acc + val[1], 0);
const avgB = b.values.length ? sumB / b.values.length : 0;
return sumA === sumB ? avgB - avgA : sumB - sumA;
});
return {
data: {
newResult: { data: { result: sortedData, resultType: '' } },
result: sortedData,
resultType: '',
},
};
};
export function fillMissingValuesForQuantities(
data: any,
timestampArray: number[],
): MetricRangePayloadProps {
const { result } = data.data;
const transformedResultArr: any[] = [];
result.forEach((item: any) => {
const timestampToQuantityMap: { [timestamp: number]: number } = {};
item.values.forEach((val: number[], index: number) => {
timestampToQuantityMap[val[0]] = item.quantity[index];
});
const quantityArray = timestampArray.map(
(timestamp: number) => timestampToQuantityMap[timestamp] ?? null,
);
transformedResultArr.push({ ...item, quantity: quantityArray });
});
return {
data: {
newResult: { data: { result: transformedResultArr, resultType: '' } },
result: transformedResultArr,
resultType: '',
},
};
}

View File

@ -27,6 +27,7 @@ const generateTooltipContent = (
idx: number,
yAxisUnit?: string,
series?: uPlot.Options['series'],
isBillingUsageGraphs?: boolean,
// eslint-disable-next-line sonarjs/cognitive-complexity
): HTMLElement => {
const container = document.createElement('div');
@ -49,12 +50,22 @@ const generateTooltipContent = (
if (Array.isArray(series) && series.length > 0) {
series.forEach((item, index) => {
if (index === 0) {
tooltipTitle = dayjs(data[0][idx] * 1000).format('MMM DD YYYY HH:mm:ss');
if (isBillingUsageGraphs) {
tooltipTitle = dayjs(data[0][idx] * 1000).format('MMM DD YYYY');
} else {
tooltipTitle = dayjs(data[0][idx] * 1000).format('MMM DD YYYY HH:mm:ss');
}
} else if (item.show) {
const { metric = {}, queryName = '', legend = '' } =
seriesList[index - 1] || {};
const {
metric = {},
queryName = '',
legend = '',
quantity = [],
unit = '',
} = seriesList[index - 1] || {};
const value = data[index][idx];
const dataIngested = quantity[idx];
const label = getLabelName(metric, queryName || '', legend || '');
const color = generateColor(label, themeColors.chartcolors);
@ -63,6 +74,7 @@ const generateTooltipContent = (
if (Number.isFinite(value)) {
const tooltipValue = getToolTipValue(value, yAxisUnit);
const dataIngestedFormated = getToolTipValue(dataIngested);
if (
duplicatedLegendLabels[label] ||
Object.prototype.hasOwnProperty.call(formattedData, label)
@ -93,7 +105,9 @@ const generateTooltipContent = (
value,
tooltipValue,
queryName,
textContent: `${tooltipItemLabel} : ${tooltipValue}`,
textContent: isBillingUsageGraphs
? `${tooltipItemLabel} : $${tooltipValue} - ${dataIngestedFormated} ${unit}`
: `${tooltipItemLabel} : ${tooltipValue}`,
};
tooltipCount += 1;
@ -168,6 +182,7 @@ const generateTooltipContent = (
const tooltipPlugin = (
apiResponse: MetricRangePayloadProps | undefined,
yAxisUnit?: string,
isBillingUsageGraphs?: boolean,
): any => {
let over: HTMLElement;
let bound: HTMLElement;
@ -228,6 +243,7 @@ const tooltipPlugin = (
idx,
yAxisUnit,
u.series,
isBillingUsageGraphs,
);
overlay.appendChild(content);
placement(overlay, anchor, 'right', 'start', { bound });

View File

@ -2,4 +2,6 @@
display: flex;
width: 100%;
color: #fff;
justify-content: center;
align-items: center;
}

View File

@ -14,6 +14,8 @@ export interface QueryData {
queryName: string;
legend?: string;
values: [number, string][];
quantity?: number[];
unit?: string;
}
export interface SeriesItem {
@ -28,6 +30,9 @@ export interface QueryDataV3 {
queryName: string;
legend?: string;
series: SeriesItem[] | null;
quantity?: number;
unitPrice?: number;
unit?: string;
}
export interface Props {