Highly ambitious ATProtocol AppView service and sdks
at main 175 lines 6.6 kB view raw
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}