Skip to content

Commit

Permalink
Working charts
Browse files Browse the repository at this point in the history
  • Loading branch information
MauAraujo committed May 3, 2024
1 parent 5386b58 commit 025d7d2
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 44 deletions.
16 changes: 12 additions & 4 deletions api/types/billing_metronome.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,23 +123,31 @@ type ListCustomerCostsRequest struct {
Limit int `schema:"limit"`
}

// Cost is the cost for a customer in a specific time range
type Cost struct {
StartTimestamp string `json:"start_timestamp"`
EndTimestamp string `json:"end_timestamp"`
CreditTypes map[string]CreditTypeCost `json:"credit_types"`
}

// CreditTypeCost is the cost for a specific credit type (e.g. CPU hours)
type CreditTypeCost struct {
Name string `json:"name"`
Cost float64 `json:"cost"`
LineItemBreakdown []LineItemBreakdownCost `json:"line_item_breakdown"`
}

// LineItemBreakdownCost is the cost breakdown by line item
type LineItemBreakdownCost struct {
Name string `json:"name"`
Cost float64 `json:"cost"`
GroupKey string `json:"group_key"`
GroupValue string `json:"group_value"`
Name string `json:"name"`
Cost float64 `json:"cost"`
}

// FormattedCost is the cost for a customer in a specific time range, flattened from the Metronome response
type FormattedCost struct {
StartTimestamp string `json:"start_timestamp"`
EndTimestamp string `json:"end_timestamp"`
Cost float64 `json:"cost"`
}

