mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +02:00
Merge branch 'main' into task/constraint_card_adjustmnets
This commit is contained in:
commit
d66359f6a4
57
frontend/src/component/addons/AddonForm/AddonForm.styles.tsx
Normal file
57
frontend/src/component/addons/AddonForm/AddonForm.styles.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { styled } from '@mui/system';
|
||||
import { FormControlLabel, TextField } from '@mui/material';
|
||||
import Autocomplete from '@mui/material/Autocomplete';
|
||||
|
||||
export const StyledForm = styled('form')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
gap: '1rem',
|
||||
});
|
||||
|
||||
export const StyledAutocomplete = styled(Autocomplete)({
|
||||
paddingBottom: '36px',
|
||||
marginTop: '0px',
|
||||
});
|
||||
export const StyledFormSection = styled('section')({
|
||||
marginBottom: '36px',
|
||||
});
|
||||
|
||||
export const StyledHelpText = styled('p')({
|
||||
marginBottom: '0.5rem',
|
||||
});
|
||||
|
||||
export const StyledContainer = styled('div')({
|
||||
maxWidth: '600px',
|
||||
});
|
||||
|
||||
export const StyledButtonContainer = styled('div')({
|
||||
marginTop: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
});
|
||||
|
||||
export const StyledButtonSection = styled('section')(({ theme }) => ({
|
||||
'padding-top': '16px',
|
||||
'& > *': {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export const StyledTextField = styled(TextField)({
|
||||
width: '100%',
|
||||
marginBottom: '1rem',
|
||||
marginTop: '0px',
|
||||
});
|
||||
|
||||
export const StyledSelectAllFormControlLabel = styled(FormControlLabel)({
|
||||
paddingBottom: '16px',
|
||||
});
|
||||
|
||||
export const StyledTitle = styled('h4')({
|
||||
marginBottom: '8px',
|
||||
});
|
||||
|
||||
export const StyledAddonParameterContainer = styled('div')({
|
||||
marginTop: '25px',
|
||||
});
|
@ -6,35 +6,39 @@ import React, {
|
||||
useState,
|
||||
VFC,
|
||||
} from 'react';
|
||||
import { Button, FormControlLabel, Switch, TextField } from '@mui/material';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import produce from 'immer';
|
||||
import { styles as themeStyles } from 'component/common';
|
||||
import { trim } from 'component/common/util';
|
||||
import { IAddon, IAddonProvider } from 'interfaces/addons';
|
||||
import { AddonParameters } from './AddonParameters/AddonParameters';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import useProjects from '../../../hooks/api/getters/useProjects/useProjects';
|
||||
import { useEnvironments } from '../../../hooks/api/getters/useEnvironments/useEnvironments';
|
||||
import { AddonMultiSelector } from './AddonMultiSelector/AddonMultiSelector';
|
||||
|
||||
const useStyles = makeStyles()(theme => ({
|
||||
nameInput: {
|
||||
marginRight: '1.5rem',
|
||||
},
|
||||
formSection: { padding: '10px 28px' },
|
||||
buttonsSection: {
|
||||
padding: '10px 28px',
|
||||
'& > *': {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}));
|
||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
||||
import {
|
||||
StyledForm,
|
||||
StyledFormSection,
|
||||
StyledHelpText,
|
||||
StyledTextField,
|
||||
StyledContainer,
|
||||
StyledButtonContainer,
|
||||
StyledButtonSection,
|
||||
} from './AddonForm.styles';
|
||||
import { useTheme } from '@mui/system';
|
||||
|
||||
interface IAddonFormProps {
|
||||
provider?: IAddonProvider;
|
||||
@ -52,7 +56,7 @@ export const AddonForm: VFC<IAddonFormProps> = ({
|
||||
const { createAddon, updateAddon } = useAddonsApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const { classes: styles } = useStyles();
|
||||
const theme = useTheme();
|
||||
const { projects: availableProjects } = useProjects();
|
||||
const selectableProjects = availableProjects.map(project => ({
|
||||
value: project.id,
|
||||
@ -67,6 +71,7 @@ export const AddonForm: VFC<IAddonFormProps> = ({
|
||||
value: event,
|
||||
label: event,
|
||||
}));
|
||||
const { uiConfig } = useUiConfig();
|
||||
const [formValues, setFormValues] = useState(initialValues);
|
||||
const [errors, setErrors] = useState<{
|
||||
containsErrors: boolean;
|
||||
@ -81,6 +86,18 @@ export const AddonForm: VFC<IAddonFormProps> = ({
|
||||
parameters: {},
|
||||
});
|
||||
const submitText = editMode ? 'Update' : 'Create';
|
||||
let url = `${uiConfig.unleashUrl}/api/admin/addons${
|
||||
editMode ? `/${formValues.id}` : ``
|
||||
}`;
|
||||
|
||||
const formatApiCode = () => {
|
||||
return `curl --location --request ${
|
||||
editMode ? 'PUT' : 'POST'
|
||||
} '${url}' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(formValues, undefined, 2)}'`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!provider) {
|
||||
@ -162,7 +179,9 @@ export const AddonForm: VFC<IAddonFormProps> = ({
|
||||
|
||||
const onSubmit: FormEventHandler<HTMLFormElement> = async event => {
|
||||
event.preventDefault();
|
||||
if (!provider) return;
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedErrors = cloneDeep(errors);
|
||||
updatedErrors.containsErrors = false;
|
||||
@ -222,97 +241,113 @@ export const AddonForm: VFC<IAddonFormProps> = ({
|
||||
} = provider ? provider : ({} as Partial<IAddonProvider>);
|
||||
|
||||
return (
|
||||
<PageContent header={`Configure ${name} addon`}>
|
||||
<section className={styles.formSection}>
|
||||
{description}
|
||||
<a href={documentationUrl} target="_blank" rel="noreferrer">
|
||||
Read more
|
||||
</a>
|
||||
<p className={themeStyles.error}>{errors.general}</p>
|
||||
</section>
|
||||
<form onSubmit={onSubmit}>
|
||||
<section className={styles.formSection}>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Provider"
|
||||
name="provider"
|
||||
value={formValues.provider}
|
||||
disabled
|
||||
variant="outlined"
|
||||
className={styles.nameInput}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={formValues.enabled}
|
||||
onClick={onEnabled}
|
||||
/>
|
||||
}
|
||||
label={formValues.enabled ? 'Enabled' : 'Disabled'}
|
||||
/>
|
||||
</section>
|
||||
<section className={styles.formSection}>
|
||||
<TextField
|
||||
size="small"
|
||||
style={{ width: '80%' }}
|
||||
minRows={4}
|
||||
multiline
|
||||
label="Description"
|
||||
name="description"
|
||||
placeholder=""
|
||||
value={formValues.description}
|
||||
error={Boolean(errors.description)}
|
||||
helperText={errors.description}
|
||||
onChange={setFieldValue('description')}
|
||||
variant="outlined"
|
||||
/>
|
||||
</section>
|
||||
<FormTemplate
|
||||
title={`${submitText} ${name} addon`}
|
||||
description={description || ''}
|
||||
documentationLink={documentationUrl}
|
||||
documentationLinkLabel="Addon documentation"
|
||||
formatApiCode={formatApiCode}
|
||||
>
|
||||
<StyledForm onSubmit={onSubmit}>
|
||||
<StyledContainer>
|
||||
<StyledFormSection>
|
||||
<StyledTextField
|
||||
size="small"
|
||||
label="Provider"
|
||||
name="provider"
|
||||
value={formValues.provider}
|
||||
disabled
|
||||
hidden={true}
|
||||
variant="outlined"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={formValues.enabled}
|
||||
onClick={onEnabled}
|
||||
/>
|
||||
}
|
||||
label={formValues.enabled ? 'Enabled' : 'Disabled'}
|
||||
/>
|
||||
</StyledFormSection>
|
||||
<StyledFormSection>
|
||||
<StyledHelpText>
|
||||
What is your addon description?
|
||||
</StyledHelpText>
|
||||
|
||||
<section className={styles.formSection}>
|
||||
<AddonMultiSelector
|
||||
options={selectableEvents || []}
|
||||
selectedItems={formValues.events}
|
||||
onChange={setEventValues}
|
||||
entityName={'event'}
|
||||
selectAllEnabled={false}
|
||||
/>
|
||||
</section>
|
||||
<section className={styles.formSection}>
|
||||
<AddonMultiSelector
|
||||
options={selectableProjects}
|
||||
selectedItems={formValues.projects || []}
|
||||
onChange={setProjects}
|
||||
entityName={'project'}
|
||||
selectAllEnabled={true}
|
||||
/>
|
||||
</section>
|
||||
<section className={styles.formSection}>
|
||||
<AddonMultiSelector
|
||||
options={selectableEnvironments}
|
||||
selectedItems={formValues.environments || []}
|
||||
onChange={setEnvironments}
|
||||
entityName={'environment'}
|
||||
selectAllEnabled={true}
|
||||
/>
|
||||
</section>
|
||||
<section className={styles.formSection}>
|
||||
<AddonParameters
|
||||
provider={provider}
|
||||
config={formValues}
|
||||
parametersErrors={errors.parameters}
|
||||
editMode={editMode}
|
||||
setParameterValue={setParameterValue}
|
||||
/>
|
||||
</section>
|
||||
<section className={styles.buttonsSection}>
|
||||
<Button type="submit" color="primary" variant="contained">
|
||||
{submitText}
|
||||
</Button>
|
||||
<Button type="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</section>
|
||||
</form>
|
||||
</PageContent>
|
||||
<StyledTextField
|
||||
size="small"
|
||||
style={{ width: '80%' }}
|
||||
minRows={4}
|
||||
multiline
|
||||
label="Description"
|
||||
name="description"
|
||||
placeholder=""
|
||||
value={formValues.description}
|
||||
error={Boolean(errors.description)}
|
||||
helperText={errors.description}
|
||||
onChange={setFieldValue('description')}
|
||||
variant="outlined"
|
||||
/>
|
||||
</StyledFormSection>
|
||||
|
||||
<StyledFormSection>
|
||||
<AddonMultiSelector
|
||||
options={selectableEvents || []}
|
||||
selectedItems={formValues.events}
|
||||
onChange={setEventValues}
|
||||
entityName={'event'}
|
||||
selectAllEnabled={false}
|
||||
description={
|
||||
'Select what events you want your addon to be notified about'
|
||||
}
|
||||
/>
|
||||
</StyledFormSection>
|
||||
<StyledFormSection>
|
||||
<AddonMultiSelector
|
||||
options={selectableProjects}
|
||||
selectedItems={formValues.projects || []}
|
||||
onChange={setProjects}
|
||||
entityName={'project'}
|
||||
selectAllEnabled={true}
|
||||
/>
|
||||
</StyledFormSection>
|
||||
<StyledFormSection>
|
||||
<AddonMultiSelector
|
||||
options={selectableEnvironments}
|
||||
selectedItems={formValues.environments || []}
|
||||
onChange={setEnvironments}
|
||||
entityName={'environment'}
|
||||
selectAllEnabled={true}
|
||||
/>
|
||||
</StyledFormSection>
|
||||
<StyledFormSection>
|
||||
<AddonParameters
|
||||
provider={provider}
|
||||
config={formValues}
|
||||
parametersErrors={errors.parameters}
|
||||
editMode={editMode}
|
||||
setParameterValue={setParameterValue}
|
||||
/>
|
||||
</StyledFormSection>
|
||||
</StyledContainer>
|
||||
<Divider />
|
||||
<StyledButtonContainer>
|
||||
<StyledButtonSection theme={theme}>
|
||||
<PermissionButton
|
||||
type="submit"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
permission={ADMIN}
|
||||
>
|
||||
{submitText}
|
||||
</PermissionButton>
|
||||
<Button type="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</StyledButtonSection>
|
||||
</StyledButtonContainer>
|
||||
</StyledForm>
|
||||
</FormTemplate>
|
||||
);
|
||||
};
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
Box,
|
||||
capitalize,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
Paper,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
@ -20,6 +19,12 @@ import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
||||
import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||
import { SelectAllButton } from '../../../admin/apiToken/ApiTokenForm/SelectProjectInput/SelectAllButton/SelectAllButton';
|
||||
import {
|
||||
StyledHelpText,
|
||||
StyledSelectAllFormControlLabel,
|
||||
StyledTitle,
|
||||
StyledAutocomplete,
|
||||
} from '../AddonForm.styles';
|
||||
|
||||
export interface IAddonMultiSelectorProps {
|
||||
options: IAutocompleteBoxOption[];
|
||||
@ -29,6 +34,7 @@ export interface IAddonMultiSelectorProps {
|
||||
onFocus?: () => void;
|
||||
entityName: string;
|
||||
selectAllEnabled: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const ALL_OPTIONS = '*';
|
||||
@ -47,6 +53,7 @@ export const AddonMultiSelector: VFC<IAddonMultiSelectorProps> = ({
|
||||
onFocus,
|
||||
entityName,
|
||||
selectAllEnabled = true,
|
||||
description,
|
||||
}) => {
|
||||
const [isWildcardSelected, selectWildcard] = useState(
|
||||
selectedItems.includes(ALL_OPTIONS)
|
||||
@ -117,68 +124,70 @@ export const AddonMultiSelector: VFC<IAddonMultiSelectorProps> = ({
|
||||
</Fragment>
|
||||
);
|
||||
const SelectAllFormControl = () => (
|
||||
<Box sx={{ mt: 1, mb: 0.25, ml: 1.5 }}>
|
||||
<FormControlLabel
|
||||
data-testid={`select-all-${entityName}s`}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={isWildcardSelected}
|
||||
onChange={onAllItemsChange}
|
||||
/>
|
||||
}
|
||||
label={`ALL current and future ${entityName}s`}
|
||||
/>
|
||||
</Box>
|
||||
<StyledSelectAllFormControlLabel
|
||||
data-testid={`select-all-${entityName}s`}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={isWildcardSelected}
|
||||
onChange={onAllItemsChange}
|
||||
/>
|
||||
}
|
||||
label={`ALL current and future ${entityName}s`}
|
||||
/>
|
||||
);
|
||||
|
||||
const HelpText = () => (
|
||||
<p>
|
||||
const DefaultHelpText = () => (
|
||||
<StyledHelpText>
|
||||
Selecting {entityName}(s) here will filter events so that your addon
|
||||
will only receive events that are tagged with one of your{' '}
|
||||
{entityName}s.
|
||||
</p>
|
||||
</StyledHelpText>
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h4>{capitalize(entityName)}s</h4>
|
||||
<StyledTitle>{capitalize(entityName)}s</StyledTitle>
|
||||
<ConditionallyRender
|
||||
condition={description !== undefined}
|
||||
show={<StyledHelpText>{description}</StyledHelpText>}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={selectAllEnabled}
|
||||
show={<HelpText />}
|
||||
show={<DefaultHelpText />}
|
||||
/>
|
||||
<span className={themeStyles.error}>{error}</span>
|
||||
<br />
|
||||
<Box sx={{ mt: -1, mb: 3 }}>
|
||||
<ConditionallyRender
|
||||
condition={selectAllEnabled}
|
||||
show={<SelectAllFormControl />}
|
||||
/>
|
||||
<Autocomplete
|
||||
disabled={isWildcardSelected}
|
||||
multiple
|
||||
limitTags={2}
|
||||
options={options}
|
||||
disableCloseOnSelect
|
||||
getOptionLabel={({ label }) => label}
|
||||
fullWidth
|
||||
groupBy={() => 'Select/Deselect all'}
|
||||
renderGroup={renderGroup}
|
||||
PaperComponent={CustomPaper}
|
||||
renderOption={renderOption}
|
||||
renderInput={renderInput}
|
||||
value={
|
||||
isWildcardSelected
|
||||
? options
|
||||
: options.filter(option =>
|
||||
selectedItems.includes(option.value)
|
||||
)
|
||||
}
|
||||
onChange={(_, input) => {
|
||||
const state = input.map(({ value }) => value);
|
||||
onChange(state);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<ConditionallyRender
|
||||
condition={selectAllEnabled}
|
||||
show={<SelectAllFormControl />}
|
||||
/>
|
||||
<StyledAutocomplete
|
||||
disabled={isWildcardSelected}
|
||||
multiple
|
||||
limitTags={2}
|
||||
options={options}
|
||||
disableCloseOnSelect
|
||||
//@ts-expect-error
|
||||
getOptionLabel={({ label }) => label}
|
||||
fullWidth
|
||||
groupBy={() => 'Select/Deselect all'}
|
||||
renderGroup={renderGroup}
|
||||
PaperComponent={CustomPaper}
|
||||
//@ts-expect-error
|
||||
renderOption={renderOption}
|
||||
renderInput={renderInput}
|
||||
value={
|
||||
isWildcardSelected
|
||||
? options
|
||||
: options.filter(option =>
|
||||
selectedItems.includes(option.value)
|
||||
)
|
||||
}
|
||||
onChange={(_, input) => {
|
||||
//@ts-expect-error
|
||||
const state = input.map(({ value }) => value);
|
||||
onChange(state);
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { TextField } from '@mui/material';
|
||||
import { IAddonConfig, IAddonProviderParams } from 'interfaces/addons';
|
||||
import { ChangeEventHandler } from 'react';
|
||||
import { StyledAddonParameterContainer } from '../../AddonForm.styles';
|
||||
|
||||
const resolveType = ({ type = 'text', sensitive = false }, value: string) => {
|
||||
if (sensitive && value === MASKED_VALUE) {
|
||||
@ -32,7 +33,7 @@ export const AddonParameter = ({
|
||||
const error = parametersErrors[definition.name];
|
||||
|
||||
return (
|
||||
<div style={{ width: '80%', marginTop: '25px' }}>
|
||||
<StyledAddonParameterContainer>
|
||||
<TextField
|
||||
size="small"
|
||||
style={{ width: '100%' }}
|
||||
@ -51,6 +52,6 @@ export const AddonParameter = ({
|
||||
variant="outlined"
|
||||
helperText={definition.description}
|
||||
/>
|
||||
</div>
|
||||
</StyledAddonParameterContainer>
|
||||
);
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
AddonParameter,
|
||||
IAddonParameterProps,
|
||||
} from './AddonParameter/AddonParameter';
|
||||
import { StyledTitle } from '../AddonForm.styles';
|
||||
|
||||
interface IAddonParametersProps {
|
||||
provider?: IAddonProvider;
|
||||
@ -21,10 +22,9 @@ export const AddonParameters = ({
|
||||
editMode,
|
||||
}: IAddonParametersProps) => {
|
||||
if (!provider) return null;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h4>Parameters</h4>
|
||||
<StyledTitle>Parameters</StyledTitle>
|
||||
{editMode ? (
|
||||
<p>
|
||||
Sensitive parameters will be masked with value "<i>*****</i>
|
||||
|
Loading…
Reference in New Issue
Block a user