ソースを参照

Working charts

Mauricio Araujo 2 年 前
コミット
3b2c0531ca

+ 12 - 4
api/types/billing_metronome.go

@@ -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 {

+ 1 - 1
dashboard/src/lib/billing/types.tsx

@@ -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[];

+ 19 - 9
dashboard/src/lib/hooks/useMetronome.ts

@@ -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,
@@ -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);
 
@@ -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,
@@ -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);
@@ -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,
           }
         );
 

+ 88 - 28
dashboard/src/main/home/project-settings/UsagePage.tsx

@@ -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";
@@ -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();
 
@@ -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",
               }),
@@ -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);
@@ -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} />
@@ -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 />
@@ -107,7 +167,7 @@ function UsagePage(): JSX.Element {
                 fill="#5886E0"
                 yKey="cpu_hours"
                 xKey="starting_on"
-                data={processedData}
+                data={processedUsage}
               />
             </BarWrapper>
           </Flex>

+ 17 - 2
internal/billing/metronome.go

@@ -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 {
@@ -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 {
@@ -356,7 +359,7 @@ 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()
 
@@ -364,6 +367,7 @@ func (m MetronomeClient) ListCustomerCosts(ctx context.Context, customerID uuid.
 		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 {
@@ -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