@flaredev/cli 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +698 -341
- package/package.json +2 -2
- package/templates/app/app/dashboard/costs/page.tsx +93 -0
- package/templates/app/app/dashboard/page.tsx +4 -7
- package/templates/app/app/page.tsx +10 -0
- package/templates/app/components/dashboard/cost-estimator.tsx +77 -0
- package/templates/app/components/dashboard/fields/field-widget.tsx +237 -186
- package/templates/app/components/dashboard/record-actions.tsx +7 -7
- package/templates/app/components/dashboard/record-detail.tsx +1 -1
- package/templates/app/components/dashboard/resource-form-sheet.tsx +86 -0
- package/templates/app/components/dashboard/resource-form.tsx +129 -33
- package/templates/app/components/dashboard/resource-icon.tsx +87 -85
- package/templates/app/components/dashboard/resource-stats.tsx +10 -9
- package/templates/app/components/dashboard/resource-table.tsx +4 -4
- package/templates/app/components/dashboard/row-actions.tsx +3 -3
- package/templates/app/lib/costs.ts +138 -0
- package/templates/app/lib/dashboard-nav.ts +5 -1
- package/templates/app/lib/dashboard.ts +66 -61
- package/templates/app/lib/number.ts +33 -0
- package/templates/app/lib/site.ts +56 -56
- package/templates/app/lib/suggest.ts +35 -0
- package/templates/app/lib/usage.ts +32 -0
- package/templates/next/.env.example +21 -0
- package/templates/next/README.md +16 -0
- package/templates/next/app/api/health/route.ts +7 -0
- package/templates/next/app/dashboard/account/password/page.tsx +19 -0
- package/templates/next/app/dashboard/account/security/page.tsx +31 -0
- package/templates/next/app/dashboard/actions.ts +277 -0
- package/templates/next/app/dashboard/layout.tsx +64 -0
- package/templates/next/app/dashboard/page.tsx +127 -0
- package/templates/next/components/dashboard/resource-chart.tsx +86 -0
- package/templates/next/lib/analytics.ts +71 -0
- package/templates/next/lib/audit.ts +80 -0
- package/templates/next/lib/auth-emails.ts +92 -0
- package/templates/next/lib/auth.ts +156 -0
- package/templates/next/lib/dashboard.ts +161 -0
- package/templates/next/lib/db.ts +24 -0
- package/templates/next/lib/mail.ts +35 -0
- package/templates/next/lib/realtime.ts +23 -0
- package/templates/next/lib/redis.ts +17 -0
- package/templates/next/lib/storage.ts +57 -0
- package/templates/next/lib/theme.ts +83 -0
- package/templates/next/lib/views.ts +23 -0
- package/templates/next/next.config.ts +13 -0
- package/templates/next/prisma/schema/base.prisma +162 -0
- package/templates/next/prisma.config.ts +15 -0
- package/templates/next/tsconfig.json +24 -0
- package/templates/app/components/dashboard/resource-form-dialog.tsx +0 -80
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flaredev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "The flare command-line tool: create, generate, migrate, and deploy Flare apps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Muke Johnbaptist",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"jiti": "^2.7.0",
|
|
42
42
|
"jsonc-parser": "^3.3.1",
|
|
43
43
|
"picocolors": "^1.1.1",
|
|
44
|
-
"@flaredev/core": "0.
|
|
44
|
+
"@flaredev/core": "0.5.1"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=22"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { DatabaseIcon, FilesIcon, HardDriveIcon, LayersIcon } from "lucide-react";
|
|
2
|
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
4
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
5
|
+
import { CostEstimator } from "@/components/dashboard/cost-estimator";
|
|
6
|
+
import { PageHeader } from "@/components/dashboard/page-header";
|
|
7
|
+
import { StatCards } from "@/components/dashboard/stat-card";
|
|
8
|
+
import { compact, FREE, gb } from "@/lib/costs";
|
|
9
|
+
import { requireDashboard, resourceStats, visibleResources } from "@/lib/dashboard";
|
|
10
|
+
import { storedBytes } from "@/lib/usage";
|
|
11
|
+
|
|
12
|
+
export const metadata = { title: "Costs" };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What this app holds, and what it would cost to run.
|
|
16
|
+
*
|
|
17
|
+
* Two halves, and the difference between them matters. The top is measured: rows this
|
|
18
|
+
* app has stored, files it has put in R2. The bottom is modelled, because a Worker can
|
|
19
|
+
* see what it wrote but not how many requests it served — that lives in Cloudflare's
|
|
20
|
+
* analytics, behind an API token this app doesn't hold and shouldn't need.
|
|
21
|
+
*/
|
|
22
|
+
export default async function CostsPage() {
|
|
23
|
+
await requireDashboard("/dashboard/costs");
|
|
24
|
+
const resources = await visibleResources();
|
|
25
|
+
|
|
26
|
+
const [counts, stored] = await Promise.all([
|
|
27
|
+
Promise.all(resources.map(async (resource) => ({ resource, total: (await resourceStats(resource.name)).total }))),
|
|
28
|
+
storedBytes(),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const rows = counts.reduce((sum, entry) => sum + entry.total, 0);
|
|
32
|
+
const r2Gb = stored.bytes / 1024 ** 3;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<PageHeader
|
|
37
|
+
title="Costs"
|
|
38
|
+
description="What this app holds, and what it would cost to run."
|
|
39
|
+
crumbs={[{ label: "Dashboard", href: "/dashboard" }, { label: "Costs" }]}
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<StatCards
|
|
43
|
+
stats={[
|
|
44
|
+
{ label: "Rows stored", value: compact(rows), icon: DatabaseIcon, hint: `across ${resources.length} resources` },
|
|
45
|
+
{ label: "Files in R2", value: compact(stored.files), icon: FilesIcon },
|
|
46
|
+
{ label: "File storage", value: gb(r2Gb), icon: HardDriveIcon, hint: `${FREE.r2StorageGb} GB free` },
|
|
47
|
+
{ label: "Resources", value: resources.length, icon: LayersIcon },
|
|
48
|
+
]}
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<Card>
|
|
52
|
+
<CardHeader>
|
|
53
|
+
<CardTitle>What's in the database</CardTitle>
|
|
54
|
+
</CardHeader>
|
|
55
|
+
<CardContent>
|
|
56
|
+
<Table>
|
|
57
|
+
<TableHeader>
|
|
58
|
+
<TableRow>
|
|
59
|
+
<TableHead>Resource</TableHead>
|
|
60
|
+
<TableHead className="text-right">Rows</TableHead>
|
|
61
|
+
</TableRow>
|
|
62
|
+
</TableHeader>
|
|
63
|
+
<TableBody>
|
|
64
|
+
{counts.map(({ resource, total }) => (
|
|
65
|
+
<TableRow key={resource.name}>
|
|
66
|
+
<TableCell>{resource.pluralLabel}</TableCell>
|
|
67
|
+
<TableCell className="text-right tabular-nums">{total.toLocaleString()}</TableCell>
|
|
68
|
+
</TableRow>
|
|
69
|
+
))}
|
|
70
|
+
</TableBody>
|
|
71
|
+
</Table>
|
|
72
|
+
</CardContent>
|
|
73
|
+
</Card>
|
|
74
|
+
|
|
75
|
+
<CostEstimator storedGb={{ d1: 0, r2: r2Gb }} />
|
|
76
|
+
|
|
77
|
+
<Alert>
|
|
78
|
+
<AlertTitle>This is an estimate, not a bill</AlertTitle>
|
|
79
|
+
<AlertDescription>
|
|
80
|
+
A Worker can see what it stored; it can't see how many requests it served or how many rows it read. Those are in{" "}
|
|
81
|
+
<a href="https://dash.cloudflare.com" target="_blank" rel="noreferrer" className="underline">
|
|
82
|
+
your Cloudflare dashboard
|
|
83
|
+
</a>
|
|
84
|
+
. The assumptions behind the numbers above are in <code>lib/costs.ts</code>, and the reasoning is in the{" "}
|
|
85
|
+
<a href="https://flare-docs.codetotech.com/guides/costs/" target="_blank" rel="noreferrer" className="underline">
|
|
86
|
+
cost guide
|
|
87
|
+
</a>
|
|
88
|
+
.
|
|
89
|
+
</AlertDescription>
|
|
90
|
+
</Alert>
|
|
91
|
+
</>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -10,7 +10,7 @@ import { StatCards, type Stat } from "@/components/dashboard/stat-card";
|
|
|
10
10
|
import { getDb } from "@/db";
|
|
11
11
|
import { passkey } from "@/db/auth-schema";
|
|
12
12
|
import { authConfig, twoFactorAvailable } from "@/lib/auth-config";
|
|
13
|
-
import { adminPermissions,
|
|
13
|
+
import { adminPermissions, resourcePath, resourceStats, trend, visibleResources } from "@/lib/dashboard";
|
|
14
14
|
import { requireSession } from "@/lib/session";
|
|
15
15
|
|
|
16
16
|
export const metadata = { title: "Dashboard" };
|
|
@@ -22,12 +22,9 @@ export default async function DashboardPage() {
|
|
|
22
22
|
|
|
23
23
|
const cards = await Promise.all(
|
|
24
24
|
resources.map(async (resource) => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
adminPermissions(resource.name),
|
|
29
|
-
]);
|
|
30
|
-
return { resource, total, recent, permissions };
|
|
25
|
+
// One query per resource for the count and the trend, not two.
|
|
26
|
+
const [stats, permissions] = await Promise.all([resourceStats(resource.name), adminPermissions(resource.name)]);
|
|
27
|
+
return { resource, total: stats.total, recent: stats, permissions };
|
|
31
28
|
}),
|
|
32
29
|
);
|
|
33
30
|
|
|
@@ -114,6 +114,16 @@ export default function Home() {
|
|
|
114
114
|
<p>
|
|
115
115
|
© {new Date().getFullYear()} {site.name}
|
|
116
116
|
</p>
|
|
117
|
+
{/* Delete this line when the app is your own — it's here so whoever set the
|
|
118
|
+
project up can find what running it actually costs. */}
|
|
119
|
+
<a
|
|
120
|
+
href="https://flare-docs.codetotech.com/guides/costs/"
|
|
121
|
+
target="_blank"
|
|
122
|
+
rel="noreferrer"
|
|
123
|
+
className="hover:text-foreground"
|
|
124
|
+
>
|
|
125
|
+
Built with Flare · what it costs to run
|
|
126
|
+
</a>
|
|
117
127
|
<nav aria-label="Footer" className="flex gap-6">
|
|
118
128
|
<Link href={site.links.terms} className="hover:text-foreground">
|
|
119
129
|
Terms
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
5
|
+
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import { compact, estimate, PER_VIEW, usd } from "@/lib/costs";
|
|
8
|
+
import { groupDigits, ungroup } from "@/lib/number";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What a month would cost at a traffic level you choose.
|
|
12
|
+
*
|
|
13
|
+
* The arithmetic is in lib/costs.ts along with the per-view assumptions it rests on.
|
|
14
|
+
* This is a model, not a bill — the app can see what it stores, not what it served.
|
|
15
|
+
*/
|
|
16
|
+
export function CostEstimator({ storedGb }: { storedGb: { d1: number; r2: number } }) {
|
|
17
|
+
const [views, setViews] = useState("50000");
|
|
18
|
+
const monthly = Math.max(0, Number(views) || 0);
|
|
19
|
+
const result = useMemo(() => estimate(monthly, storedGb), [monthly, storedGb]);
|
|
20
|
+
|
|
21
|
+
const rows = [
|
|
22
|
+
{ label: "Requests", value: compact(result.requests), of: "10M included" },
|
|
23
|
+
{ label: "D1 rows read", value: compact(result.d1RowsRead), of: "25B included" },
|
|
24
|
+
{ label: "D1 rows written", value: compact(result.d1RowsWritten), of: "50M included" },
|
|
25
|
+
{ label: "KV reads", value: compact(result.kvReads), of: "10M included" },
|
|
26
|
+
{ label: "KV writes", value: compact(result.kvWrites), of: "1M included" },
|
|
27
|
+
{ label: "CPU", value: `${compact(result.cpuMs)} ms`, of: "30M ms included" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex flex-col gap-4">
|
|
32
|
+
<Field className="max-w-xs">
|
|
33
|
+
<FieldLabel htmlFor="views">Page views a month</FieldLabel>
|
|
34
|
+
<Input
|
|
35
|
+
id="views"
|
|
36
|
+
inputMode="numeric"
|
|
37
|
+
className="tabular-nums"
|
|
38
|
+
value={groupDigits(views)}
|
|
39
|
+
onChange={(event) => setViews(ungroup(event.target.value, false))}
|
|
40
|
+
/>
|
|
41
|
+
<FieldDescription>
|
|
42
|
+
At {PER_VIEW.d1RowsRead} rows read and {PER_VIEW.cpuMs} ms of CPU per view.
|
|
43
|
+
</FieldDescription>
|
|
44
|
+
</Field>
|
|
45
|
+
|
|
46
|
+
<Card>
|
|
47
|
+
<CardContent className="flex flex-col gap-4">
|
|
48
|
+
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
49
|
+
<span className="text-sm text-muted-foreground">Estimated monthly cost</span>
|
|
50
|
+
<span className="text-3xl font-semibold tabular-nums">{usd(result.totalUsd)}</span>
|
|
51
|
+
</div>
|
|
52
|
+
<p className="text-xs text-muted-foreground">
|
|
53
|
+
{result.overageUsd < 0.005
|
|
54
|
+
? "All of it the $5 Workers Paid subscription — this traffic is inside every included allowance."
|
|
55
|
+
: `$5 subscription plus ${usd(result.overageUsd)} of usage past what's included.`}
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
<div className="grid gap-x-6 gap-y-2 border-t pt-3 sm:grid-cols-2">
|
|
59
|
+
{rows.map((row) => (
|
|
60
|
+
<div key={row.label} className="flex items-baseline justify-between gap-3 text-sm">
|
|
61
|
+
<span className="text-muted-foreground">{row.label}</span>
|
|
62
|
+
<span className="tabular-nums">
|
|
63
|
+
{row.value} <span className="text-xs text-muted-foreground">/ {row.of}</span>
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<p className="border-t pt-3 text-xs text-muted-foreground">
|
|
70
|
+
<span className="font-medium text-foreground">Free plan: </span>
|
|
71
|
+
{result.freePlanVerdict}
|
|
72
|
+
</p>
|
|
73
|
+
</CardContent>
|
|
74
|
+
</Card>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|