Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cost-dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { buildHtml, buildMarkdown, ghNewFileUrl, slugName, REPORT_DIR } from "./
import { ResourceCard } from "./components/ResourceCard";
import { SourcePanel } from "./components/SourcePanel";

const ADD: Service[] = ["ec2", "s3", "rds", "lambda"];
const ADD: Service[] = ["ec2", "s3", "rds", "lambda", "estimate"];
const WORKFLOW_URL =
"https://github.com/NASA-IMPACT/veda-github-actions/actions/workflows/aws-pricing.yml";

Expand Down
13 changes: 12 additions & 1 deletion cost-dashboard/src/components/ResourceCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// One resource in the cart: a per-service input form + its live monthly subtotal + a breakdown.
import { useMemo } from "react";
import type { PricingDoc, Resource } from "../types";
import { SERVICE_LABEL, costOf } from "../compute";
import { SERVICE_LABEL, costOf, ESTIMATE_CATEGORIES } from "../compute";
import { money, rate } from "../format";

interface Props {
Expand Down Expand Up @@ -151,6 +151,17 @@ export function ResourceCard({ resource, pricing, onChange, onRemove }: Props) {
<Num label="Memory" value={resource.params.memoryMB} min={128} step={64} onChange={(n) => set({ memoryMB: n })} suffix="MB" />
</>
)}

{resource.service === "estimate" && (
<>
<Sel label="Service" value={resource.params.category}
onChange={(v) => set({ category: v })}
options={ESTIMATE_CATEGORIES.map((c) => ({ value: c, label: c }))} />
<Num label="Est. monthly cost" value={resource.params.monthlyUSD} min={0} step={1}
onChange={(n) => set({ monthlyUSD: n })} suffix="$/mo"
hint="Rough flat figure — added to the total" />
</>
)}
</div>

<div className="card-foot">
Expand Down
6 changes: 4 additions & 2 deletions cost-dashboard/src/components/SourcePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// plus the exact bulk-file URL and the AWS-published version stamp for this region's data.
import type { PricingDoc, Service } from "../types";

const ORDER: Service[] = ["ec2", "s3", "rds", "lambda"];
const SHORT: Record<Service, string> = { ec2: "EC2", s3: "S3", rds: "RDS", lambda: "Lambda" };
// Only the live-priced services have an AWS source row ("estimate" is a manual flat figure).
type PricedService = Exclude<Service, "estimate">;
const ORDER: PricedService[] = ["ec2", "s3", "rds", "lambda"];
const SHORT: Record<PricedService, string> = { ec2: "EC2", s3: "S3", rds: "RDS", lambda: "Lambda" };

function fmtVersion(v: string): string {
// AWS versions look like "20260728175247" (YYYYMMDDHHMMSS) → "2026-07-28".
Expand Down
16 changes: 16 additions & 0 deletions cost-dashboard/src/compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@ export const SERVICE_LABEL: Record<Service, string> = {
s3: "S3 — object storage",
rds: "RDS — managed database",
lambda: "Lambda — serverless functions",
estimate: "Estimate — flat monthly figure",
};

// Presets for the flat "Estimate" line item — supporting services we don't price live but that still
// belong in the total. "Other" lets you name anything via the card's name field.
export const ESTIMATE_CATEGORIES = [
"CloudFront", "Amplify", "Secrets Manager", "Route 53", "CloudWatch",
"SNS / SQS", "KMS", "WAF", "Data transfer", "Other",
];

export function costOf(r: Resource, p: PricingDoc): Cost {
const b: LineItem[] = [];

Expand Down Expand Up @@ -78,6 +86,14 @@ export function costOf(r: Resource, p: PricingDoc): Cost {
return total(b);
}

if (r.service === "estimate") {
const amount = Math.max(0, r.params.monthlyUSD || 0);
return total(
[{ label: `${r.params.category || "Other"} — flat monthly estimate`, amount }],
"Manual estimate — a flat figure you entered, not from live AWS pricing.",
);
}

// lambda
const lam = p.lambda;
const reqCost = r.params.requests * lam.requestUsd;
Expand Down
2 changes: 2 additions & 0 deletions cost-dashboard/src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,7 @@ export function makeResource(service: Service, p: PricingDoc): Resource {
params: { instanceKey: pickRds(p), count: 1, hours: DEFAULT_HOURS,
storageType: firstKey(Object.keys(p.rds.storage), "gp3"), storageGB: 20 },
};
if (service === "estimate")
return { id, name, service, params: { category: "CloudFront", monthlyUSD: 10 } };
return { id, name, service, params: { requests: 1000000, durationMs: 200, memoryMB: 512 } };
}
2 changes: 2 additions & 0 deletions cost-dashboard/src/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export function configSummary(r: Resource): string {
`${r.params.storageGB} GB ${r.params.storageType}`;
case "lambda":
return `${r.params.requests.toLocaleString()} reqs/mo, ${r.params.durationMs} ms, ${r.params.memoryMB} MB`;
case "estimate":
return `${r.params.category} — flat monthly estimate`;
}
}