type Plan struct {
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/lib/billing/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export type Cost = z.infer<typeof CostValidator>;
export const CostValidator = z.object({
start_timestamp: z.string(),
end_timestamp: z.string(),
credit_types: z.any(),
cost: z.number(),
});

export type InvoiceList = Invoice[];
Expand Down
28 changes: 19 additions & 9 deletions dashboard/src/lib/hooks/useMetronome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { useContext } from "react";
import { useQuery } from "@tanstack/react-query";

import {
CostList,
CostValidator,
CreditGrantsValidator,
InvoiceValidator,
PlanValidator,
ReferralDetailsValidator,
UsageValidator,
type CostList,
type CreditGrants,
type InvoiceList,
type Plan,
Expand Down Expand Up @@ -115,8 +115,9 @@ export const useCustomerPlan = (): TGetPlan => {
};

export const useCustomerUsage = (
windowSize: string,
currentPeriod: boolean
startingOn: Date | null,
endingBefore: Date | null,
windowSize: string
): TGetUsage => {
const { currentProject } = useContext(Context);

Expand All @@ -132,12 +133,17 @@ export const useCustomerUsage = (
return null;
}

if (startingOn === null || endingBefore === null) {
return null;
}

try {
const res = await api.getCustomerUsage(
"<token>",
{
starting_on: startingOn.toISOString(),
ending_before: endingBefore.toISOString(),
window_size: windowSize,
current_period: currentPeriod,
},
{
project_id: currentProject?.id,
Expand All @@ -157,8 +163,8 @@ export const useCustomerUsage = (
};

export const useCustomerCosts = (
startingOn: string,
endingBefore: string,
startingOn: Date | null,
endingBefore: Date | null,
limit: number
): TGetCosts => {
const { currentProject } = useContext(Context);
Expand All @@ -175,15 +181,19 @@ export const useCustomerCosts = (
return null;
}

if (startingOn === null || endingBefore === null) {
return null;
}

try {
const res = await api.getCustomerCosts(
"<token>",
{},
{
project_id: currentProject?.id,
starting_on: startingOn,
ending_before: endingBefore,
limit: limit,
starting_on: startingOn.toISOString(),
ending_before: endingBefore.toISOString(),
limit,
}
);

Expand Down
116 changes: 88 additions & 28 deletions dashboard/src/main/home/project-settings/UsagePage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, { useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import styled from "styled-components";

import Fieldset from "components/porter/Fieldset";
Expand All @@ -13,20 +15,49 @@ import {

import Bars from "./Bars";

dayjs.extend(utc);

function UsagePage(): JSX.Element {
const [currentPeriodStart, setCurrentPeriodStart] = useState("4-17-24");
const [currentPeriodEnd, setCurrentPeriodEnd] = useState("4-17-24");
const costLimitDays = 30;
const [currentPeriodStart, setCurrentPeriodStart] = useState<Date | null>(
null
);
const [currentPeriodEnd, setCurrentPeriodEnd] = useState<Date | null>(null);
const [currentPeriodDuration, setcurrentPeriodDuration] = useState(30);

const { usage } = useCustomerUsage("day", true);
const { costs } = useCustomerCosts(
const { usage } = useCustomerUsage(
currentPeriodStart,
currentPeriodEnd,
costLimitDays
"day"
);
const { plan } = useCustomerPlan();
const { costs } = useCustomerCosts(
currentPeriodStart,
currentPeriodEnd,
currentPeriodDuration
);
let totalCost = 0;

// Initial period setup
useEffect(() => {
if (plan) {
const now = new Date();
const endDate = dayjs.utc(now).startOf("day").toDate();
const startDate = dayjs
.utc(now)
.subtract(1, "month")
.startOf("day")
.toDate();

const processedData = useMemo(() => {
// Set the limit to the current period's number of days
const numberOfDays = startDate.getUTCDate();

setcurrentPeriodDuration(numberOfDays);
setCurrentPeriodStart(startDate);
setCurrentPeriodEnd(endDate);
}
}, [plan]);

const processedUsage = useMemo(() => {
const before = usage;
const resultMap = new Map();

Expand All @@ -36,12 +67,12 @@ function UsagePage(): JSX.Element {
usage_metrics: Array<{ starting_on: string; value: number }>;
}) => {
const metricName = metric.metric_name.toLowerCase().replace(" ", "_");
metric.usage_metrics.forEach(({ starting_on, value }) => {
if (resultMap.has(starting_on)) {
resultMap.get(starting_on)[metricName] = value;
metric.usage_metrics.forEach(({ starting_on: startingOn, value }) => {
if (resultMap.has(startingOn)) {
resultMap.get(startingOn)[metricName] = value;
} else {
resultMap.set(starting_on, {
starting_on: new Date(starting_on).toLocaleDateString("en-US", {
resultMap.set(startingOn, {
starting_on: new Date(startingOn).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
}),
Expand All @@ -57,16 +88,44 @@ function UsagePage(): JSX.Element {
return x;
}, [usage]);

const processedCosts = useMemo(() => {
return costs?.map((dailyCost) => {
dailyCost.start_timestamp = new Date(
dailyCost.start_timestamp
).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
dailyCost.cost = dailyCost.cost / 100;
totalCost += dailyCost.cost;
return dailyCost;
});
}, [costs]);

const generateOptions = (): Array<{ value: string; label: string }> => {
const options = [];

let startDate = dayjs.utc(currentPeriodStart);
const endDate = dayjs.utc(currentPeriodEnd);

while (startDate.isBefore(endDate)) {
const nextDate = startDate.add(1, "month");
options.push({
value: startDate.format("M-D-YY"),
label: `${startDate.format("M/D/YY")} - ${nextDate.format("M/D/YY")}`,
});

startDate = startDate.add(1, "month");
}
return options;
};

const options = generateOptions();

return (
<>
<Select
options={[
{ value: "4-17-24", label: "4/17/24 - 5/17/24" },
{ value: "3-17-24", label: "3/17/24 - 4/17/24" },
{ value: "2-17-24", label: "2/17/24 - 3/17/24" },
{ value: "1-17-24", label: "1/17/24 - 2/17/24" },
{ value: "12-17-23", label: "12/17/23 - 1/17/24" },
]}
options={options}
value={currentPeriodStart}
setValue={(value) => {
setCurrentPeriodStart(value);
Expand All @@ -75,18 +134,19 @@ function UsagePage(): JSX.Element {
prefix={<>Billing period</>}
/>
<Spacer y={1} />
{/* usage?.length &&
{costs &&
costs.length > 0 &&
usage &&
usage.length > 0 &&
usage[0].usage_metrics.length > 0 ? ( */}
{true ? (
usage[0].usage_metrics.length > 0 ? (
<>
<BarWrapper>
<Total>Total cost: $457.58</Total>
<Total>Total cost: ${totalCost.toFixed(2)}</Total>
<Bars
fill="#8784D2"
yKey="cost"
xKey="starting_on"
data={processedData}
xKey="start_timestamp"
data={processedCosts}
/>
</BarWrapper>
<Spacer y={0.5} />
Expand All @@ -97,7 +157,7 @@ function UsagePage(): JSX.Element {
fill="#8784D2"
yKey="gib_hours"
xKey="starting_on"
data={processedData}
data={processedUsage}
/>
</BarWrapper>
<Spacer x={1} inline />
Expand All @@ -107,7 +167,7 @@ function UsagePage(): JSX.Element {
fill="#5886E0"
yKey="cpu_hours"
xKey="starting_on"
data={processedData}
data={processedUsage}
/>
</BarWrapper>
</Flex>
Expand Down
19 changes: 17 additions & 2 deletions internal/billing/metronome.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ func (m MetronomeClient) ListCustomerPlan(ctx context.Context, customerID uuid.U
return plan, telemetry.Error(ctx, span, err, "customer id empty")
}

customerID = uuid.MustParse("5ccc9830-a9ba-4df9-a1e5-74a9fb3a960e")
path := fmt.Sprintf("/customers/%s/plans", customerID)

var result struct {
Expand Down Expand Up @@ -307,6 +308,8 @@ func (m MetronomeClient) ListCustomerUsage(ctx context.Context, customerID uuid.
return usage, telemetry.Error(ctx, span, err, "customer id empty")
}

customerID = uuid.MustParse("5ccc9830-a9ba-4df9-a1e5-74a9fb3a960e")

if len(m.billableMetrics) == 0 {
billableMetrics, err := m.listBillableMetricIDs(ctx, customerID)
if err != nil {
Expand Down Expand Up @@ -356,14 +359,15 @@ func (m MetronomeClient) ListCustomerUsage(ctx context.Context, customerID uuid.
}

// ListCustomerCosts will return the costs for a customer over a time period
func (m MetronomeClient) ListCustomerCosts(ctx context.Context, customerID uuid.UUID, startingOn string, endingBefore string, limit int) (costs []types.Cost, err error) {
func (m MetronomeClient) ListCustomerCosts(ctx context.Context, customerID uuid.UUID, startingOn string, endingBefore string, limit int) (costs []types.FormattedCost, err error) {
ctx, span := telemetry.NewSpan(ctx, "list-customer-costs")
defer span.End()

if customerID == uuid.Nil {
return costs, telemetry.Error(ctx, span, err, "customer id empty")
}

customerID = uuid.MustParse("5ccc9830-a9ba-4df9-a1e5-74a9fb3a960e")
path := fmt.Sprintf("customers/%s/costs", customerID)

var result struct {
Expand All @@ -377,7 +381,18 @@ func (m MetronomeClient) ListCustomerCosts(ctx context.Context, customerID uuid.
return costs, telemetry.Error(ctx, span, err, "failed to create credits grant")
}

return result.Data, nil
for _, customerCost := range result.Data {
formattedCost := types.FormattedCost{
StartTimestamp: customerCost.StartTimestamp,
EndTimestamp: customerCost.EndTimestamp,
}
for _, creditType := range customerCost.CreditTypes {
formattedCost.Cost += creditType.Cost
}
costs = append(costs, formattedCost)
}

return costs, nil
}

// IngestEvents sends a list of billing events to Metronome's ingest endpoint
Expand Down

0 comments on commit 025d7d2

Please sign in to comment.