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 }