mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-15 01:16:22 +02:00
454 lines
17 KiB
TypeScript
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>
|
|
);
|
|
};
|