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  }