Skip to content

Commit

Permalink
Consolidate usage and billing tabs (#4596)
Browse files Browse the repository at this point in the history
Co-authored-by: Mauricio Araujo <[email protected]>
  • Loading branch information
jusrhee and MauAraujo committed May 6, 2024
1 parent 3eb974b commit 219407a
Show file tree
Hide file tree
Showing 13 changed files with 759 additions and 386 deletions.
50 changes: 50 additions & 0 deletions api/server/handlers/billing/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,53 @@ func (c *ListCustomerUsageHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
}
c.WriteResult(w, r, usage)
}

// ListCustomerCostsHandler returns customer usage aggregations like CPU and RAM hours.
type ListCustomerCostsHandler struct {
handlers.PorterHandlerReadWriter
}

// NewListCustomerCostsHandler returns a new ListCustomerCostsHandler
func NewListCustomerCostsHandler(
config *config.Config,
decoderValidator shared.RequestDecoderValidator,
writer shared.ResultWriter,
) *ListCustomerCostsHandler {
return &ListCustomerCostsHandler{
PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
}
}

func (c *ListCustomerCostsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx, span := telemetry.NewSpan(r.Context(), "serve-list-customer-costs")
defer span.End()

proj, _ := ctx.Value(types.ProjectScope).(*models.Project)

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "metronome-config-exists", Value: c.Config().BillingManager.MetronomeConfigLoaded},
telemetry.AttributeKV{Key: "metronome-enabled", Value: proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient)},
telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
)

if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) {
c.WriteResult(w, r, "")
return
}

req := &types.ListCustomerCostsRequest{}

if ok := c.DecodeAndValidate(w, r, req); !ok {
err := telemetry.Error(ctx, span, nil, "error decoding list customer costs request")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
return
}

usage, err := c.Config().BillingManager.MetronomeClient.ListCustomerCosts(ctx, proj.UsageID, req.StartingOn, req.EndingBefore, req.Limit)
if err != nil {
err := telemetry.Error(ctx, span, err, "error listing customer costs")
c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}
c.WriteResult(w, r, usage)
}
28 changes: 28 additions & 0 deletions api/server/router/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,34 @@ func getProjectRoutes(
Router: r,
})

// GET /api/projects/{project_id}/billing/costs -> project.NewListCustomerCostsHandler
listCustomerCostsEndpoint := factory.NewAPIEndpoint(
&types.APIRequestMetadata{
Verb: types.APIVerbGet,
Method: types.HTTPVerbGet,
Path: &types.Path{
Parent: basePath,
RelativePath: relPath + "/billing/costs",
},
Scopes: []types.PermissionScope{
types.UserScope,
types.ProjectScope,
},
},
)

listCustomerCostsHandler := billing.NewListCustomerCostsHandler(
config,
factory.GetDecoderValidator(),
factory.GetResultWriter(),
)

routes = append(routes, &router.Route{
Endpoint: listCustomerCostsEndpoint,
Handler: listCustomerCostsHandler,
Router: r,
})

// GET /api/projects/{project_id}/billing/invoices -> project.NewListCustomerInvoicesHandler
listCustomerInvoicesEndpoint := factory.NewAPIEndpoint(
&types.APIRequestMetadata{
Expand Down
35 changes: 34 additions & 1 deletion api/types/billing_metronome.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,40 @@ type BillableMetric struct {
Name string `json:"name"`
}

// Plan is a pricing plan to which a user is currently subscribed
// ListCustomerCostsRequest is the request to list costs for a customer
type ListCustomerCostsRequest struct {
StartingOn string `schema:"starting_on"`
EndingBefore string `schema:"ending_before"`
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"`
}

// 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 {
ID uuid.UUID `json:"id"`
PlanID uuid.UUID `json:"plan_id"`
Expand Down
12 changes: 7 additions & 5 deletions dashboard/src/components/TabRegion.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React, { Component } from "react";
import styled from "styled-components";

import TabSelector from "./TabSelector";
import Loading from "./Loading";
import TabSelector from "./TabSelector";

export interface TabOption {
export type TabOption = {
label: string;
value: string;
}
};

type PropsType = {
options: TabOption[];
Expand All @@ -31,7 +31,7 @@ export default class TabRegion extends Component<PropsType, StateType> {
: "";

componentDidUpdate(prevProps: PropsType) {
let { options, currentTab } = this.props;
const { options, currentTab } = this.props;
if (prevProps.options !== options) {
if (options.filter((x) => x.value === currentTab).length === 0) {
this.props.setCurrentTab(this.defaultTab());
Expand All @@ -50,7 +50,9 @@ export default class TabRegion extends Component<PropsType, StateType> {
options={this.props.options}
color={this.props.color}
currentTab={this.props.currentTab}
setCurrentTab={(x: string) => this.props.setCurrentTab(x)}
setCurrentTab={(x: string) => {
this.props.setCurrentTab(x);
}}
addendum={this.props.addendum}
/>
<Gap />
Expand Down
12 changes: 10 additions & 2 deletions dashboard/src/lib/billing/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ export const PlanValidator = z

export type UsageMetric = z.infer<typeof UsageMetricValidator>;
export const UsageMetricValidator = z.object({
// starting_on and ending_before are ISO 8601 date strings
// starting_on and ending_before are RFC 3339 date strings
// that represent the timeframe where the metric was ingested.
// If the granularity is set per day, the starting_on field
// represents the dat the metric was ingested.
// represents the day the metric was ingested.
starting_on: z.string(),
ending_before: z.string(),
value: z.number(),
Expand All @@ -51,6 +51,14 @@ export const CreditGrantsValidator = z.object({
remaining_credits: z.number(),
});

export type CostList = Cost[];
export type Cost = z.infer<typeof CostValidator>;
export const CostValidator = z.object({
start_timestamp: z.string(),
end_timestamp: z.string(),
cost: z.number(),
});

export type InvoiceList = Invoice[];
export type Invoice = z.infer<typeof InvoiceValidator>;
export const InvoiceValidator = z.object({
Expand Down
Loading

0 comments on commit 219407a

Please sign in to comment.