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/ProjectSettings.tsx b/dashboard/src/main/home/project-settings/ProjectSettings.tsx index 7b20c3391b..3c73f0fc93 100644 --- a/dashboard/src/main/home/project-settings/ProjectSettings.tsx +++ b/dashboard/src/main/home/project-settings/ProjectSettings.tsx @@ -96,15 +96,15 @@ function ProjectSettings(props: any) { }); } - // if ( - // currentProject?.billing_enabled && - // currentProject?.metronome_enabled - // ) { - // tabOpts.push({ - // value: "usage", - // label: "Usage", - // }); - // } + if ( + currentProject?.billing_enabled && + currentProject?.metronome_enabled + ) { + tabOpts.push({ + value: "usage", + label: "Usage", + }); + } tabOpts.push({ value: "additional-settings", diff --git a/dashboard/src/main/home/project-settings/UsagePage.tsx b/dashboard/src/main/home/project-settings/UsagePage.tsx index 32e31e3f77..78384445b7 100644 --- a/dashboard/src/main/home/project-settings/UsagePage.tsx +++ b/dashboard/src/main/home/project-settings/UsagePage.tsx @@ -1,169 +1,137 @@ -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 planStartDate = dayjs.utc(plan?.starting_on).startOf("month"); - const startDate = dayjs.utc(plan?.starting_on); - const endDate = dayjs().utc().startOf("day"); - const numberOfDays = startDate.daysInMonth(); - - const [currentPeriodStart, setCurrentPeriodStart] = useState( - startDate.toDate() + const [currentPeriod, setCurrentPeriod] = useState( + dayjs().utc().startOf("month") ); - const [currentPeriodEnd, setCurrentPeriodEnd] = useState(endDate.toDate()); - const [currentPeriodDuration, setCurrentPeriodDuration] = - useState(numberOfDays); - - const { usage } = useCustomerUsage( - currentPeriodStart, - currentPeriodEnd, - "day" - ); - const { costs } = useCustomerCosts( - currentPeriodStart, - currentPeriodEnd, - currentPeriodDuration + 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 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, "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.month().toString(), + 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.month().toString(), + label: optionDate.format("MMMM YYYY"), + }); } + return options; }; - const options = generateOptions(); + const processedUsage = useMemo(() => { + if (!usageList?.length) { + return null; + } + + const periodUsage = usageList.find( + (usage) => + dayjs(usage.from_datetime).utc().month() === currentPeriod.month() + ); + + if (!periodUsage) { + return null; + } + + 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 ( <>