forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
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}