github.com/pluralsh/plural-cli@v0.9.5/pkg/ui/web/src/routes/installer/ConfigurationItem.tsx (about)

     1  import { FormField, Input, useActive } from '@pluralsh/design-system'
     2  import { Switch } from 'honorable'
     3  import StartCase from 'lodash/startCase'
     4  import {
     5    useContext,
     6    useEffect,
     7    useMemo,
     8    useState,
     9  } from 'react'
    10  
    11  import { WailsContext } from '../../context/wails'
    12  import { Datatype } from '../../graphql/generated/graphql'
    13  import { PluralProject } from '../../types/client'
    14  
    15  import ConfigurationFileInput from './ConfigurationFileInput'
    16  import { InstallerContext } from './context'
    17  
    18  type ModifierFunction = (value: string, trim?: boolean) => string
    19  
    20  const modifierFactory = (type: Datatype, project: PluralProject): ModifierFunction => {
    21    switch (type) {
    22    case Datatype.String:
    23    case Datatype.Int:
    24    case Datatype.Password:
    25      return stringModifier
    26    case Datatype.Bucket:
    27      return bucketModifier.bind({ project })
    28    case Datatype.Domain:
    29      return domainModifier.bind({ project })
    30    }
    31  
    32    return stringModifier
    33  }
    34  
    35  const stringModifier = value => value
    36  
    37  function bucketModifier(this: {project: PluralProject}, value: string, trim = false) {
    38    const { project } = this
    39    const bucketPrefix = project?.bucketPrefix
    40    const cluster = project?.cluster
    41    const prefix = `${bucketPrefix}-${cluster}-`
    42  
    43    if (trim) return value?.replace(prefix, '')
    44  
    45    return bucketPrefix && cluster ? `${prefix}${value}` : value
    46  }
    47  function domainModifier(this: {project: PluralProject}, value: string, trim = false) {
    48    const { project } = this
    49    const subdomain = project?.network?.subdomain || ''
    50    const suffix = subdomain ? `.${subdomain}` : ''
    51  
    52    if (trim) return value?.replace(suffix, '')
    53  
    54    return subdomain ? `${value}${suffix}` : value
    55  }
    56  
    57  const createValidator = (regex: RegExp, optional: boolean, error: string) => (value): {valid: boolean, message: string} => ({
    58    valid: (value ? regex.test(value) : optional),
    59    message: error,
    60  })
    61  
    62  /**
    63   * Creates validator for domain uniqueness check.
    64   *
    65   * @param ctx - object that maps field name to an object field with value, validity, etc.
    66   * @param fieldName - field name being checked
    67   * @param appName - active application name
    68   * @param registeredDomains - a set of domains used by already installed applications
    69   * @param usedDomains - object that maps key (appName-fieldName) to the domain name.
    70   *                      It is basically a list of unique domains used by the installer locally.
    71   */
    72  const createUniqueDomainValidator
    73    = (
    74      ctx: Record<string, any>,
    75      fieldName: string,
    76      appName: string,
    77      registeredDomains: Set<string>,
    78      usedDomains: Record<string, string>
    79    ) => (value): { valid: boolean; message: string } => {
    80      const domains = new Set<string>(registeredDomains)
    81  
    82      Object.entries(ctx)
    83        .filter(([name, field]) => field.type === Datatype.Domain
    84              && name !== fieldName
    85              && field.value?.length > 0)
    86        .forEach(([_, field]) => domains.add(field.value))
    87  
    88      Object.entries(usedDomains)
    89        .filter(([key]) => key !== domainFieldKey(appName, fieldName))
    90        .forEach(([_, domain]) => domains.add(domain))
    91  
    92      return {
    93        valid: !domains.has(value),
    94        message: `Domain ${value} already used.`,
    95      }
    96    }
    97  
    98  const domainFieldKey = (appName, fieldName) => `${appName}-${fieldName}`
    99  
   100  function ConfigurationField({
   101    config, ctx, setValue,
   102  }) {
   103    const {
   104      name,
   105      default: defaultValue,
   106      placeholder,
   107      documentation,
   108      validation,
   109      optional,
   110      type,
   111    } = config
   112    const { project, context } = useContext(WailsContext)
   113    const { domains, setDomains } = useContext(InstallerContext)
   114    const { active } = useActive()
   115  
   116    const value = useMemo(() => ctx[name]?.value, [ctx, name])
   117    const validators = useMemo(() => [
   118      createValidator(new RegExp(validation?.regex ? `^${validation?.regex}$` : /.*/),
   119        config.optional,
   120        validation?.message),
   121      ...(type === Datatype.Domain
   122        ? [
   123          createUniqueDomainValidator(
   124            ctx,
   125            name,
   126              active.label!,
   127              new Set<string>(context.domains ?? []),
   128              domains
   129          ),
   130        ]
   131        : []),
   132    ],
   133    [active.label, config.optional, context.domains, ctx, domains, name, type, validation?.message, validation?.regex])
   134    const { valid, message } = useMemo(() => {
   135      for (const validator of validators) {
   136        const result = validator(value)
   137  
   138        if (!result.valid) return result
   139      }
   140  
   141      return { valid: true, message: '' }
   142    }, [validators, value])
   143    const modifier = useMemo(() => modifierFactory(config.type, project),
   144      [config.type, project])
   145  
   146    const isFile = type === Datatype.File
   147  
   148    const [local, setLocal] = useState(modifier(value, true) || (isFile ? null : defaultValue))
   149  
   150    useEffect(() => (local
   151      ? setValue(
   152        name, modifier(local), valid, type
   153      )
   154      : setValue(
   155        name, local, valid, type
   156      )),
   157    [local, modifier, name, setValue, type, valid])
   158  
   159    useEffect(() => {
   160      if (type !== Datatype.Domain || !value) return
   161  
   162      setDomains(domains => ({
   163        ...domains,
   164        ...{ [domainFieldKey(active.label, name)]: value },
   165      }))
   166    }, [active.label, name, setDomains, type, value])
   167  
   168    const isInt = type === Datatype.Int
   169    const isPassword
   170        = type === Datatype.Password
   171        || ['private_key', 'public_key'].includes(config.name)
   172  
   173    const inputFieldType = isInt
   174      ? 'number'
   175      : isPassword
   176        ? 'password'
   177        : 'text'
   178  
   179    return (
   180      <FormField
   181        label={StartCase(name)}
   182        hint={message || documentation}
   183        error={!valid}
   184        required={!optional}
   185      >
   186        {isFile ? (
   187          <ConfigurationFileInput
   188            value={local ?? ''}
   189            onChange={val => {
   190              setLocal(val?.text ?? '')
   191            }}
   192          />
   193        ) : (
   194          <Input
   195            placeholder={placeholder}
   196            value={local}
   197            type={inputFieldType}
   198            error={!valid}
   199            prefix={
   200              config.type === Datatype.Bucket
   201                ? `${project?.bucketPrefix}-`
   202                : ''
   203            }
   204            suffix={
   205              config.type === Datatype.Domain
   206                ? `.${project?.network?.subdomain}`
   207                : ''
   208            }
   209            onChange={({ target: { value } }) => setLocal(value)}
   210          />
   211        )}
   212      </FormField>
   213    )
   214  }
   215  
   216  function BoolConfiguration({ config: { name, default: def }, ctx, setValue }) {
   217    const value: boolean = `${ctx[name]?.value}`.toLowerCase() === 'true'
   218  
   219    useEffect(() => {
   220      if (value === undefined && def) {
   221        setValue(name, def)
   222      }
   223    }, [value, def, name, setValue])
   224  
   225    return (
   226      <Switch
   227        checked={value}
   228        onChange={({ target: { checked } }) => setValue(name, checked)}
   229      >
   230        {StartCase(name)}
   231      </Switch>
   232    )
   233  }
   234  
   235  export function ConfigurationItem({ config, ctx, setValue }) {
   236    switch (config.type) {
   237    case Datatype.Bool:
   238      return (
   239        <BoolConfiguration
   240          config={config}
   241          ctx={ctx}
   242          setValue={setValue}
   243        />
   244      )
   245    default:
   246      return (
   247        <ConfigurationField
   248          config={config}
   249          ctx={ctx}
   250          setValue={setValue}
   251        />
   252      )
   253    }
   254  }