From 7acaea00442f6271ac49195860b196cde95d6772 Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Fri, 25 Oct 2024 08:31:04 -0500 Subject: [PATCH 01/16] Remove the use of index.d.ts files Allows us to have more consistent styles with this code and the other code that we write that is compiled for js consumption. If we need a .d.ts file we can let the compiler create it. --- .../director/components/DirectorDropdown.tsx | 2 +- .../CustomRegistrationField/BooleanField.tsx | 4 +-- .../EnumerationField.tsx | 4 +-- .../EpochTimeField.tsx | 4 +-- .../CustomRegistrationField/IntegerField.tsx | 4 +-- .../CustomRegistrationField/PubkeyField.tsx | 4 +-- .../CustomRegistrationField/StringField.tsx | 4 +-- .../CustomRegistrationField/index.d.ts | 28 ------------------- .../CustomRegistrationField/index.tsx | 22 ++++++++++++--- .../frontend/app/registry/components/Form.tsx | 6 ++-- .../app/registry/components/PostPage.tsx | 2 +- .../app/registry/components/PutPage.tsx | 2 +- .../frontend/app/registry/components/index.ts | 5 ++++ .../frontend/app/registry/components/util.tsx | 7 ++++- .../Namespace/InformationDropdown.tsx | 3 +- .../frontend/components/Namespace/index.d.tsx | 21 -------------- .../frontend/components/Namespace/index.tsx | 23 +++++++++++++++ web_ui/frontend/{index.d.ts => index.ts} | 8 ++++-- 18 files changed, 76 insertions(+), 77 deletions(-) delete mode 100644 web_ui/frontend/app/registry/components/CustomRegistrationField/index.d.ts create mode 100644 web_ui/frontend/app/registry/components/index.ts delete mode 100644 web_ui/frontend/components/Namespace/index.d.tsx rename web_ui/frontend/{index.d.ts => index.ts} (89%) diff --git a/web_ui/frontend/app/director/components/DirectorDropdown.tsx b/web_ui/frontend/app/director/components/DirectorDropdown.tsx index 15d7bcb03..60d1c433d 100644 --- a/web_ui/frontend/app/director/components/DirectorDropdown.tsx +++ b/web_ui/frontend/app/director/components/DirectorDropdown.tsx @@ -107,7 +107,7 @@ const directoryListToTreeHelper = ( tree[path[0]] = {}; } - tree[path[0]] = directoryListToTreeHelper(path.slice(1), tree[path[0]]); + tree[path[0]] = directoryListToTreeHelper(path.slice(1), tree[path[0]] as StringTree); return tree; }; diff --git a/web_ui/frontend/app/registry/components/CustomRegistrationField/BooleanField.tsx b/web_ui/frontend/app/registry/components/CustomRegistrationField/BooleanField.tsx index f541024ff..b3bbfe61f 100644 --- a/web_ui/frontend/app/registry/components/CustomRegistrationField/BooleanField.tsx +++ b/web_ui/frontend/app/registry/components/CustomRegistrationField/BooleanField.tsx @@ -9,7 +9,7 @@ import React, { ChangeEvent, ReactNode, SyntheticEvent, useMemo } from 'react'; import { createId } from '@/components/configuration/util'; import FormHelperText from '@mui/material/FormHelperText'; -import type { CustomRegistrationFieldProps } from './index.d'; +import type { BaseCustomRegistrationFieldProps } from './index'; const BooleanField = ({ onChange, @@ -18,7 +18,7 @@ const BooleanField = ({ required, description, value, -}: CustomRegistrationFieldProps) => { +}: BaseCustomRegistrationFieldProps) => { const id = useMemo(() => createId(name), [name]); const labelId = useMemo(() => `${id}-label`, [id]); diff --git a/web_ui/frontend/app/registry/components/CustomRegistrationField/EnumerationField.tsx b/web_ui/frontend/app/registry/components/CustomRegistrationField/EnumerationField.tsx index d0f6b699b..ec43e79e1 100644 --- a/web_ui/frontend/app/registry/components/CustomRegistrationField/EnumerationField.tsx +++ b/web_ui/frontend/app/registry/components/CustomRegistrationField/EnumerationField.tsx @@ -1,7 +1,7 @@ import { Autocomplete, TextField } from '@mui/material'; import React, { useMemo } from 'react'; -import type { CustomRegistrationFieldProps } from './index.d'; +import type { BaseCustomRegistrationFieldProps } from './index'; const EnumerationField = ({ onChange, @@ -11,7 +11,7 @@ const EnumerationField = ({ description, value, options, -}: CustomRegistrationFieldProps) => { +}: BaseCustomRegistrationFieldProps) => { const textValue = useMemo( () => options?.find((option) => option.id === value), [value, options] diff --git a/web_ui/frontend/app/registry/components/CustomRegistrationField/EpochTimeField.tsx b/web_ui/frontend/app/registry/components/CustomRegistrationField/EpochTimeField.tsx index 38d8c7a22..141aefc42 100644 --- a/web_ui/frontend/app/registry/components/CustomRegistrationField/EpochTimeField.tsx +++ b/web_ui/frontend/app/registry/components/CustomRegistrationField/EpochTimeField.tsx @@ -6,7 +6,7 @@ import FormControl from '@mui/material/FormControl'; import FormHelperText from '@mui/material/FormHelperText'; import { DateTime } from 'luxon'; -import type { CustomRegistrationFieldProps } from './index.d'; +import type { BaseCustomRegistrationFieldProps } from './index'; const EpochTimeField = ({ onChange, @@ -15,7 +15,7 @@ const EpochTimeField = ({ required, description, value, -}: CustomRegistrationFieldProps) => { +}: BaseCustomRegistrationFieldProps) => { return ( { if (value && isNaN(Number(value))) { @@ -17,7 +17,7 @@ const IntegerField = ({ required, description, value, -}: CustomRegistrationFieldProps) => { +}: BaseCustomRegistrationFieldProps) => { const [error, setError] = React.useState(undefined); // Check that the value is a number or undefined throwing error if not diff --git a/web_ui/frontend/app/registry/components/CustomRegistrationField/PubkeyField.tsx b/web_ui/frontend/app/registry/components/CustomRegistrationField/PubkeyField.tsx index 703e0d15e..6d7d5135b 100644 --- a/web_ui/frontend/app/registry/components/CustomRegistrationField/PubkeyField.tsx +++ b/web_ui/frontend/app/registry/components/CustomRegistrationField/PubkeyField.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { StringField } from './StringField'; -import type { CustomRegistrationFieldProps } from './index.d'; +import type { BaseCustomRegistrationFieldProps } from './index'; const JWKPlaceholder = { keys: [ @@ -25,7 +25,7 @@ const pubkeyValidator = (value: string) => { } }; -const PubkeyField = ({ ...props }: CustomRegistrationFieldProps) => { +const PubkeyField = ({ ...props }: BaseCustomRegistrationFieldProps) => { return ( & - CustomRegistrationFieldProps; + BaseCustomRegistrationFieldProps; interface StringFieldProps extends TextFieldProps { validator?: (value: string) => string | undefined; diff --git a/web_ui/frontend/app/registry/components/CustomRegistrationField/index.d.ts b/web_ui/frontend/app/registry/components/CustomRegistrationField/index.d.ts deleted file mode 100644 index e00428efe..000000000 --- a/web_ui/frontend/app/registry/components/CustomRegistrationField/index.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { CustomRegistrationField } from '@/components/configuration'; -import { Alert as AlertType, Namespace } from '@/index'; - -export interface NamespaceFormPage { - update: (data: Partial) => Promise; -} - -export interface CustomRegistrationProps extends CustomRegistrationField { - displayed_name: string; -} - -export type CustomRegistrationPropsEnum = - | (CustomRegistrationProps & { type: 'int' }) - | (CustomRegistrationProps & { type: 'string' }) - | (CustomRegistrationProps & { type: 'bool' }) - | (CustomRegistrationProps & { type: 'datetime' }) - | (CustomRegistrationProps & { type: 'enum' }); - -export interface CustomRegistrationFieldProps - extends CustomRegistrationProps { - onChange: (value: T | null) => void; - value?: T; -} - -export type CustomRegistrationFieldPropsEnum = - CustomRegistrationFieldProps & { - type: 'int' | 'string' | 'bool' | 'datetime' | 'enum'; - }; diff --git a/web_ui/frontend/app/registry/components/CustomRegistrationField/index.tsx b/web_ui/frontend/app/registry/components/CustomRegistrationField/index.tsx index 7c64a464e..dfd07a814 100644 --- a/web_ui/frontend/app/registry/components/CustomRegistrationField/index.tsx +++ b/web_ui/frontend/app/registry/components/CustomRegistrationField/index.tsx @@ -1,17 +1,31 @@ -import type { CustomRegistrationField } from '@/components/configuration/index'; + import { BooleanField } from './BooleanField'; -import { ErrorField } from './ErrorField'; import { StringField } from './StringField'; import { IntegerField } from './IntegerField'; import PubkeyField from './PubkeyField'; -import { CustomRegistrationFieldPropsEnum } from './index.d'; import EpochTimeField from '@/app/registry/components/CustomRegistrationField/EpochTimeField'; import EnumerationField from '@/app/registry/components/CustomRegistrationField/EnumerationField'; +import { CustomRegistrationField as CustomRegistrationFieldConfiguration } from '@/components/configuration'; +import type { CustomRegistrationField } from '@/components/configuration'; + +export type CustomRegistrationFieldProps = + | (BaseCustomRegistrationFieldProps & { type: 'int' }) + | (BaseCustomRegistrationFieldProps & { type: 'string' }) + | (BaseCustomRegistrationFieldProps & { type: 'bool' }) + | (BaseCustomRegistrationFieldProps & { type: 'datetime' }) + | (BaseCustomRegistrationFieldProps & { type: 'enum' }); + +export interface BaseCustomRegistrationFieldProps + extends CustomRegistrationFieldConfiguration { + onChange: (value: T | null) => void; + value?: T; + displayed_name: string; +} const CustomRegistrationField = ({ ...props -}: CustomRegistrationFieldPropsEnum) => { +}: CustomRegistrationFieldProps) => { // If the field is the pubkey field, render the pubkey field if (props.type == 'string' && props.name === 'pubkey') { return ; diff --git a/web_ui/frontend/app/registry/components/Form.tsx b/web_ui/frontend/app/registry/components/Form.tsx index 24f8fa128..37326f65c 100644 --- a/web_ui/frontend/app/registry/components/Form.tsx +++ b/web_ui/frontend/app/registry/components/Form.tsx @@ -11,7 +11,7 @@ import { populateKey, submitNamespaceForm, } from '@/app/registry/components/util'; -import { CustomRegistrationPropsEnum } from './CustomRegistrationField/index.d'; +import { CustomRegistrationFieldProps } from './CustomRegistrationField'; import { getErrorMessage } from '@/helpers/util'; interface FormProps { @@ -20,7 +20,7 @@ interface FormProps { } const getRegistrationFields = async (): Promise< - CustomRegistrationPropsEnum[] + Omit[] > => { const response = await fetch('/api/v1.0/registry_ui/namespaces', { method: 'OPTIONS', @@ -57,7 +57,7 @@ const Form = ({ namespace, onSubmit }: FormProps) => { namespace || {} ); - const { data: fields, error } = useSWR( + const { data: fields, error } = useSWR[]>( 'getRegistrationFields', getRegistrationFields, { fallbackData: [] } diff --git a/web_ui/frontend/app/registry/components/PostPage.tsx b/web_ui/frontend/app/registry/components/PostPage.tsx index 6c8ad7be2..f5d6991ee 100644 --- a/web_ui/frontend/app/registry/components/PostPage.tsx +++ b/web_ui/frontend/app/registry/components/PostPage.tsx @@ -25,7 +25,7 @@ import { Alert as AlertType, Namespace } from '@/index'; import Form from '@/app/registry/components/Form'; import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; import { submitNamespaceForm } from '@/app/registry/components/util'; -import type { NamespaceFormPage } from './CustomRegistrationField/index.d'; +import { NamespaceFormPage } from '@/app/registry/components'; const PostPage = ({ update }: NamespaceFormPage) => { const [fromUrl, setFromUrl] = useState(undefined); diff --git a/web_ui/frontend/app/registry/components/PutPage.tsx b/web_ui/frontend/app/registry/components/PutPage.tsx index b5e1ebce1..813aa1acb 100644 --- a/web_ui/frontend/app/registry/components/PutPage.tsx +++ b/web_ui/frontend/app/registry/components/PutPage.tsx @@ -41,7 +41,7 @@ import { getNamespace, submitNamespaceForm, } from '@/app/registry/components/util'; -import type { NamespaceFormPage } from './CustomRegistrationField/index.d'; +import { NamespaceFormPage } from '@/app/registry/components' const PutPage = ({ update }: NamespaceFormPage) => { const [id, setId] = useState(undefined); diff --git a/web_ui/frontend/app/registry/components/index.ts b/web_ui/frontend/app/registry/components/index.ts new file mode 100644 index 000000000..86214d7a3 --- /dev/null +++ b/web_ui/frontend/app/registry/components/index.ts @@ -0,0 +1,5 @@ +import { Alert as AlertType, Namespace } from '@/index'; + +export interface NamespaceFormPage { + update: (data: Partial) => Promise; +} diff --git a/web_ui/frontend/app/registry/components/util.tsx b/web_ui/frontend/app/registry/components/util.tsx index 0eb3398ae..9a55fd031 100644 --- a/web_ui/frontend/app/registry/components/util.tsx +++ b/web_ui/frontend/app/registry/components/util.tsx @@ -26,7 +26,12 @@ export const calculateKeys = (key: string) => { return [key]; }; -export const getValue = (o: any, key: string[]): string | undefined => { +/** + * Get the value of a key in an object + * @param o Object to get the value from + * @param key List of keys to traverse + */ +export const getValue = (o: Record | undefined, key: string[]): any => { if (o === undefined) { return undefined; } diff --git a/web_ui/frontend/components/Namespace/InformationDropdown.tsx b/web_ui/frontend/components/Namespace/InformationDropdown.tsx index db87fc751..752fe37b6 100644 --- a/web_ui/frontend/components/Namespace/InformationDropdown.tsx +++ b/web_ui/frontend/components/Namespace/InformationDropdown.tsx @@ -1,7 +1,6 @@ import { Box, Tooltip, Collapse, Grid, Typography } from '@mui/material'; import React from 'react'; -import { NamespaceAdminMetadata } from './index.d'; -import { Dropdown, InformationSpan } from '@/components'; +import { Dropdown, InformationSpan, NamespaceAdminMetadata } from '@/components'; interface InformationDropdownProps { adminMetadata: NamespaceAdminMetadata; diff --git a/web_ui/frontend/components/Namespace/index.d.tsx b/web_ui/frontend/components/Namespace/index.d.tsx deleted file mode 100644 index c7292df96..000000000 --- a/web_ui/frontend/components/Namespace/index.d.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { PendingCardProps } from './PendingCard'; -import { CardProps } from './Card'; - -export interface NamespaceAdminMetadata { - user_id: string; - description: string; - site_name: string; - institution: string; - security_contact_user_id: string; - status: 'Pending' | 'Approved' | 'Denied' | 'Unknown'; - approver_id: number; - approved_at: string; - created_at: string; - updated_at: string; -} - -export interface FlatObject { - [key: string]: Exclude; -} - -export type NamespaceCardProps = CardProps & PendingCardProps; diff --git a/web_ui/frontend/components/Namespace/index.tsx b/web_ui/frontend/components/Namespace/index.tsx index 77dbe881d..7f7e4ac4b 100644 --- a/web_ui/frontend/components/Namespace/index.tsx +++ b/web_ui/frontend/components/Namespace/index.tsx @@ -15,6 +15,29 @@ export { NamespaceIcon, }; +import { PendingCardProps } from './PendingCard'; +import { CardProps } from './Card'; + +export interface NamespaceAdminMetadata { + user_id: string; + description: string; + site_name: string; + institution: string; + security_contact_user_id: string; + status: 'Pending' | 'Approved' | 'Denied' | 'Unknown'; + approver_id: number; + approved_at: string; + created_at: string; + updated_at: string; +} + +export interface FlatObject { + [key: string]: Exclude; +} + +export type NamespaceCardProps = CardProps & PendingCardProps; + + export const getServerType = (namespace: Namespace) => { // If the namespace is empty the value is undefined if (namespace?.prefix == null || namespace.prefix == '') { diff --git a/web_ui/frontend/index.d.ts b/web_ui/frontend/index.ts similarity index 89% rename from web_ui/frontend/index.d.ts rename to web_ui/frontend/index.ts index 7be168325..dfe6b1e9c 100644 --- a/web_ui/frontend/index.d.ts +++ b/web_ui/frontend/index.ts @@ -34,9 +34,11 @@ export interface Capabilities { DirectReads: boolean; } -export type StringTree = Record; +export interface StringTree { + [key: string]: StringTree | true; +} -interface Alert { +export interface Alert { severity: 'error' | 'warning' | 'info' | 'success'; message: string; } @@ -50,7 +52,7 @@ export interface Namespace { custom_fields?: Record; } -interface Institution { +export interface Institution { id: string; name: string; } From 0b672326b75cd2bb39c1ff97d050e1af9371c375 Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Fri, 25 Oct 2024 16:20:35 -0500 Subject: [PATCH 02/16] Uniform Error Handling --- .../app/(login)/initialization/code/page.tsx | 50 ++-- .../(login)/initialization/password/page.tsx | 61 ++-- web_ui/frontend/app/(login)/login/page.tsx | 46 ++- .../app/director/components/DirectorCard.tsx | 107 ++----- web_ui/frontend/app/director/page.tsx | 22 +- web_ui/frontend/app/layout.tsx | 7 +- .../frontend/app/registry/cache/edit/page.tsx | 3 +- .../app/registry/cache/register/page.tsx | 5 +- .../frontend/app/registry/components/Form.tsx | 32 +-- .../app/registry/components/PostPage.tsx | 43 +-- .../app/registry/components/PutPage.tsx | 64 +++-- .../frontend/app/registry/components/index.ts | 2 +- .../frontend/app/registry/components/util.tsx | 52 +--- web_ui/frontend/app/registry/denied/page.tsx | 76 ++--- .../app/registry/namespace/edit/page.tsx | 2 +- .../app/registry/namespace/register/page.tsx | 2 +- .../app/registry/origin/edit/page.tsx | 4 +- .../app/registry/origin/register/page.tsx | 4 +- web_ui/frontend/app/registry/page.tsx | 81 ++---- web_ui/frontend/app/test/page.tsx | 55 ++++ web_ui/frontend/components/AlertPortal.tsx | 32 ++- web_ui/frontend/components/AlertProvider.tsx | 72 +++++ web_ui/frontend/components/CodeBlock.tsx | 25 ++ web_ui/frontend/components/Namespace/Card.tsx | 33 +-- .../components/Namespace/DeniedCard.tsx | 108 ++----- .../components/Namespace/PendingCard.tsx | 81 ++---- .../ThemeProvider.tsx} | 6 +- web_ui/frontend/helpers/api.ts | 267 ++++++++++++++++++ .../frontend/helpers/{login.tsx => login.ts} | 0 web_ui/frontend/helpers/{util.tsx => util.ts} | 74 ++++- 30 files changed, 815 insertions(+), 601 deletions(-) create mode 100644 web_ui/frontend/app/test/page.tsx create mode 100644 web_ui/frontend/components/AlertProvider.tsx create mode 100644 web_ui/frontend/components/CodeBlock.tsx rename web_ui/frontend/{public/theme.tsx => components/ThemeProvider.tsx} (92%) create mode 100644 web_ui/frontend/helpers/api.ts rename web_ui/frontend/helpers/{login.tsx => login.ts} (100%) rename web_ui/frontend/helpers/{util.tsx => util.ts} (50%) diff --git a/web_ui/frontend/app/(login)/initialization/code/page.tsx b/web_ui/frontend/app/(login)/initialization/code/page.tsx index 05f784bf6..18791c9b9 100644 --- a/web_ui/frontend/app/(login)/initialization/code/page.tsx +++ b/web_ui/frontend/app/(login)/initialization/code/page.tsx @@ -18,13 +18,16 @@ 'use client'; -import { Box, Typography, Grow } from '@mui/material'; +import { Box, Typography } from '@mui/material'; import { useRouter } from 'next/navigation'; -import { useState } from 'react'; +import { useContext, useState } from 'react'; import CodeInput, { Code } from '../../components/CodeInput'; import LoadingButton from '../../components/LoadingButton'; -import { getErrorMessage } from '@/helpers/util'; +import { initLogin } from '@/helpers/api'; +import { alertOnError } from '@/helpers/util'; +import { AlertDispatchContext } from '@/components/AlertProvider'; + export default function Home() { const router = useRouter(); @@ -37,11 +40,11 @@ export default function Home() { undefined, ]); let [loading, setLoading] = useState(false); - let [error, setError] = useState(undefined); + + const dispatch = useContext(AlertDispatchContext) const setCode = (code: Code) => { _setCode(code); - setError(undefined); if (!code.includes(undefined)) { submit(code.map((x) => x!.toString()).join('')); @@ -51,26 +54,15 @@ export default function Home() { async function submit(code: string) { setLoading(true); - try { - let response = await fetch('/api/v1.0/auth/initLogin', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - code: code, - }), - }); - - if (response.ok) { - router.push('../password/'); - } else { - setLoading(false); - setError(await getErrorMessage(response)); - } - } catch { + let response = await alertOnError( + async () => await initLogin(code), + 'Could not login', + dispatch + ) + if(response) { + router.push('../password/'); + } else { setLoading(false); - setError('Could not connect to server'); } } @@ -97,16 +89,6 @@ export default function Home() {
- - - {error} - - (''); let [confirmPassword, _setConfirmPassword] = useState(''); let [loading, setLoading] = useState(false); - let [error, setError] = useState(undefined); + async function submit(password: string) { setLoading(true); - try { - let response = await fetch('/api/v1.0/auth/resetLogin', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - password: password, - }), - }); - - if (response.ok) { - router.push('/'); - } else { - setLoading(false); - setError(await getErrorMessage(response)); - } - } catch { + let response = await alertOnError( + async () => await resetLogin(password), + 'Could not login', + dispatch + ) + if(response) { + router.push('../password/'); + } else { setLoading(false); - setError('Could not connect to server'); } } @@ -66,7 +60,16 @@ export default function Home() { if (password == confirmPassword) { submit(password); } else { - setError('Passwords do not match'); + dispatch({ + type: 'openAlert', + payload: { + alertProps: { + severity: 'warning' + }, + message: 'Passwords do not match', + onClose: () => dispatch({ type: 'closeAlert' }) + } + }) } } @@ -89,7 +92,6 @@ export default function Home() { InputProps: { onChange: (e) => { _setPassword(e.target.value); - setError(undefined); }, }, }} @@ -102,7 +104,6 @@ export default function Home() { InputProps: { onChange: (e) => { _setConfirmPassword(e.target.value); - setError(undefined); }, }, error: password != confirmPassword, @@ -112,16 +113,6 @@ export default function Home() { /> - - - {error} - - { + + const dispatch = useContext(AlertDispatchContext); + const router = useRouter(); const { mutate } = useSWR('getUser', getUser); @@ -68,34 +74,20 @@ const AdminLogin = () => { async function submit(password: string) { setLoading(true); - let response; - try { - response = await fetch('/api/v1.0/auth/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - user: 'admin', - password: password, - }), - }); - - if (response.ok) { - await mutate(getUser); - - const url = new URL(window.location.href); - let returnUrl = url.searchParams.get('returnURL') || ''; - returnUrl = returnUrl.replace(`/view`, ''); - router.push(returnUrl ? returnUrl : '../'); - } else { - setLoading(false); - setError(await getErrorMessage(response)); - } - } catch (e) { - console.error(e); + const response = await alertOnError( + async () => await login(password), + "Could not login", + dispatch + ) + if (response) { + await mutate(getUser); + + const url = new URL(window.location.href); + let returnUrl = url.searchParams.get('returnURL') || ''; + returnUrl = returnUrl.replace(`/view`, ''); + router.push(returnUrl ? returnUrl : '../'); + } else { setLoading(false); - setError('Could not connect to server'); } } diff --git a/web_ui/frontend/app/director/components/DirectorCard.tsx b/web_ui/frontend/app/director/components/DirectorCard.tsx index 72419fd18..305ee0950 100644 --- a/web_ui/frontend/app/director/components/DirectorCard.tsx +++ b/web_ui/frontend/app/director/components/DirectorCard.tsx @@ -1,5 +1,5 @@ import { Authenticated, secureFetch } from '@/helpers/login'; -import React, { useRef, useState } from 'react'; +import React, { useContext, useRef, useState } from 'react'; import { Avatar, Box, @@ -21,8 +21,10 @@ import { NamespaceIcon } from '@/components/Namespace/index'; import useSWR from 'swr'; import Link from 'next/link'; import { User } from '@/index'; -import { getErrorMessage } from '@/helpers/util'; +import { alertOnError, getErrorMessage } from '@/helpers/util'; import { DirectorDropdown } from '@/app/director/components/DirectorDropdown'; +import { allowServer, filterServer } from '@/helpers/api'; +import { AlertDispatchContext } from '@/components/AlertProvider'; export interface DirectorCardProps { server: Server; @@ -30,11 +32,11 @@ export interface DirectorCardProps { } export const DirectorCard = ({ server, authenticated }: DirectorCardProps) => { - const [filtered, setFiltered] = useState(server.filtered); - const [error, setError] = useState(undefined); const [disabled, setDisabled] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); + const dispatch = useContext(AlertDispatchContext); + const { mutate } = useSWR('getServers'); return ( @@ -75,38 +77,34 @@ export const DirectorCard = ({ server, authenticated }: DirectorCardProps) => { { - x.stopPropagation(); + onClick={async (e) => { + e.stopPropagation(); // Disable the switch setDisabled(true); - // Provide optimistic feedback - setFiltered(!filtered); - // Update the server - let error; - if (filtered) { - error = await allowServer(server.name); - } else { - error = await filterServer(server.name); - } + await alertOnError( + async () => { + if (server.filtered) { + await allowServer(server.name); + } else { + await filterServer(server.name); + } + }, + 'Failed to toggle server status', + dispatch + ) - // Revert if we were too optimistic - if (error) { - setFiltered(!filtered); - setError(error); - } else { - mutate(); - } + mutate(); setDisabled(false); }} /> } - label={!filtered ? 'Active' : 'Disabled'} + label={server.filtered ? 'Disabled' : 'Active'} /> @@ -127,69 +125,8 @@ export const DirectorCard = ({ server, authenticated }: DirectorCardProps) => { - - setError(undefined)} - > - setError(undefined)} - severity='error' - variant='filled' - sx={{ width: '100%' }} - > - {error} -
- If this error persists on reload, please file a ticket via the (?) - in the bottom left. -
-
-
); }; -const filterServer = async (name: string): Promise => { - try { - const response = await secureFetch( - `/api/v1.0/director_ui/servers/filter/${name}`, - { - method: 'PATCH', - } - ); - if (response.ok) { - return; - } else { - return await getErrorMessage(response); - } - } catch (e) { - if (e instanceof Error) { - return e.message; - } - return 'Could not connect to server'; - } -}; - -const allowServer = async (name: string): Promise => { - try { - const response = await secureFetch( - `/api/v1.0/director_ui/servers/allow/${name}`, - { - method: 'PATCH', - } - ); - if (response.ok) { - return; - } else { - return await getErrorMessage(response); - } - } catch (e) { - if (e instanceof Error) { - return e.message; - } - return 'Could not connect to server'; - } -}; - export default DirectorCard; diff --git a/web_ui/frontend/app/director/page.tsx b/web_ui/frontend/app/director/page.tsx index 3b1f64c1a..ef7bfde10 100644 --- a/web_ui/frontend/app/director/page.tsx +++ b/web_ui/frontend/app/director/page.tsx @@ -19,23 +19,33 @@ 'use client'; import { Box, Grid, Skeleton, Typography } from '@mui/material'; -import { useMemo } from 'react'; +import { useContext, useMemo } from 'react'; import useSWR from 'swr'; import { Server } from '@/index'; import { DirectorCardList, - DirectorCard, - DirectorCardProps, } from './components'; import { getUser } from '@/helpers/login'; import FederationOverview from '@/components/FederationOverview'; import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; import { PaddedContent } from '@/components/layout'; +import { fetchApi } from '@/helpers/api'; +import { alertOnError } from '@/helpers/util'; +import { AlertDispatchContext } from '@/components/AlertProvider'; export default function Page() { - const { data } = useSWR('getServers', getServers); - const { data: user, error } = useSWR('getUser', getUser); + const dispatch = useContext(AlertDispatchContext); + + const { data } = useSWR( + 'getServers', + async () => await alertOnError(getServers, 'Failed to fetch servers', dispatch) + ); + + const { data: user, error } = useSWR( + 'getUser', + () => alertOnError(getUser, "Failed to fetch user", dispatch) + ); const cacheData = useMemo(() => { return data?.filter((server) => server.type === 'Cache'); @@ -97,7 +107,7 @@ export default function Page() { const getServers = async () => { const url = new URL('/api/v1.0/director_ui/servers', window.location.origin); - let response = await fetch(url); + let response = await fetchApi(async () => await fetch(url)); if (response.ok) { const responseData: Server[] = await response.json(); responseData.sort((a, b) => a.name.localeCompare(b.name)); diff --git a/web_ui/frontend/app/layout.tsx b/web_ui/frontend/app/layout.tsx index bc4da3fc9..10f5ca8b3 100644 --- a/web_ui/frontend/app/layout.tsx +++ b/web_ui/frontend/app/layout.tsx @@ -17,7 +17,8 @@ ***************************************************************/ import { LocalizationProvider } from '@/clientComponents'; -import { ThemeProviderClient } from '@/public/theme'; +import { ThemeProviderClient } from '@/components/ThemeProvider'; +import { AlertProvider } from '@/components/AlertProvider'; import './globals.css'; export const metadata = { @@ -34,7 +35,9 @@ export default function RootLayout({ - {children} + + {children} + diff --git a/web_ui/frontend/app/registry/cache/edit/page.tsx b/web_ui/frontend/app/registry/cache/edit/page.tsx index 66ce2d284..6c06ab12b 100644 --- a/web_ui/frontend/app/registry/cache/edit/page.tsx +++ b/web_ui/frontend/app/registry/cache/edit/page.tsx @@ -21,11 +21,12 @@ import { PutPage } from '@/app/registry/components/PutPage'; import { namespaceToCache, - putGeneralNamespace, } from '@/app/registry/components/util'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; +import { putGeneralNamespace } from '@/helpers/api'; + export default function Page() { const putCache = async (data: any) => { const cache = namespaceToCache(structuredClone(data)); diff --git a/web_ui/frontend/app/registry/cache/register/page.tsx b/web_ui/frontend/app/registry/cache/register/page.tsx index 6221ddb01..70386a7db 100644 --- a/web_ui/frontend/app/registry/cache/register/page.tsx +++ b/web_ui/frontend/app/registry/cache/register/page.tsx @@ -19,12 +19,13 @@ 'use client'; import { - namespaceToCache, - postGeneralNamespace, + namespaceToCache } from '@/app/registry/components/util'; import { PostPage } from '@/app/registry/components/PostPage'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; +import { postGeneralNamespace } from '@/helpers/api'; + export default function Page() { const postCache = async (data: any) => { diff --git a/web_ui/frontend/app/registry/components/Form.tsx b/web_ui/frontend/app/registry/components/Form.tsx index 37326f65c..688f92e79 100644 --- a/web_ui/frontend/app/registry/components/Form.tsx +++ b/web_ui/frontend/app/registry/components/Form.tsx @@ -1,5 +1,5 @@ import { Box, Button, Alert } from '@mui/material'; -import React, { useEffect, useState, Dispatch, SetStateAction } from 'react'; +import React, { useEffect, useState, Dispatch, SetStateAction, useContext } from 'react'; import useSWR from 'swr'; import { Namespace } from '@/index'; @@ -12,26 +12,15 @@ import { submitNamespaceForm, } from '@/app/registry/components/util'; import { CustomRegistrationFieldProps } from './CustomRegistrationField'; -import { getErrorMessage } from '@/helpers/util'; +import { alertOnError, getErrorMessage } from '@/helpers/util'; +import { optionsNamespaceRegistrationFields } from '@/helpers/api'; +import { AlertDispatchContext } from '@/components/AlertProvider'; interface FormProps { namespace?: Namespace; onSubmit: (data: Partial) => Promise; } -const getRegistrationFields = async (): Promise< - Omit[] -> => { - const response = await fetch('/api/v1.0/registry_ui/namespaces', { - method: 'OPTIONS', - }); - if (response.ok) { - return await response.json(); - } else { - throw new Error(await getErrorMessage(response)); - } -}; - const onChange = ( name: string, value: string | number | boolean | null, @@ -53,13 +42,20 @@ const onChange = ( }; const Form = ({ namespace, onSubmit }: FormProps) => { + + const dispatch = useContext(AlertDispatchContext); + const [data, setData] = useState | undefined>( namespace || {} ); - const { data: fields, error } = useSWR[]>( - 'getRegistrationFields', - getRegistrationFields, + const { data: fields, error } = useSWR[] | undefined>( + 'optionsNamespaceRegistrationFields', + async () => await alertOnError( + optionsNamespaceRegistrationFields, + "Couldn't fetch registration fields", + dispatch + ), { fallbackData: [] } ); diff --git a/web_ui/frontend/app/registry/components/PostPage.tsx b/web_ui/frontend/app/registry/components/PostPage.tsx index f5d6991ee..91ecf2478 100644 --- a/web_ui/frontend/app/registry/components/PostPage.tsx +++ b/web_ui/frontend/app/registry/components/PostPage.tsx @@ -19,44 +19,49 @@ 'use client'; import { Box, Grid, Collapse, Alert, Skeleton } from '@mui/material'; -import React, { useEffect, useState } from 'react'; - -import { Alert as AlertType, Namespace } from '@/index'; +import React, { useContext, useEffect, useState } from 'react'; import Form from '@/app/registry/components/Form'; import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; import { submitNamespaceForm } from '@/app/registry/components/util'; import { NamespaceFormPage } from '@/app/registry/components'; +import { alertOnError } from '@/helpers/util'; +import { AlertDispatchContext } from '@/components/AlertProvider'; const PostPage = ({ update }: NamespaceFormPage) => { + + const dispatch = useContext(AlertDispatchContext); + const [fromUrl, setFromUrl] = useState(undefined); - const [alert, setAlert] = useState(undefined); useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const fromUrl = urlParams.get('fromUrl'); + (async () => { + const urlParams = new URLSearchParams(window.location.search); + const fromUrl = urlParams.get('fromUrl'); - try { if (fromUrl != undefined) { - const parsedUrl = new URL(fromUrl); - setFromUrl(parsedUrl); + const parsedUrl = await alertOnError( + () => new URL(fromUrl), + "Failed to parse URL", + dispatch + ); + if (parsedUrl) { + setFromUrl(parsedUrl); + } } - } catch (e) { - setAlert({ severity: 'error', message: 'Invalid fromUrl provided' }); - } + })() }, []); return ( - - - {alert?.message} - - { - setAlert(await submitNamespaceForm(data, fromUrl, update)); + onSubmit={async (namespace) => { + await alertOnError( + async () => await submitNamespaceForm(namespace, fromUrl, update), + "Failed to update namespace", + dispatch + ) }} /> diff --git a/web_ui/frontend/app/registry/components/PutPage.tsx b/web_ui/frontend/app/registry/components/PutPage.tsx index 813aa1acb..4b3a8c820 100644 --- a/web_ui/frontend/app/registry/components/PutPage.tsx +++ b/web_ui/frontend/app/registry/components/PutPage.tsx @@ -28,7 +28,7 @@ import { } from '@mui/material'; import React, { ReactNode, - Suspense, + Suspense, useContext, useEffect, useMemo, useState, @@ -38,16 +38,19 @@ import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; import { Namespace, Alert as AlertType } from '@/index'; import Form from '@/app/registry/components/Form'; import { - getNamespace, submitNamespaceForm, } from '@/app/registry/components/util'; +import { getNamespace } from '@/helpers/api'; import { NamespaceFormPage } from '@/app/registry/components' +import { AlertDispatchContext } from '@/components/AlertProvider'; +import { alertOnError } from '@/helpers/util'; const PutPage = ({ update }: NamespaceFormPage) => { const [id, setId] = useState(undefined); const [fromUrl, setFromUrl] = useState(undefined); const [namespace, setNamespace] = useState(undefined); - const [alert, setAlert] = useState(undefined); + + const dispatch = useContext(AlertDispatchContext); useEffect(() => { const urlParams = new URLSearchParams(window.location.search); @@ -55,7 +58,14 @@ const PutPage = ({ update }: NamespaceFormPage) => { const fromUrl = urlParams.get('fromUrl'); if (id === null) { - setAlert({ severity: 'error', message: 'No Namespace ID Provided' }); + dispatch({ + type: "openAlert", + payload: { + title: "No Namespace ID Provided", + message: "Your URL should contain a query parameter 'id' with the ID of the namespace you want to edit", + onClose: () => dispatch({ type: "closeAlert" }) + } + }) return; } @@ -65,23 +75,46 @@ const PutPage = ({ update }: NamespaceFormPage) => { setFromUrl(parsedUrl); } } catch (e) { - setAlert({ severity: 'error', message: 'Invalid fromUrl provided' }); + dispatch({ + type: "openAlert", + payload: { + title: "Invalid fromUrl provided", + message: "The `fromUrl` parameter provided is not a valid URL, this will only impact your redirection on completion of this form", + alertProps: { + severity: "warning" + }, + onClose: () => dispatch({ type: "closeAlert" }) + } + }) } try { setId(parseInt(id)); } catch (e) { - setAlert({ severity: 'error', message: 'Invalid Namespace ID Provided' }); + dispatch({ + type: "openAlert", + payload: { + title: "Invalid Namespace ID provided", + message: "The Namespace Id provided is not a valid number. Please report this issue, as well as what link directed you here.", + alertProps: { + severity: "error" + }, + onClose: () => dispatch({ type: "closeAlert" }) + } + }) } }, []); useEffect(() => { (async () => { if (id !== undefined) { - try { - setNamespace(await getNamespace(id)); - } catch (e) { - setAlert({ severity: 'error', message: e as string }); + const response = await alertOnError( + async () => await getNamespace(id), + "Couldn't get namespace", + dispatch + ) + if(response){ + setNamespace(response); } } })(); @@ -91,17 +124,16 @@ const PutPage = ({ update }: NamespaceFormPage) => { - - - {alert?.message} - - {namespace ? ( { let namespace = { ...data, id: id }; - setAlert(await submitNamespaceForm(namespace, fromUrl, update)); + await alertOnError( + async () => await submitNamespaceForm(namespace, fromUrl, update), + "Failed to update namespace", + dispatch + ) }} /> ) : ( diff --git a/web_ui/frontend/app/registry/components/index.ts b/web_ui/frontend/app/registry/components/index.ts index 86214d7a3..30e5853be 100644 --- a/web_ui/frontend/app/registry/components/index.ts +++ b/web_ui/frontend/app/registry/components/index.ts @@ -1,5 +1,5 @@ import { Alert as AlertType, Namespace } from '@/index'; export interface NamespaceFormPage { - update: (data: Partial) => Promise; + update: (data: Partial) => Promise; } diff --git a/web_ui/frontend/app/registry/components/util.tsx b/web_ui/frontend/app/registry/components/util.tsx index 9a55fd031..e6c595942 100644 --- a/web_ui/frontend/app/registry/components/util.tsx +++ b/web_ui/frontend/app/registry/components/util.tsx @@ -98,61 +98,15 @@ export const namespaceToOrigin = (data: Namespace) => { return data; }; -export const getNamespace = async ( - id: string | number -): Promise => { - const url = new URL( - `/api/v1.0/registry_ui/namespaces/${id}`, - window.location.origin - ); - const response = await fetch(url); - if (response.ok) { - return await response.json(); - } else { - throw new Error(await getErrorMessage(response)); - } -}; - -export const postGeneralNamespace = async ( - data: Namespace -): Promise => { - return await handleRequestAlert('/api/v1.0/registry_ui/namespaces', { - body: JSON.stringify(data), - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - }); -}; - -export const putGeneralNamespace = async ( - data: Namespace -): Promise => { - return await handleRequestAlert( - `/api/v1.0/registry_ui/namespaces/${data.id}`, - { - body: JSON.stringify(data), - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - } - ); -}; - export const submitNamespaceForm = async ( data: Partial, toUrl: URL | undefined, - handleSubmit: (data: Partial) => Promise + handleSubmit: (data: Partial) => Promise ) => { - const submitAlert = await handleSubmit(data); + const response = await handleSubmit(data); // Clear the form on successful submit - if (submitAlert == undefined) { + if (response != undefined) { window.location.href = toUrl ? toUrl.toString() : '/view/registry/'; } - - return submitAlert; }; diff --git a/web_ui/frontend/app/registry/denied/page.tsx b/web_ui/frontend/app/registry/denied/page.tsx index f1bee0e5e..00c792651 100644 --- a/web_ui/frontend/app/registry/denied/page.tsx +++ b/web_ui/frontend/app/registry/denied/page.tsx @@ -20,69 +20,46 @@ import { Box, - Button, Grid, Typography, - Paper, Alert, - Collapse, - IconButton, + Collapse } from '@mui/material'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useContext, useMemo } from 'react'; import { - PendingCard, - Card, - CardSkeleton, - CreateNamespaceCard, + CardSkeleton } from '@/components/Namespace'; -import Link from 'next/link'; -import { Namespace, Alert as AlertType } from '@/index'; import { getUser } from '@/helpers/login'; -import { Add } from '@mui/icons-material'; import NamespaceCardList from '@/components/Namespace/NamespaceCardList'; import useSWR from 'swr'; import { CardProps } from '@/components/Namespace/Card'; -import { PendingCardProps } from '@/components/Namespace/PendingCard'; import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; import DeniedCard from '@/components/Namespace/DeniedCard'; +import { getExtendedNamespaces } from '@/helpers/api'; +import { AlertDispatchContext } from '@/components/AlertProvider'; +import { alertOnError } from '@/helpers/util'; -const getData = async () => { - let data: { namespace: Namespace }[] = []; - - const url = new URL( - '/api/v1.0/registry_ui/namespaces', - window.location.origin - ); - - const response = await fetch(url); - if (response.ok) { - const responseData: Namespace[] = await response.json(); - responseData.sort((a, b) => (a.id > b.id ? 1 : -1)); - responseData.forEach((namespace) => { - if (namespace.prefix.startsWith('/caches/')) { - namespace.type = 'cache'; - namespace.prefix = namespace.prefix.replace('/caches/', ''); - } else if (namespace.prefix.startsWith('/origins/')) { - namespace.type = 'origin'; - namespace.prefix = namespace.prefix.replace('/origins/', ''); - } else { - namespace.type = 'namespace'; - } - }); - - // Convert data to Partial CardProps - data = responseData.map((d) => { - return { namespace: d }; - }); - } +export default function Home() { - return data; -}; + const dispatch = useContext(AlertDispatchContext) -export default function Home() { - const { data } = useSWR('getNamespaces', getData); - const { data: user, error } = useSWR('getUser', getUser); + const { data } = useSWR( + 'getNamespaces', + async () => alertOnError( + getExtendedNamespaces, + "Couldn't fetch namespaces", + dispatch + ) + ) + const { data: user, error } = useSWR( + 'getUser', + async () => alertOnError( + getUser, + "Couldn't fetch user", + dispatch + ) + ); const deniedNamespaces = useMemo( () => @@ -97,11 +74,6 @@ export default function Home() { Namespace Registry - - - {error?.toString()} - - diff --git a/web_ui/frontend/app/registry/namespace/edit/page.tsx b/web_ui/frontend/app/registry/namespace/edit/page.tsx index 2abb51461..757897925 100644 --- a/web_ui/frontend/app/registry/namespace/edit/page.tsx +++ b/web_ui/frontend/app/registry/namespace/edit/page.tsx @@ -19,7 +19,7 @@ 'use client'; import { PutPage } from '@/app/registry/components/PutPage'; -import { putGeneralNamespace } from '@/app/registry/components/util'; +import { putGeneralNamespace } from '@/helpers/api'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; diff --git a/web_ui/frontend/app/registry/namespace/register/page.tsx b/web_ui/frontend/app/registry/namespace/register/page.tsx index 12d60291f..6d51df66a 100644 --- a/web_ui/frontend/app/registry/namespace/register/page.tsx +++ b/web_ui/frontend/app/registry/namespace/register/page.tsx @@ -18,7 +18,7 @@ 'use client'; -import { postGeneralNamespace } from '@/app/registry/components/util'; +import { postGeneralNamespace } from '@/helpers/api'; import { PostPage } from '@/app/registry/components/PostPage'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; diff --git a/web_ui/frontend/app/registry/origin/edit/page.tsx b/web_ui/frontend/app/registry/origin/edit/page.tsx index 683381232..25629eb54 100644 --- a/web_ui/frontend/app/registry/origin/edit/page.tsx +++ b/web_ui/frontend/app/registry/origin/edit/page.tsx @@ -20,11 +20,11 @@ import { PutPage } from '@/app/registry/components/PutPage'; import { - namespaceToOrigin, - putGeneralNamespace, + namespaceToOrigin } from '@/app/registry/components/util'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; +import { putGeneralNamespace } from '@/helpers/api'; export default function Page() { const putCache = async (data: any) => { diff --git a/web_ui/frontend/app/registry/origin/register/page.tsx b/web_ui/frontend/app/registry/origin/register/page.tsx index a89e3f83d..59e52d4a7 100644 --- a/web_ui/frontend/app/registry/origin/register/page.tsx +++ b/web_ui/frontend/app/registry/origin/register/page.tsx @@ -19,12 +19,12 @@ 'use client'; import { - namespaceToOrigin, - postGeneralNamespace, + namespaceToOrigin } from '@/app/registry/components/util'; import { PostPage } from '@/app/registry/components/PostPage'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; +import { postGeneralNamespace } from '@/helpers/api'; export default function Page() { const postCache = async (data: any) => { diff --git a/web_ui/frontend/app/registry/page.tsx b/web_ui/frontend/app/registry/page.tsx index caf27b17a..746a45d6c 100644 --- a/web_ui/frontend/app/registry/page.tsx +++ b/web_ui/frontend/app/registry/page.tsx @@ -28,7 +28,7 @@ import { Collapse, IconButton, } from '@mui/material'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState, useContext } from 'react'; import { PendingCard, @@ -44,19 +44,30 @@ import { Add } from '@mui/icons-material'; import useSWR from 'swr'; import { CardProps } from '@/components/Namespace/Card'; import { PendingCardProps } from '@/components/Namespace/PendingCard'; +import { AlertDispatchContext } from '@/components/AlertProvider'; +import { alertOnError } from '@/helpers/util'; +import { getExtendedNamespaces } from '@/helpers/api'; export default function Home() { - const [alert, setAlert] = useState(undefined); - const { data, mutate: mutateNamespaces } = useSWR<{ namespace: Namespace }[]>( + const dispatch = useContext(AlertDispatchContext); + + const { data, mutate: mutateNamespaces } = useSWR<{ namespace: Namespace }[] | undefined>( 'getNamespaces', - getData, + () => alertOnError( + getExtendedNamespaces, + 'Failed to fetch namespaces', + dispatch + ), { fallbackData: [], } ); - const { data: user, error } = useSWR('getUser', getUser); + const { data: user, error } = useSWR( + 'getUser', + async () => await alertOnError(getUser, "Error Getting User", dispatch) + ); const pendingData = useMemo(() => { return data?.filter( @@ -97,33 +108,7 @@ export default function Home() { return ( - - Namespace Registry - - - {alert?.message} - - - - {user == undefined || - (!user.authenticated && ( - - - Login to register new namespaces. - - - - - - ))} {pendingData && pendingData.length > 0 && ( setAlert(a), onUpdate: () => mutateNamespaces(), }} /> @@ -263,36 +247,3 @@ export default function Home() { ); } - -const getData = async () => { - let data: { namespace: Namespace }[] = []; - - const url = new URL( - '/api/v1.0/registry_ui/namespaces', - window.location.origin - ); - - const response = await fetch(url); - if (response.ok) { - const responseData: Namespace[] = await response.json(); - responseData.sort((a, b) => (a.id > b.id ? 1 : -1)); - responseData.forEach((namespace) => { - if (namespace.prefix.startsWith('/caches/')) { - namespace.type = 'cache'; - namespace.prefix = namespace.prefix.replace('/caches/', ''); - } else if (namespace.prefix.startsWith('/origins/')) { - namespace.type = 'origin'; - namespace.prefix = namespace.prefix.replace('/origins/', ''); - } else { - namespace.type = 'namespace'; - } - }); - - // Convert data to Partial CardProps - data = responseData.map((d) => { - return { namespace: d }; - }); - } - - return data; -}; diff --git a/web_ui/frontend/app/test/page.tsx b/web_ui/frontend/app/test/page.tsx new file mode 100644 index 000000000..96176f517 --- /dev/null +++ b/web_ui/frontend/app/test/page.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { AlertDispatchContext } from '@/components/AlertProvider'; +import React, { useContext } from 'react'; +import { Box } from '@mui/material'; +import CodeBlock from '@/components/CodeBlock'; + + +const Page = () => { + const dispatch = useContext(AlertDispatchContext); + + return ( +
+ + +
+ ); +} + +export default Page; diff --git a/web_ui/frontend/components/AlertPortal.tsx b/web_ui/frontend/components/AlertPortal.tsx index 7fc32a48c..4c482e3a4 100644 --- a/web_ui/frontend/components/AlertPortal.tsx +++ b/web_ui/frontend/components/AlertPortal.tsx @@ -1,35 +1,43 @@ import { Portal } from '@mui/base'; -import React from 'react'; -import { Alert, SnackbarProps, Snackbar } from '@mui/material'; - -import { Alert as AlertType } from '@/index'; +import React, { ReactNode } from 'react'; +import { Alert, AlertProps, Snackbar, SnackbarProps, AlertTitle } from '@mui/material'; export interface AlertPortalProps { - alert?: AlertType; onClose: () => void; + title?: string; + autoHideDuration?: number; + message?: ReactNode | string; + alertProps?: Omit; snackBarProps?: SnackbarProps; } export const AlertPortal = ({ - alert, onClose, + title, + autoHideDuration, + message, + alertProps, snackBarProps, }: AlertPortalProps) => { + + if (autoHideDuration) { + setTimeout(() => onClose(), autoHideDuration) + } + return ( - {alert?.message} + { title && {title} } + {message} diff --git a/web_ui/frontend/components/AlertProvider.tsx b/web_ui/frontend/components/AlertProvider.tsx new file mode 100644 index 000000000..91177efa9 --- /dev/null +++ b/web_ui/frontend/components/AlertProvider.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { createContext, Dispatch, useReducer } from 'react'; +import { AlertPortal, AlertPortalProps } from '@/components/AlertPortal'; +import CodeBlock from '@/components/CodeBlock'; + +const defaultAlertContext: AlertPortalProps | undefined = undefined + +export const AlertContext = + createContext(defaultAlertContext); + +export const AlertDispatchContext = createContext>( + () => {} +); + +export const AlertProvider = ({ children }: { children: React.ReactNode }) => { + const [state, dispatch] = useReducer(alertReducer, defaultAlertContext); + + return ( + + + {children} + { state && } + + + ); +}; + +const alertReducer = ( + state: AlertPortalProps | undefined, + action: AlertReducerAction +): AlertPortalProps | undefined => { + switch (action.type) { + case 'closeAlert': + return undefined; + case 'openErrorAlert': + const {title, error, onClose} = action.payload; + + return { + title, + onClose, + message: {error}, + alertProps: { + severity: 'error' + } + } + case 'openAlert': + return action.payload; + default: + return state; + } +} + +export type AlertReducerAction = closeAlertAction | openErrorAlertAction | openAlertAction; + +type closeAlertAction = { + type: 'closeAlert' +} + +type openErrorAlertAction = { + type: 'openErrorAlert'; + payload: { + title: string; + error: string; + onClose: () => void; + } +} + +type openAlertAction = { + type: 'openAlert', + payload: AlertPortalProps +} diff --git a/web_ui/frontend/components/CodeBlock.tsx b/web_ui/frontend/components/CodeBlock.tsx new file mode 100644 index 000000000..900dc58e6 --- /dev/null +++ b/web_ui/frontend/components/CodeBlock.tsx @@ -0,0 +1,25 @@ +import { stackoverflowLight } from 'react-syntax-highlighter/dist/cjs/styles/hljs'; +import SyntaxHighlighter from 'react-syntax-highlighter'; +import { Box } from '@mui/material'; + +/** + * CodeBlock component + * Copy onClick and darken onHover + * @param children + * @constructor + */ +export const CodeBlock = ({children}: {children: string | string[]}) => { + return + { + navigator.clipboard.writeText(children.toString()); + }} + > + {children} + + +} + +export default CodeBlock; diff --git a/web_ui/frontend/components/Namespace/Card.tsx b/web_ui/frontend/components/Namespace/Card.tsx index c873ec115..b2e9d0a30 100644 --- a/web_ui/frontend/components/Namespace/Card.tsx +++ b/web_ui/frontend/components/Namespace/Card.tsx @@ -1,5 +1,5 @@ import { Alert, Alert as AlertType, Namespace } from '@/index'; -import React, { useRef, useState } from 'react'; +import React, { useContext, useRef, useState } from 'react'; import { Avatar, Box, @@ -14,9 +14,11 @@ import Link from 'next/link'; import InformationDropdown from './InformationDropdown'; import { NamespaceIcon } from '@/components/Namespace/index'; import { User } from '@/index'; -import AlertPortal from '@/components/AlertPortal'; -import { deleteNamespace } from './DeniedCard'; +import { deleteNamespace } from '@/helpers/api'; import { useSWRConfig } from 'swr'; +import { AlertDispatchContext } from '@/components/AlertProvider'; +import CodeBlock from '@/components/CodeBlock'; +import { alertOnError } from '@/helpers/util'; export interface CardProps { namespace: Namespace; @@ -25,9 +27,9 @@ export interface CardProps { } export const Card = ({ namespace, authenticated, onUpdate }: CardProps) => { + const dispatch = useContext(AlertDispatchContext); const ref = useRef(null); const [transition, setTransition] = useState(false); - const [alert, setAlert] = useState(undefined); const { mutate } = useSWRConfig(); return ( <> @@ -102,20 +104,14 @@ export const Card = ({ namespace, authenticated, onUpdate }: CardProps) => { color={'error'} onClick={async (e) => { e.stopPropagation(); - try { - await deleteNamespace(namespace.id); - setAlert({ - severity: 'success', - message: 'Registration deleted', - }); - setTimeout(() => mutate('getNamespaces'), 600); - if (onUpdate) { - onUpdate(); - } - } catch (e) { - if (e instanceof Error) { - setAlert({ severity: 'error', message: e.message }); - } + await alertOnError( + async () => await deleteNamespace(namespace.id), + 'Could Not Delete Registration', + dispatch + ) + setTimeout(() => mutate('getNamespaces'), 600); + if (onUpdate) { + onUpdate(); } }} > @@ -135,7 +131,6 @@ export const Card = ({ namespace, authenticated, onUpdate }: CardProps) => { /> - setAlert(undefined)} /> ); }; diff --git a/web_ui/frontend/components/Namespace/DeniedCard.tsx b/web_ui/frontend/components/Namespace/DeniedCard.tsx index 348cb7a45..2db34baaa 100644 --- a/web_ui/frontend/components/Namespace/DeniedCard.tsx +++ b/web_ui/frontend/components/Namespace/DeniedCard.tsx @@ -1,14 +1,17 @@ -import React, { useMemo, useRef, useState } from 'react'; +import React, { useContext, useMemo, useRef, useState } from 'react'; import { green, red } from '@mui/material/colors'; import { Authenticated, secureFetch } from '@/helpers/login'; import { Avatar, Box, IconButton, Tooltip, Typography } from '@mui/material'; import { Block, Check, Delete, Edit, Person } from '@mui/icons-material'; -import { Alert as AlertType, Alert, Namespace } from '@/index'; +import { Alert, Namespace } from '@/index'; import InformationDropdown from './InformationDropdown'; import { getServerType, NamespaceIcon } from '@/components/Namespace/index'; +import { AlertContext, AlertDispatchContext } from '@/components/AlertProvider'; import { User } from '@/index'; -import AlertPortal from '@/components/AlertPortal'; import { useSWRConfig } from 'swr'; +import CodeBlock from '@/components/CodeBlock'; +import { approveNamespace, deleteNamespace } from '@/helpers/api'; +import { alertOnError } from '@/helpers/util'; export interface DeniedCardProps { namespace: Namespace; @@ -17,60 +20,11 @@ export interface DeniedCardProps { authenticated?: User; } -export const deleteNamespace = async (id: number) => { - const response = await secureFetch(`/api/v1.0/registry_ui/namespaces/${id}`, { - method: 'DELETE', - }); - - if (!response.ok) { - let alertMessage; - try { - let data = await response.json(); - if (data?.msg) { - alertMessage = data?.msg; - } - alertMessage = 'Details not provided'; - } catch (e) { - if (e instanceof Error) { - alertMessage = e.message; - } - } - - throw new Error('Failed to delete namespace: ' + alertMessage); - } -}; - -const approveNamespace = async (id: number) => { - const response = await secureFetch( - `/api/v1.0/registry_ui/namespaces/${id}/approve`, - { - method: 'PATCH', - } - ); - - if (!response.ok) { - let alertMessage; - try { - let data = await response.json(); - if (data?.msg) { - alertMessage = data?.msg; - } - alertMessage = 'Details not provided'; - } catch (e) { - if (e instanceof Error) { - alertMessage = e.message; - } - } - - throw new Error('Failed to approve registration: ' + alertMessage); - } -}; - export const DeniedCard = ({ namespace, authenticated }: DeniedCardProps) => { const ref = useRef(null); const [transition, setTransition] = useState(false); - const [alert, setAlert] = useState(undefined); - + const dispatch = useContext(AlertDispatchContext); + const alert = useContext(AlertContext) const { mutate } = useSWRConfig(); return ( @@ -86,9 +40,9 @@ export const DeniedCard = ({ namespace, authenticated }: DeniedCardProps) => { borderRadius: transition ? '10px 10px 0px 0px' : 2, transition: 'background-color .3s ease-out', bgcolor: - alert?.severity == 'success' + alert?.alertProps?.severity == 'success' ? green[100] - : alert?.severity == 'error' + : alert?.alertProps?.severity == 'error' ? red[100] : 'inherit', '&:hover': { @@ -130,18 +84,11 @@ export const DeniedCard = ({ namespace, authenticated }: DeniedCardProps) => { color={'error'} onClick={async (e) => { e.stopPropagation(); - try { - await deleteNamespace(namespace.id); - setAlert({ - severity: 'success', - message: 'Registration deleted', - }); - setTimeout(() => mutate('getNamespaces'), 600); - } catch (e) { - if (e instanceof Error) { - setAlert({ severity: 'error', message: e.message }); - } - } + await alertOnError( + () => deleteNamespace(namespace.id), + 'Could Not Delete Registration', + dispatch + ) }} > @@ -153,18 +100,12 @@ export const DeniedCard = ({ namespace, authenticated }: DeniedCardProps) => { color={'success'} onClick={async (e) => { e.stopPropagation(); - try { - await approveNamespace(namespace.id); - setAlert({ - severity: 'success', - message: 'Registration Approved', - }); - setTimeout(() => mutate('getNamespaces'), 600); - } catch (e) { - if (e instanceof Error) { - setAlert({ severity: 'error', message: e.message }); - } - } + await alertOnError( + () => approveNamespace(namespace.id), + 'Could Not Approve Registration', + dispatch + ) + setTimeout(() => mutate('getNamespaces'), 600); }} > @@ -183,13 +124,6 @@ export const DeniedCard = ({ namespace, authenticated }: DeniedCardProps) => { /> - {alert?.severity == 'error' && ( - setAlert(undefined)} - /> - )} ); }; diff --git a/web_ui/frontend/components/Namespace/PendingCard.tsx b/web_ui/frontend/components/Namespace/PendingCard.tsx index 4257ef09a..f138ab3a4 100644 --- a/web_ui/frontend/components/Namespace/PendingCard.tsx +++ b/web_ui/frontend/components/Namespace/PendingCard.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef, useState } from 'react'; +import React, { useContext, useMemo, useRef, useState } from 'react'; import { Authenticated, secureFetch } from '@/helpers/login'; import { Avatar, Box, IconButton, Tooltip, Typography } from '@mui/material'; import { Block, Check, Edit, Person } from '@mui/icons-material'; @@ -8,6 +8,9 @@ import { Alert, Namespace } from '@/index'; import InformationDropdown from './InformationDropdown'; import { getServerType, NamespaceIcon } from '@/components/Namespace/index'; import { User } from '@/index'; +import { alertOnError } from '@/helpers/util'; +import { AlertDispatchContext } from '@/components/AlertProvider'; +import { approveNamespace, denyNamespace } from '@/helpers/api'; export interface PendingCardProps { namespace: Namespace; @@ -25,61 +28,7 @@ export const PendingCard = ({ const ref = useRef(null); const [transition, setTransition] = useState(false); - const approveNamespace = async (e: React.MouseEvent) => { - e.stopPropagation(); - - try { - const response = await secureFetch( - `/api/v1.0/registry_ui/namespaces/${namespace.id}/approve`, - { - method: 'PATCH', - } - ); - - if (!response.ok) { - onAlert({ - severity: 'error', - message: `Failed to approve ${namespace.type} registration: ${namespace.prefix}`, - }); - } else { - onUpdate(); - onAlert({ - severity: 'success', - message: `Successfully approved ${namespace.type} registration: ${namespace.prefix}`, - }); - } - } catch (error) { - console.error(error); - } - }; - - const denyNamespace = async (e: React.MouseEvent) => { - e.stopPropagation(); - - try { - const response = await secureFetch( - `/api/v1.0/registry_ui/namespaces/${namespace.id}/deny`, - { - method: 'PATCH', - } - ); - - if (!response.ok) { - onAlert({ - severity: 'error', - message: `Failed to deny ${namespace.type} registration: ${namespace.prefix}`, - }); - } else { - onUpdate(); - onAlert({ - severity: 'success', - message: `Successfully denied ${namespace.type} registration: ${namespace.prefix}`, - }); - } - } catch (error) { - console.error(error); - } - }; + const dispatch = useContext(AlertDispatchContext) return ( @@ -123,7 +72,15 @@ export const PendingCard = ({ denyNamespace(e)} + onClick={async (e) => { + e.stopPropagation() + await alertOnError( + () => denyNamespace(namespace.id), + "Couldn't deny namespace", + dispatch + ) + onUpdate() + }} > @@ -132,7 +89,15 @@ export const PendingCard = ({ approveNamespace(e)} + onClick={async (e) => { + e.stopPropagation() + await alertOnError( + () => approveNamespace(namespace.id), + "Couldn't approve namespace", + dispatch + ) + onUpdate() + }} > diff --git a/web_ui/frontend/public/theme.tsx b/web_ui/frontend/components/ThemeProvider.tsx similarity index 92% rename from web_ui/frontend/public/theme.tsx rename to web_ui/frontend/components/ThemeProvider.tsx index 9d4e8cfcd..85aba2a81 100644 --- a/web_ui/frontend/public/theme.tsx +++ b/web_ui/frontend/components/ThemeProvider.tsx @@ -30,7 +30,7 @@ const poppins = Poppins({ display: 'swap', }); -let theme = createTheme({ +let themeProvider = createTheme({ palette: { primary: { main: '#0885ff', @@ -78,7 +78,7 @@ let theme = createTheme({ }, }); -theme = responsiveFontSizes(theme, { factor: 3 }); +themeProvider = responsiveFontSizes(themeProvider, { factor: 3 }); interface ThemeProviderClientProps { children: React.ReactNode; @@ -87,5 +87,5 @@ interface ThemeProviderClientProps { export const ThemeProviderClient: FC = ({ children, }) => { - return {children}; + return {children}; }; diff --git a/web_ui/frontend/helpers/api.ts b/web_ui/frontend/helpers/api.ts new file mode 100644 index 000000000..c51a6b0b5 --- /dev/null +++ b/web_ui/frontend/helpers/api.ts @@ -0,0 +1,267 @@ +import { secureFetch } from '@/helpers/login'; +import { getErrorMessage } from '@/helpers/util'; +import { Alert, Namespace } from '@/index'; +import { CustomRegistrationFieldProps } from '@/app/registry/components/CustomRegistrationField'; + +// TODO: Decide if we should standardize the output in all of these functions. Should they all be responses? + +/** + * Wraps an api request with error handling for both the request and the response if error + * @param fetchRequest The request to make to the api + * @returns The response from the api + */ +export async function fetchApi( + fetchRequest: () => Promise +): Promise { + try { + const response = await fetchRequest(); + if (!response.ok) { + let alertMessage; + try { + alertMessage = await getErrorMessage(response); + } catch (e) { + if (e instanceof Error) { + alertMessage = e.message; + } + } + throw new Error(alertMessage); + } + return response + } catch (e) { + if (e instanceof Error) { + throw Error("Fetch to API Failed", { cause: e }) + } else { + throw Error("Fetch to API Failed", { cause: e}) + } + } +} + + +/** + * Deletes a namespace + * @param id Namespace ID + */ +export const deleteNamespace = async (id: number) => { + return fetchApi( + async () => await secureFetch(`/api/v1.0/registry_ui/namespaces/${id}`, { + method: 'DELETE', + }) + ) +}; + + +/** + * Approves a namespace + * @param id Namespace ID + */ +export const approveNamespace = async (id: number) => { + return fetchApi( + async () => await secureFetch( + `/api/v1.0/registry_ui/namespaces/${id}/approve`, + { + method: 'PATCH', + } + ) + ) +} + +/** + * Denies a namespace + * @param id Namespace ID + */ +export const denyNamespace = async (id: number) => { + return fetchApi( + async () => await secureFetch( + `/api/v1.0/registry_ui/namespaces/${id}/deny`, + { + method: 'PATCH', + } + ) + ) +} + +/** + * Enables a server on the director + * @param name Server name + */ +export const allowServer = async (name: string) => { + return fetchApi( + async () => await secureFetch( + `/api/v1.0/director_ui/servers/allow/${name}`, + { + method: 'PATCH', + } + ) + ) +} + +/** + * Filters ( Disables ) a server on the director + * @param name Server name + */ +export const filterServer = async (name: string) => { + return fetchApi( + async () => await secureFetch( + `/api/v1.0/director_ui/servers/filter/${name}`, + { + method: 'PATCH', + } + ) + ) +} + +/** + * Get extended namespaces + */ +export const getExtendedNamespaces = async (): Promise<{ namespace: Namespace }[]> => { + const response = await getNamespaces() + const data: Namespace[] = await response.json(); + data.sort((a, b) => (a.id > b.id ? 1 : -1)); + data.forEach((namespace) => { + if (namespace.prefix.startsWith('/caches/')) { + namespace.type = 'cache'; + namespace.prefix = namespace.prefix.replace('/caches/', ''); + } else if (namespace.prefix.startsWith('/origins/')) { + namespace.type = 'origin'; + namespace.prefix = namespace.prefix.replace('/origins/', ''); + } else { + namespace.type = 'namespace'; + } + }); + + // TODO: This extra should be done somewhere else, why is it done? + return data.map((d) => { + return { namespace: d }; + }); +} + +/** + * Get namespaces + */ +export const getNamespaces = async (): Promise => { + const url = new URL( + '/api/v1.0/registry_ui/namespaces', + window.location.origin + ); + + const response = await fetchApi(async () => await fetch(url)); + return await response.json(); +} + +/** + * Gets a namespace by ID + * @param id Namespace ID + */ +export const getNamespace = async ( + id: string | number +): Promise => { + const url = new URL( + `/api/v1.0/registry_ui/namespaces/${id}`, + window.location.origin + ); + const response = await fetchApi(async () => await fetch(url)); + return await response.json(); +}; + +export const postGeneralNamespace = async ( + data: Namespace +): Promise => { + return await fetchApi( + async () => + await fetch('/api/v1.0/registry_ui/namespaces', { + body: JSON.stringify(data), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }) + ) +}; + +export const putGeneralNamespace = async ( + data: Namespace +): Promise => { + return await fetchApi( + async () => { + return fetch(`/api/v1.0/registry_ui/namespaces/${data.id}`, + { + body: JSON.stringify(data), + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + } + ) + } + ) +}; + +/** + * Get registration fields from options for namespace + * // TODO: Complain about the misuse of options + */ +export const optionsNamespaceRegistrationFields = async (): Promise< + Omit[] +> => { + const response = await fetchApi(async () => await fetch('/api/v1.0/registry_ui/namespaces', { + method: 'OPTIONS', + })) + return response.json() +}; + +/** + * Initializes a login via terminal code + */ +export const initLogin = async (code: string): Promise => { + return await fetchApi( + async () => + await fetch('/api/v1.0/auth/initLogin', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + code: code, + }), + }) + ) +} + +/** + * Reset ( Do initial ) Login + */ +export const resetLogin = async (password: string): Promise => { + return await fetchApi( + async () => + await fetch('/api/v1.0/auth/resetLogin', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + password: password, + }), + }) + ) +} + +/** + * Login + */ +export const login = async (password: string, user: string = "admin"): Promise => { + return await fetchApi( + async () => + await fetch('/api/v1.0/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + user: user, + password: password, + }), + }) + ) +} diff --git a/web_ui/frontend/helpers/login.tsx b/web_ui/frontend/helpers/login.ts similarity index 100% rename from web_ui/frontend/helpers/login.tsx rename to web_ui/frontend/helpers/login.ts diff --git a/web_ui/frontend/helpers/util.tsx b/web_ui/frontend/helpers/util.ts similarity index 50% rename from web_ui/frontend/helpers/util.tsx rename to web_ui/frontend/helpers/util.ts index e1c1e6be1..42bb56cba 100644 --- a/web_ui/frontend/helpers/util.tsx +++ b/web_ui/frontend/helpers/util.ts @@ -1,4 +1,6 @@ import { ServerType } from '@/index'; +import { Dispatch } from 'react'; +import { AlertReducerAction } from '@/components/AlertProvider'; const stringToTime = (time: string) => { return new Date(Date.parse(time)).toLocaleString(); @@ -36,6 +38,11 @@ export const getOauthEnabledServers = async () => { } }; +/** + * Extract the value from a object via a list of keys + * @param obj + * @param keys + */ export function getObjectValue(obj: any, keys: string[]): T | undefined { const currentValue = obj?.[keys[0]]; if (keys.length == 1) { @@ -44,21 +51,28 @@ export function getObjectValue(obj: any, keys: string[]): T | undefined { return getObjectValue(currentValue, keys.slice(1)); } +/** + * Get the error message from a response + * @param response + */ export const getErrorMessage = async (response: Response): Promise => { - let message; try { let data = await response.json(); - message = response.status + ': ' + data['msg']; + return response.status + ': ' + data['msg']; } catch (e) { - message = response.status + ': ' + response.statusText; + return response.status + ': ' + response.statusText; } - return message; }; export type TypeFunction = (x?: F) => T; export type TypeOrTypeFunction = T | TypeFunction; +/** + * Evaluate a function or return a value + * @param o Function or value + * @param functionProps Function properties + */ export function evaluateOrReturn( o: TypeOrTypeFunction, functionProps?: F @@ -70,6 +84,58 @@ export function evaluateOrReturn( return o as T; } + +/** + * Get the average of an array of numbers + * @param arr Array of numbers + */ export const average = (arr: number[]) => { return arr.reduce((a, b) => a + b, 0) / arr.length; }; + +type ErrorWithCause = Error & { cause?: Error }; + + +/** + * If an error is caught from f then display the error via an alert UI + */ +export async function alertOnError( + f: () => Promise | T | undefined, + title: string = 'Error', + dispatch: Dispatch +){ + try { + return await f(); + } catch (error) { + console.error(error); + if(error instanceof Error) { + dispatch({ + type: "openErrorAlert", + payload: { + title, + error: errorToString(error as ErrorWithCause), + onClose: () => dispatch({ type: "closeAlert" }) + } + }) + } + } +} + +/** + * Convert a error into a string + * @param error + */ +export const errorToString = (error: ErrorWithCause) : string => { + + if(error?.cause){ + + // Check that error is instance of Error + if(!(error?.cause instanceof Error)) { + console.error("Malformed error, cause is not an instance of Error", error) + } + + return `${error.message}\n↳ ${errorToString(error.cause as ErrorWithCause)}` + } + + return `${error.message}` +} From 85066067c290ee59789dd92d2c85718b0360ca0a Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Mon, 28 Oct 2024 08:44:16 -0500 Subject: [PATCH 03/16] Uniform Error Handling --- web_ui/frontend/app/(login)/login/page.tsx | 47 +++++++++++++--------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/web_ui/frontend/app/(login)/login/page.tsx b/web_ui/frontend/app/(login)/login/page.tsx index c0a1a412a..4642364b0 100644 --- a/web_ui/frontend/app/(login)/login/page.tsx +++ b/web_ui/frontend/app/(login)/login/page.tsx @@ -52,16 +52,23 @@ const AdminLogin = () => { let [password, setPassword] = useState(''); let [loading, setLoading] = useState(false); - let [error, setError] = useState(undefined); const [toggled, setToggled] = useState(false); - const { data: enabledServers } = useSWR( + const { data: enabledServers } = useSWR( 'getEnabledServers', - getEnabledServers + async () => await alertOnError( + getEnabledServers, + "Could not get enabled servers", + dispatch + ) ); - const { data: oauthServers } = useSWR( + const { data: oauthServers } = useSWR( 'getOauthEnabledServers', - getOauthEnabledServers, + async () => await alertOnError( + getOauthEnabledServers, + "Could not get oauth enabled servers", + dispatch + ), { fallbackData: [] } ); @@ -108,23 +115,12 @@ const AdminLogin = () => { sx: { width: '50%' }, onChange: (e) => { setPassword(e.target.value); - setError(undefined); }, }, }} /> - - - {error} - - { }; export default function Home() { + + const dispatch = useContext(AlertDispatchContext); + const [returnUrl, setReturnUrl] = useState(undefined); - const { data: enabledServers } = useSWR( + const { data: enabledServers } = useSWR( 'getEnabledServers', - getEnabledServers + async () => await alertOnError( + getEnabledServers, + "Could not get enabled servers", + dispatch + ) ); - const { data: oauthServers } = useSWR( + const { data: oauthServers } = useSWR( 'getOauthEnabledServers', - getOauthEnabledServers, + async () => await alertOnError( + getOauthEnabledServers, + "Could not determine if the active server had OAuth enabled", + dispatch + ), { fallbackData: [] } ); From 56c0958a24e6dc790e11557373ab5460ad371d53 Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Mon, 28 Oct 2024 09:14:03 -0500 Subject: [PATCH 04/16] Uniform Error Handling --- web_ui/frontend/app/config/Config.tsx | 33 +++++------ .../components/FederationOverview.tsx | 56 +++++++++---------- web_ui/frontend/helpers/api.ts | 10 ++++ web_ui/frontend/helpers/get.ts | 3 +- 4 files changed, 56 insertions(+), 46 deletions(-) diff --git a/web_ui/frontend/app/config/Config.tsx b/web_ui/frontend/app/config/Config.tsx index 45039f54f..97a337b25 100644 --- a/web_ui/frontend/app/config/Config.tsx +++ b/web_ui/frontend/app/config/Config.tsx @@ -27,7 +27,7 @@ import { IconButton, Alert, } from '@mui/material'; -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { AppRegistration, AssistantDirection, @@ -51,21 +51,30 @@ import StatusSnackBar, { StatusSnackBarProps, } from '@/components/StatusSnackBar'; import { ServerType } from '@/index'; -import { getEnabledServers } from '@/helpers/util'; +import { alertOnError, getEnabledServers } from '@/helpers/util'; import DownloadButton from '@/components/DownloadButton'; import { PaddedContent } from '@/components/layout'; import { ConfigDisplay, TableOfContents } from '@/app/config/components'; import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; +import { getConfig } from '@/helpers/api'; +import { AlertDispatchContext } from '@/components/AlertProvider'; function Config({ metadata }: { metadata: ParameterMetadataRecord }) { + + const dispatch = useContext(AlertDispatchContext); + const [status, setStatus] = useState( undefined ); const [patch, _setPatch] = useState({}); - const { data, mutate, error } = useSWR( + const { data, mutate, error } = useSWR( 'getConfig', - getConfig + async () => await alertOnError( + getConfigJson, + "Could not get config", + dispatch + ) ); const { data: enabledServers } = useSWR( 'getEnabledServers', @@ -94,8 +103,6 @@ function Config({ metadata }: { metadata: ParameterMetadataRecord }) { ); }, [serverConfig, patch]); - console.error(error, data); - return ( <> @@ -215,17 +222,11 @@ function Config({ metadata }: { metadata: ParameterMetadataRecord }) { ); } -const getConfig = async (): Promise => { - let response = await fetch('/api/v1.0/config'); - - if (!response.ok) { - if (response.status == 401) { - throw new Error('You must be logged in to view and access the config'); - } - throw new Error('Failed to fetch config'); +const getConfigJson = async (): Promise => { + const response = await getConfig() + if(response){ + return await response.json(); } - - return await response.json(); }; export default Config; diff --git a/web_ui/frontend/components/FederationOverview.tsx b/web_ui/frontend/components/FederationOverview.tsx index 40895926d..a47f1d1a5 100644 --- a/web_ui/frontend/components/FederationOverview.tsx +++ b/web_ui/frontend/components/FederationOverview.tsx @@ -7,6 +7,7 @@ import { Box, Typography } from '@mui/material'; import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; import Link from 'next/link'; import { getErrorMessage, getObjectValue } from '@/helpers/util'; +import { getConfig } from '@/helpers/api'; const LinkBox = ({ href, text }: { href: string; text: string }) => { return ( @@ -30,15 +31,15 @@ const LinkBox = ({ href, text }: { href: string; text: string }) => { }; const UrlData = [ - { key: ['Federation', 'NamespaceUrl', 'Value'], text: 'Namespace Registry' }, - { key: ['Federation', 'DirectorUrl', 'Value'], text: 'Director' }, - { key: ['Federation', 'RegistryUrl', 'Value'], text: 'Registry' }, + { key: ['Federation', 'NamespaceUrl'], text: 'Namespace Registry' }, + { key: ['Federation', 'DirectorUrl'], text: 'Director' }, + { key: ['Federation', 'RegistryUrl'], text: 'Registry' }, { - key: ['Federation', 'TopologyNamespaceUrl', 'Value'], + key: ['Federation', 'TopologyNamespaceUrl'], text: 'Topology Namespace', }, - { key: ['Federation', 'DiscoveryUrl', 'Value'], text: 'Discovery' }, - { key: ['Federation', 'JwkUrl', 'Value'], text: 'JWK' }, + { key: ['Federation', 'DiscoveryUrl'], text: 'Discovery' }, + { key: ['Federation', 'JwkUrl'], text: 'JWK' }, ]; const FederationOverview = () => { @@ -46,35 +47,32 @@ const FederationOverview = () => { { text: string; url: string | undefined }[] >([]); - let getConfig = async () => { - let response = await fetch('/api/v1.0/config'); - if (response.ok) { - const responseData = (await response.json()) as Config; + let getConfigJson = async () => { + const response = await getConfig() + const responseData = (await response.json()) as Config; - const federationUrls = UrlData.map(({ key, text }) => { - let url = getObjectValue(responseData, key); - if ( - url && - !url?.startsWith('http://') && - !url?.startsWith('https://') - ) { - url = 'https://' + url; - } + const federationUrls = UrlData.map(({ key, text }) => { + let url = getObjectValue(responseData, key); + if ( + url && + !url?.startsWith('http://') && + !url?.startsWith('https://') + ) { + url = 'https://' + url; + } + + return { + text, + url, + }; + }); - return { - text, - url, - }; - }); + setConfig(federationUrls); - setConfig(federationUrls); - } else { - console.error(await getErrorMessage(response)); - } }; useEffect(() => { - getConfig(); + getConfigJson(); }, []); if (config === undefined) { diff --git a/web_ui/frontend/helpers/api.ts b/web_ui/frontend/helpers/api.ts index c51a6b0b5..dbbfe8b9e 100644 --- a/web_ui/frontend/helpers/api.ts +++ b/web_ui/frontend/helpers/api.ts @@ -37,6 +37,16 @@ export async function fetchApi( } +/** + * Get config + */ +export const getConfig = async (): Promise => { + return fetchApi( + async () => await secureFetch('/api/v1.0/config') + ) +} + + /** * Deletes a namespace * @param id Namespace ID diff --git a/web_ui/frontend/helpers/get.ts b/web_ui/frontend/helpers/get.ts index ca2be9954..e9251c31a 100644 --- a/web_ui/frontend/helpers/get.ts +++ b/web_ui/frontend/helpers/get.ts @@ -1,8 +1,9 @@ import { Config, ParameterValueRecord } from '@/components/configuration'; +import { getConfig as getConfigResponse } from '@/helpers/api' import { flattenObject } from '@/app/config/util'; export const getConfig = async (): Promise => { - let response = await fetch('/api/v1.0/config'); + let response = await getConfigResponse(); let data = await response.json(); let flatData = flattenObject(data); return flatData; From 81b6d785d270cf482c83f1ccdd13a8a4d9c1cbb4 Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Tue, 29 Oct 2024 08:55:59 -0500 Subject: [PATCH 05/16] Prettier Format --- .../app/(login)/initialization/code/page.tsx | 7 +- .../(login)/initialization/password/page.tsx | 16 +- web_ui/frontend/app/(login)/login/page.tsx | 50 +++--- web_ui/frontend/app/config/Config.tsx | 21 ++- .../app/director/components/DirectorCard.tsx | 2 +- .../director/components/DirectorDropdown.tsx | 5 +- web_ui/frontend/app/director/page.tsx | 13 +- .../frontend/app/registry/cache/edit/page.tsx | 4 +- .../app/registry/cache/register/page.tsx | 5 +- .../CustomRegistrationField/PubkeyField.tsx | 4 +- .../CustomRegistrationField/index.tsx | 1 - .../frontend/app/registry/components/Form.tsx | 24 ++- .../app/registry/components/PostPage.tsx | 12 +- .../app/registry/components/PutPage.tsx | 63 +++---- .../frontend/app/registry/components/util.tsx | 5 +- web_ui/frontend/app/registry/denied/page.tsx | 35 +--- .../app/registry/origin/edit/page.tsx | 4 +- .../app/registry/origin/register/page.tsx | 4 +- web_ui/frontend/app/registry/page.tsx | 18 +- web_ui/frontend/app/test/page.tsx | 10 +- web_ui/frontend/components/AlertPortal.tsx | 13 +- web_ui/frontend/components/AlertProvider.tsx | 38 +++-- web_ui/frontend/components/CodeBlock.tsx | 28 ++-- .../components/FederationOverview.tsx | 9 +- web_ui/frontend/components/Namespace/Card.tsx | 2 +- .../components/Namespace/DeniedCard.tsx | 6 +- .../Namespace/InformationDropdown.tsx | 6 +- .../components/Namespace/PendingCard.tsx | 14 +- .../frontend/components/Namespace/index.tsx | 1 - web_ui/frontend/helpers/api.ts | 154 +++++++++--------- web_ui/frontend/helpers/get.ts | 4 +- web_ui/frontend/helpers/util.ts | 35 ++-- 32 files changed, 301 insertions(+), 312 deletions(-) diff --git a/web_ui/frontend/app/(login)/initialization/code/page.tsx b/web_ui/frontend/app/(login)/initialization/code/page.tsx index 18791c9b9..904446ada 100644 --- a/web_ui/frontend/app/(login)/initialization/code/page.tsx +++ b/web_ui/frontend/app/(login)/initialization/code/page.tsx @@ -28,7 +28,6 @@ import { initLogin } from '@/helpers/api'; import { alertOnError } from '@/helpers/util'; import { AlertDispatchContext } from '@/components/AlertProvider'; - export default function Home() { const router = useRouter(); let [code, _setCode] = useState([ @@ -41,7 +40,7 @@ export default function Home() { ]); let [loading, setLoading] = useState(false); - const dispatch = useContext(AlertDispatchContext) + const dispatch = useContext(AlertDispatchContext); const setCode = (code: Code) => { _setCode(code); @@ -58,8 +57,8 @@ export default function Home() { async () => await initLogin(code), 'Could not login', dispatch - ) - if(response) { + ); + if (response) { router.push('../password/'); } else { setLoading(false); diff --git a/web_ui/frontend/app/(login)/initialization/password/page.tsx b/web_ui/frontend/app/(login)/initialization/password/page.tsx index ce4fe85b9..a35e18c3b 100644 --- a/web_ui/frontend/app/(login)/initialization/password/page.tsx +++ b/web_ui/frontend/app/(login)/initialization/password/page.tsx @@ -30,15 +30,13 @@ import { AlertDispatchContext } from '@/components/AlertProvider'; import { initLogin, resetLogin } from '@/helpers/api'; export default function Home() { - - const dispatch = useContext(AlertDispatchContext) + const dispatch = useContext(AlertDispatchContext); const router = useRouter(); let [password, _setPassword] = useState(''); let [confirmPassword, _setConfirmPassword] = useState(''); let [loading, setLoading] = useState(false); - async function submit(password: string) { setLoading(true); @@ -46,8 +44,8 @@ export default function Home() { async () => await resetLogin(password), 'Could not login', dispatch - ) - if(response) { + ); + if (response) { router.push('../password/'); } else { setLoading(false); @@ -64,12 +62,12 @@ export default function Home() { type: 'openAlert', payload: { alertProps: { - severity: 'warning' + severity: 'warning', }, message: 'Passwords do not match', - onClose: () => dispatch({ type: 'closeAlert' }) - } - }) + onClose: () => dispatch({ type: 'closeAlert' }), + }, + }); } } diff --git a/web_ui/frontend/app/(login)/login/page.tsx b/web_ui/frontend/app/(login)/login/page.tsx index 4642364b0..a597ca523 100644 --- a/web_ui/frontend/app/(login)/login/page.tsx +++ b/web_ui/frontend/app/(login)/login/page.tsx @@ -44,7 +44,6 @@ import { login } from '@/helpers/api'; import { AlertDispatchContext } from '@/components/AlertProvider'; const AdminLogin = () => { - const dispatch = useContext(AlertDispatchContext); const router = useRouter(); @@ -56,19 +55,21 @@ const AdminLogin = () => { const { data: enabledServers } = useSWR( 'getEnabledServers', - async () => await alertOnError( - getEnabledServers, - "Could not get enabled servers", - dispatch - ) + async () => + await alertOnError( + getEnabledServers, + 'Could not get enabled servers', + dispatch + ) ); const { data: oauthServers } = useSWR( 'getOauthEnabledServers', - async () => await alertOnError( - getOauthEnabledServers, - "Could not get oauth enabled servers", - dispatch - ), + async () => + await alertOnError( + getOauthEnabledServers, + 'Could not get oauth enabled servers', + dispatch + ), { fallbackData: [] } ); @@ -83,9 +84,9 @@ const AdminLogin = () => { const response = await alertOnError( async () => await login(password), - "Could not login", + 'Could not login', dispatch - ) + ); if (response) { await mutate(getUser); @@ -160,25 +161,26 @@ const AdminLogin = () => { }; export default function Home() { - const dispatch = useContext(AlertDispatchContext); const [returnUrl, setReturnUrl] = useState(undefined); const { data: enabledServers } = useSWR( 'getEnabledServers', - async () => await alertOnError( - getEnabledServers, - "Could not get enabled servers", - dispatch - ) + async () => + await alertOnError( + getEnabledServers, + 'Could not get enabled servers', + dispatch + ) ); const { data: oauthServers } = useSWR( 'getOauthEnabledServers', - async () => await alertOnError( - getOauthEnabledServers, - "Could not determine if the active server had OAuth enabled", - dispatch - ), + async () => + await alertOnError( + getOauthEnabledServers, + 'Could not determine if the active server had OAuth enabled', + dispatch + ), { fallbackData: [] } ); diff --git a/web_ui/frontend/app/config/Config.tsx b/web_ui/frontend/app/config/Config.tsx index 97a337b25..bfd42fbde 100644 --- a/web_ui/frontend/app/config/Config.tsx +++ b/web_ui/frontend/app/config/Config.tsx @@ -27,7 +27,14 @@ import { IconButton, Alert, } from '@mui/material'; -import React, { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import React, { + memo, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; import { AppRegistration, AssistantDirection, @@ -60,7 +67,6 @@ import { getConfig } from '@/helpers/api'; import { AlertDispatchContext } from '@/components/AlertProvider'; function Config({ metadata }: { metadata: ParameterMetadataRecord }) { - const dispatch = useContext(AlertDispatchContext); const [status, setStatus] = useState( @@ -70,11 +76,8 @@ function Config({ metadata }: { metadata: ParameterMetadataRecord }) { const { data, mutate, error } = useSWR( 'getConfig', - async () => await alertOnError( - getConfigJson, - "Could not get config", - dispatch - ) + async () => + await alertOnError(getConfigJson, 'Could not get config', dispatch) ); const { data: enabledServers } = useSWR( 'getEnabledServers', @@ -223,8 +226,8 @@ function Config({ metadata }: { metadata: ParameterMetadataRecord }) { } const getConfigJson = async (): Promise => { - const response = await getConfig() - if(response){ + const response = await getConfig(); + if (response) { return await response.json(); } }; diff --git a/web_ui/frontend/app/director/components/DirectorCard.tsx b/web_ui/frontend/app/director/components/DirectorCard.tsx index 305ee0950..07b496bee 100644 --- a/web_ui/frontend/app/director/components/DirectorCard.tsx +++ b/web_ui/frontend/app/director/components/DirectorCard.tsx @@ -96,7 +96,7 @@ export const DirectorCard = ({ server, authenticated }: DirectorCardProps) => { }, 'Failed to toggle server status', dispatch - ) + ); mutate(); diff --git a/web_ui/frontend/app/director/components/DirectorDropdown.tsx b/web_ui/frontend/app/director/components/DirectorDropdown.tsx index 60d1c433d..6a20d97b3 100644 --- a/web_ui/frontend/app/director/components/DirectorDropdown.tsx +++ b/web_ui/frontend/app/director/components/DirectorDropdown.tsx @@ -107,7 +107,10 @@ const directoryListToTreeHelper = ( tree[path[0]] = {}; } - tree[path[0]] = directoryListToTreeHelper(path.slice(1), tree[path[0]] as StringTree); + tree[path[0]] = directoryListToTreeHelper( + path.slice(1), + tree[path[0]] as StringTree + ); return tree; }; diff --git a/web_ui/frontend/app/director/page.tsx b/web_ui/frontend/app/director/page.tsx index ef7bfde10..a07fb437c 100644 --- a/web_ui/frontend/app/director/page.tsx +++ b/web_ui/frontend/app/director/page.tsx @@ -22,9 +22,7 @@ import { Box, Grid, Skeleton, Typography } from '@mui/material'; import { useContext, useMemo } from 'react'; import useSWR from 'swr'; import { Server } from '@/index'; -import { - DirectorCardList, -} from './components'; +import { DirectorCardList } from './components'; import { getUser } from '@/helpers/login'; import FederationOverview from '@/components/FederationOverview'; import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; @@ -34,17 +32,16 @@ import { alertOnError } from '@/helpers/util'; import { AlertDispatchContext } from '@/components/AlertProvider'; export default function Page() { - const dispatch = useContext(AlertDispatchContext); const { data } = useSWR( 'getServers', - async () => await alertOnError(getServers, 'Failed to fetch servers', dispatch) + async () => + await alertOnError(getServers, 'Failed to fetch servers', dispatch) ); - const { data: user, error } = useSWR( - 'getUser', - () => alertOnError(getUser, "Failed to fetch user", dispatch) + const { data: user, error } = useSWR('getUser', () => + alertOnError(getUser, 'Failed to fetch user', dispatch) ); const cacheData = useMemo(() => { diff --git a/web_ui/frontend/app/registry/cache/edit/page.tsx b/web_ui/frontend/app/registry/cache/edit/page.tsx index 6c06ab12b..433d81f16 100644 --- a/web_ui/frontend/app/registry/cache/edit/page.tsx +++ b/web_ui/frontend/app/registry/cache/edit/page.tsx @@ -19,9 +19,7 @@ 'use client'; import { PutPage } from '@/app/registry/components/PutPage'; -import { - namespaceToCache, -} from '@/app/registry/components/util'; +import { namespaceToCache } from '@/app/registry/components/util'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; diff --git a/web_ui/frontend/app/registry/cache/register/page.tsx b/web_ui/frontend/app/registry/cache/register/page.tsx index 70386a7db..61d885670 100644 --- a/web_ui/frontend/app/registry/cache/register/page.tsx +++ b/web_ui/frontend/app/registry/cache/register/page.tsx @@ -18,15 +18,12 @@ 'use client'; -import { - namespaceToCache -} from '@/app/registry/components/util'; +import { namespaceToCache } from '@/app/registry/components/util'; import { PostPage } from '@/app/registry/components/PostPage'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; import { postGeneralNamespace } from '@/helpers/api'; - export default function Page() { const postCache = async (data: any) => { const cache = namespaceToCache(structuredClone(data)); diff --git a/web_ui/frontend/app/registry/components/CustomRegistrationField/PubkeyField.tsx b/web_ui/frontend/app/registry/components/CustomRegistrationField/PubkeyField.tsx index 6d7d5135b..a69e3f107 100644 --- a/web_ui/frontend/app/registry/components/CustomRegistrationField/PubkeyField.tsx +++ b/web_ui/frontend/app/registry/components/CustomRegistrationField/PubkeyField.tsx @@ -25,7 +25,9 @@ const pubkeyValidator = (value: string) => { } }; -const PubkeyField = ({ ...props }: BaseCustomRegistrationFieldProps) => { +const PubkeyField = ({ + ...props +}: BaseCustomRegistrationFieldProps) => { return ( { - const dispatch = useContext(AlertDispatchContext); const [data, setData] = useState | undefined>( namespace || {} ); - const { data: fields, error } = useSWR[] | undefined>( + const { data: fields, error } = useSWR< + Omit[] | undefined + >( 'optionsNamespaceRegistrationFields', - async () => await alertOnError( - optionsNamespaceRegistrationFields, - "Couldn't fetch registration fields", - dispatch - ), + async () => + await alertOnError( + optionsNamespaceRegistrationFields, + "Couldn't fetch registration fields", + dispatch + ), { fallbackData: [] } ); diff --git a/web_ui/frontend/app/registry/components/PostPage.tsx b/web_ui/frontend/app/registry/components/PostPage.tsx index 91ecf2478..3e22a1631 100644 --- a/web_ui/frontend/app/registry/components/PostPage.tsx +++ b/web_ui/frontend/app/registry/components/PostPage.tsx @@ -28,7 +28,6 @@ import { alertOnError } from '@/helpers/util'; import { AlertDispatchContext } from '@/components/AlertProvider'; const PostPage = ({ update }: NamespaceFormPage) => { - const dispatch = useContext(AlertDispatchContext); const [fromUrl, setFromUrl] = useState(undefined); @@ -41,14 +40,14 @@ const PostPage = ({ update }: NamespaceFormPage) => { if (fromUrl != undefined) { const parsedUrl = await alertOnError( () => new URL(fromUrl), - "Failed to parse URL", + 'Failed to parse URL', dispatch ); if (parsedUrl) { setFromUrl(parsedUrl); } } - })() + })(); }, []); return ( @@ -58,10 +57,11 @@ const PostPage = ({ update }: NamespaceFormPage) => { { await alertOnError( - async () => await submitNamespaceForm(namespace, fromUrl, update), - "Failed to update namespace", + async () => + await submitNamespaceForm(namespace, fromUrl, update), + 'Failed to update namespace', dispatch - ) + ); }} />
diff --git a/web_ui/frontend/app/registry/components/PutPage.tsx b/web_ui/frontend/app/registry/components/PutPage.tsx index 4b3a8c820..1fe498263 100644 --- a/web_ui/frontend/app/registry/components/PutPage.tsx +++ b/web_ui/frontend/app/registry/components/PutPage.tsx @@ -28,7 +28,8 @@ import { } from '@mui/material'; import React, { ReactNode, - Suspense, useContext, + Suspense, + useContext, useEffect, useMemo, useState, @@ -37,11 +38,9 @@ import React, { import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; import { Namespace, Alert as AlertType } from '@/index'; import Form from '@/app/registry/components/Form'; -import { - submitNamespaceForm, -} from '@/app/registry/components/util'; +import { submitNamespaceForm } from '@/app/registry/components/util'; import { getNamespace } from '@/helpers/api'; -import { NamespaceFormPage } from '@/app/registry/components' +import { NamespaceFormPage } from '@/app/registry/components'; import { AlertDispatchContext } from '@/components/AlertProvider'; import { alertOnError } from '@/helpers/util'; @@ -59,13 +58,14 @@ const PutPage = ({ update }: NamespaceFormPage) => { if (id === null) { dispatch({ - type: "openAlert", + type: 'openAlert', payload: { - title: "No Namespace ID Provided", - message: "Your URL should contain a query parameter 'id' with the ID of the namespace you want to edit", - onClose: () => dispatch({ type: "closeAlert" }) - } - }) + title: 'No Namespace ID Provided', + message: + "Your URL should contain a query parameter 'id' with the ID of the namespace you want to edit", + onClose: () => dispatch({ type: 'closeAlert' }), + }, + }); return; } @@ -76,32 +76,34 @@ const PutPage = ({ update }: NamespaceFormPage) => { } } catch (e) { dispatch({ - type: "openAlert", + type: 'openAlert', payload: { - title: "Invalid fromUrl provided", - message: "The `fromUrl` parameter provided is not a valid URL, this will only impact your redirection on completion of this form", + title: 'Invalid fromUrl provided', + message: + 'The `fromUrl` parameter provided is not a valid URL, this will only impact your redirection on completion of this form', alertProps: { - severity: "warning" + severity: 'warning', }, - onClose: () => dispatch({ type: "closeAlert" }) - } - }) + onClose: () => dispatch({ type: 'closeAlert' }), + }, + }); } try { setId(parseInt(id)); } catch (e) { dispatch({ - type: "openAlert", + type: 'openAlert', payload: { - title: "Invalid Namespace ID provided", - message: "The Namespace Id provided is not a valid number. Please report this issue, as well as what link directed you here.", + title: 'Invalid Namespace ID provided', + message: + 'The Namespace Id provided is not a valid number. Please report this issue, as well as what link directed you here.', alertProps: { - severity: "error" + severity: 'error', }, - onClose: () => dispatch({ type: "closeAlert" }) - } - }) + onClose: () => dispatch({ type: 'closeAlert' }), + }, + }); } }, []); @@ -112,8 +114,8 @@ const PutPage = ({ update }: NamespaceFormPage) => { async () => await getNamespace(id), "Couldn't get namespace", dispatch - ) - if(response){ + ); + if (response) { setNamespace(response); } } @@ -130,10 +132,11 @@ const PutPage = ({ update }: NamespaceFormPage) => { onSubmit={async (data) => { let namespace = { ...data, id: id }; await alertOnError( - async () => await submitNamespaceForm(namespace, fromUrl, update), - "Failed to update namespace", + async () => + await submitNamespaceForm(namespace, fromUrl, update), + 'Failed to update namespace', dispatch - ) + ); }} /> ) : ( diff --git a/web_ui/frontend/app/registry/components/util.tsx b/web_ui/frontend/app/registry/components/util.tsx index e6c595942..6ddbc0cbf 100644 --- a/web_ui/frontend/app/registry/components/util.tsx +++ b/web_ui/frontend/app/registry/components/util.tsx @@ -31,7 +31,10 @@ export const calculateKeys = (key: string) => { * @param o Object to get the value from * @param key List of keys to traverse */ -export const getValue = (o: Record | undefined, key: string[]): any => { +export const getValue = ( + o: Record | undefined, + key: string[] +): any => { if (o === undefined) { return undefined; } diff --git a/web_ui/frontend/app/registry/denied/page.tsx b/web_ui/frontend/app/registry/denied/page.tsx index 00c792651..7d1775521 100644 --- a/web_ui/frontend/app/registry/denied/page.tsx +++ b/web_ui/frontend/app/registry/denied/page.tsx @@ -18,18 +18,10 @@ 'use client'; -import { - Box, - Grid, - Typography, - Alert, - Collapse -} from '@mui/material'; +import { Box, Grid, Typography, Alert, Collapse } from '@mui/material'; import React, { useContext, useMemo } from 'react'; -import { - CardSkeleton -} from '@/components/Namespace'; +import { CardSkeleton } from '@/components/Namespace'; import { getUser } from '@/helpers/login'; import NamespaceCardList from '@/components/Namespace/NamespaceCardList'; import useSWR from 'swr'; @@ -41,24 +33,13 @@ import { AlertDispatchContext } from '@/components/AlertProvider'; import { alertOnError } from '@/helpers/util'; export default function Home() { + const dispatch = useContext(AlertDispatchContext); - const dispatch = useContext(AlertDispatchContext) - - const { data } = useSWR( - 'getNamespaces', - async () => alertOnError( - getExtendedNamespaces, - "Couldn't fetch namespaces", - dispatch - ) - ) - const { data: user, error } = useSWR( - 'getUser', - async () => alertOnError( - getUser, - "Couldn't fetch user", - dispatch - ) + const { data } = useSWR('getNamespaces', async () => + alertOnError(getExtendedNamespaces, "Couldn't fetch namespaces", dispatch) + ); + const { data: user, error } = useSWR('getUser', async () => + alertOnError(getUser, "Couldn't fetch user", dispatch) ); const deniedNamespaces = useMemo( diff --git a/web_ui/frontend/app/registry/origin/edit/page.tsx b/web_ui/frontend/app/registry/origin/edit/page.tsx index 25629eb54..4fe766aff 100644 --- a/web_ui/frontend/app/registry/origin/edit/page.tsx +++ b/web_ui/frontend/app/registry/origin/edit/page.tsx @@ -19,9 +19,7 @@ 'use client'; import { PutPage } from '@/app/registry/components/PutPage'; -import { - namespaceToOrigin -} from '@/app/registry/components/util'; +import { namespaceToOrigin } from '@/app/registry/components/util'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; import { putGeneralNamespace } from '@/helpers/api'; diff --git a/web_ui/frontend/app/registry/origin/register/page.tsx b/web_ui/frontend/app/registry/origin/register/page.tsx index 59e52d4a7..f52464fef 100644 --- a/web_ui/frontend/app/registry/origin/register/page.tsx +++ b/web_ui/frontend/app/registry/origin/register/page.tsx @@ -18,9 +18,7 @@ 'use client'; -import { - namespaceToOrigin -} from '@/app/registry/components/util'; +import { namespaceToOrigin } from '@/app/registry/components/util'; import { PostPage } from '@/app/registry/components/PostPage'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; diff --git a/web_ui/frontend/app/registry/page.tsx b/web_ui/frontend/app/registry/page.tsx index 746a45d6c..ab5204b69 100644 --- a/web_ui/frontend/app/registry/page.tsx +++ b/web_ui/frontend/app/registry/page.tsx @@ -49,16 +49,18 @@ import { alertOnError } from '@/helpers/util'; import { getExtendedNamespaces } from '@/helpers/api'; export default function Home() { - const dispatch = useContext(AlertDispatchContext); - const { data, mutate: mutateNamespaces } = useSWR<{ namespace: Namespace }[] | undefined>( + const { data, mutate: mutateNamespaces } = useSWR< + { namespace: Namespace }[] | undefined + >( 'getNamespaces', - () => alertOnError( - getExtendedNamespaces, - 'Failed to fetch namespaces', - dispatch - ), + () => + alertOnError( + getExtendedNamespaces, + 'Failed to fetch namespaces', + dispatch + ), { fallbackData: [], } @@ -66,7 +68,7 @@ export default function Home() { const { data: user, error } = useSWR( 'getUser', - async () => await alertOnError(getUser, "Error Getting User", dispatch) + async () => await alertOnError(getUser, 'Error Getting User', dispatch) ); const pendingData = useMemo(() => { diff --git a/web_ui/frontend/app/test/page.tsx b/web_ui/frontend/app/test/page.tsx index 96176f517..5f6e11e7e 100644 --- a/web_ui/frontend/app/test/page.tsx +++ b/web_ui/frontend/app/test/page.tsx @@ -5,7 +5,6 @@ import React, { useContext } from 'react'; import { Box } from '@mui/material'; import CodeBlock from '@/components/CodeBlock'; - const Page = () => { const dispatch = useContext(AlertDispatchContext); @@ -16,9 +15,9 @@ const Page = () => { dispatch({ type: 'openAlert', payload: { - title: "Response Time Slow", + title: 'Response Time Slow', alertProps: { - severity: 'error' + severity: 'error', }, message: ( @@ -34,8 +33,7 @@ const Page = () => { ), onClose: () => dispatch({ type: 'closeAlert' }), - - } + }, }); }} > @@ -50,6 +48,6 @@ const Page = () => { ); -} +}; export default Page; diff --git a/web_ui/frontend/components/AlertPortal.tsx b/web_ui/frontend/components/AlertPortal.tsx index 4c482e3a4..4c75a0574 100644 --- a/web_ui/frontend/components/AlertPortal.tsx +++ b/web_ui/frontend/components/AlertPortal.tsx @@ -1,6 +1,12 @@ import { Portal } from '@mui/base'; import React, { ReactNode } from 'react'; -import { Alert, AlertProps, Snackbar, SnackbarProps, AlertTitle } from '@mui/material'; +import { + Alert, + AlertProps, + Snackbar, + SnackbarProps, + AlertTitle, +} from '@mui/material'; export interface AlertPortalProps { onClose: () => void; @@ -19,9 +25,8 @@ export const AlertPortal = ({ alertProps, snackBarProps, }: AlertPortalProps) => { - if (autoHideDuration) { - setTimeout(() => onClose(), autoHideDuration) + setTimeout(() => onClose(), autoHideDuration); } return ( @@ -36,7 +41,7 @@ export const AlertPortal = ({ severity={alertProps?.severity} sx={{ width: '100%' }} > - { title && {title} } + {title && {title}} {message}
diff --git a/web_ui/frontend/components/AlertProvider.tsx b/web_ui/frontend/components/AlertProvider.tsx index 91177efa9..11e356488 100644 --- a/web_ui/frontend/components/AlertProvider.tsx +++ b/web_ui/frontend/components/AlertProvider.tsx @@ -4,10 +4,11 @@ import { createContext, Dispatch, useReducer } from 'react'; import { AlertPortal, AlertPortalProps } from '@/components/AlertPortal'; import CodeBlock from '@/components/CodeBlock'; -const defaultAlertContext: AlertPortalProps | undefined = undefined +const defaultAlertContext: AlertPortalProps | undefined = undefined; -export const AlertContext = - createContext(defaultAlertContext); +export const AlertContext = createContext( + defaultAlertContext +); export const AlertDispatchContext = createContext>( () => {} @@ -20,7 +21,7 @@ export const AlertProvider = ({ children }: { children: React.ReactNode }) => { {children} - { state && } + {state && } ); @@ -34,28 +35,31 @@ const alertReducer = ( case 'closeAlert': return undefined; case 'openErrorAlert': - const {title, error, onClose} = action.payload; + const { title, error, onClose } = action.payload; return { title, onClose, message: {error}, alertProps: { - severity: 'error' - } - } + severity: 'error', + }, + }; case 'openAlert': return action.payload; default: return state; } -} +}; -export type AlertReducerAction = closeAlertAction | openErrorAlertAction | openAlertAction; +export type AlertReducerAction = + | closeAlertAction + | openErrorAlertAction + | openAlertAction; type closeAlertAction = { - type: 'closeAlert' -} + type: 'closeAlert'; +}; type openErrorAlertAction = { type: 'openErrorAlert'; @@ -63,10 +67,10 @@ type openErrorAlertAction = { title: string; error: string; onClose: () => void; - } -} + }; +}; type openAlertAction = { - type: 'openAlert', - payload: AlertPortalProps -} + type: 'openAlert'; + payload: AlertPortalProps; +}; diff --git a/web_ui/frontend/components/CodeBlock.tsx b/web_ui/frontend/components/CodeBlock.tsx index 900dc58e6..f742793e7 100644 --- a/web_ui/frontend/components/CodeBlock.tsx +++ b/web_ui/frontend/components/CodeBlock.tsx @@ -8,18 +8,20 @@ import { Box } from '@mui/material'; * @param children * @constructor */ -export const CodeBlock = ({children}: {children: string | string[]}) => { - return - { - navigator.clipboard.writeText(children.toString()); - }} - > - {children} - - -} +export const CodeBlock = ({ children }: { children: string | string[] }) => { + return ( + + { + navigator.clipboard.writeText(children.toString()); + }} + > + {children} + + + ); +}; export default CodeBlock; diff --git a/web_ui/frontend/components/FederationOverview.tsx b/web_ui/frontend/components/FederationOverview.tsx index a47f1d1a5..575e3e8ca 100644 --- a/web_ui/frontend/components/FederationOverview.tsx +++ b/web_ui/frontend/components/FederationOverview.tsx @@ -48,16 +48,12 @@ const FederationOverview = () => { >([]); let getConfigJson = async () => { - const response = await getConfig() + const response = await getConfig(); const responseData = (await response.json()) as Config; const federationUrls = UrlData.map(({ key, text }) => { let url = getObjectValue(responseData, key); - if ( - url && - !url?.startsWith('http://') && - !url?.startsWith('https://') - ) { + if (url && !url?.startsWith('http://') && !url?.startsWith('https://')) { url = 'https://' + url; } @@ -68,7 +64,6 @@ const FederationOverview = () => { }); setConfig(federationUrls); - }; useEffect(() => { diff --git a/web_ui/frontend/components/Namespace/Card.tsx b/web_ui/frontend/components/Namespace/Card.tsx index b2e9d0a30..fcbb80836 100644 --- a/web_ui/frontend/components/Namespace/Card.tsx +++ b/web_ui/frontend/components/Namespace/Card.tsx @@ -108,7 +108,7 @@ export const Card = ({ namespace, authenticated, onUpdate }: CardProps) => { async () => await deleteNamespace(namespace.id), 'Could Not Delete Registration', dispatch - ) + ); setTimeout(() => mutate('getNamespaces'), 600); if (onUpdate) { onUpdate(); diff --git a/web_ui/frontend/components/Namespace/DeniedCard.tsx b/web_ui/frontend/components/Namespace/DeniedCard.tsx index 2db34baaa..6ef6999cb 100644 --- a/web_ui/frontend/components/Namespace/DeniedCard.tsx +++ b/web_ui/frontend/components/Namespace/DeniedCard.tsx @@ -24,7 +24,7 @@ export const DeniedCard = ({ namespace, authenticated }: DeniedCardProps) => { const ref = useRef(null); const [transition, setTransition] = useState(false); const dispatch = useContext(AlertDispatchContext); - const alert = useContext(AlertContext) + const alert = useContext(AlertContext); const { mutate } = useSWRConfig(); return ( @@ -88,7 +88,7 @@ export const DeniedCard = ({ namespace, authenticated }: DeniedCardProps) => { () => deleteNamespace(namespace.id), 'Could Not Delete Registration', dispatch - ) + ); }} > @@ -104,7 +104,7 @@ export const DeniedCard = ({ namespace, authenticated }: DeniedCardProps) => { () => approveNamespace(namespace.id), 'Could Not Approve Registration', dispatch - ) + ); setTimeout(() => mutate('getNamespaces'), 600); }} > diff --git a/web_ui/frontend/components/Namespace/InformationDropdown.tsx b/web_ui/frontend/components/Namespace/InformationDropdown.tsx index 752fe37b6..b83b5df00 100644 --- a/web_ui/frontend/components/Namespace/InformationDropdown.tsx +++ b/web_ui/frontend/components/Namespace/InformationDropdown.tsx @@ -1,6 +1,10 @@ import { Box, Tooltip, Collapse, Grid, Typography } from '@mui/material'; import React from 'react'; -import { Dropdown, InformationSpan, NamespaceAdminMetadata } from '@/components'; +import { + Dropdown, + InformationSpan, + NamespaceAdminMetadata, +} from '@/components'; interface InformationDropdownProps { adminMetadata: NamespaceAdminMetadata; diff --git a/web_ui/frontend/components/Namespace/PendingCard.tsx b/web_ui/frontend/components/Namespace/PendingCard.tsx index f138ab3a4..b661fe284 100644 --- a/web_ui/frontend/components/Namespace/PendingCard.tsx +++ b/web_ui/frontend/components/Namespace/PendingCard.tsx @@ -28,7 +28,7 @@ export const PendingCard = ({ const ref = useRef(null); const [transition, setTransition] = useState(false); - const dispatch = useContext(AlertDispatchContext) + const dispatch = useContext(AlertDispatchContext); return ( @@ -73,13 +73,13 @@ export const PendingCard = ({ sx={{ bgcolor: '#ff00001a', mx: 1 }} color={'error'} onClick={async (e) => { - e.stopPropagation() + e.stopPropagation(); await alertOnError( () => denyNamespace(namespace.id), "Couldn't deny namespace", dispatch - ) - onUpdate() + ); + onUpdate(); }} > @@ -90,13 +90,13 @@ export const PendingCard = ({ sx={{ bgcolor: '#2e7d3224', mx: 1 }} color={'success'} onClick={async (e) => { - e.stopPropagation() + e.stopPropagation(); await alertOnError( () => approveNamespace(namespace.id), "Couldn't approve namespace", dispatch - ) - onUpdate() + ); + onUpdate(); }} > diff --git a/web_ui/frontend/components/Namespace/index.tsx b/web_ui/frontend/components/Namespace/index.tsx index 7f7e4ac4b..3a43facd7 100644 --- a/web_ui/frontend/components/Namespace/index.tsx +++ b/web_ui/frontend/components/Namespace/index.tsx @@ -37,7 +37,6 @@ export interface FlatObject { export type NamespaceCardProps = CardProps & PendingCardProps; - export const getServerType = (namespace: Namespace) => { // If the namespace is empty the value is undefined if (namespace?.prefix == null || namespace.prefix == '') { diff --git a/web_ui/frontend/helpers/api.ts b/web_ui/frontend/helpers/api.ts index dbbfe8b9e..acd04ce28 100644 --- a/web_ui/frontend/helpers/api.ts +++ b/web_ui/frontend/helpers/api.ts @@ -26,26 +26,22 @@ export async function fetchApi( } throw new Error(alertMessage); } - return response + return response; } catch (e) { if (e instanceof Error) { - throw Error("Fetch to API Failed", { cause: e }) + throw Error('Fetch to API Failed', { cause: e }); } else { - throw Error("Fetch to API Failed", { cause: e}) + throw Error('Fetch to API Failed', { cause: e }); } } } - /** * Get config */ export const getConfig = async (): Promise => { - return fetchApi( - async () => await secureFetch('/api/v1.0/config') - ) -} - + return fetchApi(async () => await secureFetch('/api/v1.0/config')); +}; /** * Deletes a namespace @@ -53,27 +49,25 @@ export const getConfig = async (): Promise => { */ export const deleteNamespace = async (id: number) => { return fetchApi( - async () => await secureFetch(`/api/v1.0/registry_ui/namespaces/${id}`, { - method: 'DELETE', - }) - ) + async () => + await secureFetch(`/api/v1.0/registry_ui/namespaces/${id}`, { + method: 'DELETE', + }) + ); }; - /** * Approves a namespace * @param id Namespace ID */ export const approveNamespace = async (id: number) => { return fetchApi( - async () => await secureFetch( - `/api/v1.0/registry_ui/namespaces/${id}/approve`, - { + async () => + await secureFetch(`/api/v1.0/registry_ui/namespaces/${id}/approve`, { method: 'PATCH', - } - ) - ) -} + }) + ); +}; /** * Denies a namespace @@ -81,14 +75,12 @@ export const approveNamespace = async (id: number) => { */ export const denyNamespace = async (id: number) => { return fetchApi( - async () => await secureFetch( - `/api/v1.0/registry_ui/namespaces/${id}/deny`, - { + async () => + await secureFetch(`/api/v1.0/registry_ui/namespaces/${id}/deny`, { method: 'PATCH', - } - ) - ) -} + }) + ); +}; /** * Enables a server on the director @@ -96,14 +88,12 @@ export const denyNamespace = async (id: number) => { */ export const allowServer = async (name: string) => { return fetchApi( - async () => await secureFetch( - `/api/v1.0/director_ui/servers/allow/${name}`, - { + async () => + await secureFetch(`/api/v1.0/director_ui/servers/allow/${name}`, { method: 'PATCH', - } - ) - ) -} + }) + ); +}; /** * Filters ( Disables ) a server on the director @@ -111,20 +101,20 @@ export const allowServer = async (name: string) => { */ export const filterServer = async (name: string) => { return fetchApi( - async () => await secureFetch( - `/api/v1.0/director_ui/servers/filter/${name}`, - { + async () => + await secureFetch(`/api/v1.0/director_ui/servers/filter/${name}`, { method: 'PATCH', - } - ) - ) -} + }) + ); +}; /** * Get extended namespaces */ -export const getExtendedNamespaces = async (): Promise<{ namespace: Namespace }[]> => { - const response = await getNamespaces() +export const getExtendedNamespaces = async (): Promise< + { namespace: Namespace }[] +> => { + const response = await getNamespaces(); const data: Namespace[] = await response.json(); data.sort((a, b) => (a.id > b.id ? 1 : -1)); data.forEach((namespace) => { @@ -143,7 +133,7 @@ export const getExtendedNamespaces = async (): Promise<{ namespace: Namespace }[ return data.map((d) => { return { namespace: d }; }); -} +}; /** * Get namespaces @@ -156,7 +146,7 @@ export const getNamespaces = async (): Promise => { const response = await fetchApi(async () => await fetch(url)); return await response.json(); -} +}; /** * Gets a namespace by ID @@ -177,35 +167,31 @@ export const postGeneralNamespace = async ( data: Namespace ): Promise => { return await fetchApi( - async () => - await fetch('/api/v1.0/registry_ui/namespaces', { - body: JSON.stringify(data), - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - }) - ) + async () => + await fetch('/api/v1.0/registry_ui/namespaces', { + body: JSON.stringify(data), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }) + ); }; export const putGeneralNamespace = async ( data: Namespace ): Promise => { - return await fetchApi( - async () => { - return fetch(`/api/v1.0/registry_ui/namespaces/${data.id}`, - { - body: JSON.stringify(data), - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - } - ) - } - ) + return await fetchApi(async () => { + return fetch(`/api/v1.0/registry_ui/namespaces/${data.id}`, { + body: JSON.stringify(data), + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }); + }); }; /** @@ -215,10 +201,13 @@ export const putGeneralNamespace = async ( export const optionsNamespaceRegistrationFields = async (): Promise< Omit[] > => { - const response = await fetchApi(async () => await fetch('/api/v1.0/registry_ui/namespaces', { - method: 'OPTIONS', - })) - return response.json() + const response = await fetchApi( + async () => + await fetch('/api/v1.0/registry_ui/namespaces', { + method: 'OPTIONS', + }) + ); + return response.json(); }; /** @@ -236,8 +225,8 @@ export const initLogin = async (code: string): Promise => { code: code, }), }) - ) -} + ); +}; /** * Reset ( Do initial ) Login @@ -254,13 +243,16 @@ export const resetLogin = async (password: string): Promise => { password: password, }), }) - ) -} + ); +}; /** * Login */ -export const login = async (password: string, user: string = "admin"): Promise => { +export const login = async ( + password: string, + user: string = 'admin' +): Promise => { return await fetchApi( async () => await fetch('/api/v1.0/auth/login', { @@ -273,5 +265,5 @@ export const login = async (password: string, user: string = "admin"): Promise => { diff --git a/web_ui/frontend/helpers/util.ts b/web_ui/frontend/helpers/util.ts index 42bb56cba..a79ad2196 100644 --- a/web_ui/frontend/helpers/util.ts +++ b/web_ui/frontend/helpers/util.ts @@ -84,7 +84,6 @@ export function evaluateOrReturn( return o as T; } - /** * Get the average of an array of numbers * @param arr Array of numbers @@ -95,28 +94,27 @@ export const average = (arr: number[]) => { type ErrorWithCause = Error & { cause?: Error }; - /** * If an error is caught from f then display the error via an alert UI */ -export async function alertOnError( +export async function alertOnError( f: () => Promise | T | undefined, title: string = 'Error', dispatch: Dispatch -){ +) { try { return await f(); } catch (error) { console.error(error); - if(error instanceof Error) { + if (error instanceof Error) { dispatch({ - type: "openErrorAlert", + type: 'openErrorAlert', payload: { title, error: errorToString(error as ErrorWithCause), - onClose: () => dispatch({ type: "closeAlert" }) - } - }) + onClose: () => dispatch({ type: 'closeAlert' }), + }, + }); } } } @@ -125,17 +123,18 @@ export async function alertOnError( * Convert a error into a string * @param error */ -export const errorToString = (error: ErrorWithCause) : string => { - - if(error?.cause){ - +export const errorToString = (error: ErrorWithCause): string => { + if (error?.cause) { // Check that error is instance of Error - if(!(error?.cause instanceof Error)) { - console.error("Malformed error, cause is not an instance of Error", error) + if (!(error?.cause instanceof Error)) { + console.error( + 'Malformed error, cause is not an instance of Error', + error + ); } - return `${error.message}\n↳ ${errorToString(error.cause as ErrorWithCause)}` + return `${error.message}\n↳ ${errorToString(error.cause as ErrorWithCause)}`; } - return `${error.message}` -} + return `${error.message}`; +}; From 9c080478289a27b82e7420815cf744dffcb6b25e Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Tue, 12 Nov 2024 12:29:43 -0600 Subject: [PATCH 06/16] Update Error Handling - Move registry pages behind auth check - Create get.ts which holds api endpoints that get updated - Fix AuthenticatedContent.tsx to check it auth value is true --- .../frontend/app/registry/cache/edit/page.tsx | 5 +- .../app/registry/cache/register/page.tsx | 5 +- .../frontend/app/registry/components/Form.tsx | 10 ++- .../app/registry/components/PutPage.tsx | 2 +- web_ui/frontend/app/registry/denied/page.tsx | 4 +- .../app/registry/namespace/edit/page.tsx | 5 +- .../app/registry/namespace/register/page.tsx | 5 +- .../app/registry/origin/edit/page.tsx | 5 +- .../app/registry/origin/register/page.tsx | 5 +- web_ui/frontend/app/registry/page.tsx | 4 +- .../components/FederationOverview.tsx | 58 +++----------- web_ui/frontend/components/Namespace/Card.tsx | 2 +- .../components/Namespace/DeniedCard.tsx | 2 +- .../layout/AuthenticatedContent.tsx | 6 +- web_ui/frontend/dev/image/nginx.conf | 2 + web_ui/frontend/helpers/api.ts | 66 +++++----------- web_ui/frontend/helpers/get.ts | 78 ++++++++++++++++++- 17 files changed, 148 insertions(+), 116 deletions(-) diff --git a/web_ui/frontend/app/registry/cache/edit/page.tsx b/web_ui/frontend/app/registry/cache/edit/page.tsx index 433d81f16..c9f9eaec3 100644 --- a/web_ui/frontend/app/registry/cache/edit/page.tsx +++ b/web_ui/frontend/app/registry/cache/edit/page.tsx @@ -24,6 +24,7 @@ import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; import { putGeneralNamespace } from '@/helpers/api'; +import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; export default function Page() { const putCache = async (data: any) => { @@ -43,7 +44,9 @@ export default function Page() {
- + + +
diff --git a/web_ui/frontend/app/registry/cache/register/page.tsx b/web_ui/frontend/app/registry/cache/register/page.tsx index 61d885670..0364bc5f5 100644 --- a/web_ui/frontend/app/registry/cache/register/page.tsx +++ b/web_ui/frontend/app/registry/cache/register/page.tsx @@ -23,6 +23,7 @@ import { PostPage } from '@/app/registry/components/PostPage'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; import { postGeneralNamespace } from '@/helpers/api'; +import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; export default function Page() { const postCache = async (data: any) => { @@ -42,7 +43,9 @@ export default function Page() {
- + + + diff --git a/web_ui/frontend/app/registry/components/Form.tsx b/web_ui/frontend/app/registry/components/Form.tsx index f235c85f7..230999708 100644 --- a/web_ui/frontend/app/registry/components/Form.tsx +++ b/web_ui/frontend/app/registry/components/Form.tsx @@ -58,12 +58,16 @@ const Form = ({ namespace, onSubmit }: FormProps) => { Omit[] | undefined >( 'optionsNamespaceRegistrationFields', - async () => - await alertOnError( + async () => { + const response = await alertOnError( optionsNamespaceRegistrationFields, "Couldn't fetch registration fields", dispatch - ), + ); + if(response){ + return await response.json(); + } + }, { fallbackData: [] } ); diff --git a/web_ui/frontend/app/registry/components/PutPage.tsx b/web_ui/frontend/app/registry/components/PutPage.tsx index 1fe498263..ee2e1b850 100644 --- a/web_ui/frontend/app/registry/components/PutPage.tsx +++ b/web_ui/frontend/app/registry/components/PutPage.tsx @@ -116,7 +116,7 @@ const PutPage = ({ update }: NamespaceFormPage) => { dispatch ); if (response) { - setNamespace(response); + setNamespace(await response.json()); } } })(); diff --git a/web_ui/frontend/app/registry/denied/page.tsx b/web_ui/frontend/app/registry/denied/page.tsx index 7d1775521..851fb6d03 100644 --- a/web_ui/frontend/app/registry/denied/page.tsx +++ b/web_ui/frontend/app/registry/denied/page.tsx @@ -28,14 +28,14 @@ import useSWR from 'swr'; import { CardProps } from '@/components/Namespace/Card'; import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; import DeniedCard from '@/components/Namespace/DeniedCard'; -import { getExtendedNamespaces } from '@/helpers/api'; +import { getExtendedNamespaces } from '@/helpers/get'; import { AlertDispatchContext } from '@/components/AlertProvider'; import { alertOnError } from '@/helpers/util'; export default function Home() { const dispatch = useContext(AlertDispatchContext); - const { data } = useSWR('getNamespaces', async () => + const { data } = useSWR('getExtendedNamespaces', async () => alertOnError(getExtendedNamespaces, "Couldn't fetch namespaces", dispatch) ); const { data: user, error } = useSWR('getUser', async () => diff --git a/web_ui/frontend/app/registry/namespace/edit/page.tsx b/web_ui/frontend/app/registry/namespace/edit/page.tsx index 757897925..de07c32fd 100644 --- a/web_ui/frontend/app/registry/namespace/edit/page.tsx +++ b/web_ui/frontend/app/registry/namespace/edit/page.tsx @@ -22,6 +22,7 @@ import { PutPage } from '@/app/registry/components/PutPage'; import { putGeneralNamespace } from '@/helpers/api'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; +import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; export default function Page() { const putCache = async (data: any) => { @@ -40,7 +41,9 @@ export default function Page() { - + + + diff --git a/web_ui/frontend/app/registry/namespace/register/page.tsx b/web_ui/frontend/app/registry/namespace/register/page.tsx index 6d51df66a..48d727f74 100644 --- a/web_ui/frontend/app/registry/namespace/register/page.tsx +++ b/web_ui/frontend/app/registry/namespace/register/page.tsx @@ -22,6 +22,7 @@ import { postGeneralNamespace } from '@/helpers/api'; import { PostPage } from '@/app/registry/components/PostPage'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; +import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; export default function Page() { const postCache = async (data: any) => { @@ -40,7 +41,9 @@ export default function Page() { - + + + diff --git a/web_ui/frontend/app/registry/origin/edit/page.tsx b/web_ui/frontend/app/registry/origin/edit/page.tsx index 4fe766aff..a40a7b81b 100644 --- a/web_ui/frontend/app/registry/origin/edit/page.tsx +++ b/web_ui/frontend/app/registry/origin/edit/page.tsx @@ -23,6 +23,7 @@ import { namespaceToOrigin } from '@/app/registry/components/util'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; import { putGeneralNamespace } from '@/helpers/api'; +import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; export default function Page() { const putCache = async (data: any) => { @@ -42,7 +43,9 @@ export default function Page() { - + + + diff --git a/web_ui/frontend/app/registry/origin/register/page.tsx b/web_ui/frontend/app/registry/origin/register/page.tsx index f52464fef..dc61fc18b 100644 --- a/web_ui/frontend/app/registry/origin/register/page.tsx +++ b/web_ui/frontend/app/registry/origin/register/page.tsx @@ -23,6 +23,7 @@ import { PostPage } from '@/app/registry/components/PostPage'; import { Box, Grid, Typography } from '@mui/material'; import React from 'react'; import { postGeneralNamespace } from '@/helpers/api'; +import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; export default function Page() { const postCache = async (data: any) => { @@ -42,7 +43,9 @@ export default function Page() { - + + + diff --git a/web_ui/frontend/app/registry/page.tsx b/web_ui/frontend/app/registry/page.tsx index ab5204b69..0835c9fc9 100644 --- a/web_ui/frontend/app/registry/page.tsx +++ b/web_ui/frontend/app/registry/page.tsx @@ -46,7 +46,7 @@ import { CardProps } from '@/components/Namespace/Card'; import { PendingCardProps } from '@/components/Namespace/PendingCard'; import { AlertDispatchContext } from '@/components/AlertProvider'; import { alertOnError } from '@/helpers/util'; -import { getExtendedNamespaces } from '@/helpers/api'; +import { getExtendedNamespaces } from '@/helpers/get'; export default function Home() { const dispatch = useContext(AlertDispatchContext); @@ -54,7 +54,7 @@ export default function Home() { const { data, mutate: mutateNamespaces } = useSWR< { namespace: Namespace }[] | undefined >( - 'getNamespaces', + 'getExtendedNamespaces', () => alertOnError( getExtendedNamespaces, diff --git a/web_ui/frontend/components/FederationOverview.tsx b/web_ui/frontend/components/FederationOverview.tsx index 575e3e8ca..d386c62f7 100644 --- a/web_ui/frontend/components/FederationOverview.tsx +++ b/web_ui/frontend/components/FederationOverview.tsx @@ -8,6 +8,8 @@ import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; import Link from 'next/link'; import { getErrorMessage, getObjectValue } from '@/helpers/util'; import { getConfig } from '@/helpers/api'; +import { getFederationUrls } from '@/helpers/get'; +import useSWR from 'swr'; const LinkBox = ({ href, text }: { href: string; text: string }) => { return ( @@ -30,66 +32,28 @@ const LinkBox = ({ href, text }: { href: string; text: string }) => { ); }; -const UrlData = [ - { key: ['Federation', 'NamespaceUrl'], text: 'Namespace Registry' }, - { key: ['Federation', 'DirectorUrl'], text: 'Director' }, - { key: ['Federation', 'RegistryUrl'], text: 'Registry' }, - { - key: ['Federation', 'TopologyNamespaceUrl'], - text: 'Topology Namespace', - }, - { key: ['Federation', 'DiscoveryUrl'], text: 'Discovery' }, - { key: ['Federation', 'JwkUrl'], text: 'JWK' }, -]; const FederationOverview = () => { - const [config, setConfig] = useState< - { text: string; url: string | undefined }[] - >([]); - let getConfigJson = async () => { - const response = await getConfig(); - const responseData = (await response.json()) as Config; - - const federationUrls = UrlData.map(({ key, text }) => { - let url = getObjectValue(responseData, key); - if (url && !url?.startsWith('http://') && !url?.startsWith('https://')) { - url = 'https://' + url; - } - - return { - text, - url, - }; - }); - - setConfig(federationUrls); - }; - - useEffect(() => { - getConfigJson(); - }, []); - - if (config === undefined) { - return; - } + const {data : federationUrls , error} = useSWR( + 'getFederationUrls', + getFederationUrls, + {fallbackData: []} + ); return ( - u?.role == 'admin'} - > - {!Object.values(config).every((x) => x == undefined) ? ( + <> + {!Object.values(federationUrls).every((x) => x == undefined) ? ( Federation Overview ) : null} - {config.map(({ text, url }) => { + {federationUrls.map(({ text, url }) => { if (url) { return ; } })} - + ); }; diff --git a/web_ui/frontend/components/Namespace/Card.tsx b/web_ui/frontend/components/Namespace/Card.tsx index fcbb80836..01d443243 100644 --- a/web_ui/frontend/components/Namespace/Card.tsx +++ b/web_ui/frontend/components/Namespace/Card.tsx @@ -109,7 +109,7 @@ export const Card = ({ namespace, authenticated, onUpdate }: CardProps) => { 'Could Not Delete Registration', dispatch ); - setTimeout(() => mutate('getNamespaces'), 600); + setTimeout(() => mutate('getExtendedNamespaces'), 600); if (onUpdate) { onUpdate(); } diff --git a/web_ui/frontend/components/Namespace/DeniedCard.tsx b/web_ui/frontend/components/Namespace/DeniedCard.tsx index 6ef6999cb..93617c531 100644 --- a/web_ui/frontend/components/Namespace/DeniedCard.tsx +++ b/web_ui/frontend/components/Namespace/DeniedCard.tsx @@ -105,7 +105,7 @@ export const DeniedCard = ({ namespace, authenticated }: DeniedCardProps) => { 'Could Not Approve Registration', dispatch ); - setTimeout(() => mutate('getNamespaces'), 600); + setTimeout(() => mutate('getExtendedNamespaces'), 600); }} > diff --git a/web_ui/frontend/components/layout/AuthenticatedContent.tsx b/web_ui/frontend/components/layout/AuthenticatedContent.tsx index 5eef49bcc..966a98ab8 100644 --- a/web_ui/frontend/components/layout/AuthenticatedContent.tsx +++ b/web_ui/frontend/components/layout/AuthenticatedContent.tsx @@ -69,7 +69,7 @@ const AuthenticatedContent = ({ if (data && checkAuthentication) { return checkAuthentication(data); } else { - return data?.authenticated !== undefined; + return data?.authenticated === undefined ? false : data.authenticated; } }, [data, checkAuthentication]); @@ -84,9 +84,9 @@ const AuthenticatedContent = ({ // Redirect to login page if not authenticated and redirect is true useEffect(() => { if (!isValidating && !authenticated && redirect) { - router.push('/login/?returnURL=' + pageUrl); + router.replace('/login/?returnURL=' + pageUrl); } - }, [data, isValidating]); + }, [data, isValidating, authenticated]); // If there was a error then print it to the screen if (error) { diff --git a/web_ui/frontend/dev/image/nginx.conf b/web_ui/frontend/dev/image/nginx.conf index 97fa6bc7e..4c5f83fda 100644 --- a/web_ui/frontend/dev/image/nginx.conf +++ b/web_ui/frontend/dev/image/nginx.conf @@ -56,6 +56,8 @@ http { proxy_connect_timeout 10s; proxy_set_header X-Real-IP $remote_addr; proxy_pass http://host.docker.internal:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; } gzip on; diff --git a/web_ui/frontend/helpers/api.ts b/web_ui/frontend/helpers/api.ts index acd04ce28..00b9921a8 100644 --- a/web_ui/frontend/helpers/api.ts +++ b/web_ui/frontend/helpers/api.ts @@ -1,9 +1,12 @@ +/** + * API Helper Functions + * + * Strictly return the response from the API, throwing an error if the response is not ok + */ + import { secureFetch } from '@/helpers/login'; import { getErrorMessage } from '@/helpers/util'; -import { Alert, Namespace } from '@/index'; -import { CustomRegistrationFieldProps } from '@/app/registry/components/CustomRegistrationField'; - -// TODO: Decide if we should standardize the output in all of these functions. Should they all be responses? +import { Namespace } from '@/index'; /** * Wraps an api request with error handling for both the request and the response if error @@ -60,7 +63,7 @@ export const deleteNamespace = async (id: number) => { * Approves a namespace * @param id Namespace ID */ -export const approveNamespace = async (id: number) => { +export const approveNamespace = async (id: number): Promise => { return fetchApi( async () => await secureFetch(`/api/v1.0/registry_ui/namespaces/${id}/approve`, { @@ -73,7 +76,7 @@ export const approveNamespace = async (id: number) => { * Denies a namespace * @param id Namespace ID */ -export const denyNamespace = async (id: number) => { +export const denyNamespace = async (id: number): Promise => { return fetchApi( async () => await secureFetch(`/api/v1.0/registry_ui/namespaces/${id}/deny`, { @@ -86,7 +89,7 @@ export const denyNamespace = async (id: number) => { * Enables a server on the director * @param name Server name */ -export const allowServer = async (name: string) => { +export const allowServer = async (name: string): Promise => { return fetchApi( async () => await secureFetch(`/api/v1.0/director_ui/servers/allow/${name}`, { @@ -99,7 +102,7 @@ export const allowServer = async (name: string) => { * Filters ( Disables ) a server on the director * @param name Server name */ -export const filterServer = async (name: string) => { +export const filterServer = async (name: string): Promise => { return fetchApi( async () => await secureFetch(`/api/v1.0/director_ui/servers/filter/${name}`, { @@ -108,33 +111,6 @@ export const filterServer = async (name: string) => { ); }; -/** - * Get extended namespaces - */ -export const getExtendedNamespaces = async (): Promise< - { namespace: Namespace }[] -> => { - const response = await getNamespaces(); - const data: Namespace[] = await response.json(); - data.sort((a, b) => (a.id > b.id ? 1 : -1)); - data.forEach((namespace) => { - if (namespace.prefix.startsWith('/caches/')) { - namespace.type = 'cache'; - namespace.prefix = namespace.prefix.replace('/caches/', ''); - } else if (namespace.prefix.startsWith('/origins/')) { - namespace.type = 'origin'; - namespace.prefix = namespace.prefix.replace('/origins/', ''); - } else { - namespace.type = 'namespace'; - } - }); - - // TODO: This extra should be done somewhere else, why is it done? - return data.map((d) => { - return { namespace: d }; - }); -}; - /** * Get namespaces */ @@ -144,8 +120,7 @@ export const getNamespaces = async (): Promise => { window.location.origin ); - const response = await fetchApi(async () => await fetch(url)); - return await response.json(); + return await fetchApi(async () => await fetch(url)); }; /** @@ -154,13 +129,12 @@ export const getNamespaces = async (): Promise => { */ export const getNamespace = async ( id: string | number -): Promise => { +): Promise => { const url = new URL( `/api/v1.0/registry_ui/namespaces/${id}`, window.location.origin ); - const response = await fetchApi(async () => await fetch(url)); - return await response.json(); + return await fetchApi(async () => await fetch(url)); }; export const postGeneralNamespace = async ( @@ -168,7 +142,7 @@ export const postGeneralNamespace = async ( ): Promise => { return await fetchApi( async () => - await fetch('/api/v1.0/registry_ui/namespaces', { + await secureFetch('/api/v1.0/registry_ui/namespaces', { body: JSON.stringify(data), method: 'POST', headers: { @@ -183,7 +157,7 @@ export const putGeneralNamespace = async ( data: Namespace ): Promise => { return await fetchApi(async () => { - return fetch(`/api/v1.0/registry_ui/namespaces/${data.id}`, { + return secureFetch(`/api/v1.0/registry_ui/namespaces/${data.id}`, { body: JSON.stringify(data), method: 'PUT', headers: { @@ -196,18 +170,14 @@ export const putGeneralNamespace = async ( /** * Get registration fields from options for namespace - * // TODO: Complain about the misuse of options */ -export const optionsNamespaceRegistrationFields = async (): Promise< - Omit[] -> => { - const response = await fetchApi( +export const optionsNamespaceRegistrationFields = async (): Promise => { + return await fetchApi( async () => await fetch('/api/v1.0/registry_ui/namespaces', { method: 'OPTIONS', }) ); - return response.json(); }; /** diff --git a/web_ui/frontend/helpers/get.ts b/web_ui/frontend/helpers/get.ts index adb772466..a32d8ecbf 100644 --- a/web_ui/frontend/helpers/get.ts +++ b/web_ui/frontend/helpers/get.ts @@ -1,6 +1,15 @@ -import { ParameterValueRecord } from '@/components/configuration'; -import { getConfig as getConfigResponse } from '@/helpers/api'; +/** + * API wrappers for manipulating fetched data + * + * @module helpers/get + */ + + +import { Config, ParameterValueRecord } from '@/components/configuration'; +import { getConfig as getConfigResponse, getNamespaces } from '@/helpers/api'; import { flattenObject } from '@/app/config/util'; +import { Namespace } from '@/index'; +import { getObjectValue } from '@/helpers/util'; export const getConfig = async (): Promise => { let response = await getConfigResponse(); @@ -8,3 +17,68 @@ export const getConfig = async (): Promise => { let flatData = flattenObject(data); return flatData; }; + +/** + * Get extended namespaces + */ +export const getExtendedNamespaces = async (): Promise< + { namespace: Namespace }[] +> => { + const response = await getNamespaces(); + const data: Namespace[] = await response.json(); + data.sort((a, b) => (a.id > b.id ? 1 : -1)); + data.forEach((namespace) => { + if (namespace.prefix.startsWith('/caches/')) { + namespace.type = 'cache'; + namespace.prefix = namespace.prefix.replace('/caches/', ''); + } else if (namespace.prefix.startsWith('/origins/')) { + namespace.type = 'origin'; + namespace.prefix = namespace.prefix.replace('/origins/', ''); + } else { + namespace.type = 'namespace'; + } + }); + + return data.map((d) => { + return { namespace: d }; + }); +}; + +/** + * Get federation URLs + */ +export const getFederationUrls = async () => { + try { + const response = await getConfigResponse(); + const responseData = (await response.json()) as Config; + + const federationUrls = UrlData.map(({ key, text }) => { + let url = getObjectValue(responseData, key); + if (url && !url?.startsWith('http://') && !url?.startsWith('https://')) { + url = 'https://' + url; + } + + return { + text, + url, + }; + }); + + return federationUrls + } catch (e) { + console.error(e); + return []; + } + +} +const UrlData = [ + { key: ['Federation', 'NamespaceUrl'], text: 'Namespace Registry' }, + { key: ['Federation', 'DirectorUrl'], text: 'Director' }, + { key: ['Federation', 'RegistryUrl'], text: 'Registry' }, + { + key: ['Federation', 'TopologyNamespaceUrl'], + text: 'Topology Namespace', + }, + { key: ['Federation', 'DiscoveryUrl'], text: 'Discovery' }, + { key: ['Federation', 'JwkUrl'], text: 'JWK' }, +]; From 54739f39baac62b66b9ff7f4eeca593aed38e25d Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Tue, 12 Nov 2024 14:08:34 -0600 Subject: [PATCH 07/16] Format --- .../frontend/app/registry/components/Form.tsx | 2 +- .../app/registry/namespace/register/page.tsx | 2 +- .../components/FederationOverview.tsx | 6 ++---- web_ui/frontend/helpers/api.ts | 21 +++++++++---------- web_ui/frontend/helpers/get.ts | 6 ++---- 5 files changed, 16 insertions(+), 21 deletions(-) diff --git a/web_ui/frontend/app/registry/components/Form.tsx b/web_ui/frontend/app/registry/components/Form.tsx index 230999708..971cf1e04 100644 --- a/web_ui/frontend/app/registry/components/Form.tsx +++ b/web_ui/frontend/app/registry/components/Form.tsx @@ -64,7 +64,7 @@ const Form = ({ namespace, onSubmit }: FormProps) => { "Couldn't fetch registration fields", dispatch ); - if(response){ + if (response) { return await response.json(); } }, diff --git a/web_ui/frontend/app/registry/namespace/register/page.tsx b/web_ui/frontend/app/registry/namespace/register/page.tsx index 48d727f74..be3aaeae2 100644 --- a/web_ui/frontend/app/registry/namespace/register/page.tsx +++ b/web_ui/frontend/app/registry/namespace/register/page.tsx @@ -42,7 +42,7 @@ export default function Page() { - + diff --git a/web_ui/frontend/components/FederationOverview.tsx b/web_ui/frontend/components/FederationOverview.tsx index d386c62f7..f47d36841 100644 --- a/web_ui/frontend/components/FederationOverview.tsx +++ b/web_ui/frontend/components/FederationOverview.tsx @@ -32,13 +32,11 @@ const LinkBox = ({ href, text }: { href: string; text: string }) => { ); }; - const FederationOverview = () => { - - const {data : federationUrls , error} = useSWR( + const { data: federationUrls, error } = useSWR( 'getFederationUrls', getFederationUrls, - {fallbackData: []} + { fallbackData: [] } ); return ( diff --git a/web_ui/frontend/helpers/api.ts b/web_ui/frontend/helpers/api.ts index 00b9921a8..9c083e351 100644 --- a/web_ui/frontend/helpers/api.ts +++ b/web_ui/frontend/helpers/api.ts @@ -127,9 +127,7 @@ export const getNamespaces = async (): Promise => { * Gets a namespace by ID * @param id Namespace ID */ -export const getNamespace = async ( - id: string | number -): Promise => { +export const getNamespace = async (id: string | number): Promise => { const url = new URL( `/api/v1.0/registry_ui/namespaces/${id}`, window.location.origin @@ -171,14 +169,15 @@ export const putGeneralNamespace = async ( /** * Get registration fields from options for namespace */ -export const optionsNamespaceRegistrationFields = async (): Promise => { - return await fetchApi( - async () => - await fetch('/api/v1.0/registry_ui/namespaces', { - method: 'OPTIONS', - }) - ); -}; +export const optionsNamespaceRegistrationFields = + async (): Promise => { + return await fetchApi( + async () => + await fetch('/api/v1.0/registry_ui/namespaces', { + method: 'OPTIONS', + }) + ); + }; /** * Initializes a login via terminal code diff --git a/web_ui/frontend/helpers/get.ts b/web_ui/frontend/helpers/get.ts index a32d8ecbf..629554330 100644 --- a/web_ui/frontend/helpers/get.ts +++ b/web_ui/frontend/helpers/get.ts @@ -4,7 +4,6 @@ * @module helpers/get */ - import { Config, ParameterValueRecord } from '@/components/configuration'; import { getConfig as getConfigResponse, getNamespaces } from '@/helpers/api'; import { flattenObject } from '@/app/config/util'; @@ -64,13 +63,12 @@ export const getFederationUrls = async () => { }; }); - return federationUrls + return federationUrls; } catch (e) { console.error(e); return []; } - -} +}; const UrlData = [ { key: ['Federation', 'NamespaceUrl'], text: 'Namespace Registry' }, { key: ['Federation', 'DirectorUrl'], text: 'Director' }, From ca52f27475edcbc7055fdd005309816c67250b7c Mon Sep 17 00:00:00 2001 From: alexandertuna Date: Tue, 12 Nov 2024 16:49:34 -0600 Subject: [PATCH 08/16] Update macos.mdx to make architectures match --- docs/pages/install/macos.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/install/macos.mdx b/docs/pages/install/macos.mdx index 23e51d5c9..b6f7e5185 100644 --- a/docs/pages/install/macos.mdx +++ b/docs/pages/install/macos.mdx @@ -20,7 +20,7 @@ Pelican provides a binary executable file instead of a `DMG` installer for MacOS Example to install Pelican executable for an Apple Silicon Mac: ```bash - curl -LO https://github.com/PelicanPlatform/pelican/releases/download/v7.10.5/pelican_Darwin_x86_64.tar.gz + curl -LO https://github.com/PelicanPlatform/pelican/releases/download/v7.10.5/pelican_Darwin_arm64.tar.gz tar -zxvf pelican_Darwin_arm64.tar.gz ``` From 364ec4554d2ff4ce88b08ce1e965d22c44551080 Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Wed, 13 Nov 2024 16:03:16 -0600 Subject: [PATCH 09/16] Add director endpoints Add director endpoints to facilitate easier mapping between endpoints and the servers that work with them. - /servers/:name - /servers/:name/namespaces - /namespaces --- director/director_ui.go | 292 +++++++++++++++++++++++++++++++---- director/director_ui_test.go | 238 ++++++++++++++++++++++++++++ swagger/pelican-swagger.yaml | 136 ++++++++++++++++ 3 files changed, 632 insertions(+), 34 deletions(-) diff --git a/director/director_ui.go b/director/director_ui.go index 2930a217a..c4d433994 100644 --- a/director/director_ui.go +++ b/director/director_ui.go @@ -37,6 +37,7 @@ type ( ServerType string `form:"server_type"` // "cache" or "origin" } + // A response struct for a server Ad that provides a minimal view into the servers data listServerResponse struct { Name string `json:"name"` StorageType server_structs.OriginStorageType `json:"storageType"` @@ -60,6 +61,65 @@ type ( NamespacePrefixes []string `json:"namespacePrefixes"` } + // A response struct for a server Ad that provides a detailed view into the servers data + serverResponse struct { + Name string `json:"name"` + StorageType server_structs.OriginStorageType `json:"storageType"` + DisableDirectorTest bool `json:"disableDirectorTest"` + // AuthURL is Deprecated. For Pelican severs, URL is used as the base URL for object access. + // This is to maintain compatibility with the topology servers, where it uses AuthURL for + // accessing protected objects and URL for public objects. + AuthURL string `json:"authUrl"` + BrokerURL string `json:"brokerUrl"` + URL string `json:"url"` // This is server's XRootD URL for file transfer + WebURL string `json:"webUrl"` // This is server's Web interface and API + Type string `json:"type"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Caps server_structs.Capabilities `json:"capabilities"` + Filtered bool `json:"filtered"` + FilteredType string `json:"filteredType"` + FromTopology bool `json:"fromTopology"` + HealthStatus HealthTestStatus `json:"healthStatus"` + IOLoad float64 `json:"ioLoad"` + Namespaces []NamespaceAdV2Response `json:"namespaces"` + } + + // TokenIssuerResponse creates a response struct for TokenIssuer + TokenIssuerResponse struct { + BasePaths []string `json:"base-paths"` + RestrictedPaths []string `json:"restricted-paths"` + IssuerUrl string `json:"issuer"` + } + + // TokenGenResponse creates a response struct for TokenGen + TokenGenResponse struct { + Strategy server_structs.StrategyType `json:"strategy"` + VaultServer string `json:"vault-server"` + MaxScopeDepth uint `json:"max-scope-depth"` + CredentialIssuer string `json:"issuer"` + } + + // NamespaceAdV2Response creates a response struct for NamespaceAdV2 + NamespaceAdV2Response struct { + Path string `json:"path"` + Caps server_structs.Capabilities `json:"capabilities"` + Generation []TokenGenResponse `json:"token-generation"` + Issuer []TokenIssuerResponse `json:"token-issuer"` + FromTopology bool `json:"from-topology"` + } + + // NamespaceAdV2MappedResponse creates a response struct for NamespaceAdV2 with mapped origins and caches + NamespaceAdV2MappedResponse struct { + Path string `json:"path"` + Caps server_structs.Capabilities `json:"capabilities"` + Generation []TokenGenResponse `json:"token-generation"` + Issuer []TokenIssuerResponse `json:"token-issuer"` + FromTopology bool `json:"from-topology"` + Origins []string `json:"origins"` + Caches []string `json:"caches"` + } + statRequest struct { MinResponses int `form:"min_responses"` MaxResponses int `form:"max_responses"` @@ -107,46 +167,207 @@ func listServers(ctx *gin.Context) { defer healthTestUtilsMutex.RUnlock() resList := make([]listServerResponse, 0) for _, server := range servers { - healthStatus := HealthStatusUnknown - healthUtil, ok := healthTestUtils[server.URL.String()] - if ok { - healthStatus = healthUtil.Status + res := advertisementToServerResponse(server) + listRes := serverResponseToListServerResponse(res) + resList = append(resList, listRes) + } + ctx.JSON(http.StatusOK, resList) +} + +// Convert NamespaceAdV2 to namespaceResponse +func namespaceAdV2ToResponse(ns *server_structs.NamespaceAdV2) NamespaceAdV2Response { + res := NamespaceAdV2Response{ + Path: ns.Path, + Caps: ns.Caps, + FromTopology: ns.FromTopology, + } + for _, gen := range ns.Generation { + res.Generation = append(res.Generation, TokenGenResponse{ + Strategy: gen.Strategy, + VaultServer: gen.VaultServer, + MaxScopeDepth: gen.MaxScopeDepth, + CredentialIssuer: gen.CredentialIssuer.String(), + }) + } + for _, issuer := range ns.Issuer { + res.Issuer = append(res.Issuer, TokenIssuerResponse{ + BasePaths: issuer.BasePaths, + RestrictedPaths: issuer.RestrictedPaths, + IssuerUrl: issuer.IssuerUrl.String(), + }) + } + return res +} + +// namespaceAdV2ToMappedResponse converts a NamespaceAdV2 to a NamespaceAdV2MappedResponse +func namespaceAdV2ToMappedResponse(ns *server_structs.NamespaceAdV2) NamespaceAdV2MappedResponse { + nsRes := namespaceAdV2ToResponse(ns) + return NamespaceAdV2MappedResponse{ + Path: nsRes.Path, + Caps: nsRes.Caps, + Generation: nsRes.Generation, + Issuer: nsRes.Issuer, + Origins: []string{}, + Caches: []string{}, + } +} + +// Convert Advertisement to serverResponse +func advertisementToServerResponse(ad *server_structs.Advertisement) serverResponse { + healthStatus := HealthStatusUnknown + healthUtil, ok := healthTestUtils[ad.URL.String()] + if ok { + healthStatus = healthUtil.Status + } else { + if ad.DisableDirectorTest { + healthStatus = HealthStatusDisabled } else { - if server.DisableDirectorTest { - healthStatus = HealthStatusDisabled - } else { - if !server.FromTopology { - log.Debugf("listServers: healthTestUtils not found for server at %s", server.URL.String()) - } + if !ad.FromTopology { + log.Debugf("advertisementToServerResponse: healthTestUtils not found for server at %s", ad.URL.String()) } } - filtered, ft := checkFilter(server.Name) - - res := listServerResponse{ - Name: server.Name, - StorageType: server.StorageType, - DisableDirectorTest: server.DisableDirectorTest, - BrokerURL: server.BrokerURL.String(), - // For web UI, if authURL is not set, we don't want to confuse user by copying server URL as authURL - AuthURL: server.AuthURL.String(), - URL: server.URL.String(), - WebURL: server.WebURL.String(), - Type: server.Type, - Latitude: server.Latitude, - Longitude: server.Longitude, - Caps: server.Caps, - Filtered: filtered, - FilteredType: ft.String(), - FromTopology: server.FromTopology, - HealthStatus: healthStatus, - IOLoad: server.GetIOLoad(), + } + filtered, ft := checkFilter(ad.Name) + res := serverResponse{ + Name: ad.Name, + StorageType: ad.StorageType, + DisableDirectorTest: ad.DisableDirectorTest, + BrokerURL: ad.BrokerURL.String(), + AuthURL: ad.AuthURL.String(), + URL: ad.URL.String(), + WebURL: ad.WebURL.String(), + Type: ad.Type, + Latitude: ad.Latitude, + Longitude: ad.Longitude, + Caps: ad.Caps, + Filtered: filtered, + FilteredType: ft.String(), + FromTopology: ad.FromTopology, + HealthStatus: healthStatus, + IOLoad: ad.GetIOLoad(), + } + for _, ns := range ad.NamespaceAds { + nsRes := namespaceAdV2ToResponse(&ns) + res.Namespaces = append(res.Namespaces, nsRes) + } + return res +} + +// Convert serverResponse to a listServerResponse +func serverResponseToListServerResponse(res serverResponse) listServerResponse { + listRes := listServerResponse{ + Name: res.Name, + StorageType: res.StorageType, + DisableDirectorTest: res.DisableDirectorTest, + BrokerURL: res.BrokerURL, + AuthURL: res.AuthURL, + URL: res.URL, + WebURL: res.WebURL, + Type: res.Type, + Latitude: res.Latitude, + Longitude: res.Longitude, + Caps: res.Caps, + Filtered: res.Filtered, + FilteredType: res.FilteredType, + FromTopology: res.FromTopology, + HealthStatus: res.HealthStatus, + IOLoad: res.IOLoad, + } + for _, ns := range res.Namespaces { + listRes.NamespacePrefixes = append(listRes.NamespacePrefixes, ns.Path) + } + return listRes +} + +// Given a server name returns the server advertisement +func getServer(serverName string) *server_structs.Advertisement { + servers := listAdvertisement([]server_structs.ServerType{server_structs.OriginType, server_structs.CacheType}) + for _, server := range servers { + if server.Name == serverName { + return server } - for _, ns := range server.NamespaceAds { - res.NamespacePrefixes = append(res.NamespacePrefixes, ns.Path) + } + return nil +} + +// API wrapper around getServer to return a serverResponse +func getServerHandler(ctx *gin.Context) { + serverName := ctx.Param("name") + if serverName == "" { + ctx.JSON(http.StatusBadRequest, server_structs.SimpleApiResp{ + Status: server_structs.RespFailed, + Msg: "Server name is required", + }) + return + } + server := getServer(serverName) + if server == nil { + ctx.JSON(http.StatusNotFound, server_structs.SimpleApiResp{ + Status: server_structs.RespFailed, + Msg: "Server not found", + }) + return + } + serverResponse := advertisementToServerResponse(server) + ctx.JSON(http.StatusOK, serverResponse) +} + +// Get all namespaces for a server +func listServerNamespaces(ctx *gin.Context) { + serverName := ctx.Param("name") + if serverName == "" { + ctx.JSON(http.StatusBadRequest, server_structs.SimpleApiResp{ + Status: server_structs.RespFailed, + Msg: "Server name is required", + }) + return + } + server := getServer(serverName) + if server == nil { + ctx.JSON(http.StatusNotFound, server_structs.SimpleApiResp{ + Status: server_structs.RespFailed, + Msg: "Server not found", + }) + return + } + var nsRes []NamespaceAdV2Response + for _, n := range server.NamespaceAds { + nsRes = append(nsRes, namespaceAdV2ToResponse(&n)) + } + ctx.JSON(http.StatusOK, nsRes) +} + +// Get list of all namespaces as a response +func listNamespaceResponses() []NamespaceAdV2MappedResponse { + + namespaceMap := make(map[string]NamespaceAdV2MappedResponse) + + for _, a := range listAdvertisement([]server_structs.ServerType{server_structs.OriginType, server_structs.CacheType}) { + s := a.ServerAd + for _, ns := range a.NamespaceAds { + + // If the namespace is not in the map, add it + if _, ok := namespaceMap[ns.Path]; !ok { + namespaceMap[ns.Path] = namespaceAdV2ToMappedResponse(&ns) + } + + // Add the server name to its type + nsRes := namespaceMap[ns.Path] + if s.Type == server_structs.OriginType.String() { + nsRes.Origins = append(nsRes.Origins, s.Name) + } else if s.Type == server_structs.CacheType.String() { + nsRes.Caches = append(nsRes.Caches, s.Name) + } + namespaceMap[ns.Path] = nsRes } - resList = append(resList, res) } - ctx.JSON(http.StatusOK, resList) + + return maps.Values(namespaceMap) +} + +// Get list of all namespaces +func listNamespacesHandler(ctx *gin.Context) { + ctx.JSON(http.StatusOK, listNamespaceResponses()) } // Issue a stat query to origins for an object and return which origins serve the object @@ -366,10 +587,13 @@ func RegisterDirectorWebAPI(router *gin.RouterGroup) { // Follow RESTful schema { directorWebAPI.GET("/servers", listServers) + directorWebAPI.GET("/servers/:name", getServerHandler) + directorWebAPI.GET("/servers/:name/namespaces", listServerNamespaces) directorWebAPI.PATCH("/servers/filter/*name", web_ui.AuthHandler, web_ui.AdminAuthHandler, handleFilterServer) directorWebAPI.PATCH("/servers/allow/*name", web_ui.AuthHandler, web_ui.AdminAuthHandler, handleAllowServer) directorWebAPI.GET("/servers/origins/stat/*path", web_ui.AuthHandler, queryOrigins) directorWebAPI.HEAD("/servers/origins/stat/*path", web_ui.AuthHandler, queryOrigins) + directorWebAPI.GET("/namespaces", listNamespacesHandler) directorWebAPI.GET("/contact", handleDirectorContact) } } diff --git a/director/director_ui_test.go b/director/director_ui_test.go index 853daa260..2102bede3 100644 --- a/director/director_ui_test.go +++ b/director/director_ui_test.go @@ -169,3 +169,241 @@ func TestListServers(t *testing.T) { require.Equal(t, 400, w.Code) }) } + +func TestGetServer(t *testing.T) { + router := gin.Default() + + router.GET("/servers/:name", getServerHandler) + router.GET("/servers/:name/namespaces", listServerNamespaces) + + serverAds.DeleteAll() + mockOriginNamespace := mockNamespaceAds(5, "origin1") + mockCacheNamespace := mockNamespaceAds(4, "cache1") + serverAds.Set(mockOriginServerAd.URL.String(), + &server_structs.Advertisement{ + ServerAd: mockOriginServerAd, + NamespaceAds: mockOriginNamespace, + }, ttlcache.DefaultTTL) + serverAds.Set(mockCacheServerAd.URL.String(), + &server_structs.Advertisement{ + ServerAd: mockCacheServerAd, + NamespaceAds: mockCacheNamespace, + }, ttlcache.DefaultTTL) + + require.True(t, serverAds.Has(mockOriginServerAd.URL.String())) + require.True(t, serverAds.Has(mockCacheServerAd.URL.String())) + + expectedListOriginResNss := []NamespaceAdV2Response{} + for _, ns := range mockOriginNamespace { + expectedListOriginResNss = append(expectedListOriginResNss, namespaceAdV2ToResponse(&ns)) + } + + expectedListCacheResNss := []NamespaceAdV2Response{} + for _, ns := range mockCacheNamespace { + expectedListCacheResNss = append(expectedListCacheResNss, namespaceAdV2ToResponse(&ns)) + } + + expectedlistOriginRes := serverResponse{ + Name: mockOriginServerAd.Name, + BrokerURL: mockOriginServerAd.BrokerURL.String(), + AuthURL: "", + URL: mockOriginServerAd.URL.String(), + WebURL: mockOriginServerAd.WebURL.String(), + Type: mockOriginServerAd.Type, + Latitude: mockOriginServerAd.Latitude, + Longitude: mockOriginServerAd.Longitude, + Caps: mockOriginServerAd.Caps, + FromTopology: mockOriginServerAd.FromTopology, + HealthStatus: HealthStatusUnknown, + Namespaces: expectedListOriginResNss, + } + + expectedlistCacheRes := serverResponse{ + Name: mockCacheServerAd.Name, + BrokerURL: mockCacheServerAd.BrokerURL.String(), + AuthURL: "", + URL: mockCacheServerAd.URL.String(), + WebURL: mockCacheServerAd.WebURL.String(), + Type: mockCacheServerAd.Type, + Latitude: mockCacheServerAd.Latitude, + Longitude: mockCacheServerAd.Longitude, + Caps: mockCacheServerAd.Caps, + FromTopology: mockCacheServerAd.FromTopology, + HealthStatus: HealthStatusUnknown, + Namespaces: expectedListCacheResNss, + } + + t.Run("get-origin", func(t *testing.T) { + // Create a request to the endpoint + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/servers/"+mockOriginServerAd.Name, nil) + router.ServeHTTP(w, req) + + // Check the response + require.Equal(t, 200, w.Code) + + var got serverResponse + err := json.Unmarshal(w.Body.Bytes(), &got) + require.NoError(t, err) + assert.Equal(t, expectedlistOriginRes, got, "Response data does not match expected") + }) + + t.Run("get-cache", func(t *testing.T) { + // Create a request to the endpoint + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/servers/"+mockCacheServerAd.Name, nil) + router.ServeHTTP(w, req) + + // Check the response + require.Equal(t, 200, w.Code) + + var got serverResponse + err := json.Unmarshal(w.Body.Bytes(), &got) + require.NoError(t, err) + assert.Equal(t, expectedlistCacheRes, got, "Response data does not match expected") + }) + + t.Run("get-non-existent-server", func(t *testing.T) { + // Create a request to the endpoint + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/servers/non-existent-server", nil) + router.ServeHTTP(w, req) + + // Check the response + require.Equal(t, 404, w.Code) + }) + + t.Run("get-namespaces-of-server", func(t *testing.T) { + // Create a request to the endpoint + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/servers/"+mockOriginServerAd.Name+"/namespaces", nil) + router.ServeHTTP(w, req) + + // Check the response + require.Equal(t, 200, w.Code) + + // Check the data + var got []NamespaceAdV2Response + err := json.Unmarshal(w.Body.Bytes(), &got) + require.NoError(t, err) + require.Equal(t, len(mockOriginNamespace), len(got)) + for i := range got { + assert.Equal(t, namespaceAdV2ToResponse(&mockOriginNamespace[i]), got[i], "Response data does not match expected") + } + }) + + t.Run("get-namespaces-of-non-existent-server", func(t *testing.T) { + // Create a request to the endpoint + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/servers/non-existent-server/namespaces", nil) + router.ServeHTTP(w, req) + + // Check the response + require.Equal(t, 404, w.Code) + }) +} + +func TestGetNamespaces(t *testing.T) { + router := gin.Default() + + router.GET("/namespaces", listNamespacesHandler) + + serverAds.DeleteAll() + mockOriginNamespace := mockNamespaceAds(5, "origin1") + mockCacheNamespace := mockNamespaceAds(4, "cache1") + serverAds.Set(mockOriginServerAd.URL.String(), + &server_structs.Advertisement{ + ServerAd: mockOriginServerAd, + NamespaceAds: mockOriginNamespace, + }, ttlcache.DefaultTTL) + serverAds.Set(mockCacheServerAd.URL.String(), + &server_structs.Advertisement{ + ServerAd: mockCacheServerAd, + NamespaceAds: mockCacheNamespace, + }, ttlcache.DefaultTTL) + + t.Run("get-all-namespaces", func(t *testing.T) { + // Create a request to the endpoint + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/namespaces", nil) + router.ServeHTTP(w, req) + + // Check the response + require.Equal(t, 200, w.Code) + + // Check the data + var got []NamespaceAdV2MappedResponse + err := json.Unmarshal(w.Body.Bytes(), &got) + require.NoError(t, err) + require.Equal(t, len(mockOriginNamespace)+len(mockCacheNamespace), len(got)) + + // Create the list of expected responses we should see by adding origin/cache names + var expected []NamespaceAdV2MappedResponse + for _, ns := range mockOriginNamespace { + nsRes := namespaceAdV2ToMappedResponse(&ns) + nsRes.Origins = append(nsRes.Origins, mockOriginServerAd.Name) + expected = append(expected, nsRes) + } + for _, ns := range mockCacheNamespace { + nsRes := namespaceAdV2ToMappedResponse(&ns) + nsRes.Caches = append(nsRes.Caches, mockCacheServerAd.Name) + expected = append(expected, nsRes) + } + + // Check that the namespaces are as expected + for _, ns := range expected { + assert.Contains(t, got, ns, "Response data does not match expected") + } + }) + + t.Run("get-all-namespaces-crossover", func(t *testing.T) { + + // Set things up with namespaces that cross over between origin and cache + serverAds.DeleteAll() + mockNamespaceSet0 := mockNamespaceAds(5, "origin1") + mockNamespaceSet1 := mockNamespaceAds(4, "cache1") + serverAds.Set(mockOriginServerAd.URL.String(), + &server_structs.Advertisement{ + ServerAd: mockOriginServerAd, + NamespaceAds: append(mockNamespaceSet0, mockNamespaceSet1...), + }, ttlcache.DefaultTTL) + serverAds.Set(mockCacheServerAd.URL.String(), + &server_structs.Advertisement{ + ServerAd: mockCacheServerAd, + NamespaceAds: mockNamespaceSet0, + }, ttlcache.DefaultTTL) + + // Create a request to the endpoint + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/namespaces", nil) + router.ServeHTTP(w, req) + + // Check the response + require.Equal(t, 200, w.Code) + + // Check the data + var got []NamespaceAdV2MappedResponse + err := json.Unmarshal(w.Body.Bytes(), &got) + require.NoError(t, err) + require.Equal(t, len(mockNamespaceSet0)+len(mockNamespaceSet1), len(got)) + + // Create the list of expected responses we should see by adding origin/cache names + expected := make(map[string]NamespaceAdV2MappedResponse) + for _, ns := range append(mockNamespaceSet1, mockNamespaceSet0...) { + nsRes := namespaceAdV2ToMappedResponse(&ns) + nsRes.Origins = append(nsRes.Origins, mockOriginServerAd.Name) + expected[nsRes.Path] = nsRes + } + // Going to cheat a bit here and use that fact that I know origins superset cache namespaces + for _, ns := range mockNamespaceSet0 { + nsMappedRes := expected[ns.Path] + nsMappedRes.Caches = append(nsMappedRes.Caches, mockCacheServerAd.Name) + expected[ns.Path] = nsMappedRes + } + + // Check that the namespaces are as expected + for _, ns := range expected { + assert.Contains(t, got, ns, "Response data does not match expected") + } + }) +} diff --git a/swagger/pelican-swagger.yaml b/swagger/pelican-swagger.yaml index 56f37f3cb..140bf5141 100644 --- a/swagger/pelican-swagger.yaml +++ b/swagger/pelican-swagger.yaml @@ -235,6 +235,70 @@ definitions: custom_fields: type: object description: The custom fields user registered, configurable by setting Registry.CustomRegistrationFields. + TokenGeneration: + type: object + properties: + strategy: + type: string + example: "OAuth2" + vault-server: + type: string + max-scope-depth: + type: integer + issuer: + type: string + format: uri + TokenIssuer: + type: object + properties: + issuer: + type: string + format: uri + base-paths: + type: array + items: + type: string + restricted-paths: + type: array + items: + type: string + NamespaceAdV2: + type: object + properties: + Caps: + type: object + $ref: "#/definitions/OriginExportCapabilities" + path: + type: string + example: "/barten/time" + token-generation: + type: array + items: + type: object + $ref: "#/definitions/TokenGeneration" + token-issuer: + type: array + items: + type: object + $ref: "#/definitions/TokenIssuer" + from-topology: + type: boolean + example: false + NamespaceAdV2Mapped: + allOf: + - $ref: "#/definitions/NamespaceAdV2" + - type: object + properties: + origins: + type: array + items: + type: string + example: ["example-origin.com"] + caches: + type: array + items: + type: string + example: ["example-cache.com"] NamespaceForRegistration: type: object required: @@ -1639,6 +1703,78 @@ paths: schema: type: object $ref: "#/definitions/ErrorModelV2" + /director_ui/servers/{name}: + get: + tags: + - "director_ui" + summary: Get details of a specific server + parameters: + - name: name + in: path + required: true + type: string + description: The name of the server + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: "#/definitions/DirectorServerResponseDetailed" + "400": + description: Bad request, invalid server name + schema: + $ref: "#/definitions/ErrorModelV2" + "404": + description: Server not found + schema: + $ref: "#/definitions/ErrorModelV2" + /director_ui/servers/{name}/namespaces: + get: + tags: + - "director_ui" + summary: Get a list of namespaces for a specific server + parameters: + - name: name + in: path + required: true + type: string + description: The name of the server + produces: + - application/json + responses: + "200": + description: OK + schema: + type: array + items: + $ref: "#/definitions/NamespaceAdV2" + "400": + description: Bad request, invalid server name + schema: + $ref: "#/definitions/ErrorModelV2" + "404": + description: Server not found + schema: + $ref: "#/definitions/ErrorModelV2" + /director_ui/namespaces: + get: + tags: + - "director_ui" + summary: Get a list of namespaces advertised to the director + produces: + - application/json + responses: + "200": + description: OK + schema: + type: array + items: + $ref: "#/definitions/NamespaceAdV2Mapped" + "400": + description: Bad request + schema: + $ref: "#/definitions/ErrorModelV2" /director_ui/servers/filter/{name}: patch: summary: Filter a server from director redirecting From 6062afee5308f53d21b8e3201616168dff0b210c Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Thu, 14 Nov 2024 08:43:24 -0600 Subject: [PATCH 10/16] Use homegrown map to slice converter --- director/director_ui.go | 3 ++- utils/utils.go | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/director/director_ui.go b/director/director_ui.go index c4d433994..ac9cde612 100644 --- a/director/director_ui.go +++ b/director/director_ui.go @@ -29,6 +29,7 @@ import ( "github.com/pelicanplatform/pelican/param" "github.com/pelicanplatform/pelican/server_structs" + "github.com/pelicanplatform/pelican/utils" "github.com/pelicanplatform/pelican/web_ui" ) @@ -362,7 +363,7 @@ func listNamespaceResponses() []NamespaceAdV2MappedResponse { } } - return maps.Values(namespaceMap) + return utils.MapToSlice(namespaceMap) } // Get list of all namespaces diff --git a/utils/utils.go b/utils/utils.go index 87b589ab4..3915f47f4 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -144,3 +144,12 @@ func ExtractProjectFromUserAgent(userAgents []string) string { return "" } + +// Convert map to slice of values +func MapToSlice[K comparable, V any](m map[K]V) []V { + s := make([]V, 0, len(m)) + for _, v := range m { + s = append(s, v) + } + return s +} From eb02c40f86074ec4fc5be9a278728a0b729dfaff Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Thu, 14 Nov 2024 13:12:29 -0600 Subject: [PATCH 11/16] camelCase api json response keys --- director/director_ui.go | 20 ++++++++++---------- swagger/pelican-swagger.yaml | 18 +++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/director/director_ui.go b/director/director_ui.go index ac9cde612..79e343d83 100644 --- a/director/director_ui.go +++ b/director/director_ui.go @@ -88,16 +88,16 @@ type ( // TokenIssuerResponse creates a response struct for TokenIssuer TokenIssuerResponse struct { - BasePaths []string `json:"base-paths"` - RestrictedPaths []string `json:"restricted-paths"` + BasePaths []string `json:"basePaths"` + RestrictedPaths []string `json:"restrictedPaths"` IssuerUrl string `json:"issuer"` } // TokenGenResponse creates a response struct for TokenGen TokenGenResponse struct { Strategy server_structs.StrategyType `json:"strategy"` - VaultServer string `json:"vault-server"` - MaxScopeDepth uint `json:"max-scope-depth"` + VaultServer string `json:"vaultServer"` + MaxScopeDepth uint `json:"maxScopeDepth"` CredentialIssuer string `json:"issuer"` } @@ -105,18 +105,18 @@ type ( NamespaceAdV2Response struct { Path string `json:"path"` Caps server_structs.Capabilities `json:"capabilities"` - Generation []TokenGenResponse `json:"token-generation"` - Issuer []TokenIssuerResponse `json:"token-issuer"` - FromTopology bool `json:"from-topology"` + Generation []TokenGenResponse `json:"tokenGeneration"` + Issuer []TokenIssuerResponse `json:"tokenIssuer"` + FromTopology bool `json:"fromTopology"` } // NamespaceAdV2MappedResponse creates a response struct for NamespaceAdV2 with mapped origins and caches NamespaceAdV2MappedResponse struct { Path string `json:"path"` Caps server_structs.Capabilities `json:"capabilities"` - Generation []TokenGenResponse `json:"token-generation"` - Issuer []TokenIssuerResponse `json:"token-issuer"` - FromTopology bool `json:"from-topology"` + Generation []TokenGenResponse `json:"tokenGeneration"` + Issuer []TokenIssuerResponse `json:"tokenIssuer"` + FromTopology bool `json:"fromTopology"` Origins []string `json:"origins"` Caches []string `json:"caches"` } diff --git a/swagger/pelican-swagger.yaml b/swagger/pelican-swagger.yaml index 140bf5141..55dc1a258 100644 --- a/swagger/pelican-swagger.yaml +++ b/swagger/pelican-swagger.yaml @@ -241,9 +241,9 @@ definitions: strategy: type: string example: "OAuth2" - vault-server: + vaultServer: type: string - max-scope-depth: + maxScopeDepth: type: integer issuer: type: string @@ -254,11 +254,11 @@ definitions: issuer: type: string format: uri - base-paths: + basePaths: type: array items: type: string - restricted-paths: + restrictedPaths: type: array items: type: string @@ -271,17 +271,17 @@ definitions: path: type: string example: "/barten/time" - token-generation: + tokenGeneration: type: array items: type: object $ref: "#/definitions/TokenGeneration" - token-issuer: + tokenIssuer: type: array items: type: object $ref: "#/definitions/TokenIssuer" - from-topology: + fromTopology: type: boolean example: false NamespaceAdV2Mapped: @@ -462,7 +462,7 @@ definitions: type: string description: Type of the server. Origin|Cache example: Origin - latitute: + latitude: type: number description: The latitute of the server based on its IP address default: 0 @@ -1720,7 +1720,7 @@ paths: "200": description: OK schema: - $ref: "#/definitions/DirectorServerResponseDetailed" + $ref: "#/definitions/DirectorServerResponse" "400": description: Bad request, invalid server name schema: From 595b1483066a78e384456d51a11b3cd32cecbd8c Mon Sep 17 00:00:00 2001 From: Patrick Date: Fri, 15 Nov 2024 16:21:50 +0000 Subject: [PATCH 12/16] Remove unnecessary metric collection call --- director/director.go | 1 - 1 file changed, 1 deletion(-) diff --git a/director/director.go b/director/director.go index 07dd24cb9..f8f8f6344 100644 --- a/director/director.go +++ b/director/director.go @@ -344,7 +344,6 @@ func redirectToCache(ginCtx *gin.Context) { collectDirectorRedirectionMetric(ginCtx, "cache") } }() - defer collectDirectorRedirectionMetric(ginCtx, "cache") err := versionCompatCheck(reqVer, service) if err != nil { log.Warningf("A version incompatibility was encountered while redirecting to a cache and no response was served: %v", err) From b8ea982d6e54f19a5b762c5c52a20e8dbb5e6229 Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Fri, 15 Nov 2024 17:12:07 +0000 Subject: [PATCH 13/16] Add director dblocation defaults --- config/config.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/config.go b/config/config.go index 7a222380e..245c2edba 100644 --- a/config/config.go +++ b/config/config.go @@ -970,6 +970,7 @@ func SetServerDefaults(v *viper.Viper) error { v.SetDefault(param.Origin_DbLocation.GetName(), "/var/lib/pelican/origin.sqlite") v.SetDefault(param.Director_GeoIPLocation.GetName(), "/var/cache/pelican/maxmind/GeoLite2-City.mmdb") v.SetDefault(param.Registry_DbLocation.GetName(), "/var/lib/pelican/registry.sqlite") + v.SetDefault(param.Director_DbLocation.GetName(), "/var/lib/pelican/director.sqlite") // The lotman db will actually take this path and create the lot at /path/.lot/lotman_cpp.sqlite v.SetDefault(param.Lotman_DbLocation.GetName(), "/var/lib/pelican") v.SetDefault(param.Monitoring_DataLocation.GetName(), "/var/lib/pelican/monitoring/data") @@ -980,6 +981,7 @@ func SetServerDefaults(v *viper.Viper) error { v.SetDefault(param.Origin_DbLocation.GetName(), filepath.Join(configDir, "origin.sqlite")) v.SetDefault(param.Director_GeoIPLocation.GetName(), filepath.Join(configDir, "maxmind", "GeoLite2-City.mmdb")) v.SetDefault(param.Registry_DbLocation.GetName(), filepath.Join(configDir, "ns-registry.sqlite")) + v.SetDefault(param.Director_DbLocation.GetName(), filepath.Join(configDir, "director.sqlite")) // Lotdb will live at /.lot/lotman_cpp.sqlite v.SetDefault(param.Lotman_DbLocation.GetName(), configDir) v.SetDefault(param.Monitoring_DataLocation.GetName(), filepath.Join(configDir, "monitoring/data")) From f6fa24f7856ceddfa848bfe4e7a831d76b0d14e6 Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Thu, 14 Nov 2024 20:53:27 +0000 Subject: [PATCH 14/16] Set default value for empty stringSlice to [] in docs --- docs/parameters.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/parameters.yaml b/docs/parameters.yaml index 47e263d9c..028a661d2 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -44,7 +44,7 @@ description: |+ Subdirectories of the provided directories are not read. Only the root config file's `ConfigLocations` key is used, and any redefinitions are ignored. type: stringSlice -default: none +default: [] components: ["*"] --- name: Debug @@ -566,7 +566,7 @@ description: |+ This configuration is meant mostly to be used by passing the -v flag from the command line. Paths exported with this configuration will inherit the origin's abilities, so individual export configurations are not possible. type: stringSlice -default: none +default: [] components: ["origin"] --- name: Origin.EnablePublicReads @@ -765,7 +765,7 @@ name: Origin.ScitokensRestrictedPaths description: |+ Enable the built-in issuer daemon for the origin. type: stringSlice -default: none +default: [] components: ["origin"] --- name: Origin.ScitokensMapSubject @@ -1210,7 +1210,7 @@ description: |+ the cache is allowed to access any namespace that's advertised to the director. Otherwise, it will only be allowed to access the listed namespaces. type: stringSlice -default: none +default: [] components: ["cache"] --- name: Cache.SelfTest @@ -1285,7 +1285,7 @@ description: |+ If present, the hostname is taken from the X-Forwarded-Host header in the request. Otherwise, Host is used. type: stringSlice -default: none +default: [] components: ["director"] --- name: Director.CacheSortMethod @@ -1314,7 +1314,7 @@ description: |+ If present, the hostname is taken from the X-Forwarded-Host header in the request. Otherwise, Host is used. type: stringSlice -default: none +default: [] components: ["director"] --- name: Director.MaxMindKeyFile @@ -1447,7 +1447,7 @@ description: |+ A list of server resource names that the Director should consider in downtime, preventing the Director from issuing redirects to them. Additional downtimes are aggregated from Topology (when the Director is served in OSDF mode), and the Web UI. type: stringSlice -default: none +default: [] components: ["director"] --- name: Director.SupportContactEmail @@ -1489,7 +1489,7 @@ description: |+ This setting allows for compatibility with specific legacy OSDF origins and is not needed for new origins. type: stringSlice -default: none +default: [] components: ["director"] hidden: true --- @@ -1852,7 +1852,7 @@ description: |+ The "subject" claim should be the "CILogon User Identifier" from CILogon user page: https://cilogon.org/ type: stringSlice -default: none +default: [] components: ["registry","origin","cache"] --- name: Server.StartupTimeout @@ -2477,7 +2477,7 @@ name: Shoveler.OutputDestinations description: |+ A list of destinations to forward XRootD monitoring packet to. type: stringSlice -default: none +default: [] components: ["origin", "cache"] --- name: Shoveler.VerifyHeader From 48e18b5a3271d4290830dd8b6133772fc53b7ffe Mon Sep 17 00:00:00 2001 From: Emma Turetsky Date: Mon, 18 Nov 2024 16:18:21 +0000 Subject: [PATCH 15/16] Fixes an issue where a misplaced `defer ticker.Stop()` stopped the re-advertisement of caches/origins --- launcher_utils/advertise.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/launcher_utils/advertise.go b/launcher_utils/advertise.go index 6daddad55..4ca803ccd 100644 --- a/launcher_utils/advertise.go +++ b/launcher_utils/advertise.go @@ -68,9 +68,8 @@ func LaunchPeriodicAdvertise(ctx context.Context, egrp *errgroup.Group, servers doAdvertise(ctx, servers) ticker := time.NewTicker(1 * time.Minute) - defer ticker.Stop() egrp.Go(func() error { - + defer ticker.Stop() for { select { case <-ticker.C: From 1081270636ac56481740bd676f1a943efb7ddee8 Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Mon, 18 Nov 2024 13:22:12 -0600 Subject: [PATCH 16/16] Redirect app to root on success password --- web_ui/frontend/app/(login)/initialization/password/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_ui/frontend/app/(login)/initialization/password/page.tsx b/web_ui/frontend/app/(login)/initialization/password/page.tsx index a35e18c3b..78027d89c 100644 --- a/web_ui/frontend/app/(login)/initialization/password/page.tsx +++ b/web_ui/frontend/app/(login)/initialization/password/page.tsx @@ -46,7 +46,7 @@ export default function Home() { dispatch ); if (response) { - router.push('../password/'); + router.push('/'); } else { setLoading(false); }