|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import { Box, Typography } from "@mui/material"; |
4 | | -import { useCallback, useEffect, useMemo, useState } from "react"; |
5 | | -import { useSessionStorage } from "usehooks-ts"; |
| 3 | +import { Box } from "@mui/material"; |
6 | 4 |
|
7 | | -import ObjectInput from "@/components/client/ObjectInput"; |
8 | | -import ObjectListComponent from "@/components/client/ObjectListComponent"; |
9 | | -import { |
10 | | - Federation, |
11 | | - ObjectList, |
12 | | - ObjectPrefixToNamespaceKeyMap, |
13 | | - TokenPermission, |
14 | | - UnauthenticatedError, |
15 | | - fetchFederation, |
16 | | - fetchNamespace, |
17 | | - generateCodeVerifier, |
18 | | - getAuthorizationCode, |
19 | | - getToken, |
20 | | - list, |
21 | | - parseObjectUrl, |
22 | | - permissions, |
23 | | - get, |
24 | | - startAuthorizationCodeFlow, |
25 | | -} from "../../../../src/index"; |
26 | | -import { downloadResponse } from "../../../../src/util"; |
| 5 | +import PelicanWebClient from "@/components/client/PelicanWebClient"; |
| 6 | +import { useSessionStorage } from "usehooks-ts"; |
27 | 7 |
|
28 | 8 | function Page() { |
29 | | - // Pelican client state |
30 | | - const [federations, setFederations] = useSessionStorage<Record<string, Federation>>("federations", {}); |
31 | | - |
32 | | - // Map of object prefix to federation and namespace |
33 | | - const [prefixToNamespace, setPrefixToNamespace] = useSessionStorage<ObjectPrefixToNamespaceKeyMap>( |
34 | | - "prefixToNamespace", |
35 | | - {} |
36 | | - ); |
37 | | - |
38 | | - // PKCE Code Verifier for OIDC Authorization Code Flow |
39 | | - const [codeVerifier, setCodeVerifier] = useSessionStorage<string | undefined>("codeVerifier", undefined); |
40 | | - |
41 | | - // Initialize the code verifier if not present |
42 | | - useEffect(() => { |
43 | | - if (!codeVerifier) { |
44 | | - setCodeVerifier(generateCodeVerifier()); |
45 | | - } |
46 | | - }, [codeVerifier]); |
47 | | - |
48 | | - // Run list on load |
49 | | - useEffect(() => { |
50 | | - (async () => { |
51 | | - await updateObjectUrlState( |
52 | | - objectUrl, |
53 | | - federations, |
54 | | - setFederations, |
55 | | - prefixToNamespace, |
56 | | - setPrefixToNamespace, |
57 | | - setPermissions, |
58 | | - setLoginRequired, |
59 | | - setObjectList |
60 | | - ); |
61 | | - })(); |
62 | | - }, []); |
63 | | - |
64 | | - // On load, check if there is a code in the URL to exchange for a token |
65 | | - useEffect(() => { |
66 | | - (async () => { |
67 | | - const { federationHostname, namespacePrefix, code } = getAuthorizationCode(); |
68 | | - |
69 | | - // If there is a code in the URL, exchange it for a token |
70 | | - if (code && federationHostname && namespacePrefix && codeVerifier) { |
71 | | - const namespace = federations[federationHostname]?.namespaces[namespacePrefix]; |
72 | | - const token = await getToken( |
73 | | - namespace?.oidcConfiguration, |
74 | | - codeVerifier, |
75 | | - namespace?.clientId, |
76 | | - namespace?.clientSecret, |
77 | | - code |
78 | | - ); |
79 | | - setFederations({ |
80 | | - ...federations, |
81 | | - [federationHostname]: { |
82 | | - ...federations[federationHostname], |
83 | | - namespaces: { |
84 | | - ...federations[federationHostname]?.namespaces, |
85 | | - [namespacePrefix]: { |
86 | | - ...federations[federationHostname]?.namespaces[namespacePrefix], |
87 | | - token: token.accessToken, |
88 | | - }, |
89 | | - }, |
90 | | - }, |
91 | | - }); |
92 | | - } |
93 | | - })(); |
94 | | - }, [federations, codeVerifier]); |
95 | | - |
96 | | - // UI State |
97 | | - let [loginRequired, setLoginRequired] = useState<boolean>(false); |
98 | | - let [objectUrl, setObjectUrl] = useSessionStorage<string>("objectUrl", ""); |
99 | | - let [permissions, setPermissions] = useState<TokenPermission[] | undefined>(undefined); |
100 | | - let [objectList, setObjectList] = useState<ObjectList[]>([]); |
101 | | - let [loading, setLoading] = useState<boolean>(false); |
102 | | - |
103 | | - let { federationHostname, objectPrefix, objectPath } = useMemo(() => { |
104 | | - try { |
105 | | - return parseObjectUrl(objectUrl); |
106 | | - } catch { |
107 | | - return { federationHostname: "", objectPrefix: "", objectPath: "" }; |
108 | | - } |
109 | | - }, [objectUrl]); |
110 | | - |
111 | | - const handleRefetchObject = useCallback( |
112 | | - async (url: string) => { |
113 | | - console.log("Object URL changed to", url); |
114 | | - setLoading(true); |
115 | | - setObjectUrl(url); |
116 | | - await updateObjectUrlState( |
117 | | - url, |
118 | | - federations, |
119 | | - setFederations, |
120 | | - prefixToNamespace, |
121 | | - setPrefixToNamespace, |
122 | | - setPermissions, |
123 | | - setLoginRequired, |
124 | | - setObjectList |
125 | | - ); |
126 | | - setLoading(false); |
127 | | - }, |
128 | | - [federations, prefixToNamespace] |
129 | | - ); |
130 | | - |
131 | | - const handleLogin = useCallback(async () => { |
132 | | - if (!codeVerifier) return; |
133 | | - |
134 | | - try { |
135 | | - const { federationHostname, objectPrefix } = parseObjectUrl(objectUrl); |
136 | | - const federation = federations[federationHostname]; |
137 | | - const namespaceKey = prefixToNamespace[objectPrefix]; |
138 | | - const namespace = federation.namespaces[namespaceKey.namespace]; |
139 | | - |
140 | | - startAuthorizationCodeFlow(codeVerifier, namespace, federation); |
141 | | - } catch (error) { |
142 | | - console.error("Login failed:", error); |
143 | | - } |
144 | | - }, [codeVerifier, objectUrl, federations, prefixToNamespace]); |
145 | | - |
146 | | - const handleExplore = useCallback( |
147 | | - (href: string) => { |
148 | | - handleRefetchObject(`pelican://${federationHostname}${href}/`); |
149 | | - }, |
150 | | - [federationHostname, handleRefetchObject] |
151 | | - ); |
152 | | - |
153 | | - const handleDownload = useCallback( |
154 | | - async (href: string) => { |
155 | | - try { |
156 | | - const response = await get( |
157 | | - `pelican://${federationHostname}${href}`, |
158 | | - federations[federationHostname], |
159 | | - federations[federationHostname].namespaces?.[prefixToNamespace[objectPrefix]?.namespace] |
160 | | - ); |
161 | | - downloadResponse(response); |
162 | | - } catch (error) { |
163 | | - console.error("Download failed:", error); |
164 | | - } |
165 | | - }, |
166 | | - [federationHostname, federations, prefixToNamespace, objectPrefix] |
167 | | - ); |
| 9 | + const [objectUrl] = useSessionStorage<string>("pelican-object-url", "pelican://osg-htc.org/ncar/"); |
168 | 10 |
|
169 | 11 | return ( |
170 | 12 | <Box minHeight={"90vh"} margin={4} width={"1200px"} mx={"auto"}> |
171 | | - <Box mt={6} mx={"auto"} width={"100%"} display={"flex"} flexDirection={"column"}> |
172 | | - <Box pt={2}> |
173 | | - <ObjectInput |
174 | | - objectUrl={objectUrl} |
175 | | - setObjectUrl={setObjectUrl} |
176 | | - handleRefetchObject={handleRefetchObject} |
177 | | - loginRequired={loginRequired && !!codeVerifier} |
178 | | - loading={loading} |
179 | | - onLoginClick={handleLogin} |
180 | | - /> |
181 | | - <Typography variant={"subtitle2"}> |
182 | | - Namespace Permissions: {permissions ? permissions.join(", ") : "Unknown"} |
183 | | - </Typography> |
184 | | - </Box> |
185 | | - </Box> |
186 | | - <ObjectListComponent objectList={objectList} onExplore={handleExplore} onDownload={handleDownload} /> |
| 13 | + <PelicanWebClient startingUrl={objectUrl} authentication readonly />" |
187 | 14 | </Box> |
188 | 15 | ); |
189 | 16 | } |
190 | 17 |
|
191 | | -/** |
192 | | - * Pull in objectUrl related information into React state. |
193 | | - */ |
194 | | -const updateObjectUrlState = async ( |
195 | | - objectUrl: string, |
196 | | - federations: Record<string, Federation>, |
197 | | - setFederations: (f: Record<string, Federation>) => void, |
198 | | - prefixToNamespace: ObjectPrefixToNamespaceKeyMap, |
199 | | - setPrefixToNamespace: (m: ObjectPrefixToNamespaceKeyMap) => void, |
200 | | - setPermissions: (p: TokenPermission[]) => void, |
201 | | - setLoginRequired: (b: boolean) => void, |
202 | | - setObjectList: (l: ObjectList[]) => void |
203 | | -) => { |
204 | | - // Parse the object URL |
205 | | - let federationHostname, objectPrefix, objectPath; |
206 | | - try { |
207 | | - const parsed = parseObjectUrl(objectUrl); |
208 | | - federationHostname = parsed.federationHostname; |
209 | | - objectPrefix = parsed.objectPrefix; |
210 | | - objectPath = parsed.objectPath; |
211 | | - } catch {} |
212 | | - |
213 | | - if (!federationHostname || !objectPrefix || !objectPath) { |
214 | | - // Total failure to parse URL, reset everything |
215 | | - setLoginRequired(false); |
216 | | - setPermissions([]); |
217 | | - setObjectList([]); |
218 | | - return; |
219 | | - } |
220 | | - |
221 | | - // If we haven't registered the federation |
222 | | - try { |
223 | | - if (!(federationHostname in federations)) { |
224 | | - const federation = await fetchFederation(federationHostname); |
225 | | - federations = { |
226 | | - ...federations, |
227 | | - [federationHostname]: federation, |
228 | | - }; |
229 | | - setFederations(federations); |
230 | | - } |
231 | | - } catch {} |
232 | | - |
233 | | - // If we haven't mapped this prefix to a namespace |
234 | | - try { |
235 | | - if (!(objectPrefix in prefixToNamespace)) { |
236 | | - const namespace = await fetchNamespace(objectPath, federations[federationHostname]); |
237 | | - prefixToNamespace = { |
238 | | - ...prefixToNamespace, |
239 | | - [objectPrefix]: { |
240 | | - federation: federationHostname, |
241 | | - namespace: namespace.prefix, |
242 | | - }, |
243 | | - }; |
244 | | - setPrefixToNamespace(prefixToNamespace); |
245 | | - |
246 | | - // If we haven't registered this namespace |
247 | | - if (!(namespace.prefix in federations[federationHostname].namespaces)) { |
248 | | - setFederations({ |
249 | | - ...federations, |
250 | | - [federationHostname]: { |
251 | | - ...federations[federationHostname], |
252 | | - namespaces: { |
253 | | - ...federations[federationHostname]?.namespaces, |
254 | | - [namespace.prefix]: namespace, |
255 | | - }, |
256 | | - }, |
257 | | - }); |
258 | | - } |
259 | | - } |
260 | | - } catch {} |
261 | | - |
262 | | - // Try to list |
263 | | - try { |
264 | | - try { |
265 | | - setObjectList( |
266 | | - ( |
267 | | - await list( |
268 | | - `pelican://${objectPrefix}`, |
269 | | - federations[federationHostname], |
270 | | - federations[federationHostname].namespaces?.[prefixToNamespace[objectPrefix]?.namespace] |
271 | | - ) |
272 | | - ).reverse() |
273 | | - ); |
274 | | - setLoginRequired(false); |
275 | | - } catch (e) { |
276 | | - setObjectList( |
277 | | - ( |
278 | | - await list( |
279 | | - `pelican://${federationHostname}${objectPath}`, |
280 | | - federations[federationHostname], |
281 | | - federations[federationHostname].namespaces?.[prefixToNamespace[objectPrefix]?.namespace] |
282 | | - ) |
283 | | - ).reverse() |
284 | | - ); |
285 | | - setLoginRequired(false); |
286 | | - } |
287 | | - } catch (e) { |
288 | | - if (e instanceof UnauthenticatedError) { |
289 | | - setLoginRequired(true); |
290 | | - setObjectList([]); |
291 | | - } |
292 | | - } |
293 | | - |
294 | | - // Check permissions |
295 | | - try { |
296 | | - if (federations[federationHostname].namespaces?.[prefixToNamespace[objectPrefix]?.namespace]) { |
297 | | - const perms = await permissions( |
298 | | - objectUrl, |
299 | | - federations[federationHostname].namespaces?.[prefixToNamespace[objectPrefix]?.namespace] |
300 | | - ); |
301 | | - setPermissions(perms); |
302 | | - } |
303 | | - } catch {} |
304 | | -}; |
305 | | - |
306 | 18 | export default Page; |
0 commit comments