stagent 0.6.3 → 0.8.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.
- package/README.md +21 -2
- package/dist/cli.js +226 -1
- package/docs/.coverage-gaps.json +66 -16
- package/docs/.last-generated +1 -1
- package/docs/features/dashboard-kanban.md +13 -7
- package/docs/features/settings.md +15 -3
- package/docs/features/tables.md +122 -0
- package/docs/index.md +3 -2
- package/docs/journeys/developer.md +26 -16
- package/docs/journeys/personal-use.md +23 -9
- package/docs/journeys/power-user.md +40 -14
- package/docs/journeys/work-use.md +43 -15
- package/docs/manifest.json +27 -17
- package/package.json +3 -1
- package/src/app/api/chat/entities/search/route.ts +12 -3
- package/src/app/api/projects/[id]/route.ts +37 -0
- package/src/app/api/projects/__tests__/delete-project.test.ts +12 -0
- package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
- package/src/app/api/snapshots/[id]/route.ts +44 -0
- package/src/app/api/snapshots/route.ts +54 -0
- package/src/app/api/snapshots/settings/route.ts +67 -0
- package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
- package/src/app/api/tables/[id]/charts/route.ts +72 -0
- package/src/app/api/tables/[id]/columns/route.ts +70 -0
- package/src/app/api/tables/[id]/export/route.ts +94 -0
- package/src/app/api/tables/[id]/history/route.ts +15 -0
- package/src/app/api/tables/[id]/import/route.ts +111 -0
- package/src/app/api/tables/[id]/route.ts +86 -0
- package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
- package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
- package/src/app/api/tables/[id]/rows/route.ts +101 -0
- package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
- package/src/app/api/tables/[id]/triggers/route.ts +122 -0
- package/src/app/api/tables/route.ts +65 -0
- package/src/app/api/tables/templates/route.ts +92 -0
- package/src/app/globals.css +14 -0
- package/src/app/settings/page.tsx +2 -0
- package/src/app/tables/[id]/page.tsx +67 -0
- package/src/app/tables/page.tsx +21 -0
- package/src/app/tables/templates/page.tsx +19 -0
- package/src/components/book/book-reader.tsx +62 -9
- package/src/components/book/content-blocks.tsx +6 -1
- package/src/components/chat/chat-table-result.tsx +139 -0
- package/src/components/documents/document-browser.tsx +1 -1
- package/src/components/projects/project-form-sheet.tsx +3 -27
- package/src/components/schedules/schedule-form.tsx +5 -27
- package/src/components/settings/data-management-section.tsx +17 -12
- package/src/components/settings/database-snapshots-section.tsx +469 -0
- package/src/components/shared/app-sidebar.tsx +2 -0
- package/src/components/shared/document-picker-sheet.tsx +214 -11
- package/src/components/tables/table-browser.tsx +234 -0
- package/src/components/tables/table-cell-editor.tsx +226 -0
- package/src/components/tables/table-chart-builder.tsx +288 -0
- package/src/components/tables/table-chart-view.tsx +146 -0
- package/src/components/tables/table-column-header.tsx +103 -0
- package/src/components/tables/table-column-sheet.tsx +331 -0
- package/src/components/tables/table-create-sheet.tsx +240 -0
- package/src/components/tables/table-detail-sheet.tsx +144 -0
- package/src/components/tables/table-detail-tabs.tsx +278 -0
- package/src/components/tables/table-grid.tsx +61 -0
- package/src/components/tables/table-history-tab.tsx +148 -0
- package/src/components/tables/table-import-wizard.tsx +542 -0
- package/src/components/tables/table-list-table.tsx +95 -0
- package/src/components/tables/table-relation-combobox.tsx +217 -0
- package/src/components/tables/table-row-sheet.tsx +271 -0
- package/src/components/tables/table-spreadsheet.tsx +394 -0
- package/src/components/tables/table-template-gallery.tsx +162 -0
- package/src/components/tables/table-template-preview.tsx +219 -0
- package/src/components/tables/table-toolbar.tsx +79 -0
- package/src/components/tables/table-triggers-tab.tsx +446 -0
- package/src/components/tables/types.ts +6 -0
- package/src/components/tables/use-spreadsheet-keys.ts +171 -0
- package/src/components/tables/utils.ts +29 -0
- package/src/components/tasks/task-create-panel.tsx +5 -31
- package/src/components/tasks/task-edit-dialog.tsx +5 -27
- package/src/components/workflows/workflow-form-view.tsx +11 -35
- package/src/components/workflows/workflow-status-view.tsx +1 -1
- package/src/instrumentation.ts +3 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
- package/src/lib/agents/claude-agent.ts +3 -1
- package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +23 -0
- package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +10 -0
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +1 -1
- package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
- package/src/lib/agents/runtime/openai-direct.ts +29 -0
- package/src/lib/book/chapter-generator.ts +81 -5
- package/src/lib/book/chapter-mapping.ts +58 -24
- package/src/lib/book/content.ts +83 -47
- package/src/lib/book/markdown-parser.ts +1 -1
- package/src/lib/book/reading-paths.ts +8 -8
- package/src/lib/book/types.ts +1 -1
- package/src/lib/book/update-detector.ts +4 -1
- package/src/lib/chat/stagent-tools.ts +2 -0
- package/src/lib/chat/tool-catalog.ts +34 -0
- package/src/lib/chat/tools/table-tools.ts +955 -0
- package/src/lib/chat/tools/workflow-tools.ts +9 -1
- package/src/lib/constants/table-status.ts +68 -0
- package/src/lib/data/__tests__/clear.test.ts +1 -1
- package/src/lib/data/clear.ts +45 -0
- package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
- package/src/lib/data/seed-data/conversations.ts +350 -42
- package/src/lib/data/seed-data/documents.ts +564 -591
- package/src/lib/data/seed-data/learned-context.ts +101 -22
- package/src/lib/data/seed-data/notifications.ts +344 -70
- package/src/lib/data/seed-data/profile-test-results.ts +92 -11
- package/src/lib/data/seed-data/profiles.ts +144 -46
- package/src/lib/data/seed-data/projects.ts +50 -18
- package/src/lib/data/seed-data/repo-imports.ts +28 -13
- package/src/lib/data/seed-data/schedules.ts +208 -41
- package/src/lib/data/seed-data/table-templates.ts +234 -0
- package/src/lib/data/seed-data/tasks.ts +614 -116
- package/src/lib/data/seed-data/usage-ledger.ts +182 -103
- package/src/lib/data/seed-data/user-tables.ts +203 -0
- package/src/lib/data/seed-data/views.ts +52 -7
- package/src/lib/data/seed-data/workflows.ts +231 -84
- package/src/lib/data/seed.ts +55 -14
- package/src/lib/data/tables.ts +417 -0
- package/src/lib/db/bootstrap.ts +227 -0
- package/src/lib/db/index.ts +9 -0
- package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
- package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
- package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
- package/src/lib/db/schema.ts +368 -0
- package/src/lib/snapshots/auto-backup.ts +132 -0
- package/src/lib/snapshots/retention.ts +64 -0
- package/src/lib/snapshots/snapshot-manager.ts +429 -0
- package/src/lib/tables/computed.ts +61 -0
- package/src/lib/tables/context-builder.ts +139 -0
- package/src/lib/tables/formula-engine.ts +415 -0
- package/src/lib/tables/history.ts +115 -0
- package/src/lib/tables/import.ts +343 -0
- package/src/lib/tables/query-builder.ts +152 -0
- package/src/lib/tables/trigger-evaluator.ts +146 -0
- package/src/lib/tables/types.ts +141 -0
- package/src/lib/tables/validation.ts +119 -0
- package/src/lib/utils/stagent-paths.ts +20 -0
- package/src/lib/workflows/types.ts +1 -1
- package/tsconfig.json +3 -1
- /package/docs/features/{playbook.md → user-guide.md} +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import {
|
|
7
|
+
Sheet,
|
|
8
|
+
SheetContent,
|
|
9
|
+
SheetHeader,
|
|
10
|
+
SheetTitle,
|
|
11
|
+
SheetFooter,
|
|
12
|
+
} from "@/components/ui/sheet";
|
|
13
|
+
import { ExternalLink, Trash2 } from "lucide-react";
|
|
14
|
+
import { toast } from "sonner";
|
|
15
|
+
import { tableSourceVariant } from "@/lib/constants/table-status";
|
|
16
|
+
import { formatRowCount, formatColumnCount } from "./utils";
|
|
17
|
+
import type { TableWithRelations } from "./types";
|
|
18
|
+
import type { ColumnDef } from "@/lib/tables/types";
|
|
19
|
+
|
|
20
|
+
interface TableDetailSheetProps {
|
|
21
|
+
table: TableWithRelations;
|
|
22
|
+
open: boolean;
|
|
23
|
+
onOpenChange: (open: boolean) => void;
|
|
24
|
+
onDeleted: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function TableDetailSheet({
|
|
28
|
+
table,
|
|
29
|
+
open,
|
|
30
|
+
onOpenChange,
|
|
31
|
+
onDeleted,
|
|
32
|
+
}: TableDetailSheetProps) {
|
|
33
|
+
const router = useRouter();
|
|
34
|
+
|
|
35
|
+
let columns: ColumnDef[] = [];
|
|
36
|
+
try {
|
|
37
|
+
columns = JSON.parse(table.columnSchema) as ColumnDef[];
|
|
38
|
+
} catch {
|
|
39
|
+
// Invalid schema
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function handleDelete() {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(`/api/tables/${table.id}`, {
|
|
45
|
+
method: "DELETE",
|
|
46
|
+
});
|
|
47
|
+
if (res.ok) {
|
|
48
|
+
toast.success("Table deleted");
|
|
49
|
+
onOpenChange(false);
|
|
50
|
+
onDeleted();
|
|
51
|
+
} else {
|
|
52
|
+
toast.error("Failed to delete table");
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
toast.error("Failed to delete table");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
61
|
+
<SheetContent side="right" className="w-[420px] sm:max-w-[420px]">
|
|
62
|
+
<SheetHeader>
|
|
63
|
+
<SheetTitle>{table.name}</SheetTitle>
|
|
64
|
+
</SheetHeader>
|
|
65
|
+
|
|
66
|
+
<div className="px-6 pb-6 space-y-4 overflow-y-auto">
|
|
67
|
+
{table.description && (
|
|
68
|
+
<p className="text-sm text-muted-foreground">
|
|
69
|
+
{table.description}
|
|
70
|
+
</p>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<div className="flex items-center gap-2 text-sm">
|
|
74
|
+
<Badge variant={tableSourceVariant[table.source]}>
|
|
75
|
+
{table.source}
|
|
76
|
+
</Badge>
|
|
77
|
+
<span className="text-muted-foreground">
|
|
78
|
+
{formatColumnCount(table.columnCount)}
|
|
79
|
+
</span>
|
|
80
|
+
<span className="text-muted-foreground">
|
|
81
|
+
{formatRowCount(table.rowCount)}
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{table.projectName && (
|
|
86
|
+
<div className="text-sm">
|
|
87
|
+
<span className="text-muted-foreground">Project: </span>
|
|
88
|
+
{table.projectName}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{columns.length > 0 && (
|
|
93
|
+
<div className="space-y-1">
|
|
94
|
+
<h4 className="text-sm font-medium">Columns</h4>
|
|
95
|
+
<div className="rounded-md border divide-y">
|
|
96
|
+
{columns.map((col) => (
|
|
97
|
+
<div
|
|
98
|
+
key={col.name}
|
|
99
|
+
className="flex items-center justify-between px-3 py-2 text-sm"
|
|
100
|
+
>
|
|
101
|
+
<span>{col.displayName}</span>
|
|
102
|
+
<Badge variant="outline" className="text-xs">
|
|
103
|
+
{col.dataType}
|
|
104
|
+
</Badge>
|
|
105
|
+
</div>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
<div className="text-xs text-muted-foreground">
|
|
112
|
+
Created{" "}
|
|
113
|
+
{table.createdAt
|
|
114
|
+
? new Date(table.createdAt).toLocaleDateString()
|
|
115
|
+
: "—"}
|
|
116
|
+
{" · "}
|
|
117
|
+
Updated{" "}
|
|
118
|
+
{table.updatedAt
|
|
119
|
+
? new Date(table.updatedAt).toLocaleDateString()
|
|
120
|
+
: "—"}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<SheetFooter className="px-6">
|
|
125
|
+
<Button
|
|
126
|
+
variant="destructive"
|
|
127
|
+
size="sm"
|
|
128
|
+
onClick={handleDelete}
|
|
129
|
+
>
|
|
130
|
+
<Trash2 className="h-4 w-4 mr-1" />
|
|
131
|
+
Delete
|
|
132
|
+
</Button>
|
|
133
|
+
<Button
|
|
134
|
+
size="sm"
|
|
135
|
+
onClick={() => router.push(`/tables/${table.id}`)}
|
|
136
|
+
>
|
|
137
|
+
<ExternalLink className="h-4 w-4 mr-1" />
|
|
138
|
+
Open
|
|
139
|
+
</Button>
|
|
140
|
+
</SheetFooter>
|
|
141
|
+
</SheetContent>
|
|
142
|
+
</Sheet>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Tabs,
|
|
6
|
+
TabsContent,
|
|
7
|
+
TabsList,
|
|
8
|
+
TabsTrigger,
|
|
9
|
+
} from "@/components/ui/tabs";
|
|
10
|
+
import { Badge } from "@/components/ui/badge";
|
|
11
|
+
import { TableSpreadsheet } from "./table-spreadsheet";
|
|
12
|
+
import { TableTriggersTab } from "./table-triggers-tab";
|
|
13
|
+
import { TableHistoryTab } from "./table-history-tab";
|
|
14
|
+
import { TableChartBuilder, type EditChartData } from "./table-chart-builder";
|
|
15
|
+
import { TableChartView } from "./table-chart-view";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
import { BarChart3, Pencil, Plus, Trash2 } from "lucide-react";
|
|
18
|
+
import { EmptyState } from "@/components/shared/empty-state";
|
|
19
|
+
import { ConfirmDialog } from "@/components/shared/confirm-dialog";
|
|
20
|
+
import { toast } from "sonner";
|
|
21
|
+
import { tableSourceVariant } from "@/lib/constants/table-status";
|
|
22
|
+
import type { ColumnDef } from "@/lib/tables/types";
|
|
23
|
+
import type { UserTableRowRow } from "@/lib/db/schema";
|
|
24
|
+
|
|
25
|
+
interface ChartView {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
config: {
|
|
29
|
+
type: "bar" | "line" | "pie" | "scatter";
|
|
30
|
+
xColumn: string;
|
|
31
|
+
yColumn?: string;
|
|
32
|
+
aggregation?: "sum" | "avg" | "count" | "min" | "max";
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface TableMeta {
|
|
37
|
+
source: string;
|
|
38
|
+
projectName: string | null;
|
|
39
|
+
rowCount: number;
|
|
40
|
+
createdAt: string | null;
|
|
41
|
+
updatedAt: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface TableDetailTabsProps {
|
|
45
|
+
tableId: string;
|
|
46
|
+
columns: ColumnDef[];
|
|
47
|
+
initialRows: UserTableRowRow[];
|
|
48
|
+
tableMeta?: TableMeta;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function TableDetailTabs({
|
|
52
|
+
tableId,
|
|
53
|
+
columns,
|
|
54
|
+
initialRows,
|
|
55
|
+
tableMeta,
|
|
56
|
+
}: TableDetailTabsProps) {
|
|
57
|
+
const [charts, setCharts] = useState<ChartView[]>([]);
|
|
58
|
+
const [chartBuilderOpen, setChartBuilderOpen] = useState(false);
|
|
59
|
+
const [editChart, setEditChart] = useState<EditChartData | null>(null);
|
|
60
|
+
const [deleteTarget, setDeleteTarget] = useState<ChartView | null>(null);
|
|
61
|
+
|
|
62
|
+
// Parse rows for chart rendering
|
|
63
|
+
const parsedRows = initialRows.map((r) => ({
|
|
64
|
+
data: typeof r.data === "string" ? JSON.parse(r.data) as Record<string, unknown> : r.data as Record<string, unknown>,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
const fetchCharts = useCallback(async () => {
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(`/api/tables/${tableId}/charts`);
|
|
70
|
+
if (res.ok) setCharts(await res.json());
|
|
71
|
+
} catch { /* silent */ }
|
|
72
|
+
}, [tableId]);
|
|
73
|
+
|
|
74
|
+
useEffect(() => { fetchCharts(); }, [fetchCharts]);
|
|
75
|
+
|
|
76
|
+
function handleEdit(chart: ChartView) {
|
|
77
|
+
setEditChart({
|
|
78
|
+
id: chart.id,
|
|
79
|
+
name: chart.name,
|
|
80
|
+
config: chart.config,
|
|
81
|
+
});
|
|
82
|
+
setChartBuilderOpen(true);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function handleDelete() {
|
|
86
|
+
if (!deleteTarget) return;
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch(`/api/tables/${tableId}/charts/${deleteTarget.id}`, {
|
|
89
|
+
method: "DELETE",
|
|
90
|
+
});
|
|
91
|
+
if (res.ok || res.status === 204) {
|
|
92
|
+
toast.success(`Chart "${deleteTarget.name}" deleted`);
|
|
93
|
+
fetchCharts();
|
|
94
|
+
} else {
|
|
95
|
+
toast.error("Failed to delete chart");
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
toast.error("Failed to delete chart");
|
|
99
|
+
} finally {
|
|
100
|
+
setDeleteTarget(null);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleBuilderClose(open: boolean) {
|
|
105
|
+
setChartBuilderOpen(open);
|
|
106
|
+
if (!open) setEditChart(null);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
<Tabs defaultValue="data" className="w-full">
|
|
112
|
+
<TabsList>
|
|
113
|
+
<TabsTrigger value="data">Data</TabsTrigger>
|
|
114
|
+
<TabsTrigger value="charts">Charts</TabsTrigger>
|
|
115
|
+
<TabsTrigger value="triggers">Triggers</TabsTrigger>
|
|
116
|
+
<TabsTrigger value="history">History</TabsTrigger>
|
|
117
|
+
{tableMeta && <TabsTrigger value="details">Details</TabsTrigger>}
|
|
118
|
+
</TabsList>
|
|
119
|
+
|
|
120
|
+
<TabsContent value="data" className="mt-4">
|
|
121
|
+
<TableSpreadsheet
|
|
122
|
+
tableId={tableId}
|
|
123
|
+
columns={columns}
|
|
124
|
+
initialRows={initialRows}
|
|
125
|
+
/>
|
|
126
|
+
</TabsContent>
|
|
127
|
+
|
|
128
|
+
<TabsContent value="charts" className="mt-4">
|
|
129
|
+
<div className="space-y-4">
|
|
130
|
+
<div className="flex justify-end">
|
|
131
|
+
<Button variant="outline" size="sm" onClick={() => { setEditChart(null); setChartBuilderOpen(true); }}>
|
|
132
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
133
|
+
New Chart
|
|
134
|
+
</Button>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{charts.length === 0 ? (
|
|
138
|
+
<EmptyState
|
|
139
|
+
icon={BarChart3}
|
|
140
|
+
heading="No charts yet"
|
|
141
|
+
description="Create a chart to visualize your table data."
|
|
142
|
+
action={
|
|
143
|
+
<Button onClick={() => setChartBuilderOpen(true)}>
|
|
144
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
145
|
+
Create Chart
|
|
146
|
+
</Button>
|
|
147
|
+
}
|
|
148
|
+
/>
|
|
149
|
+
) : (
|
|
150
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
151
|
+
{charts.map((chart) => (
|
|
152
|
+
<div key={chart.id} className="border rounded-lg p-4">
|
|
153
|
+
<div className="flex items-center justify-between mb-2">
|
|
154
|
+
<h4 className="text-sm font-medium truncate">{chart.name}</h4>
|
|
155
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
156
|
+
<Button
|
|
157
|
+
variant="ghost"
|
|
158
|
+
size="icon"
|
|
159
|
+
className="h-7 w-7"
|
|
160
|
+
onClick={() => handleEdit(chart)}
|
|
161
|
+
aria-label={`Edit ${chart.name}`}
|
|
162
|
+
>
|
|
163
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
164
|
+
</Button>
|
|
165
|
+
<Button
|
|
166
|
+
variant="ghost"
|
|
167
|
+
size="icon"
|
|
168
|
+
className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
|
169
|
+
onClick={() => setDeleteTarget(chart)}
|
|
170
|
+
aria-label={`Delete ${chart.name}`}
|
|
171
|
+
>
|
|
172
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<TableChartView
|
|
177
|
+
config={chart.config}
|
|
178
|
+
title=""
|
|
179
|
+
rows={parsedRows}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
<TableChartBuilder
|
|
187
|
+
tableId={tableId}
|
|
188
|
+
columns={columns.map((c) => ({
|
|
189
|
+
name: c.name,
|
|
190
|
+
displayName: c.displayName,
|
|
191
|
+
dataType: c.dataType,
|
|
192
|
+
}))}
|
|
193
|
+
open={chartBuilderOpen}
|
|
194
|
+
onOpenChange={handleBuilderClose}
|
|
195
|
+
onChartSaved={fetchCharts}
|
|
196
|
+
editChart={editChart}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
</TabsContent>
|
|
200
|
+
|
|
201
|
+
<TabsContent value="triggers" className="mt-4">
|
|
202
|
+
<TableTriggersTab tableId={tableId} />
|
|
203
|
+
</TabsContent>
|
|
204
|
+
|
|
205
|
+
<TabsContent value="history" className="mt-4">
|
|
206
|
+
<TableHistoryTab tableId={tableId} />
|
|
207
|
+
</TabsContent>
|
|
208
|
+
|
|
209
|
+
{tableMeta && (
|
|
210
|
+
<TabsContent value="details" className="mt-4">
|
|
211
|
+
<div className="space-y-4 max-w-lg">
|
|
212
|
+
<div className="grid grid-cols-2 gap-y-3 text-sm">
|
|
213
|
+
<span className="text-muted-foreground">Source</span>
|
|
214
|
+
<div>
|
|
215
|
+
<Badge variant={tableSourceVariant[tableMeta.source as keyof typeof tableSourceVariant] ?? "outline"}>
|
|
216
|
+
{tableMeta.source}
|
|
217
|
+
</Badge>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<span className="text-muted-foreground">Project</span>
|
|
221
|
+
<span>{tableMeta.projectName ?? "—"}</span>
|
|
222
|
+
|
|
223
|
+
<span className="text-muted-foreground">Columns</span>
|
|
224
|
+
<span>{columns.length}</span>
|
|
225
|
+
|
|
226
|
+
<span className="text-muted-foreground">Rows</span>
|
|
227
|
+
<span>{tableMeta.rowCount}</span>
|
|
228
|
+
|
|
229
|
+
<span className="text-muted-foreground">Created</span>
|
|
230
|
+
<span>
|
|
231
|
+
{tableMeta.createdAt
|
|
232
|
+
? new Date(tableMeta.createdAt).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })
|
|
233
|
+
: "—"}
|
|
234
|
+
</span>
|
|
235
|
+
|
|
236
|
+
<span className="text-muted-foreground">Updated</span>
|
|
237
|
+
<span>
|
|
238
|
+
{tableMeta.updatedAt
|
|
239
|
+
? new Date(tableMeta.updatedAt).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })
|
|
240
|
+
: "—"}
|
|
241
|
+
</span>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{columns.length > 0 && (
|
|
245
|
+
<div className="space-y-2">
|
|
246
|
+
<h4 className="text-sm font-medium">Column Schema</h4>
|
|
247
|
+
<div className="rounded-md border divide-y">
|
|
248
|
+
{columns.map((col) => (
|
|
249
|
+
<div
|
|
250
|
+
key={col.name}
|
|
251
|
+
className="flex items-center justify-between px-3 py-2 text-sm"
|
|
252
|
+
>
|
|
253
|
+
<span>{col.displayName}</span>
|
|
254
|
+
<Badge variant="outline" className="text-xs">
|
|
255
|
+
{col.dataType}
|
|
256
|
+
</Badge>
|
|
257
|
+
</div>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
</TabsContent>
|
|
264
|
+
)}
|
|
265
|
+
</Tabs>
|
|
266
|
+
|
|
267
|
+
<ConfirmDialog
|
|
268
|
+
open={!!deleteTarget}
|
|
269
|
+
onOpenChange={(open) => { if (!open) setDeleteTarget(null); }}
|
|
270
|
+
title={`Delete chart "${deleteTarget?.name}"?`}
|
|
271
|
+
description="This will permanently remove this chart. The underlying table data is not affected."
|
|
272
|
+
confirmLabel="Delete Chart"
|
|
273
|
+
onConfirm={handleDelete}
|
|
274
|
+
destructive
|
|
275
|
+
/>
|
|
276
|
+
</>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import { Table2 } from "lucide-react";
|
|
6
|
+
import { tableSourceVariant } from "@/lib/constants/table-status";
|
|
7
|
+
import { formatRowCount, formatColumnCount } from "./utils";
|
|
8
|
+
import type { TableWithRelations } from "./types";
|
|
9
|
+
|
|
10
|
+
interface TableGridProps {
|
|
11
|
+
tables: TableWithRelations[];
|
|
12
|
+
onSelect: (id: string) => void;
|
|
13
|
+
onOpen: (id: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function TableGrid({ tables, onSelect, onOpen }: TableGridProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
19
|
+
{tables.map((t) => (
|
|
20
|
+
<Card
|
|
21
|
+
key={t.id}
|
|
22
|
+
className="cursor-pointer hover:border-primary/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-lg"
|
|
23
|
+
tabIndex={0}
|
|
24
|
+
onClick={() => onSelect(t.id)}
|
|
25
|
+
onDoubleClick={() => onOpen(t.id)}
|
|
26
|
+
onKeyDown={(e) => {
|
|
27
|
+
if (e.key === "Enter") onOpen(t.id);
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<CardHeader className="pb-2">
|
|
31
|
+
<div className="flex items-start justify-between">
|
|
32
|
+
<div className="flex items-center gap-2">
|
|
33
|
+
<Table2 className="h-4 w-4 text-muted-foreground" />
|
|
34
|
+
<CardTitle className="text-sm font-medium">
|
|
35
|
+
{t.name}
|
|
36
|
+
</CardTitle>
|
|
37
|
+
</div>
|
|
38
|
+
<Badge variant={tableSourceVariant[t.source]} className="text-xs">
|
|
39
|
+
{t.source}
|
|
40
|
+
</Badge>
|
|
41
|
+
</div>
|
|
42
|
+
</CardHeader>
|
|
43
|
+
<CardContent>
|
|
44
|
+
{t.description && (
|
|
45
|
+
<p className="text-xs text-muted-foreground mb-2 line-clamp-2">
|
|
46
|
+
{t.description}
|
|
47
|
+
</p>
|
|
48
|
+
)}
|
|
49
|
+
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
50
|
+
<span>{formatColumnCount(t.columnCount)}</span>
|
|
51
|
+
<span>{formatRowCount(t.rowCount)}</span>
|
|
52
|
+
{t.projectName && (
|
|
53
|
+
<span className="truncate">{t.projectName}</span>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
</CardContent>
|
|
57
|
+
</Card>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { toast } from "sonner";
|
|
7
|
+
import { History, RotateCcw, User, Bot } from "lucide-react";
|
|
8
|
+
import { EmptyState } from "@/components/shared/empty-state";
|
|
9
|
+
|
|
10
|
+
interface HistoryEntry {
|
|
11
|
+
id: string;
|
|
12
|
+
rowId: string;
|
|
13
|
+
tableId: string;
|
|
14
|
+
previousData: string;
|
|
15
|
+
changedBy: string;
|
|
16
|
+
changeType: "update" | "delete";
|
|
17
|
+
createdAt: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface TableHistoryTabProps {
|
|
21
|
+
tableId: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function TableHistoryTab({ tableId }: TableHistoryTabProps) {
|
|
25
|
+
const [entries, setEntries] = useState<HistoryEntry[]>([]);
|
|
26
|
+
const [loading, setLoading] = useState(true);
|
|
27
|
+
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
28
|
+
|
|
29
|
+
const fetchHistory = useCallback(async () => {
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(`/api/tables/${tableId}/history?limit=100`);
|
|
32
|
+
if (res.ok) {
|
|
33
|
+
setEntries(await res.json());
|
|
34
|
+
}
|
|
35
|
+
} catch { /* silent */ }
|
|
36
|
+
finally { setLoading(false); }
|
|
37
|
+
}, [tableId]);
|
|
38
|
+
|
|
39
|
+
useEffect(() => { fetchHistory(); }, [fetchHistory]);
|
|
40
|
+
|
|
41
|
+
async function handleRollback(entry: HistoryEntry) {
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(`/api/tables/${tableId}/rows/${entry.rowId}/history`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: { "Content-Type": "application/json" },
|
|
46
|
+
body: JSON.stringify({ historyEntryId: entry.id }),
|
|
47
|
+
});
|
|
48
|
+
if (res.ok) {
|
|
49
|
+
toast.success("Row restored to previous version");
|
|
50
|
+
fetchHistory();
|
|
51
|
+
} else {
|
|
52
|
+
const err = await res.json().catch(() => ({}));
|
|
53
|
+
toast.error(err.error || "Failed to rollback");
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
toast.error("Failed to rollback");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatDate(dateStr: string) {
|
|
61
|
+
try {
|
|
62
|
+
return new Date(dateStr).toLocaleString();
|
|
63
|
+
} catch {
|
|
64
|
+
return dateStr;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatData(json: string) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.stringify(JSON.parse(json), null, 2);
|
|
71
|
+
} catch {
|
|
72
|
+
return json;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (loading) {
|
|
77
|
+
return <p className="text-sm text-muted-foreground p-4">Loading history...</p>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (entries.length === 0) {
|
|
81
|
+
return (
|
|
82
|
+
<EmptyState
|
|
83
|
+
icon={History}
|
|
84
|
+
heading="No history yet"
|
|
85
|
+
description="Row changes will be tracked here as you edit data."
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="space-y-2 p-4">
|
|
92
|
+
<p className="text-xs text-muted-foreground mb-3">
|
|
93
|
+
{entries.length} change{entries.length !== 1 ? "s" : ""} recorded
|
|
94
|
+
</p>
|
|
95
|
+
<div className="space-y-1">
|
|
96
|
+
{entries.map((entry) => (
|
|
97
|
+
<div
|
|
98
|
+
key={entry.id}
|
|
99
|
+
className="border rounded-lg p-3 space-y-2"
|
|
100
|
+
>
|
|
101
|
+
<div className="flex items-center justify-between">
|
|
102
|
+
<div className="flex items-center gap-2 text-sm">
|
|
103
|
+
{entry.changedBy === "agent" ? (
|
|
104
|
+
<Bot className="h-3.5 w-3.5 text-muted-foreground" />
|
|
105
|
+
) : (
|
|
106
|
+
<User className="h-3.5 w-3.5 text-muted-foreground" />
|
|
107
|
+
)}
|
|
108
|
+
<Badge variant={entry.changeType === "delete" ? "destructive" : "secondary"}>
|
|
109
|
+
{entry.changeType}
|
|
110
|
+
</Badge>
|
|
111
|
+
<span className="text-muted-foreground text-xs">
|
|
112
|
+
Row {entry.rowId.slice(0, 8)}...
|
|
113
|
+
</span>
|
|
114
|
+
<span className="text-muted-foreground text-xs">
|
|
115
|
+
{formatDate(entry.createdAt)}
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="flex items-center gap-1">
|
|
119
|
+
<Button
|
|
120
|
+
variant="ghost"
|
|
121
|
+
size="sm"
|
|
122
|
+
onClick={() => setExpandedId(expandedId === entry.id ? null : entry.id)}
|
|
123
|
+
>
|
|
124
|
+
{expandedId === entry.id ? "Hide" : "View"}
|
|
125
|
+
</Button>
|
|
126
|
+
{entry.changeType !== "delete" && (
|
|
127
|
+
<Button
|
|
128
|
+
variant="outline"
|
|
129
|
+
size="sm"
|
|
130
|
+
onClick={() => handleRollback(entry)}
|
|
131
|
+
>
|
|
132
|
+
<RotateCcw className="h-3 w-3 mr-1" />
|
|
133
|
+
Restore
|
|
134
|
+
</Button>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
{expandedId === entry.id && (
|
|
139
|
+
<pre className="text-xs bg-muted p-2 rounded overflow-x-auto max-h-48">
|
|
140
|
+
{formatData(entry.previousData)}
|
|
141
|
+
</pre>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|