|
1 | | -'use client' |
2 | | - |
3 | | -import Box from "@mui/material/Box"; |
4 | | -import {IconButton, TextField, Typography} from "@mui/material"; |
5 | | -import { Button} from "@mui/material"; |
6 | | -import {Grid} from "@mui/material"; |
7 | | -import {useCallback, useEffect, useMemo, useState} from "react"; |
8 | | -import Card from "@mui/material/Card"; |
9 | | -import CardContent from "@mui/material/CardContent"; |
10 | | -import Divider from "@mui/material/Divider"; |
11 | | -import LinearProgress from "@mui/material/LinearProgress"; |
12 | | -import { useSessionStorage } from "usehooks-ts"; |
13 | | -import {MenuOpen, Download, Lock} from "@mui/icons-material"; |
14 | | - |
15 | | -import { |
16 | | - ObjectList, |
17 | | - TokenPermission, |
18 | | - Federation, |
19 | | - ObjectPrefixToNamespaceKeyMap, |
20 | | - UnauthenticatedError, |
21 | | - generateCodeVerifier, |
22 | | - getAuthorizationCode, |
23 | | - getToken, |
24 | | - startAuthorizationCodeFlow, |
25 | | - parseObjectUrl, |
26 | | - fetchFederation, |
27 | | - fetchNamespace, |
28 | | - list, |
29 | | - get, |
30 | | - put, |
31 | | - permissions |
32 | | -} from "../../../src/index"; |
33 | | -import {downloadResponse} from "../../../src/util"; |
| 1 | +import { redirect } from "next/navigation"; |
34 | 2 |
|
35 | 3 | function Page() { |
36 | | - |
37 | | - // Pelican client state |
38 | | - const [federations, setFederations, removeFederations] = useSessionStorage<Record<string, Federation>>('federations', {}) |
39 | | - const [prefixToNamespace, setPrefixToNamespace, removePrefixToNamespace] = useSessionStorage<ObjectPrefixToNamespaceKeyMap>('prefixToNamespace', {}) |
40 | | - const [codeVerifier, setCodeVerifier, removeCodeVerifier] = useSessionStorage<string | undefined>('codeVerifier', undefined) |
41 | | - |
42 | | - // Initialize code verifier if not present |
43 | | - useEffect(() => { |
44 | | - if(!codeVerifier){ |
45 | | - setCodeVerifier(generateCodeVerifier()) |
46 | | - } |
47 | | - }, [codeVerifier]) |
48 | | - |
49 | | - // Run list on load |
50 | | - useEffect(() => { |
51 | | - (async () => { |
52 | | - await onObjectUrlChange(objectUrl, federations, setFederations, prefixToNamespace, setPrefixToNamespace, setPermissions, setLoginRequired, setObjectList) |
53 | | - })() |
54 | | - }, []) |
55 | | - |
56 | | - // On load, check if there is a code in the URL to exchange for a token |
57 | | - useEffect(() => {(async () => { |
58 | | - |
59 | | - const {federationHostname, namespacePrefix, code} = getAuthorizationCode() |
60 | | - |
61 | | - // If there is a code in the URL, exchange it for a token |
62 | | - if (code && federationHostname && namespacePrefix && codeVerifier) { |
63 | | - const namespace = federations[federationHostname]?.namespaces[namespacePrefix] |
64 | | - const token = await getToken(namespace?.oidcConfiguration, codeVerifier, namespace?.clientId, namespace?.clientSecret, code) |
65 | | - setFederations({ |
66 | | - ...federations, |
67 | | - [federationHostname]: { |
68 | | - ...federations[federationHostname], |
69 | | - namespaces: { |
70 | | - ...federations[federationHostname]?.namespaces, |
71 | | - [namespacePrefix]: { |
72 | | - ...federations[federationHostname]?.namespaces[namespacePrefix], |
73 | | - token: token.accessToken |
74 | | - } |
75 | | - } |
76 | | - } |
77 | | - }) |
78 | | - } |
79 | | - |
80 | | - })()}, [federations, codeVerifier]) |
81 | | - |
82 | | - // UI State |
83 | | - let [loginRequired, setLoginRequired] = useState<boolean>(false); |
84 | | - let [objectUrl, _setObjectUrl] = useSessionStorage<string>('objectUrl', "pelican://osg-htc.org/ncar"); |
85 | | - let [permissions, setPermissions] = useState<TokenPermission[] | undefined>(undefined); |
86 | | - let [object, setObject] = useState<File | undefined>(undefined); |
87 | | - let [objectList, setObjectList] = useState<ObjectList[] | undefined>([]); |
88 | | - let [loading, setLoading] = useState<boolean>(false); |
89 | | - |
90 | | - let {federationHostname, objectPrefix, objectPath} = useMemo(() => parseObjectUrl(objectUrl), [objectUrl]) |
91 | | - |
92 | | - const setObjectUrl = useCallback(async (url: string) => { |
93 | | - setLoading(true); |
94 | | - _setObjectUrl(url) |
95 | | - await onObjectUrlChange(url, federations, setFederations, prefixToNamespace, setPrefixToNamespace, setPermissions, setLoginRequired, setObjectList) |
96 | | - setLoading(false) |
97 | | - }, [federations, prefixToNamespace]) |
98 | | - |
99 | | - return ( |
100 | | - <Box minHeight={"90vh"}> |
101 | | - <Grid height={"100%"} justifyContent={"center"} container gap={2}> |
102 | | - <Grid size={{xl: 4, md: 8, xs: 11}} display={"flex"} flexDirection={"column"}> |
103 | | - <Box mt={6} mx={"auto"} width={"100%"} display={"flex"} flexDirection={"column"}> |
104 | | - <Box pt={2}> |
105 | | - <Box display={'flex'} flexDirection={'column'}> |
106 | | - <Box display={'flex'} alignItems={'center'}> |
107 | | - <TextField fullWidth onChange={async (e) => { |
108 | | - await setObjectUrl(e.target.value) |
109 | | - }} value={objectUrl} id="outlined-basic" label="Object Name" variant="outlined"/> |
110 | | - {loginRequired && codeVerifier && ( |
111 | | - <IconButton |
112 | | - onClick={async () => { |
113 | | - const {federationHostname, objectPrefix} = parseObjectUrl(objectUrl) |
114 | | - |
115 | | - const federation = federations[federationHostname] |
116 | | - const namespaceKey = prefixToNamespace[objectPrefix] |
117 | | - const namespace = federation.namespaces[namespaceKey.namespace] |
118 | | - |
119 | | - startAuthorizationCodeFlow(codeVerifier, namespace, federation) |
120 | | - }} |
121 | | - > |
122 | | - <Lock/> |
123 | | - </IconButton> |
124 | | - )} |
125 | | - </Box> |
126 | | - {loading && <LinearProgress />} |
127 | | - </Box> |
128 | | - <Typography variant={'subtitle2'}> |
129 | | - Namespace Permissions: {permissions ? permissions.join(", ") : "Unknown"} |
130 | | - </Typography> |
131 | | - <Box mt={2}> |
132 | | - <input |
133 | | - type="file" |
134 | | - onChange={e => { |
135 | | - if (e.target.files && e.target.files[0]) { |
136 | | - setObjectUrl(`pelican://localhost:80/mnt/${e.target.files[0].name}`); |
137 | | - setObject(e.target.files?.[0]) |
138 | | - } |
139 | | - }} |
140 | | - /> |
141 | | - </Box> |
142 | | - </Box> |
143 | | - </Box> |
144 | | - <Box mt={6} mx={"auto"} width={"100%"} display={"flex"} flexDirection={"column"}> |
145 | | - <Box pt={2}> |
146 | | - {objectList?.length === 0 ? ( |
147 | | - <Typography variant="body2" color="textSecondary">No objects found.</Typography> |
148 | | - ) : ( |
149 | | - objectList?.map((obj, index) => ( |
150 | | - <Card key={index} sx={{mb: 2}} variant="outlined"> |
151 | | - <CardContent> |
152 | | - <Box display={"flex"} justifyContent={"space-between"} alignItems={"center"}> |
153 | | - <Typography variant="h6" gutterBottom>{obj.href}</Typography> |
154 | | - { obj.iscollection && |
155 | | - <Button endIcon={<MenuOpen/>} onClick={() => setObjectUrl(`pelican://${federationHostname}${obj.href}/`)}> |
156 | | - Explore |
157 | | - </Button> |
158 | | - } |
159 | | - { !obj.iscollection && |
160 | | - <Button |
161 | | - endIcon={<Download/>} |
162 | | - onClick={async () => { |
163 | | - try { |
164 | | - downloadResponse(await get(`pelican://${federationHostname}${obj.href}`, federations[federationHostname], federations[federationHostname].namespaces?.[prefixToNamespace[objectPrefix]?.namespace])) |
165 | | - } catch{} |
166 | | - }} |
167 | | - > |
168 | | - Download |
169 | | - </Button> |
170 | | - } |
171 | | - </Box> |
172 | | - |
173 | | - <Divider sx={{my: 1}}/> |
174 | | - <Typography |
175 | | - variant="body2"><strong>Type:</strong> {obj.resourcetype}{obj.iscollection ? " (Collection)" : ""} |
176 | | - </Typography> |
177 | | - <Typography variant="body2"><strong>Size:</strong> {obj.getcontentlength} bytes</Typography> |
178 | | - <Typography variant="body2"><strong>Last Modified:</strong> {obj.getlastmodified}</Typography> |
179 | | - <Typography variant="body2"><strong>Executable:</strong> {obj.executable}</Typography> |
180 | | - <Typography variant="body2"><strong>Status:</strong> {obj.status}</Typography> |
181 | | - </CardContent> |
182 | | - </Card> |
183 | | - )) |
184 | | - )} |
185 | | - </Box> |
186 | | - </Box> |
187 | | - </Grid> |
188 | | - <Grid size={{xl: 7, md: 8, xs: 11}} display={"flex"} flexDirection={"column"}> |
189 | | - <Box pt={1} mx={"auto"}> |
190 | | - {/*<Button variant="contained" onClick={submit}>{object ? 'Upload' : 'Download'}</Button>*/} |
191 | | - <Button onClick={() => { |
192 | | - setFederations({}) |
193 | | - setPrefixToNamespace({}) |
194 | | - }}>Clear Federations</Button> |
195 | | - </Box> |
196 | | - <Box mt={6} mx={"auto"} width={"100%"} display={"flex"} flexDirection={"column"}> |
197 | | - <Typography variant="h6" gutterBottom>Client Federations ( for debug only )</Typography> |
198 | | - <Box overflow={'auto'}> |
199 | | - <pre> |
200 | | - <code> |
201 | | - {JSON.stringify(federations, null, 2)} |
202 | | - </code> |
203 | | - </pre> |
204 | | - </Box> |
205 | | - </Box> |
206 | | - </Grid> |
207 | | - </Grid> |
208 | | - </Box> |
209 | | - ) |
210 | | -} |
211 | | - |
212 | | -/** |
213 | | - * Pull in objectUrl related information into React state |
214 | | - * @param objectUrl |
215 | | - * @param federations |
216 | | - * @param setFederations |
217 | | - * @param prefixToNamespace |
218 | | - * @param setPrefixToNamespace |
219 | | - * @param setPermissions |
220 | | - * @param setLoginRequired |
221 | | - * @param setObjectList |
222 | | - */ |
223 | | -const onObjectUrlChange = async (objectUrl: string, federations: Record<string, Federation>, setFederations: (f: Record<string, Federation>) => void, prefixToNamespace: ObjectPrefixToNamespaceKeyMap, setPrefixToNamespace: (m: ObjectPrefixToNamespaceKeyMap) => void, setPermissions: (p: TokenPermission[]) => void, setLoginRequired: (b: boolean) => void, setObjectList: (l: ObjectList[]) => void) => { |
224 | | - |
225 | | - let {federationHostname, objectPrefix, objectPath} = parseObjectUrl(objectUrl) |
226 | | - |
227 | | - // If we haven't registered the federation |
228 | | - try { |
229 | | - if(!(federationHostname in federations)) { |
230 | | - const federation = await fetchFederation(federationHostname) |
231 | | - federations = { |
232 | | - ...federations, |
233 | | - [federationHostname]: federation |
234 | | - } |
235 | | - setFederations(federations) |
236 | | - } |
237 | | - } catch {} |
238 | | - |
239 | | - |
240 | | - // If we haven't mapped this prefix to a namespace |
241 | | - try { |
242 | | - if(!(objectPrefix in prefixToNamespace)) { |
243 | | - const namespace = await fetchNamespace(objectPath, federations[federationHostname]) |
244 | | - prefixToNamespace = { |
245 | | - ...prefixToNamespace, |
246 | | - [objectPrefix]: { |
247 | | - federation: federationHostname, |
248 | | - namespace: namespace.prefix |
249 | | - } |
250 | | - } |
251 | | - setPrefixToNamespace(prefixToNamespace) |
252 | | - |
253 | | - // If we haven't registered this namespace |
254 | | - if(!(namespace.prefix in federations[federationHostname].namespaces)) { |
255 | | - setFederations({ |
256 | | - ...federations, |
257 | | - [federationHostname]: { |
258 | | - ...federations[federationHostname], |
259 | | - namespaces: { |
260 | | - ...federations[federationHostname]?.namespaces, |
261 | | - [namespace.prefix]: namespace |
262 | | - } |
263 | | - } |
264 | | - }) |
265 | | - } |
266 | | - } |
267 | | - } catch {} |
268 | | - |
269 | | - // Try to list |
270 | | - try { |
271 | | - try { |
272 | | - setObjectList((await list(`pelican://${objectPrefix}`, federations[federationHostname], federations[federationHostname].namespaces?.[prefixToNamespace[objectPrefix]?.namespace])).reverse()) |
273 | | - setLoginRequired(false) |
274 | | - } catch (e) { |
275 | | - setObjectList((await list(`pelican://${federationHostname}${objectPath}`, federations[federationHostname], federations[federationHostname].namespaces?.[prefixToNamespace[objectPrefix]?.namespace])).reverse()) |
276 | | - setLoginRequired(false) |
277 | | - } |
278 | | - } catch (e) { |
279 | | - if(e instanceof UnauthenticatedError) { |
280 | | - setLoginRequired(true) |
281 | | - setObjectList([]) |
282 | | - } |
283 | | - } |
284 | | - |
285 | | - // Check permissions |
286 | | - try { |
287 | | - if(federations[federationHostname].namespaces?.[prefixToNamespace[objectPrefix]?.namespace]){ |
288 | | - const perms = await permissions(objectUrl, federations[federationHostname].namespaces?.[prefixToNamespace[objectPrefix]?.namespace]) |
289 | | - setPermissions(perms) |
290 | | - } |
291 | | - } catch {} |
| 4 | + redirect("/v1"); |
292 | 5 | } |
293 | 6 |
|
294 | 7 | export default Page; |
0 commit comments