@flaredev/cli 0.3.0 → 0.4.0

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.
Files changed (42) hide show
  1. package/dist/index.js +385 -209
  2. package/package.json +2 -2
  3. package/templates/app/app/dashboard/account/layout.tsx +30 -0
  4. package/templates/app/app/dashboard/account/page.tsx +11 -33
  5. package/templates/app/app/dashboard/account/password/page.tsx +22 -0
  6. package/templates/app/app/dashboard/account/security/page.tsx +34 -0
  7. package/templates/app/app/dashboard/account/sessions/page.tsx +9 -0
  8. package/templates/app/app/dashboard/actions.ts +35 -0
  9. package/templates/app/app/dashboard/layout.tsx +28 -4
  10. package/templates/app/app/dashboard/observability/page.tsx +4 -3
  11. package/templates/app/app/dashboard/page.tsx +2 -2
  12. package/templates/app/components/account/account-nav.tsx +39 -0
  13. package/templates/app/components/account/{security-settings.tsx → sections.tsx} +507 -522
  14. package/templates/app/components/dashboard/column-menu.tsx +66 -0
  15. package/templates/app/components/dashboard/dashboard-header.tsx +115 -78
  16. package/templates/app/components/dashboard/dashboard-sidebar.tsx +86 -24
  17. package/templates/app/components/dashboard/dropzone.tsx +92 -0
  18. package/templates/app/components/dashboard/editable.ts +12 -0
  19. package/templates/app/components/dashboard/fields/file-field.tsx +306 -302
  20. package/templates/app/components/dashboard/import-dialog.tsx +60 -7
  21. package/templates/app/components/dashboard/inline-cell.tsx +165 -0
  22. package/templates/app/components/dashboard/local-time.tsx +77 -0
  23. package/templates/app/components/dashboard/query.ts +2 -0
  24. package/templates/app/components/dashboard/record-actions.tsx +113 -0
  25. package/templates/app/components/dashboard/record-detail.tsx +200 -0
  26. package/templates/app/components/dashboard/resource-chart.tsx +91 -0
  27. package/templates/app/components/dashboard/resource-stats.tsx +6 -11
  28. package/templates/app/components/dashboard/resource-table.tsx +405 -331
  29. package/templates/app/components/dashboard/row-actions.tsx +156 -144
  30. package/templates/app/components/dashboard/save-view-button.tsx +86 -0
  31. package/templates/app/components/dashboard/user-menu.tsx +91 -0
  32. package/templates/app/components/ui/sonner.tsx +33 -13
  33. package/templates/app/db/flare-schema.ts +22 -0
  34. package/templates/app/lib/api.ts +12 -0
  35. package/templates/app/lib/dashboard.ts +165 -138
  36. package/templates/app/lib/views.ts +29 -0
  37. package/templates/app/migrations/0000_init.sql +11 -0
  38. package/templates/app/migrations/meta/0000_snapshot.json +63 -1
  39. package/templates/app/vite.config.ts +41 -31
  40. package/templates/locks/package-lock.json +3 -3
  41. package/templates/locks/pnpm-lock.yaml +4 -4
  42. package/templates/locks/yarn.lock +3548 -0
