mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-28 08:49:42 +02:00
fix: remove cost calculation from dograh codebase
This commit is contained in:
parent
7d4e2e06a9
commit
8f241b89d2
39 changed files with 1067 additions and 1460 deletions
|
|
@ -1,11 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { CircleDollarSign, CreditCard, RefreshCw } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { createMpsCreditPurchaseUrlApiV1OrganizationsUsageMpsCreditsPurchaseUrlPost, getBillingCreditsApiV1OrganizationsBillingCreditsGet } from "@/client/sdk.gen";
|
||||
import type { MpsBillingCreditsResponse } from "@/client/types.gen";
|
||||
import type { MpsBillingCreditsResponse, MpsCreditLedgerEntryResponse } from "@/client/types.gen";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
|
@ -19,7 +21,6 @@ import {
|
|||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useAppConfig } from "@/context/AppConfigContext";
|
||||
import { useOrgConfig } from "@/context/OrgConfigContext";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
const formatCredits = (value: number | null | undefined) => (
|
||||
|
|
@ -50,18 +51,58 @@ const formatDate = (value: string) => (
|
|||
})
|
||||
);
|
||||
|
||||
const metricLabels: Record<string, string> = {
|
||||
voice_minutes: "Voice usage",
|
||||
platform_usage: "Platform usage",
|
||||
};
|
||||
|
||||
const formatTitleCase = (value: string | null | undefined) => (
|
||||
value ? value.replaceAll("_", " ").replace(/\b\w/g, (letter) => letter.toUpperCase()) : "-"
|
||||
);
|
||||
|
||||
const getLedgerEntryLabel = (entry: MpsCreditLedgerEntryResponse) => {
|
||||
if (entry.metric_code) {
|
||||
return metricLabels[entry.metric_code] ?? formatTitleCase(entry.metric_code);
|
||||
}
|
||||
|
||||
if (entry.entry_type === "grant") {
|
||||
return "Credit grant";
|
||||
}
|
||||
|
||||
if (entry.entry_type === "purchase") {
|
||||
return "Credit purchase";
|
||||
}
|
||||
|
||||
return formatTitleCase(entry.entry_type);
|
||||
};
|
||||
|
||||
const formatBillableQuantity = (entry: MpsCreditLedgerEntryResponse) => {
|
||||
if (entry.billable_quantity == null || !entry.quantity_unit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const unit = entry.quantity_unit === "minute" ? "min" : entry.quantity_unit;
|
||||
return `${formatCredits(entry.billable_quantity)} ${unit}`;
|
||||
};
|
||||
|
||||
const getRunHref = (entry: MpsCreditLedgerEntryResponse) => {
|
||||
if (!entry.workflow_id || !entry.workflow_run_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `/workflow/${entry.workflow_id}/run/${entry.workflow_run_id}`;
|
||||
};
|
||||
|
||||
export default function BillingPage() {
|
||||
const auth = useAuth();
|
||||
const { config } = useAppConfig();
|
||||
const { orgContext, loading: orgConfigLoading } = useOrgConfig();
|
||||
const [credits, setCredits] = useState<MpsBillingCreditsResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [purchasing, setPurchasing] = useState(false);
|
||||
|
||||
const isManagedServiceV2 = Boolean(orgContext?.model_services.uses_managed_service_v2);
|
||||
const isBillingV2 = isManagedServiceV2 && credits?.billing_version === "v2";
|
||||
const canPurchaseCredits = isManagedServiceV2 && config?.deploymentMode !== "oss";
|
||||
const isBillingV2 = credits?.billing_version === "v2";
|
||||
const canPurchaseCredits = isBillingV2 && config?.deploymentMode !== "oss";
|
||||
const totalQuota = credits?.total_quota ?? 0;
|
||||
const remainingCredits = credits?.remaining_credits ?? 0;
|
||||
const usedCredits = credits?.total_credits_used ?? 0;
|
||||
|
|
@ -132,7 +173,7 @@ export default function BillingPage() {
|
|||
}
|
||||
};
|
||||
|
||||
if (loading || orgConfigLoading) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="space-y-2">
|
||||
|
|
@ -206,13 +247,14 @@ export default function BillingPage() {
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
{ledgerEntries.length > 0 ? (
|
||||
<div className="bg-card border rounded-lg overflow-hidden shadow-sm">
|
||||
<div className="bg-card border rounded-lg overflow-x-auto shadow-sm">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Activity</TableHead>
|
||||
<TableHead>Origin</TableHead>
|
||||
<TableHead>Run</TableHead>
|
||||
<TableHead className="text-right">Delta</TableHead>
|
||||
<TableHead className="text-right">Balance</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
|
|
@ -221,11 +263,39 @@ export default function BillingPage() {
|
|||
<TableBody>
|
||||
{ledgerEntries.map((entry) => {
|
||||
const delta = entry.credits_delta ?? 0;
|
||||
const runHref = getRunHref(entry);
|
||||
const billableQuantity = formatBillableQuantity(entry);
|
||||
return (
|
||||
<TableRow key={entry.id}>
|
||||
<TableCell>{formatDate(entry.created_at)}</TableCell>
|
||||
<TableCell className="capitalize">{entry.entry_type.replaceAll("_", " ")}</TableCell>
|
||||
<TableCell>{entry.origin || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">{getLedgerEntryLabel(entry)}</span>
|
||||
{billableQuantity && (
|
||||
<span className="text-xs text-muted-foreground">{billableQuantity}</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{entry.origin ? (
|
||||
<Badge variant="secondary">{formatTitleCase(entry.origin)}</Badge>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{entry.workflow_run_id ? (
|
||||
runHref ? (
|
||||
<Link className="font-medium text-primary hover:underline" href={runHref}>
|
||||
#{entry.workflow_run_id}
|
||||
</Link>
|
||||
) : (
|
||||
<span>#{entry.workflow_run_id}</span>
|
||||
)
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={`text-right font-medium ${delta >= 0 ? "text-green-600" : "text-destructive"}`}>
|
||||
{delta >= 0 ? "+" : ""}
|
||||
{formatCredits(delta)}
|
||||
|
|
@ -251,7 +321,6 @@ export default function BillingPage() {
|
|||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Credit Usage</CardTitle>
|
||||
<CardDescription>Current legacy MPS credit allocation.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Progress value={usagePercent} />
|
||||
|
|
|
|||
|
|
@ -3176,6 +3176,38 @@ export type MpsCreditLedgerEntryResponse = {
|
|||
* Payment Order Id
|
||||
*/
|
||||
payment_order_id?: number | null;
|
||||
/**
|
||||
* Metric Code
|
||||
*/
|
||||
metric_code?: string | null;
|
||||
/**
|
||||
* Correlation Id
|
||||
*/
|
||||
correlation_id?: string | null;
|
||||
/**
|
||||
* Aggregation Key
|
||||
*/
|
||||
aggregation_key?: string | null;
|
||||
/**
|
||||
* Usage Event Id
|
||||
*/
|
||||
usage_event_id?: number | null;
|
||||
/**
|
||||
* Workflow Run Id
|
||||
*/
|
||||
workflow_run_id?: number | null;
|
||||
/**
|
||||
* Workflow Id
|
||||
*/
|
||||
workflow_id?: number | null;
|
||||
/**
|
||||
* Billable Quantity
|
||||
*/
|
||||
billable_quantity?: number | null;
|
||||
/**
|
||||
* Quantity Unit
|
||||
*/
|
||||
quantity_unit?: string | null;
|
||||
/**
|
||||
* Metadata
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,18 +2,33 @@
|
|||
* Extract a human-readable message from a backend error response.
|
||||
*
|
||||
* The generated API client returns `{ error }` on failure (it does not throw),
|
||||
* and FastAPI shapes that error as either `{ detail: string }` (HTTPException)
|
||||
* or `{ detail: [{ msg, loc, ... }] }` (422 validation). This normalizes both
|
||||
* to a single string so it can be rendered or thrown directly — never pass the
|
||||
* raw `detail` to React, as the 422 array crashes rendering.
|
||||
* and FastAPI shapes that error as `{ detail: string }`, `{ detail:
|
||||
* [{ msg, loc, ... }] }`, or backend validation arrays like `{ detail:
|
||||
* [{ model, message }] }`. This normalizes those to a single string so it can
|
||||
* be rendered or thrown directly.
|
||||
*/
|
||||
export function detailFromError(err: unknown, fallback = "Request failed"): string {
|
||||
if (typeof err === "string") return err;
|
||||
const e = err as { detail?: unknown };
|
||||
if (typeof e?.detail === "string") return e.detail;
|
||||
if (Array.isArray(e?.detail) && e.detail.length > 0) {
|
||||
const first = e.detail[0] as { msg?: string };
|
||||
if (first?.msg) return first.msg;
|
||||
const messages = e.detail
|
||||
.map((item) => {
|
||||
if (typeof item === "string") return item;
|
||||
if (!item || typeof item !== "object") return null;
|
||||
const detail = item as { message?: unknown; msg?: unknown; model?: unknown };
|
||||
const message = typeof detail.message === "string"
|
||||
? detail.message
|
||||
: typeof detail.msg === "string"
|
||||
? detail.msg
|
||||
: null;
|
||||
if (!message) return null;
|
||||
return typeof detail.model === "string" && detail.model
|
||||
? `${detail.model}: ${message}`
|
||||
: message;
|
||||
})
|
||||
.filter((message): message is string => Boolean(message));
|
||||
if (messages.length > 0) return messages.join("\n");
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue