|
| 1 | +/** |
| 2 | + * Form component for configuring App Group Lifecycle Plugins. |
| 3 | + * Used in CreateUpdate dialogs (edit mode). |
| 4 | + */ |
| 5 | + |
| 6 | +import * as React from 'react'; |
| 7 | +import Box from '@mui/material/Box'; |
| 8 | +import CircularProgress from '@mui/material/CircularProgress'; |
| 9 | +import FormControl from '@mui/material/FormControl'; |
| 10 | +import FormHelperText from '@mui/material/FormHelperText'; |
| 11 | +import MenuItem from '@mui/material/MenuItem'; |
| 12 | +import Select, {SelectChangeEvent} from '@mui/material/Select'; |
| 13 | +import TextField from '@mui/material/TextField'; |
| 14 | +import Typography from '@mui/material/Typography'; |
| 15 | +import Checkbox from '@mui/material/Checkbox'; |
| 16 | +import FormControlLabel from '@mui/material/FormControlLabel'; |
| 17 | +import {useFormContext, Controller} from 'react-hook-form'; |
| 18 | + |
| 19 | +import { |
| 20 | + useGetAppGroupLifecyclePlugins, |
| 21 | + useGetAppGroupLifecyclePluginAppConfigProperties, |
| 22 | + useGetAppGroupLifecyclePluginGroupConfigProperties, |
| 23 | + PluginConfigProperty, |
| 24 | +} from '../api/apiComponents'; |
| 25 | + |
| 26 | +type PluginConfiguration = { |
| 27 | + [propertyId: string]: any; |
| 28 | +}; |
| 29 | + |
| 30 | +interface AppGroupLifecyclePluginConfigurationFormProps { |
| 31 | + /** |
| 32 | + * Entity type at which the plugin is configured ('app' or 'group') |
| 33 | + */ |
| 34 | + entityType: 'app' | 'group'; |
| 35 | + |
| 36 | + /** |
| 37 | + * Currently selected plugin ID (if any) |
| 38 | + */ |
| 39 | + selectedPluginId?: string | null; |
| 40 | + |
| 41 | + /** |
| 42 | + * Current configuration values |
| 43 | + */ |
| 44 | + currentConfig?: PluginConfiguration; |
| 45 | + |
| 46 | + /** |
| 47 | + * Callback when plugin selection changes (app level only) |
| 48 | + */ |
| 49 | + onPluginChange?: (pluginId: string | null) => void; |
| 50 | +} |
| 51 | + |
| 52 | +/** |
| 53 | + * Renders a single configuration field based on its schema |
| 54 | + */ |
| 55 | +function ConfigField({property, value, fieldName}: {property: PluginConfigProperty; value: any; fieldName: string}) { |
| 56 | + const {register, control} = useFormContext(); |
| 57 | + |
| 58 | + switch (property.type) { |
| 59 | + case 'boolean': |
| 60 | + return ( |
| 61 | + <FormControl fullWidth sx={{mb: 2}}> |
| 62 | + <Controller |
| 63 | + name={fieldName} |
| 64 | + control={control} |
| 65 | + defaultValue={value ?? property.default_value ?? false} |
| 66 | + render={({field}) => ( |
| 67 | + <FormControlLabel control={<Checkbox {...field} checked={field.value} />} label={property.display_name} /> |
| 68 | + )} |
| 69 | + /> |
| 70 | + {property.help_text && <FormHelperText>{property.help_text}</FormHelperText>} |
| 71 | + </FormControl> |
| 72 | + ); |
| 73 | + |
| 74 | + case 'number': |
| 75 | + return ( |
| 76 | + <TextField |
| 77 | + fullWidth |
| 78 | + label={property.display_name} |
| 79 | + type="number" |
| 80 | + helperText={property.help_text} |
| 81 | + required={property.required} |
| 82 | + defaultValue={value ?? property.default_value} |
| 83 | + {...register(fieldName, { |
| 84 | + required: property.required, |
| 85 | + valueAsNumber: true, |
| 86 | + })} |
| 87 | + sx={{mb: 2}} |
| 88 | + /> |
| 89 | + ); |
| 90 | + |
| 91 | + case 'text': |
| 92 | + default: |
| 93 | + return ( |
| 94 | + <TextField |
| 95 | + fullWidth |
| 96 | + label={property.display_name} |
| 97 | + helperText={property.help_text} |
| 98 | + required={property.required} |
| 99 | + defaultValue={value ?? property.default_value ?? ''} |
| 100 | + {...register(fieldName, { |
| 101 | + required: property.required, |
| 102 | + })} |
| 103 | + sx={{mb: 2}} |
| 104 | + /> |
| 105 | + ); |
| 106 | + } |
| 107 | +} |
| 108 | + |
| 109 | +export default function AppGroupLifecyclePluginConfigurationForm({ |
| 110 | + entityType, |
| 111 | + selectedPluginId, |
| 112 | + currentConfig = {}, |
| 113 | + onPluginChange, |
| 114 | +}: AppGroupLifecyclePluginConfigurationFormProps) { |
| 115 | + const {data: plugins, isLoading: pluginsLoading} = useGetAppGroupLifecyclePlugins(); |
| 116 | + |
| 117 | + const useConfigPropertiesHook = |
| 118 | + entityType === 'app' |
| 119 | + ? useGetAppGroupLifecyclePluginAppConfigProperties |
| 120 | + : useGetAppGroupLifecyclePluginGroupConfigProperties; |
| 121 | + |
| 122 | + const {data: configProperties, isLoading: configLoading} = useConfigPropertiesHook( |
| 123 | + {pathParams: {pluginId: selectedPluginId || ''}}, |
| 124 | + {enabled: !!selectedPluginId}, |
| 125 | + ); |
| 126 | + |
| 127 | + const selectedPlugin = React.useMemo(() => { |
| 128 | + if (!plugins || !selectedPluginId) return null; |
| 129 | + return plugins.find((p) => p.id === selectedPluginId) || null; |
| 130 | + }, [plugins, selectedPluginId]); |
| 131 | + |
| 132 | + const handlePluginSelectionChange = (event: SelectChangeEvent<string>) => { |
| 133 | + const newPluginId = event.target.value; |
| 134 | + onPluginChange?.(newPluginId || null); |
| 135 | + }; |
| 136 | + |
| 137 | + if (pluginsLoading) { |
| 138 | + return ( |
| 139 | + <Box sx={{display: 'flex', justifyContent: 'center', p: 2}}> |
| 140 | + <CircularProgress size={24} /> |
| 141 | + </Box> |
| 142 | + ); |
| 143 | + } |
| 144 | + |
| 145 | + if (!plugins || plugins.length === 0) { |
| 146 | + return null; |
| 147 | + } |
| 148 | + |
| 149 | + return ( |
| 150 | + <Box sx={{mt: 3}}> |
| 151 | + <Typography variant="h6" gutterBottom> |
| 152 | + Configure {entityType === 'app' ? 'an' : 'the'} App Group Lifecycle Plugin |
| 153 | + </Typography> |
| 154 | + |
| 155 | + {/* Plugin Selection (app-level only) */} |
| 156 | + {entityType === 'app' && ( |
| 157 | + <FormControl fullWidth sx={{mb: 3}}> |
| 158 | + <Select value={selectedPluginId || ''} onChange={handlePluginSelectionChange} displayEmpty> |
| 159 | + <MenuItem value=""> |
| 160 | + <em>None</em> |
| 161 | + </MenuItem> |
| 162 | + {plugins.map((plugin) => ( |
| 163 | + <MenuItem key={plugin.id} value={plugin.id}> |
| 164 | + {plugin.display_name} |
| 165 | + </MenuItem> |
| 166 | + ))} |
| 167 | + </Select> |
| 168 | + {selectedPlugin && <FormHelperText>{selectedPlugin.description}</FormHelperText>} |
| 169 | + </FormControl> |
| 170 | + )} |
| 171 | + |
| 172 | + {/* Display Selected Plugin (group-level only) */} |
| 173 | + {entityType === 'group' && selectedPlugin && ( |
| 174 | + <> |
| 175 | + <Typography variant="subtitle1" gutterBottom> |
| 176 | + Selected Plugin |
| 177 | + </Typography> |
| 178 | + <Box sx={{pl: 2}}> |
| 179 | + <Typography variant="body2" gutterBottom> |
| 180 | + <strong>{selectedPlugin.display_name}</strong>: {selectedPlugin.description} |
| 181 | + </Typography> |
| 182 | + </Box> |
| 183 | + </> |
| 184 | + )} |
| 185 | + |
| 186 | + {/* Plugin Configuration */} |
| 187 | + {selectedPluginId && ( |
| 188 | + <> |
| 189 | + {configLoading ? ( |
| 190 | + <Box sx={{display: 'flex', justifyContent: 'center', p: 2}}> |
| 191 | + <CircularProgress size={24} /> |
| 192 | + </Box> |
| 193 | + ) : configProperties && Object.keys(configProperties).length > 0 ? ( |
| 194 | + <> |
| 195 | + <Typography variant="subtitle1" gutterBottom> |
| 196 | + Configuration |
| 197 | + </Typography> |
| 198 | + <Box sx={{pl: 2}}> |
| 199 | + {Object.entries(configProperties).map(([propertyId, property]) => { |
| 200 | + const fieldName = `plugin_data.${selectedPluginId}.configuration.${propertyId}`; |
| 201 | + return ( |
| 202 | + <ConfigField |
| 203 | + key={propertyId} |
| 204 | + property={property} |
| 205 | + value={currentConfig[propertyId]} |
| 206 | + fieldName={fieldName} |
| 207 | + /> |
| 208 | + ); |
| 209 | + })} |
| 210 | + </Box> |
| 211 | + </> |
| 212 | + ) : null} |
| 213 | + </> |
| 214 | + )} |
| 215 | + </Box> |
| 216 | + ); |
| 217 | +} |
0 commit comments