@@ -0,0 +1,91 @@
1
+ import { sql } from "drizzle-orm";
2
+ import type { Resource } from "@flaredev/core";
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4
+ import { getDb } from "@/db";
5
+ import { resourceTables } from "@/resources/server";
6
+ import { cached, resourceTag, TTL } from "@/lib/cache";
7
+
8
+ export interface DayCount {
9
+ /** "2026-09-23" */
10
+ date: string;
11
+ total: number;
12
+ }
13
+
14
+ /**
15
+ * How many records were created a day, grouped in SQL rather than by reading rows: one
16
+ * query over the created_at index, whatever the table's size.
17
+ */
18
+ export function createdPerDay(name: string, days = 30): Promise<DayCount[]> {
19
+ const counts = cached(
20
+ async (resourceName: string, window: number) => {
21
+ const entry = (resourceTables as unknown as Record<string, { table: unknown } | undefined>)[resourceName];
22
+ const table = entry?.table as { createdAt?: unknown } | undefined;
23
+ if (!table?.createdAt) return [];
24
+ const since = new Date(Date.now() - window * 86_400_000);
25
+ const rows = (await getDb()
26
+ .select({
27
+ date: sql<string>`strftime('%Y-%m-%d', ${table.createdAt} / 1000, 'unixepoch')`,
28
+ total: sql<number>`count(*)`,
29
+ })
30
+ .from(table as never)
31
+ .where(sql`${table.createdAt} >= ${since.getTime()}`)
32
+ .groupBy(sql`1`)
33
+ .orderBy(sql`1`)) as DayCount[];
34
+ return rows;
35
+ },
36
+ ["flare", "created-per-day"],
37
+ { tags: [resourceTag(name)], revalidate: TTL.short },
38
+ );
39
+ return counts(name, days);
40
+ }
41
+
42
+ /** Fill the gaps: a day with nothing created still needs a column. */
43
+ function series(rows: DayCount[], days: number): DayCount[] {
44
+ const found = new Map(rows.map((row) => [row.date, row.total]));
45
+ return Array.from({ length: days }, (_, index) => {
46
+ const date = new Date(Date.now() - (days - 1 - index) * 86_400_000).toISOString().slice(0, 10);
47
+ return { date, total: found.get(date) ?? 0 };
48
+ });
49
+ }
50
+
51
+ /**
52
+ * New records a day for the last month, above the table.
53
+ *
54
+ * Drawn with CSS rather than a charting library: thirty numbers don't justify shipping
55
+ * one to the browser, and this renders on the server with the rest of the page.
56
+ */
57
+ export async function ResourceChart({ resource, days = 30 }: { resource: Resource; days?: number }) {
58
+ const rows = series(await createdPerDay(resource.name, days), days);
59
+ const total = rows.reduce((sum, row) => sum + row.total, 0);
60
+ if (total === 0) return null;
61
+
62
+ const peak = Math.max(1, ...rows.map((row) => row.total));
63
+ const month = new Intl.DateTimeFormat(undefined, { day: "numeric", month: "short" });
64
+
65
+ return (
66
+ <Card>
67
+ <CardHeader>
68
+ <CardTitle className="text-base">New {resource.pluralLabel.toLowerCase()}</CardTitle>
69
+ <CardDescription>
70
+ {total.toLocaleString()} in the last {days} days.
71
+ </CardDescription>
72
+ </CardHeader>
73
+ <CardContent>
74
+ <div className="flex h-28 items-end gap-0.5" role="img" aria-label={`New ${resource.pluralLabel.toLowerCase()} a day for the last ${days} days`}>
75
+ {rows.map((row) => (
76
+ <div
77
+ key={row.date}
78
+ title={`${row.date}: ${row.total}`}
79
+ style={{ height: `${Math.max(2, Math.round((row.total / peak) * 100))}%` }}
80
+ className="flex-1 rounded-t-sm bg-primary/70 transition-colors hover:bg-primary"
81
+ />
82
+ ))}
83
+ </div>
84
+ <div className="mt-2 flex justify-between text-xs text-muted-foreground">
85
+ <span>{month.format(new Date(`${rows[0]!.date}T00:00:00Z`))}</span>
86
+ <span>{month.format(new Date(`${rows[rows.length - 1]!.date}T00:00:00Z`))}</span>
87
+ </div>
88
+ </CardContent>
89
+ </Card>
90
+ );
91
+ }
@@ -1,5 +1,5 @@
1
1
  import { storedFields, optionLabel, type Resource } from "@flaredev/core";
2
- import { dashboardStore, recentCounts, recordCount, trend } from "@/lib/dashboard";
2
+ import { recentCounts, recordCount, trend, valueCounts } from "@/lib/dashboard";
3
3
  import { resourceIcon } from "./resource-icon";
4
4
  import { StatCards, type Stat } from "./stat-card";
5
5
 
@@ -31,19 +31,14 @@ export async function ResourceStats({ resource }: { resource: Resource }) {
31
31
  if (statusField && total > 0) {
32
32
  const [key, def] = statusField;
33
33
  if (def.kind === "enum") {
34
- const store = dashboardStore(resource.name);
34
+ const counts = await valueCounts(resource.name, key);
35
35
  // Two options at most: a strip of eight cards is a wall, not a summary.
36
- const counts = await Promise.all(
37
- def.options.slice(0, 2).map(async (option) => {
38
- const result = await store.list(new URLSearchParams({ perPage: "1", [`filter[${key}]`]: option }));
39
- return { option, total: result.ok ? result.data.meta.total : 0 };
40
- }),
41
- );
42
- for (const { option, total: count } of counts) {
36
+ for (const option of def.options.slice(0, 2)) {
37
+ const value = counts[option] ?? 0;
43
38
  stats.push({
44
39
  label: optionLabel(def, option),
45
- value: count,
46
- hint: total > 0 ? `${Math.round((count / total) * 100)}% of all` : undefined,
40
+ value,
41
+ hint: `${Math.round((value / total) * 100)}% of all`,
47
42
  });
48
43
  }
49
44
  }