mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	chore: redesign action configurations (#6588)
https://linear.app/unleash/issue/2-2050/redesign-action-definitions-oss Depends on a PR on Enterprise that will provide a new endpoint with action configurations. We should also clean up the now unnecessary constants and hook.
This commit is contained in:
		
							parent
							
								
									f45dbc647e
								
							
						
					
					
						commit
						a86b8d36b3
					
				| @ -6,8 +6,8 @@ import { ProjectActionsFormItem } from '../ProjectActionsFormItem'; | |||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
| import { useServiceAccountAccessMatrix } from 'hooks/api/getters/useServiceAccountAccessMatrix/useServiceAccountAccessMatrix'; | import { useServiceAccountAccessMatrix } from 'hooks/api/getters/useServiceAccountAccessMatrix/useServiceAccountAccessMatrix'; | ||||||
| import { useEffect, useMemo } from 'react'; | import { useEffect, useMemo } from 'react'; | ||||||
| import { ProjectActionsActionParameterAutocomplete } from './ProjectActionsActionParameter/ProjectActionsActionParameterAutocomplete'; | import { ProjectActionsActionParameter } from './ProjectActionsActionParameter/ProjectActionsActionParameter'; | ||||||
| import { ActionDefinitions } from './useActionDefinitions'; | import { ActionConfigurations } from 'interfaces/action'; | ||||||
| import { ProjectActionsActionSelect } from './ProjectActionsActionSelect'; | import { ProjectActionsActionSelect } from './ProjectActionsActionSelect'; | ||||||
| 
 | 
 | ||||||
| const StyledItemBody = styled('div')(({ theme }) => ({ | const StyledItemBody = styled('div')(({ theme }) => ({ | ||||||
| @ -36,7 +36,7 @@ interface IProjectActionsItemProps { | |||||||
|     stateChanged: (action: ActionsActionState) => void; |     stateChanged: (action: ActionsActionState) => void; | ||||||
|     actorId: number; |     actorId: number; | ||||||
|     onDelete: () => void; |     onDelete: () => void; | ||||||
|     actionDefinitions: ActionDefinitions; |     actionConfigurations: ActionConfigurations; | ||||||
|     validated: boolean; |     validated: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -46,7 +46,7 @@ export const ProjectActionsActionItem = ({ | |||||||
|     stateChanged, |     stateChanged, | ||||||
|     actorId, |     actorId, | ||||||
|     onDelete, |     onDelete, | ||||||
|     actionDefinitions, |     actionConfigurations, | ||||||
|     validated, |     validated, | ||||||
| }: IProjectActionsItemProps) => { | }: IProjectActionsItemProps) => { | ||||||
|     const { action: actionName, executionParams, error } = action; |     const { action: actionName, executionParams, error } = action; | ||||||
| @ -57,10 +57,10 @@ export const ProjectActionsActionItem = ({ | |||||||
|         executionParams.environment as string, |         executionParams.environment as string, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const actionDefinition = actionDefinitions.get(actionName); |     const actionConfiguration = actionConfigurations.get(actionName); | ||||||
| 
 | 
 | ||||||
|     const hasPermission = useMemo(() => { |     const hasPermission = useMemo(() => { | ||||||
|         const requiredPermissions = actionDefinition?.permissions; |         const requiredPermissions = actionConfiguration?.permissions; | ||||||
| 
 | 
 | ||||||
|         const { environment: actionEnvironment } = executionParams; |         const { environment: actionEnvironment } = executionParams; | ||||||
| 
 | 
 | ||||||
| @ -80,7 +80,7 @@ export const ProjectActionsActionItem = ({ | |||||||
|                     environment === actionEnvironment, |                     environment === actionEnvironment, | ||||||
|             ), |             ), | ||||||
|         ); |         ); | ||||||
|     }, [actionDefinition, permissions]); |     }, [actionConfiguration, permissions]); | ||||||
| 
 | 
 | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         stateChanged({ |         stateChanged({ | ||||||
| @ -89,7 +89,7 @@ export const ProjectActionsActionItem = ({ | |||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         const requiredParameters = |         const requiredParameters = | ||||||
|             actionDefinition?.parameters |             actionConfiguration?.parameters | ||||||
|                 .filter(({ optional }) => !optional) |                 .filter(({ optional }) => !optional) | ||||||
|                 .map(({ name }) => name) || []; |                 .map(({ name }) => name) || []; | ||||||
| 
 | 
 | ||||||
| @ -99,7 +99,7 @@ export const ProjectActionsActionItem = ({ | |||||||
|                 error: 'Please fill all required fields.', |                 error: 'Please fill all required fields.', | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|     }, [actionDefinition, executionParams]); |     }, [actionConfiguration, executionParams]); | ||||||
| 
 | 
 | ||||||
|     const header = ( |     const header = ( | ||||||
|         <> |         <> | ||||||
| @ -115,7 +115,9 @@ export const ProjectActionsActionItem = ({ | |||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const parameters = |     const parameters = | ||||||
|         actionDefinition?.parameters.filter(({ hidden }) => !hidden) || []; |         actionConfiguration?.parameters.filter( | ||||||
|  |             ({ type }) => type !== 'hidden', | ||||||
|  |         ) || []; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|         <ProjectActionsFormItem index={index} header={header} separator='THEN'> |         <ProjectActionsFormItem index={index} header={header} separator='THEN'> | ||||||
| @ -130,7 +132,7 @@ export const ProjectActionsActionItem = ({ | |||||||
|                                     action: value, |                                     action: value, | ||||||
|                                 }) |                                 }) | ||||||
|                             } |                             } | ||||||
|                             actionDefinitions={actionDefinitions} |                             actionConfigurations={actionConfigurations} | ||||||
|                         /> |                         /> | ||||||
|                     </StyledFieldContainer> |                     </StyledFieldContainer> | ||||||
|                 </StyledItemRow> |                 </StyledItemRow> | ||||||
| @ -138,21 +140,24 @@ export const ProjectActionsActionItem = ({ | |||||||
|                     condition={parameters.length > 0} |                     condition={parameters.length > 0} | ||||||
|                     show={ |                     show={ | ||||||
|                         <StyledItemRow> |                         <StyledItemRow> | ||||||
|                             {parameters.map(({ name, label, options }) => ( |                             {parameters.map((parameter) => ( | ||||||
|                                 <StyledFieldContainer key={name}> |                                 <StyledFieldContainer key={parameter.name}> | ||||||
|                                     <ProjectActionsActionParameterAutocomplete |                                     <ProjectActionsActionParameter | ||||||
|                                         label={label} |                                         parameter={parameter} | ||||||
|                                         value={executionParams[name] as string} |                                         value={ | ||||||
|  |                                             executionParams[ | ||||||
|  |                                                 parameter.name | ||||||
|  |                                             ] as string | ||||||
|  |                                         } | ||||||
|                                         onChange={(value) => |                                         onChange={(value) => | ||||||
|                                             stateChanged({ |                                             stateChanged({ | ||||||
|                                                 ...action, |                                                 ...action, | ||||||
|                                                 executionParams: { |                                                 executionParams: { | ||||||
|                                                     ...executionParams, |                                                     ...executionParams, | ||||||
|                                                     [name]: value, |                                                     [parameter.name]: value, | ||||||
|                                                 }, |                                                 }, | ||||||
|                                             }) |                                             }) | ||||||
|                                         } |                                         } | ||||||
|                                         options={options} |  | ||||||
|                                     /> |                                     /> | ||||||
|                                 </StyledFieldContainer> |                                 </StyledFieldContainer> | ||||||
|                             ))} |                             ))} | ||||||
|  | |||||||
| @ -0,0 +1,31 @@ | |||||||
|  | import { ActionConfigurationParameter } from 'interfaces/action'; | ||||||
|  | import { ProjectActionsActionParameterAutocomplete } from './ProjectActionsActionParameterAutocomplete'; | ||||||
|  | 
 | ||||||
|  | interface IProjectActionsActionParameterProps { | ||||||
|  |     parameter: ActionConfigurationParameter; | ||||||
|  |     value: string; | ||||||
|  |     onChange: (value: string) => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const ProjectActionsActionParameter = ({ | ||||||
|  |     parameter, | ||||||
|  |     value, | ||||||
|  |     onChange, | ||||||
|  | }: IProjectActionsActionParameterProps) => { | ||||||
|  |     const { label, type } = parameter; | ||||||
|  | 
 | ||||||
|  |     if (type === 'select') { | ||||||
|  |         const { options } = parameter; | ||||||
|  | 
 | ||||||
|  |         return ( | ||||||
|  |             <ProjectActionsActionParameterAutocomplete | ||||||
|  |                 label={label} | ||||||
|  |                 value={value} | ||||||
|  |                 onChange={onChange} | ||||||
|  |                 options={options} | ||||||
|  |             /> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return null; | ||||||
|  | }; | ||||||
| @ -1,5 +1,5 @@ | |||||||
| import { Autocomplete, TextField, styled } from '@mui/material'; | import { Autocomplete, TextField, styled } from '@mui/material'; | ||||||
| import { ActionDefinitions } from './useActionDefinitions'; | import { ActionConfigurations } from 'interfaces/action'; | ||||||
| 
 | 
 | ||||||
| const StyledActionOption = styled('div')(({ theme }) => ({ | const StyledActionOption = styled('div')(({ theme }) => ({ | ||||||
|     display: 'flex', |     display: 'flex', | ||||||
| @ -13,13 +13,13 @@ const StyledActionOption = styled('div')(({ theme }) => ({ | |||||||
| interface IProjectActionsActionSelectProps { | interface IProjectActionsActionSelectProps { | ||||||
|     value: string; |     value: string; | ||||||
|     onChange: (value: string) => void; |     onChange: (value: string) => void; | ||||||
|     actionDefinitions: ActionDefinitions; |     actionConfigurations: ActionConfigurations; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ProjectActionsActionSelect = ({ | export const ProjectActionsActionSelect = ({ | ||||||
|     value, |     value, | ||||||
|     onChange, |     onChange, | ||||||
|     actionDefinitions, |     actionConfigurations, | ||||||
| }: IProjectActionsActionSelectProps) => { | }: IProjectActionsActionSelectProps) => { | ||||||
|     const renderActionOption = ( |     const renderActionOption = ( | ||||||
|         props: React.HTMLAttributes<HTMLLIElement>, |         props: React.HTMLAttributes<HTMLLIElement>, | ||||||
| @ -33,7 +33,7 @@ export const ProjectActionsActionSelect = ({ | |||||||
|         </li> |         </li> | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const actionOptions = [...actionDefinitions].map( |     const actionOptions = [...actionConfigurations].map( | ||||||
|         ([key, actionDefinition]) => ({ |         ([key, actionDefinition]) => ({ | ||||||
|             key, |             key, | ||||||
|             ...actionDefinition, |             ...actionDefinition, | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; | |||||||
| import Add from '@mui/icons-material/Add'; | import Add from '@mui/icons-material/Add'; | ||||||
| import { IServiceAccount } from 'interfaces/service-account'; | import { IServiceAccount } from 'interfaces/service-account'; | ||||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
| import { useActionDefinitions } from './useActionDefinitions'; | import { useActionConfigurations } from 'hooks/api/getters/useActionConfigurations/useActionConfigurations'; | ||||||
| 
 | 
 | ||||||
| const StyledDivider = styled(Divider)(({ theme }) => ({ | const StyledDivider = styled(Divider)(({ theme }) => ({ | ||||||
|     margin: theme.spacing(2, 0), |     margin: theme.spacing(2, 0), | ||||||
| @ -45,7 +45,7 @@ export const ProjectActionsFormStepActions = ({ | |||||||
|     validated, |     validated, | ||||||
| }: IProjectActionsFormStepActionsProps) => { | }: IProjectActionsFormStepActionsProps) => { | ||||||
|     const projectId = useRequiredPathParam('projectId'); |     const projectId = useRequiredPathParam('projectId'); | ||||||
|     const actionDefinitions = useActionDefinitions(projectId); |     const { actionConfigurations } = useActionConfigurations(projectId); | ||||||
| 
 | 
 | ||||||
|     const addAction = (projectId: string) => { |     const addAction = (projectId: string) => { | ||||||
|         const id = uuidv4(); |         const id = uuidv4(); | ||||||
| @ -114,7 +114,7 @@ export const ProjectActionsFormStepActions = ({ | |||||||
|                             actions.filter((a) => a.id !== action.id), |                             actions.filter((a) => a.id !== action.id), | ||||||
|                         ) |                         ) | ||||||
|                     } |                     } | ||||||
|                     actionDefinitions={actionDefinitions} |                     actionConfigurations={actionConfigurations} | ||||||
|                     validated={validated} |                     validated={validated} | ||||||
|                 /> |                 /> | ||||||
|             ))} |             ))} | ||||||
|  | |||||||
| @ -1,67 +0,0 @@ | |||||||
| import { useEffect, useState } from 'react'; |  | ||||||
| import { ActionDefinitionParameter } from '@server/util/constants/action-parameters'; |  | ||||||
| import { ACTIONS, ActionDefinition } from '@server/util/constants/actions'; |  | ||||||
| import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch'; |  | ||||||
| import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; |  | ||||||
| 
 |  | ||||||
| type ActionDefinitionParameterWithOption = ActionDefinitionParameter & { |  | ||||||
|     options: string[]; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| type ActionDefinitionWithParameterOptions = Omit< |  | ||||||
|     ActionDefinition, |  | ||||||
|     'parameters' |  | ||||||
| > & { |  | ||||||
|     parameters: ActionDefinitionParameterWithOption[]; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export type ActionDefinitions = Map< |  | ||||||
|     string, |  | ||||||
|     ActionDefinitionWithParameterOptions |  | ||||||
| >; |  | ||||||
| 
 |  | ||||||
| export const useActionDefinitions = (projectId: string): ActionDefinitions => { |  | ||||||
|     const { project, loading: isProjectLoading } = |  | ||||||
|         useProjectOverview(projectId); |  | ||||||
|     const { features, loading: isFeaturesLoading } = useFeatureSearch({ |  | ||||||
|         project: `IS:${projectId}`, |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const [actionDefinitions, setActionDefinitions] = |  | ||||||
|         useState<ActionDefinitions>(new Map()); |  | ||||||
| 
 |  | ||||||
|     useEffect(() => { |  | ||||||
|         if (isProjectLoading || isFeaturesLoading) return; |  | ||||||
| 
 |  | ||||||
|         const optionsByType: Record< |  | ||||||
|             ActionDefinitionParameter['type'], |  | ||||||
|             string[] |  | ||||||
|         > = { |  | ||||||
|             project: [], |  | ||||||
|             environment: project.environments.map( |  | ||||||
|                 ({ environment }) => environment, |  | ||||||
|             ), |  | ||||||
|             featureToggle: features.map(({ name }) => name).sort(), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         const actionDefinitionsWithParameterOptions = new Map< |  | ||||||
|             string, |  | ||||||
|             ActionDefinitionWithParameterOptions |  | ||||||
|         >( |  | ||||||
|             [...ACTIONS].map(([key, action]) => [ |  | ||||||
|                 key, |  | ||||||
|                 { |  | ||||||
|                     ...action, |  | ||||||
|                     parameters: action.parameters.map((parameter) => ({ |  | ||||||
|                         ...parameter, |  | ||||||
|                         options: optionsByType[parameter.type], |  | ||||||
|                     })), |  | ||||||
|                 }, |  | ||||||
|             ]), |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         setActionDefinitions(actionDefinitionsWithParameterOptions); |  | ||||||
|     }, [projectId, project, features, isProjectLoading, isFeaturesLoading]); |  | ||||||
| 
 |  | ||||||
|     return actionDefinitions; |  | ||||||
| }; |  | ||||||
| @ -0,0 +1,41 @@ | |||||||
|  | import { useMemo } from 'react'; | ||||||
|  | import { formatApiPath } from 'utils/formatPath'; | ||||||
|  | import handleErrorResponses from '../httpErrorResponseHandler'; | ||||||
|  | import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR'; | ||||||
|  | import useUiConfig from '../useUiConfig/useUiConfig'; | ||||||
|  | import { ActionConfiguration } from 'interfaces/action'; | ||||||
|  | import { useUiFlag } from 'hooks/useUiFlag'; | ||||||
|  | 
 | ||||||
|  | const DEFAULT_DATA: Record<string, ActionConfiguration> = {}; | ||||||
|  | 
 | ||||||
|  | export const useActionConfigurations = (project: string) => { | ||||||
|  |     const { isEnterprise } = useUiConfig(); | ||||||
|  |     const actionsEnabled = useUiFlag('automatedActions'); | ||||||
|  | 
 | ||||||
|  |     const { data, error, mutate } = useConditionalSWR< | ||||||
|  |         Record<string, ActionConfiguration> | ||||||
|  |     >( | ||||||
|  |         isEnterprise() && actionsEnabled, | ||||||
|  |         DEFAULT_DATA, | ||||||
|  |         formatApiPath(`api/admin/projects/${project}/actions/config`), | ||||||
|  |         fetcher, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     return useMemo( | ||||||
|  |         () => ({ | ||||||
|  |             actionConfigurations: new Map<string, ActionConfiguration>( | ||||||
|  |                 Object.entries(data || {}), | ||||||
|  |             ), | ||||||
|  |             loading: !error && !data, | ||||||
|  |             refetch: () => mutate(), | ||||||
|  |             error, | ||||||
|  |         }), | ||||||
|  |         [data, error, mutate], | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const fetcher = (path: string) => { | ||||||
|  |     return fetch(path) | ||||||
|  |         .then(handleErrorResponses('Actions configuration')) | ||||||
|  |         .then((res) => res.json()); | ||||||
|  | }; | ||||||
| @ -56,3 +56,32 @@ export interface IActionSetEvent { | |||||||
|     signal: ISignal; |     signal: ISignal; | ||||||
|     actionSet: IActionSetEventActionSet; |     actionSet: IActionSetEventActionSet; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | type BaseParameter = { | ||||||
|  |     name: string; | ||||||
|  |     label: string; | ||||||
|  |     optional?: boolean; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | type ActionConfigurationHiddenParameter = BaseParameter & { | ||||||
|  |     type: 'hidden'; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | type ActionConfigurationSelectParameter = BaseParameter & { | ||||||
|  |     type: 'select'; | ||||||
|  |     options: string[]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export type ActionConfigurationParameter = | ||||||
|  |     | ActionConfigurationHiddenParameter | ||||||
|  |     | ActionConfigurationSelectParameter; | ||||||
|  | 
 | ||||||
|  | export type ActionConfiguration = { | ||||||
|  |     label: string; | ||||||
|  |     description?: string; | ||||||
|  |     category?: string; | ||||||
|  |     permissions: string[]; | ||||||
|  |     parameters: ActionConfigurationParameter[]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export type ActionConfigurations = Map<string, ActionConfiguration>; | ||||||
|  | |||||||
| @ -1,37 +0,0 @@ | |||||||
| type ActionDefinitionParameterType = |  | ||||||
|     | 'project' |  | ||||||
|     | 'featureToggle' |  | ||||||
|     | 'environment'; |  | ||||||
| 
 |  | ||||||
| export type ActionDefinitionParameter = { |  | ||||||
|     name: string; |  | ||||||
|     label: string; |  | ||||||
|     type: ActionDefinitionParameterType; |  | ||||||
|     hidden?: boolean; |  | ||||||
|     optional?: boolean; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const projectParameter: ActionDefinitionParameter = { |  | ||||||
|     name: 'project', |  | ||||||
|     label: 'Project', |  | ||||||
|     type: 'project', |  | ||||||
|     hidden: true, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const environmentParameter: ActionDefinitionParameter = { |  | ||||||
|     name: 'environment', |  | ||||||
|     label: 'Environment', |  | ||||||
|     type: 'environment', |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const featureToggleParameter: ActionDefinitionParameter = { |  | ||||||
|     name: 'featureName', |  | ||||||
|     label: 'Feature toggle', |  | ||||||
|     type: 'featureToggle', |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const toggleFeatureParameters = [ |  | ||||||
|     projectParameter, |  | ||||||
|     environmentParameter, |  | ||||||
|     featureToggleParameter, |  | ||||||
| ]; |  | ||||||
| @ -1,36 +0,0 @@ | |||||||
| import { |  | ||||||
|     ActionDefinitionParameter, |  | ||||||
|     toggleFeatureParameters, |  | ||||||
| } from './action-parameters'; |  | ||||||
| 
 |  | ||||||
| export type ActionDefinition = { |  | ||||||
|     label: string; |  | ||||||
|     description?: string; |  | ||||||
|     category?: string; |  | ||||||
|     permissions: string[]; |  | ||||||
|     parameters: ActionDefinitionParameter[]; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const ACTIONS = new Map<string, ActionDefinition>([ |  | ||||||
|     [ |  | ||||||
|         'TOGGLE_FEATURE_ON', |  | ||||||
|         { |  | ||||||
|             label: 'Enable feature toggle', |  | ||||||
|             description: 'Enables a feature toggle for a specific environment.', |  | ||||||
|             category: 'Feature toggles', |  | ||||||
|             permissions: ['UPDATE_FEATURE_ENVIRONMENT'], |  | ||||||
|             parameters: toggleFeatureParameters, |  | ||||||
|         }, |  | ||||||
|     ], |  | ||||||
|     [ |  | ||||||
|         'TOGGLE_FEATURE_OFF', |  | ||||||
|         { |  | ||||||
|             label: 'Disable feature toggle', |  | ||||||
|             description: |  | ||||||
|                 'Disables a feature toggle for a specific environment.', |  | ||||||
|             category: 'Feature toggles', |  | ||||||
|             permissions: ['UPDATE_FEATURE_ENVIRONMENT'], |  | ||||||
|             parameters: toggleFeatureParameters, |  | ||||||
|         }, |  | ||||||
|     ], |  | ||||||
| ]); |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user