Procházet zdrojové kódy

Add embedded usage dashboard

Mauricio Araujo před 2 roky
rodič
revize
e0eaa26f5d

+ 55 - 0
api/server/handlers/billing/plan.go

@@ -103,3 +103,58 @@ func (c *ListCreditsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	c.WriteResult(w, r, credits)
 }
+
+// GetUsageDashboardHandler returns an embeddable dashboard to display information related to customer usage.
+type GetUsageDashboardHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewGetUsageDashboardHandler returns a new GetUsageDashboardHandler
+func NewGetUsageDashboardHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetUsageDashboardHandler {
+	return &GetUsageDashboardHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *GetUsageDashboardHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "get-usage-dashboard-endpoint")
+	defer span.End()
+
+	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+	if !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) {
+		c.WriteResult(w, r, "")
+
+		telemetry.WithAttributes(span,
+			telemetry.AttributeKV{Key: "metronome-enabled", Value: false},
+		)
+		return
+	}
+
+	request := &types.EmbeddableDashboardRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding embeddable usage dashboard request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	credits, err := c.Config().BillingManager.MetronomeClient.GetCustomerDashboard(ctx, proj.UsageID, request.DashboardType, request.Options, request.ColorOverrides)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting customer dashboard")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "metronome-enabled", Value: true},
+		telemetry.AttributeKV{Key: "project-id", Value: proj.ID},
+		telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
+	)
+
+	c.WriteResult(w, r, credits)
+}

+ 28 - 0
api/server/router/project.go

@@ -395,6 +395,34 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/billing/dashboard -> project.NewGetUsageDashboardHandler
+	getUsageDashboardEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/billing/dashboard",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	getUsageDashboardHandler := billing.NewGetUsageDashboardHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getUsageDashboardEndpoint,
+		Handler:  getUsageDashboardHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/billing/payment_method -> project.NewCreateBillingHandler
 	createBillingEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 16 - 5
api/types/billing_metronome.go

@@ -60,6 +60,22 @@ type ListCreditGrantsRequest struct {
 	EffectiveBefore string `json:"effective_before,omitempty"`
 }
 
