github.com/pelicanplatform/pelican@v1.0.5/web_ui/frontend/app/config/page.tsx (about) 1 /*************************************************************** 2 * 3 * Copyright (C) 2023, Pelican Project, Morgridge Institute for Research 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); you 6 * may not use this file except in compliance with the License. You may 7 * obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 ***************************************************************/ 18 19 "use client" 20 21 import RateGraph from "@/components/graphs/RateGraph"; 22 import StatusBox from "@/components/StatusBox"; 23 24 import {TimeDuration} from "@/components/graphs/prometheus"; 25 26 import { 27 Box, 28 FormControl, 29 Grid, 30 InputLabel, 31 MenuItem, 32 Select, 33 Typography, 34 Skeleton, 35 Link, 36 Container 37 } from "@mui/material"; 38 import React, {useEffect, useState} from "react"; 39 import {OverridableStringUnion} from "@mui/types"; 40 import {Variant} from "@mui/material/styles/createTypography"; 41 import {TypographyPropsVariantOverrides} from "@mui/material/Typography"; 42 import TextField from "@mui/material/TextField"; 43 import {ArrowDropDown, ArrowDropUp} from '@mui/icons-material'; 44 import {fontSize} from "@mui/system" 45 import {isLoggedIn} from "@/helpers/login"; 46 47 type duration = number | `${number}${"ns" | "us" | "µs" | "ms" |"s" | "m" | "h"}`; 48 49 export type Config = { 50 [key: string]: ConfigValue 51 } 52 53 type ConfigValue = Config | string | number | boolean | null | string[] | number[] | duration 54 55 56 function sortConfig (a: [string, ConfigValue], b: [string, ConfigValue]) { 57 58 let isConfig = (value: ConfigValue) => { return typeof value == 'object' && value !== null && !Array.isArray(value)} 59 60 if(isConfig(a[1]) && !isConfig(b[1])){ 61 return 1 62 } 63 if(!isConfig(a[1]) && isConfig(b[1])){ 64 return -1 65 } 66 return a[0].localeCompare(b[0]) 67 } 68 69 70 interface ConfigDisplayProps { 71 id: string[] 72 name: string 73 value: Config | ConfigValue 74 level: number 75 } 76 77 function ConfigDisplay({id, name, value, level = 1}: ConfigDisplayProps) { 78 79 if(name != "") { 80 id = [...id, name] 81 } 82 83 let formElement = undefined 84 85 if( 86 typeof value === 'string' || typeof value === 'number' || value === null 87 ){ 88 89 // Convert empty values to a space so that the text field is not collapsed 90 switch (value){ 91 case "": 92 value = " " 93 break 94 case null: 95 value = "None" 96 } 97 98 formElement = <TextField 99 fullWidth 100 disabled 101 size="small" 102 id={`${id.join("-")}-text-input`} 103 label={name} 104 variant={"outlined"} 105 value={value} 106 /> 107 } else if(Array.isArray(value)){ 108 formElement = <TextField 109 fullWidth 110 disabled 111 size="small" 112 id={`${id.join("-")}-text-input`} 113 label={name} 114 variant={"outlined"} 115 value={value.join(", ")} 116 /> 117 } else if(typeof value === 'boolean'){ 118 formElement = ( 119 <FormControl fullWidth> 120 <InputLabel id={`${id.join("-")}-number-input`}>{name}</InputLabel> 121 <Select 122 disabled 123 size="small" 124 labelId={`${id.join("-")}-number-input-label`} 125 id={`${id.join("-")}-number-input`} 126 label={name} 127 value={value ? 1 : 0} 128 > 129 <MenuItem value={1}>True</MenuItem> 130 <MenuItem value={0}>False</MenuItem> 131 </Select> 132 </FormControl> 133 ) 134 } 135 136 if(formElement !== undefined){ 137 return ( 138 <Box pt={2} id={id.join("-")}> 139 {formElement} 140 </Box> 141 ) 142 } 143 144 let subValues = Object.entries(value) 145 subValues.sort(sortConfig) 146 147 let configDisplays = subValues.map(([k, v]) => {return <ConfigDisplay id={id} key={k} name={k} value={v} level={level+1}/>}) 148 149 let variant: OverridableStringUnion<"inherit" | Variant, TypographyPropsVariantOverrides> 150 switch (level) { 151 case 1: 152 variant = "h1" 153 break 154 case 2: 155 variant = "h2" 156 break 157 case 3: 158 variant = "h3" 159 break 160 case 4: 161 variant = "h4" 162 break 163 case 5: 164 variant = "h5" 165 break 166 case 6: 167 variant = "h6" 168 break 169 default: 170 variant = "h6" 171 } 172 173 174 return ( 175 <> 176 { name ? <Typography id={id.join("-")} variant={variant} component={variant} mt={2}>{name}</Typography> : undefined} 177 {configDisplays} 178 </> 179 ) 180 181 } 182 183 interface TableOfContentsProps { 184 id: string[] 185 name: string 186 value: Config | ConfigValue 187 level: number 188 } 189 190 function TableOfContents({id, name, value, level = 1}: TableOfContentsProps) { 191 192 const [open, setOpen] = useState(false) 193 194 if(name != "") { 195 id = [...id, name] 196 } 197 198 let subContents = undefined 199 if(value !== null && !Array.isArray(value) && typeof value == 'object'){ 200 let subValues = Object.entries(value) 201 subValues.sort(sortConfig) 202 subContents = subValues.map(([key, value]) => { 203 return <TableOfContents id={id} key={key} name={key} value={value} level={level+1}/> 204 }) 205 } 206 207 let headerPointer = ( 208 <Box 209 sx={{ 210 "&:hover": { 211 backgroundColor: "primary.light", 212 }, 213 borderRadius: 1, 214 paddingX: "5px", 215 paddingLeft: 0+5*level + "px" 216 }} 217 > 218 <Link 219 href={subContents ? undefined : `#${id.join("-")}`} 220 sx={{ 221 cursor: "pointer", 222 textDecoration: "none", 223 color: "black", 224 display: "flex", 225 flexDirection: "row", 226 justifyContent: "space-between", 227 }} 228 onClick={() => { 229 setOpen(!open) 230 }} 231 > 232 <Typography 233 style={{ 234 fontSize: 20 - 2*level + "px", 235 fontWeight: subContents ? "600" : "normal", 236 }} 237 > 238 {name} 239 </Typography> 240 { 241 subContents ? 242 open ? <ArrowDropUp/> : <ArrowDropDown/> : 243 undefined 244 } 245 </Link> 246 </Box> 247 ) 248 249 return ( 250 <> 251 { name ? headerPointer : undefined} 252 { subContents && level != 1 ? 253 <Box sx={{ 254 display: open ? "block" : "none", 255 cursor: "pointer" 256 }} 257 > 258 {subContents} 259 </Box> : 260 subContents 261 } 262 </> 263 ) 264 } 265 266 export default function Config() { 267 268 const [config, setConfig] = useState<Config|undefined>(undefined) 269 const [error, setError] = useState<string|undefined>(undefined) 270 271 let getConfig = async () => { 272 273 //Check if the user is logged in 274 if(!(await isLoggedIn())){ 275 window.location.replace("/view/login/") 276 } 277 278 let response = await fetch("/api/v1.0/config") 279 if(response.ok) { 280 setConfig(await response.json()) 281 } else { 282 setError("Failed to fetch config, response status: " + response.status) 283 } 284 } 285 286 useEffect(() => { 287 getConfig() 288 }, []) 289 290 291 if(error){ 292 return ( 293 <Box width={"100%"}> 294 <Typography variant={"h4"} component={"h2"} mb={1}>Configuration</Typography> 295 <Typography color={"error"} variant={"body1"} component={"p"} mb={1}>Error: {error}</Typography> 296 </Box> 297 ) 298 } 299 300 return ( 301 <Container maxWidth={"xl"} sx={{"mt": 2 }}> 302 <Box width={"100%"}> 303 <Grid container spacing={2}> 304 <Grid item xs={7} md={8} lg={6}> 305 <Typography variant={"h4"} component={"h2"} mb={1}>Configuration</Typography> 306 </Grid> 307 <Grid item xs={5} md={4} lg={3}></Grid> 308 <Grid item xs={7} md={8} lg={6}> 309 <form> 310 { 311 config === undefined ? 312 <Skeleton variant="rectangular" animation="wave" height={"1000px"}/> : 313 <ConfigDisplay id={[]} name={""} value={config} level={4}/> 314 } 315 </form> 316 </Grid> 317 <Grid item xs={5} md={4} lg={3}> 318 { 319 config === undefined ? 320 <Skeleton variant="rectangular" animation="wave" height={"1000px"}/> : 321 <Box pt={2}><TableOfContents id={[]} name={""} value={config} level={1}/></Box> 322 } 323 </Grid> 324 </Grid> 325 </Box> 326 </Container> 327 328 ) 329 }