Highly ambitious ATProtocol AppView service and sdks
at fix-postgres 205 lines 6.6 kB view raw
1import { useState } from "react"; 2import { useParams } from "react-router-dom"; 3import { graphql, useLazyLoadQuery } from "react-relay"; 4import type { SliceOverviewQuery } from "../__generated__/SliceOverviewQuery.graphql.ts"; 5import Layout from "../components/Layout.tsx"; 6import { usePageMeta } from "../hooks/usePageMeta.ts"; 7import { Avatar } from "../components/Avatar.tsx"; 8import { SliceSubNav } from "../components/SliceSubNav.tsx"; 9import { LexiconTree } from "../components/LexiconTree.tsx"; 10import { CreateLexiconDialog } from "../components/CreateLexiconDialog.tsx"; 11import { Button } from "../components/Button.tsx"; 12import { Sparkline } from "../components/Sparkline.tsx"; 13import { useSessionContext } from "../lib/useSession.ts"; 14import { isSliceOwner } from "../lib/permissions.ts"; 15import { Plus } from "lucide-react"; 16import { CopyableField } from "../components/CopyableField.tsx"; 17 18export default function SliceOverview() { 19 const { handle, rkey } = useParams<{ handle: string; rkey: string }>(); 20 const { session } = useSessionContext(); 21 const [showCreateLexicon, setShowCreateLexicon] = useState(false); 22 23 const data = useLazyLoadQuery<SliceOverviewQuery>( 24 graphql` 25 query SliceOverviewQuery( 26 $sliceWhere: NetworkSlicesSliceWhereInput 27 $lexiconWhere: NetworkSlicesLexiconWhereInput 28 ) { 29 networkSlicesSlices(first: 1, where: $sliceWhere) { 30 edges { 31 node { 32 uri 33 name 34 domain 35 did 36 createdAt 37 actorHandle 38 networkSlicesActorProfile { 39 avatar { 40 url(preset: "avatar") 41 } 42 } 43 sparklines(interval: "hour", duration: "7d") { 44 ...Sparkline_sparklines 45 } 46 stats { 47 totalRecords 48 totalActors 49 collections 50 } 51 } 52 } 53 } 54 networkSlicesLexicons(first: 1000, where: $lexiconWhere) 55 @connection(key: "SliceOverview_networkSlicesLexicons") { 56 edges { 57 node { 58 ...LexiconTree_lexicon 59 nsid 60 } 61 } 62 } 63 } 64 `, 65 { 66 sliceWhere: { 67 actorHandle: { eq: handle }, 68 uri: { contains: rkey }, 69 }, 70 lexiconWhere: { 71 slice: { contains: rkey }, 72 }, 73 }, 74 { 75 fetchPolicy: "store-and-network", 76 } 77 ); 78 79 const slice = data.networkSlicesSlices?.edges[0]?.node; 80 const lexicons = [...(data.networkSlicesLexicons?.edges?.map(edge => edge.node) || [])] 81 .filter(Boolean) // Filter out null entries from deleted records 82 .sort((a, b) => a.nsid.localeCompare(b.nsid)); 83 84 // Check if current user is the slice owner 85 const isOwner = isSliceOwner(slice, session); 86 87 usePageMeta({ 88 title: `Slice by @${handle}`, 89 description: `Overview of ${slice?.name || 'slice'}`, 90 }); 91 92 return ( 93 <Layout 94 subNav={ 95 <SliceSubNav 96 handle={handle!} 97 rkey={rkey!} 98 sliceName={slice?.name} 99 activeTab="overview" 100 isOwner={isOwner} 101 /> 102 } 103 > 104 <div className="mb-8"> 105 <div className="flex items-center gap-3 mb-4"> 106 <Avatar 107 src={slice?.networkSlicesActorProfile?.avatar?.url} 108 alt={`${handle} avatar`} 109 size="md" 110 /> 111 <div> 112 <h1 className="text-2xl font-medium text-zinc-200 mb-2"> 113 {slice?.name || "Slice Overview"} 114 </h1> 115 <p className="text-sm text-zinc-500"> 116 {slice?.domain || `${handle} / ${rkey}`} 117 </p> 118 </div> 119 </div> 120 {slice?.uri && ( 121 <CopyableField value={slice.uri} label="Slice URI" variant="inline" /> 122 )} 123 </div> 124 125 {/* Stats Section */} 126 {slice?.stats && ( 127 <div className="mb-8 space-y-4"> 128 {/* Stat Cards */} 129 <div className="grid grid-cols-3 gap-4"> 130 <div className="bg-zinc-800/50 rounded p-4"> 131 <div className="text-sm text-zinc-500 mb-1">Total Records</div> 132 <div className="text-2xl font-semibold text-zinc-200"> 133 {slice.stats.totalRecords.toLocaleString()} 134 </div> 135 </div> 136 <div className="bg-zinc-800/50 rounded p-4"> 137 <div className="text-sm text-zinc-500 mb-1">Total Actors</div> 138 <div className="text-2xl font-semibold text-zinc-200"> 139 {slice.stats.totalActors.toLocaleString()} 140 </div> 141 </div> 142 <div className="bg-zinc-800/50 rounded p-4"> 143 <div className="text-sm text-zinc-500 mb-1"> 144 Total Collections 145 </div> 146 <div className="text-2xl font-semibold text-zinc-200"> 147 {slice.stats.collections.length.toLocaleString()} 148 </div> 149 </div> 150 </div> 151 152 {/* Sparkline */} 153 {slice?.sparklines && slice.sparklines.length > 0 && ( 154 <div className="bg-zinc-800/50 rounded p-4"> 155 <div className="text-sm text-zinc-500 mb-3"> 156 Activity (Last 7 Days) 157 </div> 158 <Sparkline 159 sparklines={slice.sparklines} 160 width={800} 161 height={80} 162 className="w-full" 163 /> 164 </div> 165 )} 166 </div> 167 )} 168 169 <div className="space-y-6"> 170 <div className="flex items-center justify-between mb-4"> 171 <h2 className="text-sm font-medium text-zinc-400 uppercase tracking-wider"> 172 {lexicons.length} Lexicons 173 </h2> 174 {isOwner && ( 175 <Button 176 onClick={() => setShowCreateLexicon(true)} 177 variant="primary" 178 size="sm" 179 className="flex items-center gap-1.5" 180 > 181 <Plus size={16} /> 182 Add Lexicon 183 </Button> 184 )} 185 </div> 186 {lexicons.length > 0 187 ? ( 188 <LexiconTree 189 lexicons={lexicons} 190 handle={handle!} 191 sliceRkey={rkey!} 192 /> 193 ) 194 : <p className="text-xs text-zinc-600">No lexicons defined</p>} 195 </div> 196 197 <CreateLexiconDialog 198 open={showCreateLexicon} 199 onClose={() => setShowCreateLexicon(false)} 200 sliceUri={slice?.uri || ""} 201 existingNsids={lexicons.map(lex => lex.nsid)} 202 /> 203 </Layout> 204 ); 205}