Skip to content

Commit 8df98b7

Browse files
committed
Reduce padding, add readonly/show directories info, add parent directory and hide current directory
1 parent c08fe9f commit 8df98b7

File tree

7 files changed

+629
-393
lines changed

7 files changed

+629
-393
lines changed

website/src/app/v1/page.tsx

Lines changed: 5 additions & 293 deletions
Original file line numberDiff line numberDiff line change
@@ -1,306 +1,18 @@
11
"use client";
22

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";
64

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";
277

288
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/");
16810

16911
return (
17012
<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 />"
18714
</Box>
18815
);
18916
}
19017

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-
30618
export default Page;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Box, Chip, FormControlLabel, Switch, Typography } from "@mui/material";
2+
3+
interface ClientMetadataProps {
4+
permissions: string[] | undefined;
5+
readonly: boolean;
6+
showDirectories: boolean;
7+
setShowDirectories: (show: boolean) => void;
8+
}
9+
10+
function ClientMetadata({ permissions, readonly, showDirectories, setShowDirectories }: ClientMetadataProps) {
11+
return (
12+
<Box display={"flex"} alignItems={"center"} justifyContent={"space-between"} gap={2} mt={1} mb={1}>
13+
<Box display={"flex"} alignItems={"center"} gap={2}>
14+
{permissions && (
15+
<Box display={"flex"} alignItems={"center"} gap={1}>
16+
<Typography variant="body2">Permissions:</Typography>
17+
{permissions.map((perm) => (
18+
<Chip key={perm} label={perm} size="small" />
19+
))}
20+
</Box>
21+
)}
22+
{readonly && <Chip label="Read Only" size="small" color="secondary" variant="outlined" />}
23+
</Box>
24+
<FormControlLabel
25+
control={
26+
<Switch
27+
checked={showDirectories}
28+
onChange={(e) => setShowDirectories(e.target.checked)}
29+
name="show-directories"
30+
color="primary"
31+
size="small"
32+
/>
33+
}
34+
label="Show Directories"
35+
slotProps={{
36+
typography: { variant: "body2" },
37+
}}
38+
/>
39+
</Box>
40+
);
41+
}
42+
43+
export default ClientMetadata;

0 commit comments

Comments
 (0)