Expand Down
2 changes: 1 addition & 1 deletion cost-dashboard/src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function buildHtml(resources: Resource[], p: PricingDoc, name: string, ex
.lines{margin-top:6px}
.li{display:flex;justify-content:space-between;gap:12px;font-size:12px;color:#5a6572}
.b{font-size:11px;font-weight:800;color:#fff;padding:2px 7px;border-radius:5px;background:#5a6572}
.b-ec2{background:#ff9900;color:#232f3e}.b-s3{background:#3f9c35}.b-rds{background:#2e73b8}.b-lambda{background:#c8511b}
.b-ec2{background:#ff9900;color:#232f3e}.b-s3{background:#3f9c35}.b-rds{background:#2e73b8}.b-lambda{background:#c8511b}.b-estimate{background:#7d5ba6}
.total{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;background:#232f3e;color:#fff;border-radius:12px;margin-top:16px}
.total .g{font-size:26px;font-weight:800;color:#ff9900}
.note{font-size:12px;color:#5a6572;margin:14px 2px}
Expand Down
5 changes: 5 additions & 0 deletions cost-dashboard/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
--s3: #3f9c35;
--rds: #2e73b8;
--lambda: #c8511b;
--estimate: #7d5ba6;
--ok: #1a8754;
--shadow: 0 1px 2px rgba(16, 25, 31, 0.06), 0 4px 16px rgba(16, 25, 31, 0.06);
--radius: 12px;
Expand Down Expand Up @@ -90,6 +91,7 @@ body {
.add-btn[data-service="s3"] { border-left-color: var(--s3); }
.add-btn[data-service="rds"] { border-left-color: var(--rds); }
.add-btn[data-service="lambda"] { border-left-color: var(--lambda); }
.add-btn[data-service="estimate"] { border-left-color: var(--estimate); }
.ghost-btn {
border: 1px solid var(--line); background: transparent; color: var(--muted);
font-size: 13px; padding: 8px 12px; border-radius: 8px; cursor: pointer;
Expand Down Expand Up @@ -132,6 +134,7 @@ body {
.card[data-service="s3"] { border-left-color: var(--s3); }
.card[data-service="rds"] { border-left-color: var(--rds); }
.card[data-service="lambda"] { border-left-color: var(--lambda); }
.card[data-service="estimate"] { border-left-color: var(--estimate); }

.card-head {
display: flex; align-items: center; gap: 12px;
Expand All @@ -145,6 +148,7 @@ body {
.card[data-service="s3"] .badge { background: var(--s3); }
.card[data-service="rds"] .badge { background: var(--rds); }
.card[data-service="lambda"] .badge { background: var(--lambda); }
.card[data-service="estimate"] .badge { background: var(--estimate); }
.card-title { font-weight: 600; font-size: 14px; color: var(--ink); white-space: nowrap; }
.name-input {
flex: 1; min-width: 130px; max-width: 340px;
Expand Down Expand Up @@ -245,6 +249,7 @@ span.field-input input { flex: 1; border: 1px solid var(--line); border-radius:
.chip[data-service="s3"] { border-left-color: var(--s3); }
.chip[data-service="rds"] { border-left-color: var(--rds); }
.chip[data-service="lambda"] { border-left-color: var(--lambda); }
.chip[data-service="estimate"] { border-left-color: var(--estimate); }
.summary-right { margin-left: auto; text-align: right; }
.grand { display: block; font-size: 34px; font-weight: 800; color: var(--orange); font-variant-numeric: tabular-nums; line-height: 1.05; }
.grand small { font-size: 15px; color: #c4ccd6; font-weight: 600; margin-left: 2px; }
Expand Down
11 changes: 9 additions & 2 deletions cost-dashboard/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export interface Dataset {

// ---- the "resource cart" (what the user builds) ----

export type Service = "ec2" | "s3" | "rds" | "lambda";
export type Service = "ec2" | "s3" | "rds" | "lambda" | "estimate";

export interface Ec2Params {
instanceType: string;
Expand All @@ -94,13 +94,20 @@ export interface LambdaParams {
durationMs: number;
memoryMB: number;
}
// A flat, user-entered monthly figure for services we don't price live (CloudFront, Amplify, Secrets
// Manager, …). It just contributes its `monthlyUSD` to the grand total so the picture stays complete.
export interface EstimateParams {
category: string;
monthlyUSD: number;
}

// `name` is a user label so a cart with several EC2s stays legible ("Disasters Hub API", "worker").
export type Resource =
| { id: string; name: string; service: "ec2"; params: Ec2Params }
| { id: string; name: string; service: "s3"; params: S3Params }
| { id: string; name: string; service: "rds"; params: RdsParams }
| { id: string; name: string; service: "lambda"; params: LambdaParams };
| { id: string; name: string; service: "lambda"; params: LambdaParams }
| { id: string; name: string; service: "estimate"; params: EstimateParams };

export interface LineItem {
label: string;
Expand Down