Highly ambitious ATProtocol AppView service and sdks
at main 160 lines 5.6 kB view raw
1import { useState, useEffect } from "react"; 2import { graphql, readInlineData } from "react-relay"; 3import { Link } from "react-router-dom"; 4import { AlertCircle } from "lucide-react"; 5import { validate, type LexiconDoc } from "@slices/lexicon"; 6import type { LexiconTree_lexicon$key } from "../__generated__/LexiconTree_lexicon.graphql.ts"; 7 8export const LexiconFragment = graphql` 9 fragment LexiconTree_lexicon on NetworkSlicesLexicon @inline { 10 uri 11 nsid 12 description 13 definitions 14 createdAt 15 updatedAt 16 } 17`; 18 19type LexiconData = { 20 uri: string; 21 nsid: string; 22 description: string | null | undefined; 23 definitions: string; 24 createdAt: string; 25 updatedAt: string | null | undefined; 26}; 27 28interface LexiconTreeProps { 29 lexicons: ReadonlyArray<LexiconTree_lexicon$key>; 30 handle: string; 31 sliceRkey: string; 32} 33 34export function LexiconTree({ lexicons, handle, sliceRkey }: LexiconTreeProps) { 35 // Read fragment data for each lexicon 36 const lexiconData = lexicons.map((lexicon) => 37 readInlineData(LexiconFragment, lexicon) 38 ); 39 40 // Track which lexicons are invalid 41 const [invalidLexicons, setInvalidLexicons] = useState<Set<string>>(new Set()); 42 43 // Validate all lexicons together 44 useEffect(() => { 45 const validateLexicons = async () => { 46 try { 47 // Convert lexicons to validator format 48 const lexiconsForValidation: LexiconDoc[] = []; 49 for (const lex of lexiconData) { 50 try { 51 lexiconsForValidation.push({ 52 id: lex.nsid, 53 defs: JSON.parse(lex.definitions), 54 lexicon: 1, 55 description: lex.description || undefined, 56 }); 57 } catch { 58 // If we can't parse the definitions, mark as invalid 59 } 60 } 61 62 // Validate all lexicons 63 const validationResult = await validate(lexiconsForValidation); 64 65 if (validationResult !== null) { 66 // validationResult is a map of lexicon ID to array of error strings 67 setInvalidLexicons(new Set(Object.keys(validationResult))); 68 } else { 69 setInvalidLexicons(new Set()); 70 } 71 } catch (error) { 72 console.error("Failed to validate lexicons:", error); 73 } 74 }; 75 76 validateLexicons(); 77 // eslint-disable-next-line react-hooks/exhaustive-deps 78 }, [lexicons]); 79 80 return ( 81 <div> 82 {lexiconData.map((lexicon, index) => { 83 const rkey = lexicon.uri.split("/").pop(); 84 const parts = lexicon.nsid.split("."); 85 const authority = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : parts[0]; 86 const rest = parts.length >= 2 ? parts.slice(2).join(".") : ""; 87 88 // Check if this is a record type lexicon 89 let isRecordType = false; 90 try { 91 const definitions = JSON.parse(lexicon.definitions); 92 isRecordType = definitions.main?.type === "record"; 93 } catch (e) { 94 console.error("Failed to parse lexicon definitions:", lexicon.nsid, e); 95 } 96 97 // Check if this is the first item or if the authority changed from previous item 98 const prevLexicon = index > 0 ? lexiconData[index - 1] : null; 99 const prevParts = prevLexicon?.nsid.split("."); 100 const prevAuthority = prevParts && prevParts.length >= 2 101 ? `${prevParts[0]}.${prevParts[1]}` 102 : prevParts?.[0]; 103 const showBreak = index > 0 && authority !== prevAuthority; 104 105 // Check if this lexicon is invalid 106 const isInvalid = invalidLexicons.has(lexicon.nsid); 107 108 // Split the rest into middle and last part if it's a record type 109 let middle = rest; 110 let lastPart = ""; 111 if (isRecordType && rest) { 112 const restParts = rest.split("."); 113 if (restParts.length > 1) { 114 lastPart = restParts[restParts.length - 1]; 115 middle = restParts.slice(0, -1).join("."); 116 } else { 117 lastPart = rest; 118 middle = ""; 119 } 120 } 121 122 return ( 123 <div key={lexicon.uri}> 124 {showBreak && <div className="h-3" />} 125 <Link 126 to={`/profile/${handle}/slice/${sliceRkey}/lexicons/${rkey}`} 127 state={{ lexicon }} 128 className="flex items-center gap-2 py-1 cursor-pointer hover:bg-zinc-900/50 rounded group" 129 > 130 {isInvalid && ( 131 <AlertCircle size={14} className="text-red-500 flex-shrink-0" /> 132 )} 133 <span className={`text-sm font-medium font-mono group-hover:text-zinc-100 transition-colors ${isInvalid ? "text-red-400" : ""}`}> 134 <span className={isInvalid ? "text-red-400" : "text-zinc-200"}>{authority}</span> 135 {isRecordType ? ( 136 <> 137 {middle && <span className={isInvalid ? "text-red-400" : "text-zinc-400"}>.{middle}</span>} 138 {lastPart && ( 139 <> 140 <span className={isInvalid ? "text-red-400" : "text-zinc-400"}>.</span> 141 <span className={isInvalid ? "text-red-400" : "text-cyan-400"}>{lastPart}</span> 142 </> 143 )} 144 </> 145 ) : ( 146 rest && <span className={isInvalid ? "text-red-400" : "text-zinc-400"}>.{rest}</span> 147 )} 148 </span> 149 {lexicon.description && ( 150 <span className="text-xs text-zinc-600 truncate"> 151 {lexicon.description} 152 </span> 153 )} 154 </Link> 155 </div> 156 ); 157 })} 158 </div> 159 ); 160}