Highly ambitious ATProtocol AppView service and sdks
1import { Dialog } from "./Dialog.tsx";
2import { Button } from "./Button.tsx";
3import { AlertCircle, Play } from "lucide-react";
4import { graphql, useLazyLoadQuery } from "react-relay";
5import type { SyncSummaryModalQuery } from "../__generated__/SyncSummaryModalQuery.graphql.ts";
6
7interface SyncSummaryModalProps {
8 open: boolean;
9 onClose: () => void;
10 slice: string;
11 collections: string[];
12 externalCollections: string[];
13 onConfirm: () => void;
14 isConfirming?: boolean;
15}
16
17export function SyncSummaryModal({
18 open,
19 onClose,
20 slice,
21 collections,
22 externalCollections,
23 onConfirm,
24 isConfirming = false,
25}: SyncSummaryModalProps) {
26 const data = useLazyLoadQuery<SyncSummaryModalQuery>(
27 graphql`
28 query SyncSummaryModalQuery($slice: String!, $collections: [String!], $externalCollections: [String!]) {
29 getSyncSummary(slice: $slice, collections: $collections, externalCollections: $externalCollections) {
30 totalRepos
31 cappedRepos
32 wouldBeCapped
33 appliedLimit
34 collectionsSummary {
35 collection
36 estimatedRepos
37 isExternal
38 }
39 }
40 }
41 `,
42 {
43 slice,
44 collections: collections.length > 0 ? collections : null,
45 externalCollections: externalCollections.length > 0 ? externalCollections : null,
46 },
47 {
48 fetchPolicy: 'network-only',
49 }
50 );
51
52 const summary = data.getSyncSummary;
53
54 if (!open || !summary) return null;
55
56 return (
57 <Dialog open={open} onClose={onClose} title="Sync Summary" maxWidth="2xl">
58 <div className="space-y-6">
59 {/* Repo Stats */}
60 <div className={summary.wouldBeCapped ? "grid grid-cols-2 gap-4" : ""}>
61 <div className="bg-zinc-800/50 rounded p-4">
62 <div className="text-sm text-zinc-500 mb-1">Total Repos</div>
63 <div className="text-2xl font-semibold text-zinc-200">
64 {summary.totalRepos.toLocaleString()}
65 </div>
66 </div>
67 {summary.wouldBeCapped && (
68 <div className="bg-zinc-800/50 rounded p-4">
69 <div className="text-sm text-zinc-500 mb-1">Will Sync</div>
70 <div className="text-2xl font-semibold text-zinc-200">
71 {summary.cappedRepos.toLocaleString()}
72 </div>
73 </div>
74 )}
75 </div>
76
77 {/* Capped Warning */}
78 {summary.wouldBeCapped && (
79 <div className="flex items-start gap-3 p-4 bg-yellow-950/30 border border-yellow-900/50 rounded">
80 <AlertCircle className="text-yellow-500 mt-0.5" size={20} />
81 <div className="flex-1">
82 <div className="text-sm font-medium text-yellow-400 mb-1">
83 Sync Will Be Limited
84 </div>
85 <div className="text-sm text-yellow-200/80">
86 Only the first {summary.appliedLimit.toLocaleString()} repos will be
87 synced due to system limits. {summary.totalRepos - summary.cappedRepos}{" "}
88 repos will be excluded.
89 </div>
90 </div>
91 </div>
92 )}
93
94 {/* Collections Summary */}
95 {summary.collectionsSummary.length > 0 && (
96 <div>
97 <h3 className="text-sm font-medium text-zinc-400 mb-3">Collections Breakdown</h3>
98 <div className="space-y-4">
99 {/* Primary Collections */}
100 {summary.collectionsSummary.filter((c: { isExternal: boolean }) => !c.isExternal).length > 0 && (
101 <div>
102 <h4 className="text-xs font-medium text-zinc-500 mb-2">Primary Collections</h4>
103 <div className="space-y-1 ml-4">
104 {summary.collectionsSummary
105 .filter((c: { isExternal: boolean }) => !c.isExternal)
106 .map((col: { collection: string; estimatedRepos: number; isExternal: boolean }) => (
107 <div
108 key={col.collection}
109 className="flex items-center py-1"
110 >
111 <span className="text-sm text-zinc-400 font-mono">
112 {col.collection}
113 </span>
114 <div className="flex-1 border-b border-dotted border-zinc-700 mx-2 self-end mb-1"></div>
115 <span className="text-sm text-zinc-500">
116 {col.estimatedRepos.toLocaleString()} repos
117 </span>
118 </div>
119 ))}
120 </div>
121 </div>
122 )}
123
124 {/* External Collections */}
125 {summary.collectionsSummary.filter((c: { isExternal: boolean }) => c.isExternal).length > 0 && (
126 <div>
127 <h4 className="text-xs font-medium text-zinc-500 mb-2">External Collections</h4>
128 <div className="space-y-1 ml-4">
129 {summary.collectionsSummary
130 .filter((c: { isExternal: boolean }) => c.isExternal)
131 .map((col: { collection: string; estimatedRepos: number; isExternal: boolean }) => (
132 <div
133 key={col.collection}
134 className="flex items-center py-1"
135 >
136 <span className="text-sm text-zinc-400 font-mono">
137 {col.collection}
138 </span>
139 <div className="flex-1 border-b border-dotted border-zinc-700 mx-2 self-end mb-1"></div>
140 <span className="text-sm text-zinc-600 italic">
141 External data
142 </span>
143 </div>
144 ))}
145 </div>
146 </div>
147 )}
148 </div>
149 </div>
150 )}
151
152 {/* Actions */}
153 <div className="flex items-center justify-end gap-3 pt-4 border-t border-zinc-800">
154 <Button
155 onClick={onClose}
156 disabled={isConfirming}
157 size="md"
158 >
159 Cancel
160 </Button>
161 <Button
162 onClick={onConfirm}
163 disabled={isConfirming}
164 variant="primary"
165 size="md"
166 className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-zinc-700 text-white"
167 >
168 <Play size={16} />
169 {isConfirming ? "Starting..." : "Start Sync"}
170 </Button>
171 </div>
172 </div>
173 </Dialog>
174 );
175}