1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-15 01:16:22 +02:00
unleash.unleash/frontend/src/component/integrations/IntegrationForm/IntegrationForm.tsx

454 lines
17 KiB
TypeScript

import {
type ChangeEventHandler,
type FormEventHandler,
type MouseEventHandler,
useContext,
useEffect,
useState,
type VFC,
} from 'react';
import {
Alert,
Button,
Divider,
Link,
styled,
Typography,
} from '@mui/material';
import produce from 'immer';
import { trim } from 'component/common/util';
import type { AddonSchema, AddonTypeSchema } from 'openapi';
import { IntegrationParameters } from './IntegrationParameters/IntegrationParameters';
import { IntegrationInstall } from './IntegrationInstall/IntegrationInstall';
import cloneDeep from 'lodash.clonedeep';
import { useNavigate } from 'react-router-dom';
import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import { IntegrationMultiSelector } from './IntegrationMultiSelector/IntegrationMultiSelector';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import {
CREATE_ADDON,
UPDATE_ADDON,
} from '../../providers/AccessProvider/permissions';
import {
StyledForm,
StyledAlerts,
StyledTextField,
StyledContainer,
StyledButtonContainer,
StyledButtonSection,
StyledConfigurationSection,
StyledTitle,
StyledRaisedSection,
} from './IntegrationForm.styles';
import { GO_BACK } from 'constants/navigate';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IntegrationDelete } from './IntegrationDelete/IntegrationDelete';
import { IntegrationStateSwitch } from './IntegrationStateSwitch/IntegrationStateSwitch';
import { capitalizeFirst } from 'utils/capitalizeFirst';
import { IntegrationHowToSection } from '../IntegrationHowToSection/IntegrationHowToSection';
import { IntegrationEventsModal } from '../IntegrationEvents/IntegrationEventsModal';
import AccessContext from 'contexts/AccessContext';
const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
marginBottom: theme.fontSizes.mainHeader,
}));
const StyledHeaderTitle = styled('h1')({
fontWeight: 'normal',
});
type IntegrationFormProps = {
provider?: AddonTypeSchema;
fetch: () => void;
editMode: boolean;
addon: AddonSchema | Omit<AddonSchema, 'id'>;
};
export const IntegrationForm: VFC<IntegrationFormProps> = ({
editMode,
provider,
addon: initialValues,
fetch,
}) => {
const { createAddon, updateAddon } = useAddonsApi();
const { setToastData, setToastApiError } = useToast();
const navigate = useNavigate();
const { projects: availableProjects } = useProjects();
const selectableProjects = availableProjects.map((project) => ({
value: project.id,
label: project.name,
}));
const { environments: availableEnvironments } = useEnvironments();
const selectableEnvironments = availableEnvironments.map((environment) => ({
value: environment.name,
label: environment.name,
}));
const selectableEvents = provider?.events
?.map((event) => ({
value: event,
label: event,
}))
.sort((a, b) => a.label.localeCompare(b.label));
const { uiConfig } = useUiConfig();
const [formValues, setFormValues] = useState(initialValues);
const [errors, setErrors] = useState<{
containsErrors: boolean;
parameters: Record<string, string>;
events?: string;
projects?: string;
environments?: string;
general?: string;
description?: string;
}>({
containsErrors: false,
parameters: {},
});
const [eventsModalOpen, setEventsModalOpen] = useState(false);
const { isAdmin } = useContext(AccessContext);
const submitText = editMode ? 'Update' : 'Create';
const url = `${uiConfig.unleashUrl}/api/admin/addons${
editMode ? `/${(formValues as AddonSchema).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) {
fetch();
}
}, [fetch, provider]); // empty array => fetch only first time
useEffect(() => {
setFormValues({ ...initialValues });
/* eslint-disable-next-line */
}, [initialValues.description, initialValues.provider]);
useEffect(() => {
if (provider && !formValues.provider) {
setFormValues({ ...initialValues, provider: provider.name });
}
}, [provider, initialValues, formValues.provider]);
const setFieldValue =
(field: string): ChangeEventHandler<HTMLInputElement> =>
(event) => {
event.preventDefault();
setFormValues({ ...formValues, [field]: event.target.value });
};
const onEnabled: MouseEventHandler = (event) => {
event.preventDefault();
setFormValues(({ enabled }) => ({ ...formValues, enabled: !enabled }));
};
const setParameterValue =
(param: string): ChangeEventHandler<HTMLInputElement> =>
(event) => {
event.preventDefault();
const value =
trim(event.target.value) === ''
? undefined
: event.target.value;
setFormValues(
produce((draft) => {
if (value === undefined) {
delete draft.parameters[param];
} else {
draft.parameters[param] = value;
}
}),
);
};
const setEventValues = (events: string[]) => {
setFormValues(
produce((draft) => {
draft.events = events;
}),
);
setErrors((prev) => ({
...prev,
events: undefined,
}));
};
const setProjects = (projects: string[]) => {
setFormValues(
produce((draft) => {
draft.projects = projects;
}),
);
setErrors((prev) => ({
...prev,
projects: undefined,
}));
};
const setEnvironments = (environments: string[]) => {
setFormValues(
produce((draft) => {
draft.environments = environments;
}),
);
setErrors((prev) => ({
...prev,
environments: undefined,
}));
};
const onCancel = () => {
navigate(GO_BACK);
};
const onSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
event.preventDefault();
if (!provider) {
return;
}
const updatedErrors = cloneDeep(errors);
updatedErrors.parameters = {};
updatedErrors.containsErrors = false;
// Validations
if (formValues.events.length === 0) {
updatedErrors.events = 'You must listen to at least one event';
updatedErrors.containsErrors = true;
}
provider.parameters?.forEach((parameterConfig) => {
let value = formValues.parameters[parameterConfig.name];
value = typeof value === 'string' ? trim(value) : value;
if (parameterConfig.required && !value) {
updatedErrors.parameters[parameterConfig.name] =
'This field is required';
updatedErrors.containsErrors = true;
}
});
if (updatedErrors.containsErrors) {
setErrors(updatedErrors);
return;
}
try {
if (editMode) {
await updateAddon(formValues as AddonSchema);
navigate('/integrations');
setToastData({
type: 'success',
title: 'Integration updated successfully',
});
} else {
await createAddon(formValues as Omit<AddonSchema, 'id'>);
navigate('/integrations');
setToastData({
type: 'success',
confetti: true,
title: 'Integration created successfully',
});
}
} catch (error) {
const message = formatUnknownError(error);
setToastApiError(message);
setErrors({
parameters: {},
general: message,
containsErrors: true,
});
}
};
const {
name,
displayName,
description,
documentationUrl = 'https://unleash.github.io/docs/addons',
installation,
alerts,
} = provider ? provider : ({} as Partial<AddonTypeSchema>);
return (
<FormTemplate
description={description || ''}
documentationLink={documentationUrl}
documentationLinkLabel={`${
displayName || capitalizeFirst(`${name} `)
} documentation`}
formatApiCode={formatApiCode}
footer={
<StyledButtonContainer>
<StyledButtonSection>
<PermissionButton
type='submit'
color='primary'
variant='contained'
permission={editMode ? UPDATE_ADDON : CREATE_ADDON}
onClick={onSubmit}
>
{submitText}
</PermissionButton>
<Button type='button' onClick={onCancel}>
Cancel
</Button>
</StyledButtonSection>
</StyledButtonContainer>
}
>
<StyledHeader>
<StyledHeaderTitle>
{submitText}{' '}
{displayName || (name ? capitalizeFirst(name) : '')}{' '}
integration
</StyledHeaderTitle>
<ConditionallyRender
condition={editMode && isAdmin}
show={
<Link onClick={() => setEventsModalOpen(true)}>
View events
</Link>
}
/>
</StyledHeader>
<StyledForm onSubmit={onSubmit}>
<StyledContainer>
<ConditionallyRender
condition={Boolean(alerts)}
show={() => (
<StyledAlerts>
{alerts?.map(({ type, text }) => (
<Alert severity={type} key={text}>
{text}
</Alert>
))}
</StyledAlerts>
)}
/>
<StyledTextField
size='small'
label='Provider'
name='provider'
value={formValues.provider}
disabled
hidden={true}
variant='outlined'
/>
<IntegrationHowToSection provider={provider} />
<StyledRaisedSection>
<IntegrationStateSwitch
checked={formValues.enabled}
onClick={onEnabled}
/>
</StyledRaisedSection>
<StyledRaisedSection>
<ConditionallyRender
condition={Boolean(installation)}
show={() => (
<IntegrationInstall
url={installation!.url}
title={installation!.title}
helpText={installation!.helpText}
/>
)}
/>
<IntegrationParameters
provider={provider}
config={formValues as AddonSchema}
parametersErrors={errors.parameters}
editMode={editMode}
setParameterValue={setParameterValue}
/>
</StyledRaisedSection>
<StyledConfigurationSection>
<Typography component='h3' variant='h3'>
Configuration
</Typography>
<div>
<StyledTitle>
What is your integration description?
</StyledTitle>
<StyledTextField
size='small'
minRows={1}
multiline
label='Description'
name='description'
placeholder=''
value={formValues.description}
error={Boolean(errors.description)}
helperText={errors.description}
onChange={setFieldValue('description')}
variant='outlined'
/>
</div>
<div>
<IntegrationMultiSelector
options={selectableEvents || []}
selectedItems={formValues.events}
onChange={setEventValues}
entityName='event'
error={errors.events}
description='Select which events you want your integration to be notified about.'
required
/>
</div>
<div>
<IntegrationMultiSelector
options={selectableProjects}
selectedItems={formValues.projects || []}
onChange={setProjects}
entityName='project'
description='Selecting project(s) will filter events, so that your integration only receives events related to those specific projects.'
note='If no projects are selected, the integration will receive events from all projects.'
/>
</div>
<div>
<IntegrationMultiSelector
options={selectableEnvironments}
selectedItems={formValues.environments || []}
onChange={setEnvironments}
entityName='environment'
description='Selecting environment(s) will filter events, so that your integration only receives events related to those specific environments. Global events that are not specific to an environment will still be received.'
note='If no environments are selected, the integration will receive events from all environments.'
/>
</div>
</StyledConfigurationSection>
<ConditionallyRender
condition={editMode}
show={
<>
<Divider />
<section>
<IntegrationDelete
id={(formValues as AddonSchema).id}
/>
</section>
</>
}
/>
</StyledContainer>
</StyledForm>
<IntegrationEventsModal
addon={initialValues as AddonSchema}
open={eventsModalOpen}
setOpen={setEventsModalOpen}
/>
</FormTemplate>
);
};