From ff10a0d547cbebbe320a53e7b17685b707586431 Mon Sep 17 00:00:00 2001 From: Mauricio Araujo Date: Sat, 11 May 2024 13:15:33 -0400 Subject: [PATCH] working usage page --- api/server/handlers/billing/plan.go | 2 +- api/types/billing_usage.go | 3 + dashboard/src/lib/hooks/useLago.ts | 20 +- .../main/home/project-settings/UsagePage.tsx | 218 ++++++------------ dashboard/src/shared/api.tsx | 3 +- internal/billing/usage.go | 76 +++--- 6 files changed, 138 insertions(+), 184 deletions(-) diff --git a/api/server/handlers/billing/plan.go b/api/server/handlers/billing/plan.go index b6afb23ad3..b321e77772 100644 --- a/api/server/handlers/billing/plan.go +++ b/api/server/handlers/billing/plan.go @@ -149,7 +149,7 @@ func (c *ListCustomerUsageHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ telemetry.AttributeKV{Key: "subscription_id", Value: plan.ID}, ) - usage, err := c.Config().BillingManager.LagoClient.ListCustomerUsage(ctx, plan.CustomerID, plan.ID, req.CurrentPeriod) + usage, err := c.Config().BillingManager.LagoClient.ListCustomerUsage(ctx, plan.CustomerID, plan.ID, req.CurrentPeriod, req.PreviousPeriods) if err != nil { err := telemetry.Error(ctx, span, err, "error listing customer usage") c.HandleAPIError(w, r, apierrors.NewErrInternal(err)) diff --git a/api/types/billing_usage.go b/api/types/billing_usage.go index a709d9a3ea..50714564b5 100644 --- a/api/types/billing_usage.go +++ b/api/types/billing_usage.go @@ -8,6 +8,9 @@ type ListCreditGrantsResponse struct { // ListCustomerUsageRequest is the request to list usage for a customer type ListCustomerUsageRequest struct { + // PreviousPeriods is the number of previous periods to include in the response. + PreviousPeriods int `json:"previous_periods,omitempty"` + // CurrentPeriod is whether to return only usage for the current billing period. CurrentPeriod bool `json:"current_period,omitempty"` } diff --git a/dashboard/src/lib/hooks/useLago.ts b/dashboard/src/lib/hooks/useLago.ts index 2b3863209d..6e7e45677f 100644 --- a/dashboard/src/lib/hooks/useLago.ts +++ b/dashboard/src/lib/hooks/useLago.ts @@ -30,7 +30,7 @@ type TGetInvoices = { }; type TGetUsage = { - usage: Usage | null; + usageList: Usage[] | null; }; type TGetReferralDetails = { @@ -108,16 +108,15 @@ export const useCustomerPlan = (): TGetPlan => { }; export const useCustomerUsage = ( - startingOn: Date | null, - endingBefore: Date | null, + previousPeriods: number, currentPeriod: boolean ): TGetUsage => { const { currentProject } = useContext(Context); // Fetch customer usage const usageReq = useQuery( - ["listCustomerUsage", currentProject?.id], - async (): Promise => { + ["listCustomerUsage", currentProject?.id, previousPeriods, currentPeriod], + async (): Promise => { if (!currentProject?.metronome_enabled) { return null; } @@ -126,23 +125,18 @@ export const useCustomerUsage = ( return null; } - if (startingOn === null || endingBefore === null) { - return null; - } - try { const res = await api.getCustomerUsage( "", { - starting_on: startingOn.toISOString(), - ending_before: endingBefore.toISOString(), + previous_periods: previousPeriods, current_period: currentPeriod, }, { project_id: currentProject?.id, } ); - const usage = UsageValidator.parse(res.data); + const usage = UsageValidator.array().parse(res.data); return usage; } catch (error) { return null; @@ -151,7 +145,7 @@ export const useCustomerUsage = ( ); return { - usage: usageReq.data ?? null, + usageList: usageReq.data ?? null, }; }; diff --git a/dashboard/src/main/home/project-settings/UsagePage.tsx b/dashboard/src/main/home/project-settings/UsagePage.tsx index de93949b81..d3800645ca 100644 --- a/dashboard/src/main/home/project-settings/UsagePage.tsx +++ b/dashboard/src/main/home/project-settings/UsagePage.tsx @@ -1,168 +1,127 @@ -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 Container from "components/porter/Container"; import Fieldset from "components/porter/Fieldset"; import Select from "components/porter/Select"; import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; -import { type CostList } from "lib/billing/types"; -import { - useCustomerCosts, - useCustomerPlan, - useCustomerUsage, -} from "lib/hooks/useLago"; - -import Bars from "./Bars"; +import { useCustomerPlan, useCustomerUsage } from "lib/hooks/useLago"; dayjs.extend(utc); function UsagePage(): JSX.Element { const { plan } = useCustomerPlan(); - - const startDate = dayjs.utc(plan?.starting_on); - const endDate = dayjs().utc().startOf("day"); - const numberOfDays = startDate.daysInMonth(); - - const [currentPeriodStart, setCurrentPeriodStart] = useState( - startDate.toDate() - ); - const [currentPeriodEnd, setCurrentPeriodEnd] = useState(endDate.toDate()); - const [currentPeriodDuration, setCurrentPeriodDuration] = - useState(numberOfDays); - - const { usage } = useCustomerUsage( - currentPeriodStart, - currentPeriodEnd, - "day" + const planStartDate = dayjs.utc(plan?.starting_on); + + const [currentPeriod, setCurrentPeriod] = useState(planStartDate); + const [options, setOptions] = useState< + Array<{ value: string; label: string }> + >([]); + const [previousPeriodCount, setPreviousPeriodCount] = useState(0); + const [showCurrentPeriod, setShowCurrentPeriod] = useState(true); + + const { usageList } = useCustomerUsage( + previousPeriodCount, + showCurrentPeriod ); - const { costs } = useCustomerCosts( - currentPeriodStart, - currentPeriodEnd, - currentPeriodDuration - ); - - const computeTotalCost = (costs: CostList): number => { - const total = costs.reduce((acc, curr) => acc + curr.cost, 0); - return parseFloat(total.toFixed(2)); - }; - - const processedUsage = useMemo(() => { - const before = usage; - const resultMap = new Map(); - before?.forEach( - (metric: { - metric_name: string; - usage_metrics: Array<{ starting_on: string; value: number }>; - }) => { - const metricName = metric.metric_name.toLowerCase().replace(" ", "_"); - metric.usage_metrics.forEach(({ starting_on: startingOn, value }) => { - if (resultMap.has(startingOn)) { - resultMap.get(startingOn)[metricName] = value; - } else { - resultMap.set(startingOn, { - starting_on: new Date(startingOn).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }), - [metricName]: value, - }); - } - }); - } - ); - - // Convert the map to an array of values - const x = Array.from(resultMap.values()); - 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 = parseFloat((dailyCost.cost / 100).toFixed(4)); - return dailyCost; - }) - .filter((dailyCost) => dailyCost.cost > 0); - }, [costs]); + useEffect(() => { + const newOptions = generateOptions(); + setOptions(newOptions); + }, [previousPeriodCount, showCurrentPeriod]); const generateOptions = (): Array<{ value: string; label: string }> => { const options = []; + const monthsElapsed = dayjs + .utc() + .startOf("month") + .diff(planStartDate.utc().startOf("month"), "month"); - let startDate = dayjs.utc(currentPeriodStart); - const endDate = dayjs.utc(currentPeriodEnd); - - while (startDate.isBefore(endDate)) { - const nextDate = startDate.add(1, "month"); + if (monthsElapsed <= 0) { options.push({ - value: startDate.toISOString(), - label: `${startDate.format("M/D/YY")} - ${nextDate.format("M/D/YY")}`, + value: currentPeriod.toISOString(), + label: dayjs().utc().format("MMMM YYYY"), }); + setShowCurrentPeriod(true); + return options; + } - startDate = startDate.add(1, "month"); + setPreviousPeriodCount(monthsElapsed); + for (let i = 0; i <= monthsElapsed; i++) { + const optionDate = planStartDate.add(i, "month"); + options.push({ + value: optionDate.toISOString(), + label: optionDate.format("MMMM YYYY"), + }); } + return options; }; - const options = generateOptions(); + const processedUsage = useMemo(() => { + if (!usageList || !usageList.length) { + return null; + } + + const periodUsage = usageList.find((usage) => + dayjs(usage.from_datetime).isSame(currentPeriod.month(), "month") + ); + const totalCost = periodUsage?.total_amount_cents + ? (periodUsage.total_amount_cents / 100).toFixed(4) + : ""; + const totalCpuHours = + periodUsage?.charges_usage.find((x) => + x.billable_metric.name.includes("CPU") + )?.units ?? ""; + const totalGibHours = + periodUsage?.charges_usage.find((x) => + x.billable_metric.name.includes("GiB") + )?.units ?? ""; + const currency = periodUsage?.charges_usage[0].amount_currency ?? ""; + return { + total_cost: totalCost, + total_cpu_hours: totalCpuHours, + total_gib_hours: totalGibHours, + currency, + }; + }, [usageList]); return ( <>