diff --git a/api/server/handlers/billing/plan.go b/api/server/handlers/billing/plan.go index 86d2a4b42d..b6f490fe0b 100644 --- a/api/server/handlers/billing/plan.go +++ b/api/server/handlers/billing/plan.go @@ -104,61 +104,6 @@ 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(), "serve-usage-dashboard") - defer span.End() - - proj, _ := ctx.Value(types.ProjectScope).(*models.Project) - - if !c.Config().BillingManager.MetronomeEnabled || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) { - c.WriteResult(w, r, "") - - telemetry.WithAttributes(span, - telemetry.AttributeKV{Key: "metronome-config-exists", Value: c.Config().BillingManager.MetronomeEnabled}, - telemetry.AttributeKV{Key: "metronome-enabled", Value: proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient)}, - ) - return - } - - telemetry.WithAttributes(span, - telemetry.AttributeKV{Key: "metronome-enabled", Value: true}, - telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID}, - ) - - 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 - } - - c.WriteResult(w, r, credits) -} - // ListCustomerUsageHandler returns customer usage aggregations like CPU and RAM hours. type ListCustomerUsageHandler struct { handlers.PorterHandlerReadWriter diff --git a/api/server/router/project.go b/api/server/router/project.go index 1f26d4efcb..d5e9e851fb 100644 --- a/api/server/router/project.go +++ b/api/server/router/project.go @@ -397,34 +397,6 @@ func getProjectRoutes( Router: r, }) - // POST /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/usage -> project.NewListCustomerUsageHandler listCustomerUsageEndpoint := factory.NewAPIEndpoint( &types.APIRequestMetadata{ diff --git a/dashboard/src/lib/hooks/useStripe.tsx b/dashboard/src/lib/hooks/useStripe.tsx index c32e9a2334..8d9e96a791 100644 --- a/dashboard/src/lib/hooks/useStripe.tsx +++ b/dashboard/src/lib/hooks/useStripe.tsx @@ -37,7 +37,7 @@ type TSetDefaultPaymentMethod = { }; type TCheckHasPaymentEnabled = { - hasPaymentEnabled: boolean; + hasPaymentEnabled: boolean | null; refetchPaymentEnabled: (options: { throwOnError: boolean; cancelRefetch: boolean; @@ -45,33 +45,19 @@ type TCheckHasPaymentEnabled = { }; type TGetPublishableKey = { - publishableKey: string; -}; - -type TGetUsageDashboard = { - url: string; + publishableKey: string | null; }; type TGetCredits = { - creditGrants: CreditGrants | undefined; + creditGrants: CreditGrants | null; }; type TGetPlan = { - plan: Plan | undefined; + plan: Plan | null; }; type TGetUsage = { - usage: UsageList | undefined; -}; - -const embeddableDashboardColors = { - grayDark: "Gray_dark", - grayMedium: "Gray_medium", - grayLight: "Gray_light", - grayExtraLigth: "Gray_extralight", - white: "White", - primaryMedium: "Primary_medium", - primaryLight: "Primary_light", + usage: UsageList | null; }; export const usePaymentMethods = (): TUsePaymentMethod => { @@ -87,20 +73,28 @@ export const usePaymentMethods = (): TUsePaymentMethod => { // Fetch list of payment methods const paymentMethodReq = useQuery( ["getPaymentMethods", currentProject?.id], - async (): Promise => { + async (): Promise => { + if (!currentProject?.billing_enabled) { + return null; + } + if (!currentProject?.id || currentProject.id === -1) { - return []; + return null; } - const listResponse = await api.listPaymentMethod( - "", - {}, - { project_id: currentProject?.id } - ); - const data = PaymentMethodValidator.array().parse(listResponse.data); - setPaymentMethodList(data); + try { + const listResponse = await api.listPaymentMethod( + "", + {}, + { project_id: currentProject?.id } + ); - return data; + const data = PaymentMethodValidator.array().parse(listResponse.data); + setPaymentMethodList(data); + return data; + } catch (error) { + return null + } } ); @@ -146,6 +140,10 @@ export const usePaymentMethods = (): TUsePaymentMethod => { export const useCreatePaymentMethod = (): TCreatePaymentMethod => { const { currentProject } = useContext(Context); + if (!currentProject?.billing_enabled) { + return { createPaymentMethod: async () => "" }; + } + const createPaymentMethod = async (): Promise => { const resp = await api.addPaymentMethod( "", @@ -163,14 +161,50 @@ export const useCreatePaymentMethod = (): TCreatePaymentMethod => { }; }; +export const useSetDefaultPaymentMethod = (): TSetDefaultPaymentMethod => { + const { currentProject } = useContext(Context); + + if (!currentProject?.billing_enabled) { + return { setDefaultPaymentMethod: async () => { } }; + } + + const setDefaultPaymentMethod = async ( + paymentMethodId: string + ): Promise => { + // Set payment method as default + const res = await api.setDefaultPaymentMethod( + "", + {}, + { project_id: currentProject?.id, payment_method_id: paymentMethodId } + ); + + if (res.status !== 200) { + throw Error("failed to set payment method as default"); + } + }; + + return { + setDefaultPaymentMethod, + }; +}; + export const checkIfProjectHasPayment = (): TCheckHasPaymentEnabled => { const { currentProject } = useContext(Context); + // Check if payment is enabled for the project const paymentEnabledReq = useQuery( - currentProject?.id ? ["checkPaymentEnabled", currentProject.id] : ["checkPaymentEnabled", null], - currentProject?.id - ? async (): Promise => { + ["checkPaymentEnabled", currentProject?.id], + async (): Promise => { + if (!currentProject?.billing_enabled) { + return null; + } + + if (!currentProject?.id) { + return null; + } + + try { const res = await api.getHasBilling( "", {}, @@ -179,83 +213,47 @@ export const checkIfProjectHasPayment = (): TCheckHasPaymentEnabled => { const data = z.boolean().parse(res.data); return data; + } catch (error) { + return null } - : async () => false - ); + }); return { - hasPaymentEnabled: paymentEnabledReq.data ?? false, + hasPaymentEnabled: paymentEnabledReq.data ?? null, refetchPaymentEnabled: paymentEnabledReq.refetch, }; }; - -export const useCustomeUsageDashboard = ( - dashboard: string -): TGetUsageDashboard => { - const { currentProject } = useContext(Context); - - const colorOverrides = [ - { name: embeddableDashboardColors.grayDark, value: "#121212" }, - { name: embeddableDashboardColors.grayMedium, value: "#DFDFE1" }, - { name: embeddableDashboardColors.grayLight, value: "#DFDFE1" }, - { name: embeddableDashboardColors.grayExtraLigth, value: "#DFDFE1" }, - { name: embeddableDashboardColors.white, value: "#121212" }, - { name: embeddableDashboardColors.primaryLight, value: "#121212" }, - { name: embeddableDashboardColors.primaryMedium, value: "#DFDFE1" }, - ]; - - // Return an embeddable dashboard for the customer - const dashboardReq = useQuery( - ["getUsageDashboard", currentProject?.id, dashboard], - async () => { - if (!currentProject?.id || currentProject.id === -1) { - return; - } - const res = await api.getUsageDashboard( - "", - { - dashboard, - color_overrides: colorOverrides, - }, - { - project_id: currentProject?.id, - } - ); - return res.data; - }, - { - staleTime: Infinity, - } - ); - - return { - url: dashboardReq.data, - }; -}; - export const usePublishableKey = (): TGetPublishableKey => { const { currentProject } = useContext(Context); // Fetch list of payment methods const keyReq = useQuery( ["getPublishableKey", currentProject?.id], - async () => { + async (): Promise => { + if (!currentProject?.billing_enabled) { + return null; + } + if (!currentProject?.id || currentProject.id === -1) { return null; } - const res = await api.getPublishableKey( - "", - {}, - { - project_id: currentProject?.id, - } - ); - return res.data; - } - ); + + try { + const res = await api.getPublishableKey( + "", + {}, + { + project_id: currentProject?.id, + } + ); + return res.data; + } catch (error) { + return null + } + }); return { - publishableKey: keyReq.data, + publishableKey: keyReq.data ?? null, }; }; @@ -265,23 +263,33 @@ export const usePorterCredits = (): TGetCredits => { // Fetch available credits const creditsReq = useQuery( ["getPorterCredits", currentProject?.id], - async () => { + async (): Promise => { + if (!currentProject?.metronome_enabled) { + return null; + } + if (!currentProject?.id || currentProject.id === -1) { - return; + return null; + } + + try { + const res = await api.getPorterCredits( + "", + {}, + { + project_id: currentProject?.id, + } + ); + const creditGrants = CreditGrantsValidator.parse(res.data); + return creditGrants; + } catch (error) { + return null } - const res = await api.getPorterCredits( - "", - {}, - { - project_id: currentProject?.id, - } - ); - return CreditGrantsValidator.parse(res.data); } ); return { - creditGrants: creditsReq.data, + creditGrants: creditsReq.data ?? null, }; }; @@ -290,9 +298,17 @@ export const useCustomerPlan = (): TGetPlan => { // Fetch current plan const planReq = useQuery( - currentProject?.id ? ["getCustomerPlan", currentProject.id] : ["getCustomerPlan", null], - currentProject?.id - ? async (): Promise => { + ["getCustomerPlan", currentProject?.id], + async (): Promise => { + if (!currentProject?.metronome_enabled) { + return null; + } + + if (!currentProject?.id) { + return null; + } + + try { const res = await api.getCustomerPlan( "", {}, @@ -301,12 +317,13 @@ export const useCustomerPlan = (): TGetPlan => { const plan = PlanValidator.parse(res.data); return plan; + } catch (error) { + return null } - : async () => null - ); + }); return { - plan: planReq.data, + plan: planReq.data ?? null, }; }; @@ -319,49 +336,34 @@ export const useCustomerUsage = ( // Fetch customer usage const usageReq = useQuery( ["listCustomerUsage", currentProject?.id], - async () => { - if (!currentProject?.id || currentProject.id === -1) { - return; + async (): Promise => { + if (!currentProject?.metronome_enabled) { + return null; } - const res = await api.getCustomerUsage( - "", - { - window_size: windowSize, - current_period: currentPeriod, - }, - { - project_id: currentProject?.id, - } - ); - const usage = UsageValidator.array().parse(res.data); - return usage; - } - ); - - return { - usage: usageReq.data, - }; -}; - -export const useSetDefaultPaymentMethod = (): TSetDefaultPaymentMethod => { - const { currentProject } = useContext(Context); - const setDefaultPaymentMethod = async ( - paymentMethodId: string - ): Promise => { - // Set payment method as default - const res = await api.setDefaultPaymentMethod( - "", - {}, - { project_id: currentProject?.id, payment_method_id: paymentMethodId } - ); + if (!currentProject?.id || currentProject.id === -1) { + return null; + } - if (res.status !== 200) { - throw Error("failed to set payment method as default"); - } - }; + try { + const res = await api.getCustomerUsage( + "", + { + window_size: windowSize, + current_period: currentPeriod, + }, + { + project_id: currentProject?.id, + } + ); + const usage = UsageValidator.array().parse(res.data); + return usage; + } catch (error) { + return null; + } + }); return { - setDefaultPaymentMethod, + usage: usageReq.data ?? null, }; }; diff --git a/dashboard/src/main/home/Home.tsx b/dashboard/src/main/home/Home.tsx index ba2f53879d..9024ab02f1 100644 --- a/dashboard/src/main/home/Home.tsx +++ b/dashboard/src/main/home/Home.tsx @@ -401,9 +401,9 @@ const Home: React.FC = (props) => { !trialExpired } > - {!currentProject?.sandbox_enabled && showCardBanner && plan && ( + {!currentProject?.sandbox_enabled && showCardBanner && currentProject?.billing_enabled && currentProject?.metronome_enabled && ( <> - {!trialExpired && ( + {!trialExpired && plan && ( warning Please diff --git a/dashboard/src/shared/api.tsx b/dashboard/src/shared/api.tsx index bd7fffbcf7..b87c515031 100644 --- a/dashboard/src/shared/api.tsx +++ b/dashboard/src/shared/api.tsx @@ -386,9 +386,8 @@ const getFeedEvents = baseApi< } >("GET", (pathParams) => { const { project_id, cluster_id, stack_name, page } = pathParams; - return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${ - page || 1 - }`; + return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${page || 1 + }`; }); const createEnvironment = baseApi< @@ -876,11 +875,9 @@ const detectBuildpack = baseApi< branch: string; } >("GET", (pathParams) => { - return `/api/projects/${pathParams.project_id}/gitrepos/${ - pathParams.git_repo_id - }/repos/${pathParams.kind}/${pathParams.owner}/${ - pathParams.name - }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`; }); const detectGitlabBuildpack = baseApi< @@ -911,11 +908,9 @@ const getBranchContents = baseApi< branch: string; } >("GET", (pathParams) => { - return `/api/projects/${pathParams.project_id}/gitrepos/${ - pathParams.git_repo_id - }/repos/${pathParams.kind}/${pathParams.owner}/${ - pathParams.name - }/${encodeURIComponent(pathParams.branch)}/contents`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/contents`; }); const getProcfileContents = baseApi< @@ -931,11 +926,9 @@ const getProcfileContents = baseApi< branch: string; } >("GET", (pathParams) => { - return `/api/projects/${pathParams.project_id}/gitrepos/${ - pathParams.git_repo_id - }/repos/${pathParams.kind}/${pathParams.owner}/${ - pathParams.name - }/${encodeURIComponent(pathParams.branch)}/procfile`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/procfile`; }); const getPorterYamlContents = baseApi< @@ -951,11 +944,9 @@ const getPorterYamlContents = baseApi< branch: string; } >("GET", (pathParams) => { - return `/api/projects/${pathParams.project_id}/gitrepos/${ - pathParams.git_repo_id - }/repos/${pathParams.kind}/${pathParams.owner}/${ - pathParams.name - }/${encodeURIComponent(pathParams.branch)}/porteryaml`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/porteryaml`; }); const parsePorterYaml = baseApi< @@ -1015,32 +1006,30 @@ const getBranchHead = baseApi< branch: string; } >("GET", (pathParams) => { - return `/api/projects/${pathParams.project_id}/gitrepos/${ - pathParams.git_repo_id - }/repos/${pathParams.kind}/${pathParams.owner}/${ - pathParams.name - }/${encodeURIComponent(pathParams.branch)}/head`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/head`; }); const createApp = baseApi< | { - name: string; - deployment_target_id: string; - type: "github"; - git_repo_id: number; - git_branch: string; - git_repo_name: string; - porter_yaml_path: string; - } + name: string; + deployment_target_id: string; + type: "github"; + git_repo_id: number; + git_branch: string; + git_repo_name: string; + porter_yaml_path: string; + } | { - name: string; - deployment_target_id: string; - type: "docker-registry"; - image: { - repository: string; - tag: string; - }; - }, + name: string; + deployment_target_id: string; + type: "docker-registry"; + image: { + repository: string; + tag: string; + }; + }, { project_id: number; cluster_id: number; @@ -2266,11 +2255,9 @@ const getEnvGroup = baseApi< version?: number; } >("GET", (pathParams) => { - return `/api/projects/${pathParams.id}/clusters/${ - pathParams.cluster_id - }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${ - pathParams.version ? "&version=" + pathParams.version : "" - }`; + return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id + }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${pathParams.version ? "&version=" + pathParams.version : "" + }`; }); const getConfigMap = baseApi< @@ -3476,17 +3463,6 @@ const getCustomerUsage = baseApi< } >("POST", ({ project_id }) => `/api/projects/${project_id}/billing/usage`); -const getUsageDashboard = baseApi< - { - dashboard: string; - dashboard_options?: Array<{ key: string; value: string }>; - color_overrides?: Array<{ name: string; value: string }>; - }, - { - project_id?: number; - } ->("POST", ({ project_id }) => `/api/projects/${project_id}/billing/dashboard`); - const getCustomerPlan = baseApi< {}, { @@ -3550,7 +3526,7 @@ const deletePaymentMethod = baseApi< `/api/projects/${project_id}/billing/payment_method/${payment_method_id}` ); -const getGithubStatus = baseApi<{}, {}>("GET", ({}) => `/api/status/github`); +const getGithubStatus = baseApi<{}, {}>("GET", ({ }) => `/api/status/github`); const createSecretAndOpenGitHubPullRequest = baseApi< { @@ -3628,12 +3604,12 @@ const updateAppEventWebhooks = baseApi< }); const systemStatusHistory = baseApi< -{}, -{ - projectId: number; clusterId: number; -}>("GET", (pathParams) => { - return `/api/projects/${pathParams.projectId}/clusters/${pathParams.clusterId}/system-status-history`; -}); + {}, + { + projectId: number; clusterId: number; + }>("GET", (pathParams) => { + return `/api/projects/${pathParams.projectId}/clusters/${pathParams.clusterId}/system-status-history`; + }); // Bundle export to allow default api import (api. is more readable) export default { @@ -3929,7 +3905,6 @@ export default { getPorterCredits, getCustomerPlan, getCustomerUsage, - getUsageDashboard, listPaymentMethod, addPaymentMethod, setDefaultPaymentMethod, diff --git a/internal/billing/metronome.go b/internal/billing/metronome.go index 85dc9a3eba..a0761d3e34 100644 --- a/internal/billing/metronome.go +++ b/internal/billing/metronome.go @@ -228,36 +228,6 @@ 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.DashboardOption, colorOverrides []types.ColorOverride) (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 = m.do(http.MethodPost, path, req, &result) - if err != nil { - return url, telemetry.Error(ctx, span, err, "failed to get embeddable dashboard") - } - - return result.Data["url"], nil -} - // ListCustomerUsage will return the aggregated usage for a customer func (m MetronomeClient) ListCustomerUsage(ctx context.Context, customerID uuid.UUID, startingOn string, endingBefore string, windowsSize string, currentPeriod bool) (usage []types.Usage, err error) { ctx, span := telemetry.NewSpan(ctx, "list-customer-usage")