+type ListCreditGrantsResponse struct {
+	RemainingCredits float64 `json:"remaining_credits"`
+	GrantedCredits   float64 `json:"granted_credits"`
+}
+
+type EmbeddableDashboardRequest struct {
+	// CustomerID is the id of the customer
+	CustomerID uuid.UUID `json:"customer_id,omitempty"`
+	// DashboardType is the type of dashboard to retrieve
+	DashboardType string `json:"dashboard"`
+	// Options are optional dashboard specific options
+	Options []DashboardOptions `json:"dashboard_options,omitempty"`
+	//  ColorOverrides is an optional list of colors to override
+	ColorOverrides []ColorOverrides `json:"color_overrides,omitempty"`
+}
+
 // Plan is a pricing plan to which a user is currently subscribed
 type Plan struct {
 	ID                  uuid.UUID `json:"id"`
@@ -111,11 +127,6 @@ type CreditGrant struct {
 	ExpiresAt   string      `json:"expires_at"`
 }
 
-type ListCreditGrantsResponse struct {
-	RemainingCredits float64 `json:"remaining_credits"`
-	GrantedCredits   float64 `json:"granted_credits"`
-}
-
 // DashboardOptions are optional dashboard specific options
 type DashboardOptions struct {
 	Key   string `json:"key"`

+ 5 - 1
dashboard/src/lib/hooks/useStripe.tsx

@@ -37,6 +37,10 @@ type TGetPublishableKey = {
   publishableKey: string;
 };
 
+type TGetUsageDashboard = {
+  url: string;
+};
+
 type TGetCredits = {
   creditGrants: CreditGrants;
 };
@@ -186,7 +190,7 @@ export const useCustomerDashboard = (dashboard: string): TGetUsageDashboard => {
     { name: embeddableDashboardColors.grayDark, value: "#121212" },
     { name: embeddableDashboardColors.grayMedium, value: "#DFDFE1" },
     { name: embeddableDashboardColors.grayLight, value: "#DFDFE1" },
-    { name: embeddableDashboardColors.grayExtraLigth, value: "#00ff63" },
+    { name: embeddableDashboardColors.grayExtraLigth, value: "#DFDFE1" },
     { name: embeddableDashboardColors.white, value: "#121212" },
     { name: embeddableDashboardColors.primaryLight, value: "#121212" },
     { name: embeddableDashboardColors.primaryMedium, value: "#DFDFE1" },

+ 92 - 102
dashboard/src/main/home/project-settings/BillingPage.tsx

@@ -1,4 +1,5 @@
 import React, { useContext, useEffect, useState } from "react";
+import ParentSize from "@visx/responsive/lib/components/ParentSize";
 import styled from "styled-components";
 
 import Loading from "components/Loading";
@@ -11,6 +12,7 @@ import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import {
   checkIfProjectHasPayment,
+  useCustomerDashboard,
   useCustomerPlan,
   usePaymentMethods,
   usePorterCredits,
@@ -42,6 +44,8 @@ function BillingPage(): JSX.Element {
 
   const { refetchPaymentEnabled } = checkIfProjectHasPayment();
 
+  const { url: usageDashboard } = useCustomerDashboard("usage");
+
   const formatCredits = (credits: number): string => {
     return (credits / 100).toFixed(2);
   };
@@ -97,108 +101,6 @@ function BillingPage(): JSX.Element {
 
   return (
     <>
-      {currentProject?.metronome_enabled ? (
-        <div>
-          <div>
-            <Text size={16}>Porter credit grants</Text>
-            <Spacer y={1} />
-            <Text color="helper">
-              View the amount of Porter credits you have available to spend on
-              resources within this project.
-            </Text>
-            <Spacer y={1} />
-
-            <Container>
-              <Image src={gift} style={{ marginTop: "-2px" }} />
-              <Spacer inline x={1} />
-              <Text size={20}>
-                {creditGrants !== undefined &&
-                creditGrants.remaining_credits > 0
-                  ? `$${formatCredits(
-                      creditGrants.remaining_credits
-                    )}/$${formatCredits(creditGrants.granted_credits)}`
-                  : "$ 0.00"}
-              </Text>
-            </Container>
-            <Spacer y={2} />
-          </div>
-
-          <div>
-            <Text size={16}>Plan Details</Text>
-            <Spacer y={1} />
-            <Text color="helper">
-              View the details of the current billing plan of this project.
-            </Text>
-            <Spacer y={1} />
-
-            <Text>Active Plan</Text>
-            <Spacer y={0.5} />
-            {plan !== undefined ? (
-              <Fieldset>
-                <Container row spaced>
-                  <Container row>
-                    <Text color="helper">{plan.plan_name}</Text>
-                  </Container>
-                  <Container row>
-                    {plan.trial_info !== undefined ? (
-                      <Text>
-                        Free trial ends{" "}
-                        {relativeTime(plan.trial_info.ending_before)}
-                      </Text>
-                    ) : (
-                      <Text>Started on {readableDate(plan.starting_on)}</Text>
-                    )}
-                  </Container>
-                </Container>
-              </Fieldset>
-            ) : (
-              <Loading></Loading>
-            )}
-            <Spacer y={1} />
-          </div>
-
-          <Text size={16}>Usage</Text>
-          <Spacer y={1} />
-          <iframe
-            src={usageDashboard}
-            scrolling="no"
-            frameBorder={0}
-            allowTransparency={true}
-            style={{
-              width: "100%",
-              height: "80vh",
-              overflowY: "hidden",
-              border: "none",
-              backgroundColor: "#121212",
-            }}
-          ></iframe>
-          <Spacer y={1} />
-
-          <div>
-            {plan === undefined ? (
-              <Loading></Loading>
-            ) : (
-              <Fieldset>
-                <Container row>
-                  <Text size={14}>{plan.plan_name}</Text>
-                </Container>
-                {plan.trial_info !== undefined ? (
-                  <div>
-                    <Text size={13}>
-                      Trial Ending {relativeTime(plan.trial_info.ending_before)}
-                    </Text>
-                  </div>
-                ) : (
-                  <div></div>
-                )}
-              </Fieldset>
-            )}
-          </div>
-          <Spacer y={2} />
-        </div>
-      ) : (
-        <div></div>
-      )}
       <Text size={16}>Payment methods</Text>
       <Spacer y={1} />
       <Text color="helper">
@@ -271,6 +173,94 @@ function BillingPage(): JSX.Element {
         <I className="material-icons">add</I>
         Add Payment Method
       </Button>
+      <Spacer y={2} />
+
+      {currentProject?.metronome_enabled ? (
+        <div>
+          <div>
+            <Text size={16}>Porter credit grants</Text>
+            <Spacer y={1} />
+            <Text color="helper">
+              View the amount of Porter credits you have available to spend on
+              resources within this project.
+            </Text>
+            <Spacer y={1} />
+
+            <Container>
+              <Image src={gift} style={{ marginTop: "-2px" }} />
+              <Spacer inline x={1} />
+              <Text size={20}>
+                {creditGrants !== undefined &&
+                creditGrants.remaining_credits > 0
+                  ? `$${formatCredits(
+                      creditGrants.remaining_credits
+                    )}/$${formatCredits(creditGrants.granted_credits)}`
+                  : "$ 0.00"}
+              </Text>
+            </Container>
+            <Spacer y={2} />
+          </div>
+
+          <div>
+            <Text size={16}>Plan Details</Text>
+            <Spacer y={1} />
+            <Text color="helper">
+              View the details of the current billing plan of this project.
+            </Text>
+            <Spacer y={1} />
+
+            <Text>Active Plan</Text>
+            <Spacer y={0.5} />
+            {plan !== undefined ? (
+              <Fieldset>
+                <Container row spaced>
+                  <Container row>
+                    <Text color="helper">{plan.plan_name}</Text>
+                  </Container>
+                  <Container row>
+                    {plan.trial_info !== undefined ? (
+                      <Text>
+                        Free trial ends{" "}
+                        {relativeTime(plan.trial_info.ending_before)}
+                      </Text>
+                    ) : (
+                      <Text>Started on {readableDate(plan.starting_on)}</Text>
+                    )}
+                  </Container>
+                </Container>
+              </Fieldset>
+            ) : (
+              <Loading></Loading>
+            )}
+            <Spacer y={2} />
+          </div>
+
+          <div>
+            <Text size={16}>Current Usage</Text>
+            <Spacer y={1} />
+            <Text color="helper">
+              View the current usage of this billing period.
+            </Text>
+            <Spacer y={1} />{" "}
+            <Container row style={{ width: "100%", height: "70vh" }}>
+              <ParentSize>
+                {({ width, height }) => (
+                  <iframe
+                    width={width}
+                    height={height}
+                    src={usageDashboard}
+                    scrolling="no"
+                    frameBorder={0}
+                    allowTransparency={true}
+                  ></iframe>
+                )}
+              </ParentSize>
+            </Container>
+          </div>
+        </div>
+      ) : (
+        <div></div>
+      )}
     </>
   );
 }

+ 12 - 0
dashboard/src/shared/api.tsx

@@ -3452,6 +3452,17 @@ const getPublishableKey = baseApi<
   ({ project_id }) => `/api/projects/${project_id}/billing/publishable_key`
 );
 
+const getUsageDashboard = baseApi<
+  {
+    dashboard: string;
+    dashboard_options?: { key: string; value: string }[];
+    color_overrides?: { name: string; value: string }[];
+  },
+  {
+    project_id?: number;
+  }
+>("POST", ({ project_id }) => `/api/projects/${project_id}/billing/dashboard`);
+
 const getCustomerPlan = baseApi<
   {},
   {
@@ -3865,6 +3876,7 @@ export default {
   getPublishableKey,
   getPorterCredits,
   getCustomerPlan,
+  getUsageDashboard,
   listPaymentMethod,
   addPaymentMethod,
   setDefaultPaymentMethod,

+ 30 - 0
internal/billing/metronome.go

@@ -211,6 +211,36 @@ func (m MetronomeClient) ListCustomerCredits(ctx context.Context, customerID uui
 	return response, nil
 }
 
+// GetCustomerDashboard will return an embeddable Metronome dashboard
+func (m MetronomeClient) GetCustomerDashboard(ctx context.Context, customerID uuid.UUID, dashboardType string, options []types.DashboardOptions, colorOverrides []types.ColorOverrides) (url string, err error) {
+	ctx, span := telemetry.NewSpan(ctx, "get-customer-usage-dashboard")
+	defer span.End()
+
+	if customerID == uuid.Nil {
+		return url, telemetry.Error(ctx, span, err, "customer id empty")
+	}
+
+	path := "dashboards/getEmbeddableUrl"
+
+	req := types.EmbeddableDashboardRequest{
+		CustomerID:     customerID,
+		Options:        options,
+		DashboardType:  dashboardType,
+		ColorOverrides: colorOverrides,
+	}
+
+	var result struct {
+		Data map[string]string `json:"data"`
+	}
+
+	err = do(http.MethodPost, path, m.ApiKey, req, &result)
+	if err != nil {
+		return url, telemetry.Error(ctx, span, err, "failed to get embeddable dashboard")
+	}
+
+	return result.Data["url"], nil
+}
+
 func do(method string, path string, apiKey string, body interface{}, data interface{}) (err error) {
 	client := http.Client{}
 	endpoint, err := url.JoinPath(metronomeBaseUrl, path)