formik#FormikProps TypeScript Examples
The following examples show how to use
formik#FormikProps.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.tsx From marina with MIT License | 6 votes |
LogInForm = (props: FormikProps<LogInFormValues>) => {
const { isSubmitting, handleSubmit } = props;
const openOnboardingTab = async () => {
const url = browser.runtime.getURL(`home.html#${INITIALIZE_WELCOME_ROUTE}`);
await browser.tabs.create({ url });
};
return (
<div className="flex flex-col">
<form onSubmit={handleSubmit} className="mt-10">
<Input
name="password"
type="password"
placeholder="Enter your password"
title="Password"
{...props}
/>
<Button className="w-full mb-8 text-base" disabled={isSubmitting} type="submit">
Log in
</Button>
</form>
<div className="hover:underline text-primary self-start justify-start font-bold align-bottom">
<span className="cursor-pointer" onClick={openOnboardingTab}>
Restore account
</span>
</div>
</div>
);
}
Example #2
Source File: MetricFormFields.test.tsx From abacus with GNU General Public License v2.0 | 6 votes |
test('renders as expected for conversion metric', async () => {
const metric = Fixtures.createMetric(1)
const { container } = render(
<Formik
initialValues={{
metric: metricToFormData(metric),
}}
onSubmit={
/* istanbul ignore next; This is unused */
() => undefined
}
>
{(formikProps: FormikProps<{ metric: MetricFormData }>) => <MetricFormFields formikProps={formikProps} />}
</Formik>,
)
expect(container).toMatchSnapshot()
await act(async () => {
fireEvent.click(screen.getByRole('radio', { name: 'Revenue' }))
})
})
Example #3
Source File: MetricFormFields.test.tsx From abacus with GNU General Public License v2.0 | 6 votes |
test('renders as expected for revenue metric', async () => {
const metric = Fixtures.createMetric(2)
const { container } = render(
<Formik
initialValues={{
metric: metricToFormData(metric),
}}
onSubmit={
/* istanbul ignore next; This is unused */
() => undefined
}
>
{(formikProps: FormikProps<{ metric: MetricFormData }>) => <MetricFormFields formikProps={formikProps} />}
</Formik>,
)
expect(container).toMatchSnapshot()
await act(async () => {
fireEvent.click(screen.getByRole('radio', { name: 'Conversion' }))
})
})
Example #4
Source File: MetricFormFields.test.tsx From abacus with GNU General Public License v2.0 | 6 votes |
test('renders as expected for new metric', () => {
const { container } = render(
<Formik
initialValues={{
metric: metricToFormData({}),
}}
onSubmit={
/* istanbul ignore next; This is unused */
() => undefined
}
>
{(formikProps: FormikProps<{ metric: MetricFormData }>) => <MetricFormFields formikProps={formikProps} />}
</Formik>,
)
expect(container).toMatchSnapshot()
})
Example #5
Source File: index.tsx From marina with MIT License | 6 votes |
BackUpUnlockForm = (props: FormikProps<BackUpUnlockFormValues>) => {
const { handleSubmit, isSubmitting } = props;
return (
<form onSubmit={handleSubmit}>
<div>
<h1 className="text-3xl font-medium">Unlock</h1>
<p className="mt-10 mb-5 text-base">Enter your password to save your mnemonic</p>
<Input name="password" placeholder="Password" type="password" {...props} />
</div>
<div className="">
<Button type="submit" disabled={isSubmitting}>
{!isSubmitting ? `Unlock` : `Please wait...`}
</Button>
</div>
</form>
);
}
Example #6
Source File: modal-unlock.tsx From marina with MIT License | 6 votes |
ModalUnlockForm = (props: FormikProps<ModalUnlockFormValues>) => {
const { handleSubmit, values, isSubmitting } = props;
return (
<form onSubmit={handleSubmit}>
<div>
<h2 className="mt-4 text-2xl font-medium text-center">Unlock</h2>
<p className="mt-10 mb-5 text-base text-left">Enter your password to unlock your wallet</p>
<Input name="password" placeholder="Password" type="password" {...props} />
</div>
<div className="bottom-10 right-8 absolute flex justify-end">
<div className="pr-1">
<Button
isOutline={true}
onClick={() => values.handleModalUnlockClose()}
className="bg-secondary hover:bg-secondary-light"
>
Cancel
</Button>
</div>
<div>
<Button type="submit" disabled={isSubmitting}>
{!isSubmitting ? `Unlock` : `Please wait...`}
</Button>
</div>
</div>
</form>
);
}
Example #7
Source File: explorer-custom-form.tsx From marina with MIT License | 6 votes |
SettingsExplorerForm = (props: FormikProps<SettingsExplorerFormValues>) => {
const { handleSubmit, isSubmitting, errors } = props;
return (
<form onSubmit={handleSubmit}>
<p className="font-sm mt-8 mb-1">Custom explorer</p>
<p className="font-sm mt-5 mb-1 text-left">Esplora URL</p>
<Input name="esploraURL" placeholder="Esplora valid endpoint" type="text" {...props} />
<p className="font-sm mt-5 mb-1 text-left">Electrs URL</p>
<Input name="electrsURL" placeholder="Electrs valid endpoint" type="text" {...props} />
<div className="bottom-10 right-8 absolute flex justify-end">
<div>
<Button disabled={!!errors.esploraURL || isSubmitting} type="submit">
Update
</Button>
</div>
</div>
</form>
);
}
Example #8
Source File: InfraEnvFormPage.tsx From assisted-ui-lib with Apache License 2.0 | 5 votes |
InfraEnvFormPage: React.FC<InfraEnvFormPageProps> = ({
usedNames,
onSubmit,
onClose,
onFinish,
onValuesChanged,
formRef,
}) => {
const [error, setError] = React.useState<string | undefined>();
return (
<Formik
initialValues={initialValues}
initialStatus={{ error: null }}
validate={getRichTextValidation(validationSchema(usedNames))}
onSubmit={async (values: EnvironmentStepFormValues) => {
try {
await onSubmit?.(values);
onFinish?.(values);
} catch (e) {
setError(e?.message ?? 'An error occured');
}
}}
innerRef={formRef}
>
{({ isValid, isSubmitting, submitForm }: FormikProps<EnvironmentStepFormValues>) => (
<Stack hasGutter>
<StackItem>
{onSubmit && onClose ? (
<Grid hasGutter span={8}>
<GridItem>
<Title headingLevel="h1" size={TitleSizes.xl}>
Configure environment
</Title>
</GridItem>
<GridItem>
<InfraEnvForm onValuesChanged={onValuesChanged} />
</GridItem>
</Grid>
) : (
<div className="infra-env__form">
<InfraEnvForm onValuesChanged={onValuesChanged} />
</div>
)}
</StackItem>
{error && (
<StackItem>
<Alert
variant={AlertVariant.danger}
actionClose={<AlertActionCloseButton onClose={() => setError(undefined)} />}
title="Error creating InfraEnv"
>
{error}
</Alert>
</StackItem>
)}
{onSubmit && onClose && (
<StackItem>
<>
<Button
variant="primary"
type="submit"
isDisabled={!isValid || isSubmitting}
onClick={submitForm}
>
Create {isSubmitting && <Spinner isSVG size="md" />}
</Button>
<Button variant="link" onClick={onClose} isDisabled={isSubmitting}>
Cancel
</Button>
</>
</StackItem>
)}
</Stack>
)}
</Formik>
);
}
Example #9
Source File: change-password.tsx From marina with MIT License | 5 votes |
SettingsChangePasswordForm = (props: FormikProps<SettingsChangePasswordFormValues>) => {
const { handleSubmit, errors, isSubmitting, touched } = props;
const history = useHistory();
const handleCancel = () => history.goBack();
return (
<form onSubmit={handleSubmit} className="mt-5">
<Input
name="currentPassword"
placeholder="Enter current password"
title="Current password"
type="password"
{...props}
/>
<Input
name="newPassword"
placeholder="Enter new password"
title="New password"
type="password"
{...props}
/>
<Input
name="confirmNewPassword"
placeholder="Confirm new password"
title="Confirm new password"
type="password"
{...props}
/>
<div className="flex justify-end">
<div className="pr-1">
<Button
isOutline={true}
onClick={handleCancel}
className="bg-secondary hover:bg-secondary-light"
>
Cancel
</Button>
</div>
<div>
<Button
disabled={
isSubmitting ||
!!(
(errors.currentPassword && touched.currentPassword) ||
(errors.newPassword && touched.newPassword) ||
(errors.confirmNewPassword && touched.confirmNewPassword)
)
}
type="submit"
>
Update
</Button>
</div>
</div>
</form>
);
}
Example #10
Source File: onboarding-form.tsx From marina with MIT License | 5 votes |
OnboardingFormView = (props: FormikProps<OnboardingFormValues>) => {
const { values, touched, errors, isSubmitting, handleChange, handleBlur, handleSubmit } = props;
return (
<form onSubmit={handleSubmit} className="mt-10">
<div className={cx({ 'mb-12': !errors.password || !touched.password })}>
<label className="block">
<p className="mb-2 font-medium">Create password</p>
<input
className={cx(
'border-2 focus:ring-primary focus:border-primary placeholder-grayLight block w-3/5 rounded-md',
{
'border-red': errors.password && touched.password,
'border-grayLight': !errors.password,
}
)}
id="password"
name="password"
onChange={handleChange}
onBlur={handleBlur}
placeholder="Enter your password"
type="password"
value={values.password}
/>
</label>
{errors.password && touched.password && (
<p className="text-red h-10 mt-2 text-xs">{errors.password}</p>
)}
</div>
<div className={cx({ 'mb-12': !errors.confirmPassword || !touched.confirmPassword })}>
<label className="block">
<p className="mb-2 font-medium">Confirm password</p>
<input
className={cx(
'border-2 focus:ring-primary focus:border-primary placeholder-grayLight block w-3/5 rounded-md',
{
'border-red': errors.confirmPassword && touched.confirmPassword,
'border-grayLight': !errors.confirmPassword,
}
)}
id="confirmPassword"
name="confirmPassword"
onChange={handleChange}
onBlur={handleBlur}
placeholder="Confirm your password"
type="password"
value={values.confirmPassword}
/>
</label>
{errors.confirmPassword && touched.confirmPassword && (
<p className="text-red h-10 mt-2 text-xs">{errors.confirmPassword}</p>
)}
</div>
<div className={cx({ 'mb-12': !errors.acceptTerms || !touched.acceptTerms })}>
<label htmlFor="acceptTerms" className="text-grayLight block text-base">
<input
className="focus:ring-primary text-primary border-grayLight w-4 h-4 mr-2 text-base rounded"
checked={values.acceptTerms}
id="acceptTerms"
name="acceptTerms"
onChange={handleChange}
onBlur={handleBlur}
type="checkbox"
/>
{'I’ve read and accept the '}
<OpenTerms />
</label>
{errors.acceptTerms && touched.acceptTerms && (
<p className="text-red h-10 mt-2 text-xs">{errors.acceptTerms}</p>
)}
</div>
<Button className="w-3/5 text-base" disabled={isSubmitting} type="submit">
Create
</Button>
</form>
);
}
Example #11
Source File: index.tsx From synapse-extension with MIT License | 5 votes |
innerForm = (props: FormikProps<FormValues>): React.ReactElement => {
const intl = useIntl();
const { values, touched, errors, handleChange, handleBlur, handleSubmit } = props;
return (
<Form
className="manage-contacts"
id="manage-contacts"
onSubmit={handleSubmit}
aria-label="form"
>
<TextField
size="small"
label={intl.formatMessage({ id: 'NetWorkName' })}
id="title"
name="title"
type="text"
fullWidth
value={values.title}
onChange={handleChange}
onBlur={handleBlur}
error={!!errors.title}
helperText={errors.title && touched.title && errors.title}
margin="normal"
variant="outlined"
data-testid="field-title"
/>
<FormControl>
<NativeSelect
value={values.prefix}
onChange={handleChange}
inputProps={{
name: 'prefix',
id: 'prefix',
}}
>
<option value="ckb">Mainnet</option>
<option value="ckt">Testnet</option>
<option value="local">Local</option>
</NativeSelect>
</FormControl>
<TextField
size="small"
label={intl.formatMessage({ id: 'CKB Node URL' })}
id="nodeURL"
name="nodeURL"
type="text"
fullWidth
value={values.nodeURL}
onChange={handleChange}
onBlur={handleBlur}
error={!!errors.nodeURL}
helperText={errors.nodeURL && touched.nodeURL && errors.nodeURL}
margin="normal"
variant="outlined"
data-testid="field-nodeURL"
/>
<TextField
size="small"
label={intl.formatMessage({ id: 'CKB Cache Layer URL' })}
id="cacheURL"
name="cacheURL"
type="text"
fullWidth
value={values.cacheURL}
onChange={handleChange}
onBlur={handleBlur}
error={!!errors.cacheURL}
helperText={errors.cacheURL && touched.cacheURL && errors.cacheURL}
margin="normal"
variant="outlined"
data-testid="field-cacheURL"
/>
<Button type="submit" id="submit-button" color="primary" variant="contained">
<FormattedMessage id="Add" />
</Button>
</Form>
);
}
Example #12
Source File: EditSSHKeyModal.tsx From assisted-ui-lib with Apache License 2.0 | 5 votes |
EditSSHKeyModal: React.FC<EditSSHKeyModalProps> = ({
isOpen,
onClose,
infraEnv,
onSubmit,
hasAgents,
hasBMHs,
}) => {
const [error, setError] = React.useState<string | undefined>();
const warningMsg = getWarningMessage(hasAgents, hasBMHs);
return (
<Modal
aria-label="Edit SSH public key dialog"
title="Edi SSH public key"
isOpen={isOpen}
onClose={onClose}
variant={ModalVariant.small}
hasNoBodyWrapper
id="edit-ssh-key-modal"
>
<Formik<EditSSHKeyFormikValues>
initialValues={{
sshPublicKey: infraEnv.spec?.sshAuthorizedKey || '',
}}
validationSchema={validationSchema}
onSubmit={async (values) => {
try {
await onSubmit(values, infraEnv);
onClose();
} catch (err) {
setError(err?.message || 'An error occured');
}
}}
validateOnMount
>
{({ isSubmitting, isValid, submitForm }: FormikProps<EditSSHKeyModalProps>) => (
<>
<ModalBoxBody>
<Stack hasGutter>
<StackItem>
<Alert isInline variant="warning" title={warningMsg} />
</StackItem>
<StackItem>
<UploadSSH />
</StackItem>
{error && (
<StackItem>
<Alert title={error} variant="danger" isInline />
</StackItem>
)}
</Stack>
</ModalBoxBody>
<ModalBoxFooter>
<Button onClick={submitForm} isDisabled={isSubmitting || !isValid}>
Save
</Button>
<Button onClick={onClose} variant={ButtonVariant.secondary}>
Cancel
</Button>
</ModalBoxFooter>
</>
)}
</Formik>
</Modal>
);
}
Example #13
Source File: Audience.test.tsx From abacus with GNU General Public License v2.0 | 5 votes |
test('renders as expected', async () => {
const indexedSegments: Record<number, Segment> = {
1: { segmentId: 1, name: 'us', type: SegmentType.Country },
2: { segmentId: 2, name: 'au', type: SegmentType.Country },
3: { segmentId: 3, name: 'en-US', type: SegmentType.Locale },
4: { segmentId: 4, name: 'en-AU', type: SegmentType.Locale },
}
// Turning on debug mode for the exclusion group tags
mockedUtilsGeneral.isDebugMode.mockImplementation(() => true)
const { container } = render(
<Formik
initialValues={{ experiment: experimentToFormData({}) }}
onSubmit={
/* istanbul ignore next; This is unused */
() => undefined
}
>
{(formikProps: FormikProps<{ experiment: ExperimentFormData }>) => (
<Audience {...{ formikProps, indexedSegments, completionBag }} />
)}
</Formik>,
)
expect(container).toMatchSnapshot()
const addVariationButton = screen.getByRole('button', { name: /Add variation/i })
fireEvent.click(addVariationButton)
const removeVariationButton = screen.getAllByRole('button', { name: /Remove variation/i })
fireEvent.click(removeVariationButton[0])
const segmentComboboxInput = screen.getByPlaceholderText(/Search and select to customize/)
await act(async () => {
fireEvent.change(segmentComboboxInput, { target: { value: 'AU' } })
})
const segmentOption = await screen.findByRole('option', { name: /Locale: en-AU/i })
await act(async () => {
fireEvent.click(segmentOption)
})
// eslint-disable-next-line @typescript-eslint/require-await
await act(async () => {
fireEvent.click(screen.getByLabelText(/Exclude/))
})
await changeFieldByRole('textbox', /Exclusion Groups/, 'tag_1')
await act(async () => {
fireEvent.click(screen.getByRole('option', { name: /tag_1/ }))
})
expect(container).toMatchSnapshot()
})
Example #14
Source File: Login.tsx From krmanga with MIT License | 5 votes |
Login = ({ navigation, dispatch, isLogin, loading }: IProps) => {
const [disabled, setDisabled] = useState<boolean>(false);
useEffect(() => {
if (isLogin) {
setTimeout(() => {
navigation.goBack();
}, 100);
}
}, [isLogin]);
const onSubmit = (values: Values) => {
if (disabled || loading) {
return;
}
setDisabled(true);
dispatch({
type: "user/login",
payload: values,
callback: () => {
setDisabled(false);
}
});
};
const cancel = (form: FormikProps<string>, field: FieldInputProps<string>) => {
if (field.name === "account") {
form.setFieldValue("account", "");
} else if (field.name === "password") {
form.setFieldValue("password", "");
}
};
return (
<ScrollView keyboardShouldPersistTaps="handled" style={styles.container}>
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
validationSchema={customerValidation}>
{({ handleSubmit }) => (
<View>
<Field
name="account"
placeholder="请输入用户名"
component={Input}
iconName={"icon-Account"}
cancel={cancel}
/>
<Field
name="password"
placeholder="请输入密码"
component={Input}
iconName={"icon-mima"}
secureTextEntry
cancel={cancel}
/>
<View style={styles.jumpView}>
<Text style={styles.jumpTitle}>忘记密码?</Text>
<Touchable onPress={() => navigation.navigate("Register")}>
<Text style={styles.jumpTitle}>注册账号</Text>
</Touchable>
</View>
<Touchable disabled={disabled} onPress={handleSubmit} style={styles.login}>
<Text style={styles.loginText}>登录</Text>
</Touchable>
</View>
)}
</Formik>
</ScrollView>
);
}
Example #15
Source File: SQFormTextarea.tsx From SQForm with MIT License | 5 votes |
function SQFormTextarea({
name,
label,
isDisabled = false,
placeholder = '',
size = 'auto',
onBlur,
onChange,
rows = 3,
rowsMax = 3,
maxCharacters,
inputProps = {},
muiFieldProps = {},
}: SQFormTextareaProps): JSX.Element {
const {values}: FormikProps<FormValues> = useFormikContext();
const {
fieldState: {isFieldError, isFieldRequired},
fieldHelpers: {
handleBlur,
handleChange: handleChangeHelper,
HelperTextComponent,
},
} = useForm<string, React.ChangeEvent<HTMLInputElement>>({
name,
onBlur,
onChange,
});
const [valueLength, setValueLength] = React.useState<number>(
values[name]?.length || 0
);
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
setValueLength(event.target.value.length);
handleChangeHelper(event);
};
const maxCharactersValue = inputProps.maxLength || maxCharacters;
const characterCounter =
maxCharactersValue && `: ${valueLength}/${maxCharactersValue}`;
const labelText = (
<span>
{label} {characterCounter}
</span>
);
return (
<Grid item sm={size}>
<TextField
id={toKebabCase(name)}
color="primary"
disabled={isDisabled}
error={isFieldError}
fullWidth={true}
InputLabelProps={{shrink: true}}
FormHelperTextProps={{error: isFieldError}}
name={name}
label={labelText}
multiline={true}
helperText={!isDisabled && HelperTextComponent}
placeholder={placeholder}
onChange={handleChange}
onBlur={handleBlur}
required={isFieldRequired}
minRows={rows}
maxRows={rowsMax}
variant="outlined"
value={values[name]}
inputProps={{
maxLength: maxCharacters,
...inputProps,
}}
{...muiFieldProps}
/>
</Grid>
);
}
Example #16
Source File: BMCForm.tsx From assisted-ui-lib with Apache License 2.0 | 4 votes |
BMCForm: React.FC<BMCFormProps> = ({
onCreateBMH,
onClose,
hasDHCP,
infraEnv,
bmh,
nmState,
secret,
isEdit,
usedHostnames,
}) => {
const [error, setError] = React.useState();
const handleSubmit: FormikConfig<AddBmcValues>['onSubmit'] = async (values) => {
try {
setError(undefined);
const nmState = values.nmState ? getNMState(values, infraEnv) : undefined;
await onCreateBMH(values, nmState);
onClose();
} catch (e) {
setError(e.message);
}
};
const { initValues, validationSchema } = React.useMemo(() => {
const initValues = getInitValues(bmh, nmState, secret, isEdit);
const validationSchema = getValidationSchema(usedHostnames, initValues.hostname);
return { initValues, validationSchema };
}, [usedHostnames, bmh, nmState, secret, isEdit]);
return (
<Formik
initialValues={initValues}
isInitialValid={false}
validate={getRichTextValidation(validationSchema)}
onSubmit={handleSubmit}
>
{({ isSubmitting, isValid, submitForm }: FormikProps<AddBmcValues>) => (
<>
<ModalBoxBody>
<Form id="add-bmc-form">
<InputField
label="Name"
name="name"
placeholder="Enter the name for the Host"
isRequired
isDisabled={isEdit}
/>
<RichInputField
label="Hostname"
name="hostname"
placeholder="Enter the hostname for the Host"
richValidationMessages={HOSTNAME_VALIDATION_MESSAGES}
isRequired
/>
<InputField
label="Baseboard Management Controller Address"
name="bmcAddress"
placeholder="Enter an address"
isRequired
/>
<InputField
label="Boot NIC MAC Address"
name="bootMACAddress"
placeholder="Enter an address"
description="The MAC address of the host's network connected NIC that wll be used to provision the host."
/>
<InputField
label="Username"
name="username"
placeholder="Enter a username for the BMC"
isRequired
/>
<InputField
type={TextInputTypes.password}
label="Password"
name="password"
placeholder="Enter a password for the BMC"
isRequired
/>
{!hasDHCP && (
<>
<CodeField
label="NMState"
name="nmState"
language={Language.yaml}
description="Upload a YAML file in NMstate format that includes your network configuration (static IPs, bonds, etc.)."
/>
<MacMapping />
</>
)}
</Form>
{error && (
<Alert
title="Failed to add host"
variant={AlertVariant.danger}
isInline
actionClose={<AlertActionCloseButton onClose={() => setError(undefined)} />}
>
{error}
</Alert>
)}
</ModalBoxBody>
<ModalBoxFooter>
<Button onClick={submitForm} isDisabled={isSubmitting || !isValid}>
{isEdit ? 'Submit' : 'Create'}
</Button>
<Button onClick={onClose} variant={ButtonVariant.secondary}>
Cancel
</Button>
</ModalBoxFooter>
</>
)}
</Formik>
);
}
Example #17
Source File: Register.tsx From krmanga with MIT License | 4 votes |
function Register({ navigation, dispatch, isLogin, loading }: IProps) {
const [disabled, setDisabled] = useState<boolean>(false);
useEffect(() => {
if (isLogin) {
setTimeout(() => {
navigation.goBack();
}, 100);
}
}, [isLogin]);
const onSubmit = (values: Values) => {
if (disabled || loading) {
return;
}
setDisabled(true);
dispatch({
type: "user/register",
payload: values,
callback: () => {
setDisabled(false);
}
});
};
const cancel = (form: FormikProps<string>, field: FieldInputProps<string>) => {
if (field.name === "account") {
form.setFieldValue("account", "");
} else if (field.name === "password") {
form.setFieldValue("password", "");
} else if (field.name === "repeat_password") {
form.setFieldValue("repeat_password", "");
} else if (field.name === "phone") {
form.setFieldValue("phone", "");
}
};
return (
<ScrollView keyboardShouldPersistTaps="handled" style={styles.container}>
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
validationSchema={customerValidation}>
{({ handleSubmit }) => (
<View>
<Field
name="account"
placeholder="请输入用户名"
component={Input}
iconName={"icon-Account"}
cancel={cancel}
/>
<Field
name="password"
placeholder="请输入密码"
component={Input}
iconName={"icon-mima"}
secureTextEntry
cancel={cancel}
/>
<Field
name="repeat_password"
placeholder="请再输入密码"
component={Input}
iconName={"icon-mima"}
secureTextEntry
cancel={cancel}
/>
<Field
name="phone"
placeholder="请输入手机号(选填)"
component={Input}
iconName={"icon-mobile-phone"}
cancel={cancel}
/>
<View style={styles.jumpView}>
<Text style={styles.jumpTitle}>忘记密码?</Text>
<Touchable onPress={() => navigation.navigate("Login")}>
<Text style={styles.jumpTitle}>立即登录</Text>
</Touchable>
</View>
<Touchable disabled={disabled} onPress={handleSubmit} style={styles.login}>
<Text style={styles.loginText}>注册</Text>
</Touchable>
</View>
)}
</Formik>
</ScrollView>
);
}
Example #18
Source File: EditNtpSourcesModal.tsx From assisted-ui-lib with Apache License 2.0 | 4 votes |
EditNtpSourcesModal: React.FC<EditNtpSourcesModalProps> = ({
isOpen,
onClose,
onSubmit,
infraEnv,
}) => {
const [error, setError] = React.useState();
return (
<Modal
aria-label="Edit Ntp sources dialog"
title="Edit NTP sources"
isOpen={isOpen}
onClose={onClose}
variant={ModalVariant.small}
hasNoBodyWrapper
id="edit-ntp-sources-modal"
>
<Formik<EditNtpSourcesFormikValues>
initialValues={{
enableNtpSources: infraEnv.spec?.additionalNTPSources ? 'additional' : 'auto',
additionalNtpSources: infraEnv.spec?.additionalNTPSources?.join(',') || '',
}}
onSubmit={async (values) => {
try {
await onSubmit(values, infraEnv);
onClose();
} catch (err) {
setError(err?.message || 'An error occured');
}
}}
validateOnMount
>
{({
isSubmitting,
isValid,
values,
submitForm,
}: FormikProps<EditNtpSourcesFormikValues>) => {
return (
<>
<ModalBoxBody>
<Form>
<Stack hasGutter>
<StackItem>
<RadioField
label="Auto synchronized NTP (Network Time Protocol) sources"
value="auto"
name="enableNtpSources"
/>
</StackItem>
<StackItem>
<RadioField
name="enableNtpSources"
value="additional"
description={
<Stack hasGutter>
<StackItem>
Configure your own NTP sources to sychronize the time between the
hosts that will be added to this infrastructure environment.
</StackItem>
<StackItem>
<AdditionalNTPSourcesField
name="additionalNtpSources"
isDisabled={values.enableNtpSources === 'auto'}
helperText="A comma separated list of IP or domain names of the NTP pools or servers."
/>
</StackItem>
</Stack>
}
label="Your own NTP (Network Time Protocol) sources"
/>
</StackItem>
{error && (
<StackItem>
<Alert variant="danger" title={error} />
</StackItem>
)}
</Stack>
</Form>
</ModalBoxBody>
<ModalBoxFooter>
<Button onClick={submitForm} isDisabled={isSubmitting || !isValid}>
Save
</Button>
<Button onClick={onClose} variant={ButtonVariant.secondary}>
Cancel
</Button>
</ModalBoxFooter>
</>
);
}}
</Formik>
</Modal>
);
}
Example #19
Source File: ScaleUpModal.tsx From assisted-ui-lib with Apache License 2.0 | 4 votes |
ScaleUpModal: React.FC<ScaleUpModalProps> = ({
isOpen,
onClose,
addHostsToCluster,
clusterDeployment,
agents,
onChangeHostname,
}) => {
const [editAgent, setEditAgent] = React.useState<AgentK8sResource | undefined>();
const [error, setError] = React.useState<string | undefined>();
const getInitialValues = (): ScaleUpFormValues => {
const agentSelector = getAgentSelectorFieldsFromAnnotations(
clusterDeployment?.metadata?.annotations,
);
const autoSelectHosts = agentSelector.autoSelect;
return {
autoSelectHosts,
hostCount: 1,
agentLabels: agentSelector.labels,
locations: agentSelector.locations,
selectedHostIds: [],
autoSelectedHostIds: [],
};
};
const validationSchema = React.useMemo(() => getValidationSchema(agents.length), [agents.length]);
const handleSubmit: FormikConfig<ScaleUpFormValues>['onSubmit'] = async (values) => {
const { autoSelectHosts, autoSelectedHostIds, selectedHostIds } = values;
try {
setError(undefined);
const agentsToAdd = getAgentsToAdd(
autoSelectHosts ? autoSelectedHostIds : selectedHostIds,
agents,
);
await addHostsToCluster(agentsToAdd);
onClose();
} catch (e) {
setError(e.message);
}
};
const clusterAgents = agents.filter(
(a) =>
a.spec.clusterDeploymentName?.name === clusterDeployment.metadata?.name &&
a.spec.clusterDeploymentName?.namespace === clusterDeployment.metadata?.namespace,
);
return (
<>
<Modal
aria-label="Add worker host dialog"
title="Add worker hosts"
isOpen={isOpen}
onClose={onClose}
hasNoBodyWrapper
id="scale-up-modal"
>
<Formik
initialValues={getInitialValues()}
validationSchema={validationSchema}
onSubmit={handleSubmit}
validateOnMount
>
{({ isSubmitting, isValid, submitForm }: FormikProps<ScaleUpFormValues>) => {
return (
<>
<ModalBoxBody>
<Stack hasGutter>
<StackItem>
<ScaleUpForm agents={agents} onEditHost={setEditAgent} />
</StackItem>
{error && (
<StackItem>
<Alert
title="Failed to add hosts to the cluster"
variant={AlertVariant.danger}
actionClose={
<AlertActionCloseButton onClose={() => setError(undefined)} />
}
isInline
>
{error}
</Alert>
</StackItem>
)}
</Stack>
</ModalBoxBody>
<ModalBoxFooter>
<Button onClick={submitForm} isDisabled={isSubmitting || !isValid}>
Submit
</Button>
<Button onClick={onClose} variant={ButtonVariant.secondary}>
Cancel
</Button>
</ModalBoxFooter>
</>
);
}}
</Formik>
</Modal>
{editAgent && (
<EditAgentModal
agent={editAgent}
isOpen
onClose={() => setEditAgent(undefined)}
onSave={onChangeHostname}
usedHostnames={getAgentsHostsNames(clusterAgents)}
/>
)}
</>
);
}
Example #20
Source File: DataDocChartComposer.tsx From querybook with Apache License 2.0 | 4 votes |
DataDocChartComposerComponent: React.FunctionComponent<
IProps & FormikProps<IChartFormValues>
> = ({
meta,
dataDocId,
cellAboveId,
values,
dirty,
handleSubmit,
setFieldValue,
setFieldTouched,
isEditable,
}) => {
const [formTab, setFormTab] = React.useState<'data' | 'chart' | 'visuals'>(
'data'
);
const [showTable, setShowTable] = React.useState(true);
// const [showSeriesAggTypes, setShowSeriesAggTypes] = React.useState<boolean>(
// !values.aggType || !Object.keys(values.aggSeries).length
// );
const [tableTab, setTableTab] = React.useState<'original' | 'transformed'>(
values.aggregate || values.switch ? 'transformed' : 'original'
);
const [displayExecutionId, setDisplayExecutionId] = React.useState(
values.executionId
);
const [displayStatementId, setDisplayStatementId] = React.useState(
undefined
);
const {
statementResultData,
queryExecutions,
statementExecutions,
} = useChartSource(
values.cellId,
displayExecutionId,
displayStatementId,
setFieldValue.bind(null, 'cellId'),
setFieldValue.bind(null, 'executionId'),
setDisplayStatementId,
values.limit
);
const chartData = React.useMemo(
() =>
statementResultData
? transformData(
statementResultData,
values.aggregate,
values.switch,
values.formatAggCol,
values.formatSeriesCol,
values.formatValueCols,
values.aggSeries,
values.sortIndex,
values.sortAsc,
values.xIndex
)
: null,
[
statementResultData,
values.aggregate,
values.switch,
values.formatAggCol,
values.formatSeriesCol,
values.formatValueCols,
values.aggSeries,
values.sortIndex,
values.sortAsc,
values.xIndex,
]
);
// getting redux state
const queryCellOptions = useSelector((state: IStoreState) =>
queryCellSelector(state, dataDocId)
);
React.useEffect(() => {
if (
values.sourceType === 'cell_above' &&
values.cellId !== cellAboveId
) {
setFieldValue('cellId', cellAboveId);
} else if (
(values.sourceType === 'cell' ||
values.sourceType === 'execution') &&
values.cellId == null
) {
setFieldValue('cellId', cellAboveId ?? queryCellOptions?.[0].id);
}
}, [values.sourceType]);
React.useEffect(() => {
if (chartData && values.aggType) {
handleAggTypeChange(values.aggType);
}
}, [(chartData || [])[0]?.length]);
React.useEffect(() => {
setDisplayExecutionId(values.executionId);
}, [values.executionId]);
// ------------- select options ------------------------------------------------------------------
const cellExecutionOptions = React.useMemo(() => {
if (queryExecutions) {
return queryExecutions.map((queryExecution) => ({
value: queryExecution.id,
label: `Execution ${queryExecution.id}`,
}));
} else if (displayExecutionId) {
return [
{
value: displayExecutionId,
label: `Execution ${displayExecutionId}`,
},
];
} else {
return [
{
value: -1,
label: `loading executions`,
},
];
}
}, [displayExecutionId, queryExecutions]);
const xAxisOptions = React.useMemo(() => {
if (!chartData) {
return [];
}
return chartData[0].map((col, idx) => ({
value: idx,
label: col,
}));
}, [chartData]);
const formatAggByOptions = React.useMemo(() => {
if (!statementResultData) {
return [];
}
return statementResultData[0].reduce<Array<ISelectOption<number>>>(
(optionsAcc, label, idx) => {
if (idx !== values.formatSeriesCol) {
optionsAcc.push({
value: idx,
label,
});
}
return optionsAcc;
},
[]
);
}, [statementResultData, values.formatSeriesCol]);
const makeFormatSeriesOptions = React.useMemo(() => {
if (!statementResultData) {
return [];
}
const columns = statementResultData[0];
const options: Array<ISelectOption<number>> = [];
for (let i = 0; i < columns.length; i++) {
if (i !== values.formatAggCol) {
options.push({
value: i,
label: columns[i],
});
}
}
return options;
}, [statementResultData, values.formatAggCol]);
const makeFormatValueOptions = React.useMemo(() => {
if (!statementResultData) {
return [];
}
const columns = statementResultData[0];
const options: Array<ISelectOption<number>> = [];
for (let i = 0; i < columns.length; i++) {
if (i !== values.xIndex && i !== values.formatSeriesCol) {
options.push({
value: i,
label: columns[i],
});
}
}
return options;
}, [statementResultData, values.formatSeriesCol, values.xIndex]);
const makeSeriesValsAndOptions = React.useCallback(
(selectedValues: boolean) => {
if (!chartData || !chartData.length) {
return [{ value: 0, label: 'loading series' }];
}
const valsArray = chartData[0];
const optionIdxs = selectedValues
? range(valsArray.length).filter(
(val) =>
!values.hiddenSeries.includes(val) &&
val !== values.xIndex
)
: values.hiddenSeries;
const options = optionIdxs.map((i) => ({
value: i,
label: valsArray[i],
color:
ColorPalette[
values.coloredSeries[i] ?? i % ColorPalette.length
].color,
}));
return options;
},
[chartData, values.xIndex, values.hiddenSeries, values.coloredSeries]
);
const getAxesScaleType = React.useCallback(
(colIndex: number) => {
for (let row = 1; row < chartData?.length; row++) {
const cell = chartData?.[row]?.[colIndex];
if (cell != null) {
return getDefaultScaleType(chartData?.[row]?.[colIndex]);
}
}
// Unknown type, use linear as a default
return 'linear';
},
[chartData]
);
const seriesColorOptions = ColorPalette.map((color, idx) => ({
value: idx,
label: color.name,
color: color.color,
}));
const seriesAggOptions = Object.entries(aggTypes).map(([val, key]) => ({
value: val as ChartDataAggType,
label: key,
}));
// ------------- event handlers ------------------------------------------------------------------
const handleHiddenSeriesChange = (
selectedVals: Array<ISelectOption<number>>
) => {
const hiddenSeries = [];
const selectedSeries = selectedVals.map((obj) => obj.value);
for (let i = 0; i < chartData[0].length; i++) {
if (i !== values.xIndex && !selectedSeries.includes(i)) {
hiddenSeries.push(i);
}
}
setFieldValue('hiddenSeries', hiddenSeries);
};
const handleAggTypeChange = (aggType: ChartDataAggType) => {
setFieldValue('aggType', aggType);
const newAggSeries = {};
for (let i = 0; i < chartData[0].length; i++) {
newAggSeries[i] = aggType;
}
setFieldValue('aggSeries', newAggSeries);
};
// ------------- DOM elements ------------------------------------------------------------------
const tabsDOM = (
<Tabs
selectedTabKey={formTab}
onSelect={(selectedTab: 'data' | 'chart' | 'visuals') =>
setFormTab(selectedTab)
}
items={formTabs}
wide
/>
);
const renderPickerDOM = () => {
if (values.sourceType === 'custom') {
return null; // Custom data is sourced from internal context
}
const showQueryExecution =
values.sourceType !== 'execution' && queryExecutions.length;
const showStatementExecution =
displayExecutionId != null && statementExecutions.length;
const queryExecutionPicker =
showQueryExecution || showStatementExecution ? (
<FormField
label={`${
showQueryExecution ? 'Query Execution' : 'Statement'
} (Display Only)`}
stacked
help="Not Saved. Defaults to latest."
>
{showQueryExecution && (
<QueryExecutionPicker
queryExecutionId={displayExecutionId}
onSelection={setDisplayExecutionId}
queryExecutions={queryExecutions}
autoSelect
shortVersion
/>
)}
{showStatementExecution && (
<StatementExecutionPicker
statementExecutionId={displayStatementId}
onSelection={setDisplayStatementId}
statementExecutions={statementExecutions}
total={statementExecutions.length}
autoSelect
/>
)}
</FormField>
) : null;
return (
<div className="DataDocChartComposer-exec-picker">
{queryExecutionPicker}
</div>
);
};
const dataSourceDOM = (
<>
<FormSectionHeader>Source</FormSectionHeader>
<FormField stacked label="Type">
<SimpleReactSelect
value={values.sourceType}
onChange={(val) => {
if (
values.sourceType === 'execution' &&
val !== 'execution'
) {
setFieldValue('executionId', null);
}
setFieldValue('sourceType', val);
}}
options={Object.entries(sourceTypes).map(([key, val]) => ({
value: key,
label: val,
}))}
/>
</FormField>
{values.sourceType !== 'cell_above' ? (
<FormField stacked label="Cell">
<ReactSelectField
name="cellId"
options={queryCellOptions.map((val) => ({
value: val.id,
label: val.title,
}))}
/>
</FormField>
) : null}
{values.sourceType === 'execution' ? (
<SimpleField
stacked
type="react-select"
options={cellExecutionOptions}
name="executionId"
label="Execution"
/>
) : null}
{renderPickerDOM()}
<SimpleField
stacked
type="react-select"
options={StatementExecutionResultSizes.map((size) => ({
value: size,
label: formatNumber(size, 'Row'),
}))}
name="limit"
label="Row Limit"
help="Max number of rows to fetch from the result"
/>
</>
);
const dataTransformationDOM = (
<>
<FormSectionHeader>Transformation</FormSectionHeader>
<SimpleField
type="checkbox"
name="aggregate"
label="Aggregate"
onChange={(val) => {
setFieldValue('aggregate', val);
setFieldValue('hiddenSeries', []);
if (val) {
handleAggTypeChange('sum');
setTableTab('transformed');
} else {
setFieldValue('aggType', undefined);
setFieldValue('aggSeries', {});
}
}}
help="By default, all rows will be aggregated"
/>
{values.aggregate ? (
<>
<FormField stacked label="Aggregate By">
<SimpleReactSelect
value={values.aggType}
onChange={(val) => handleAggTypeChange(val)}
options={seriesAggOptions}
/>
</FormField>
<div className="DataDocChartComposer-info m8">
Value must be selected for aggregation by row/column.
</div>
<SimpleField
stacked
type="react-select"
label="Row"
name="formatAggCol"
options={formatAggByOptions}
isDisabled={!statementResultData}
withDeselect
/>
<SimpleField
stacked
label="Column"
type="react-select"
name="formatSeriesCol"
options={makeFormatSeriesOptions}
isDisabled={!statementResultData}
withDeselect
/>
<SimpleField
stacked
label="Value"
type="react-select"
name="formatValueCols"
value={values.formatValueCols[0]}
options={makeFormatValueOptions}
isDisabled={!statementResultData}
onChange={(val) => {
if (val == null) {
setFieldValue('formatValueCols', []);
} else {
setFieldValue('formatValueCols', [val]);
}
}}
withDeselect
/>
</>
) : null}
<SimpleField
type="checkbox"
label="Switch Rows/Columns"
name="switch"
help="Switch is applied after aggregation"
/>
</>
);
const dataTabDOM = (
<>
{dataSourceDOM}
{dataTransformationDOM}
</>
);
const chartOptionsDOM = (
<>
<FormField stacked label="Type">
<SimpleReactSelect
value={values.chartType}
onChange={(val) => {
setFieldValue('chartType', val);
// area defaults to true
if (val === 'area') {
setFieldValue('stack', true);
} else if (
[
'line',
'pie',
'doughnut',
'scatter',
'bubble',
].includes(val)
) {
// these charts cannot be stacked
setFieldValue('stack', false);
if (val === 'bubble' && !values.zIndex) {
setFieldValue('zIndex', 2);
}
}
}}
options={Object.entries(chartTypes).map(([key, val]) => ({
value: key,
label: val,
}))}
/>
</FormField>
{['bar', 'histogram'].includes(values.chartType) ? (
<SimpleField type="checkbox" label="Stack Chart" name="stack" />
) : null}
</>
);
let axesDOM: React.ReactChild = null;
if (values.chartType !== 'table') {
const noAxesConfig = ['pie', 'doughnut'].includes(values.chartType);
const getAxisDOM = (
prefix: string,
axisMeta: IChartAxisMeta,
scaleType: ChartScaleType,
scaleOptions: ChartScaleType[] = ChartScaleOptions
) => {
if (noAxesConfig) {
return null;
}
scaleType = getAutoDetectedScaleType(scaleOptions, scaleType);
const allScaleOptions = [
{
label: `auto detect (${scaleType})`,
value: null,
},
].concat(
scaleOptions.map((value) => ({
label: value,
value,
}))
);
let axisRangeDOM: React.ReactNode;
const assumedScale = axisMeta.scale ?? scaleType;
if (assumedScale === 'linear' || assumedScale === 'logarithmic') {
axisRangeDOM = (
<FormField stacked label="Range">
<Level margin="8px">
<NumberField
name={`${prefix}.min`}
placeholder="Min"
/>
<NumberField
name={`${prefix}.max`}
placeholder="Max"
/>
</Level>
</FormField>
);
}
return (
<>
<SimpleField
stacked
type="react-select"
name={`${prefix}.scale`}
options={allScaleOptions}
/>
{axisRangeDOM}
</>
);
};
const detectedXAxisScale = getAxesScaleType(values.xIndex);
const xAxisDOM = (
<>
<FormSectionHeader>X Axis</FormSectionHeader>
<FormField stacked label="X Axis">
<ReactSelectField
name={`xIndex`}
options={xAxisOptions}
isDisabled={!statementResultData}
/>
</FormField>
{getAxisDOM(
'xAxis',
values.xAxis,
detectedXAxisScale === 'linear'
? 'category'
: detectedXAxisScale,
chartTypeToAllowedAxisType[values.chartType].x
)}
</>
);
let yAxisDOM: React.ReactChild;
if (!noAxesConfig) {
const yAxisSeries = makeSeriesValsAndOptions(true);
const defaultYAxisScaleType = yAxisSeries.length
? getAxesScaleType(yAxisSeries[0].value)
: null;
yAxisDOM = (
<>
<FormSectionHeader>Y Axis</FormSectionHeader>
<FormField stacked label="Series">
<Select
styles={defaultReactSelectStyles}
value={yAxisSeries}
onChange={(val: any[]) =>
handleHiddenSeriesChange(val)
}
options={makeSeriesValsAndOptions(false)}
isMulti
/>
</FormField>
{getAxisDOM(
'yAxis',
values.yAxis,
defaultYAxisScaleType,
chartTypeToAllowedAxisType[values.chartType].y
)}
</>
);
}
const zAxisDOM =
values.chartType === 'bubble' ? (
<>
<FormSectionHeader>Z Axis</FormSectionHeader>
<FormField stacked label="Z Axis">
<ReactSelectField
name={`zIndex`}
options={xAxisOptions}
isDisabled={!statementResultData}
/>
</FormField>
</>
) : null;
axesDOM = (
<>
{xAxisDOM}
{yAxisDOM}
{zAxisDOM}
</>
);
}
const sortDOM = (
<>
<FormSectionHeader>Sort</FormSectionHeader>
<SimpleField
stacked
type="react-select"
options={xAxisOptions}
name="sortIndex"
label="Sort Index"
withDeselect
onChange={(val) => {
setFieldValue('sortIndex', val);
if (val != null) {
setTableTab('transformed');
}
}}
/>
<SimpleField
stacked
type="react-select"
options={[
{ value: true, label: 'Ascending' },
{ value: false, label: 'Descending' },
]}
name="sortAsc"
label="Sort Direction"
/>
</>
);
const chartTabDOM = (
<>
{chartOptionsDOM}
{axesDOM}
{sortDOM}
</>
);
const seriesColorDOM = chartData
? chartData[0].map((col, seriesIdx) => {
if (seriesIdx === 0 || values.hiddenSeries.includes(seriesIdx)) {
return null;
}
const colorIdx =
seriesIdx in values.coloredSeries
? values.coloredSeries[seriesIdx]
: seriesIdx % ColorPalette.length;
return (
<FormField
stacked
key={col}
label={() => (
<>
<b>{col}</b> Color
</>
)}
>
<ReactSelectField
value={colorIdx}
name={`coloredSeries[${seriesIdx}]`}
options={seriesColorOptions}
/>
</FormField>
);
})
: null;
const visualsTabDOM =
values.chartType === 'table' ? (
<FormField stacked label="Title">
<Field name="title" />
</FormField>
) : (
<>
<FormField stacked label="Title">
<Field name="title" />
</FormField>
{['pie', 'doughnut'].includes(values.chartType) ? null : (
<>
<SimpleField
stacked
label="X Axis Label"
name="xAxis.label"
type="input"
/>
<SimpleField
stacked
label="Y Axis Label"
name="yAxis.label"
type="input"
/>
</>
)}
<SimpleField
stacked
label="Chart Height"
name="size"
type="react-select"
help="If set from not auto to auto height, refresh the page to see change."
options={[
{
value: ChartSize.SMALL,
label: 'Small (1/3 height)',
},
{
value: ChartSize.MEDIUM,
label: 'Medium (1/2 height)',
},
{
value: ChartSize.LARGE,
label: 'Large (full height)',
},
{
value: ChartSize.AUTO,
label: 'Auto height',
},
]}
/>
<FormSectionHeader>Legend</FormSectionHeader>
<SimpleField
label="Visible"
name="legendDisplay"
type="checkbox"
/>
<SimpleField
stacked
label="Position"
name="legendPosition"
type="react-select"
options={['top', 'bottom', 'left', 'right']}
/>
<FormSectionHeader>Values</FormSectionHeader>
<SimpleField
stacked
label="Display"
name="valueDisplay"
type="react-select"
options={[
{
value: ChartValueDisplayType.FALSE,
label: 'Hide Values',
},
{
value: ChartValueDisplayType.TRUE,
label: 'Show Values',
},
{
value: ChartValueDisplayType.AUTO,
label: 'Show Values without Overlap',
},
]}
onChange={(val) => {
setFieldValue('valueDisplay', val);
if (val) {
if (values.valuePosition == null) {
setFieldValue('valuePosition', 'center');
}
if (values.valueAlignment == null) {
setFieldValue('valueAlignment', 'center');
}
}
}}
/>
{values.valueDisplay ? (
<>
<SimpleField
stacked
label="Position"
name="valuePosition"
type="react-select"
options={['center', 'start', 'end']}
/>
<SimpleField
stacked
label="Alignment"
name="valueAlignment"
type="react-select"
options={[
'center',
'start',
'end',
'right',
'left',
'top',
'bottom',
]}
/>
</>
) : null}
{['pie', 'doughnut', 'table'].includes(
values.chartType
) ? null : (
<>
<FormSectionHeader>Colors</FormSectionHeader>
{seriesColorDOM}
</>
)}
{['line', 'area'].includes(values.chartType) ? (
<>
<FormSectionHeader>Line Formatting</FormSectionHeader>
<SimpleField
label="Connect missing data"
name="connectMissing"
type="checkbox"
/>
</>
) : null}
</>
);
const formDOM = (
<FormWrapper size={7} className="DataDocChartComposer-form">
<DisabledSection disabled={!isEditable}>
{formTab === 'data' && dataTabDOM}
{formTab === 'chart' && chartTabDOM}
{formTab === 'visuals' && visualsTabDOM}
</DisabledSection>
</FormWrapper>
);
const hideTableButtonDOM = (
<IconButton
icon={showTable ? 'ChevronDown' : 'ChevronUp'}
onClick={() => setShowTable(!showTable)}
noPadding
/>
);
let dataDOM: JSX.Element;
let dataSwitch: JSX.Element;
if (chartData && showTable) {
if (values.aggregate || values.switch || values.sortIndex != null) {
dataSwitch = (
<div className="toggleTableDataSwitch">
<Tabs
selectedTabKey={tableTab}
onSelect={(key: 'original' | 'transformed') =>
setTableTab(key)
}
items={tableTabs}
/>
</div>
);
}
dataDOM = (
<div className="DataDocChartComposer-data">
<StatementResultTable
data={
tableTab === 'original'
? statementResultData
: chartData
}
paginate={true}
maxNumberOfRowsToShow={5}
/>
</div>
);
}
const tableDOM = (
<div className="DataDocChartComposer-bottom">
<Level>
<LevelItem>{dataSwitch}</LevelItem>
<LevelItem>{hideTableButtonDOM}</LevelItem>
</Level>
{dataDOM}
</div>
);
const chartDOM =
values.chartType === 'table' ? (
<DataDocChartCellTable data={chartData} title={values.title} />
) : (
<DataDocChart
data={chartData}
meta={formValsToMeta(values, meta)}
chartJSOptions={{ maintainAspectRatio: false }}
/>
);
const makeLeftDOM = () => (
<div className="DataDocChartComposer-left mr16">
<div className="DataDocChartComposer-chart mt8">
<div className="DataDocChartComposer-chart-sizer">
{chartData ? chartDOM : null}
</div>
</div>
{tableDOM}
</div>
);
return (
<div className="DataDocChartComposer mh16 pb16">
{makeLeftDOM()}
<div className="DataDocChartComposer-right">
{tabsDOM}
{formDOM}
{isEditable ? (
<div className="DataDocChartComposer-button">
<SoftButton
onClick={() => handleSubmit()}
title="Save"
fullWidth
pushable={false}
/>
</div>
) : null}
</div>
</div>
);
}
Example #21
Source File: IrregularityForm.tsx From frontend with MIT License | 4 votes |
export default function IrregularityForm({ campaign, person }: Props) {
const { t } = useTranslation('irregularity')
const classes = useStyles()
const formRef = useRef<FormikProps<IrregularityFormData>>(null)
const [fail, setFail] = useState(false)
const [success, setSuccess] = useState(false)
const [files, setFiles] = useState<File[]>([])
const [activeStep, setActiveStep] = useState<Steps>(Steps.GREETING)
const [failedStep, setFailedStep] = useState<Steps>(Steps.NONE)
const steps: StepType[] = [
{
component: <Greeting />,
},
{
component: <Contacts />,
},
{
component: <Info files={files} setFiles={setFiles} />,
},
]
const isStepFailed = (step: Steps | number): boolean => {
return step === failedStep
}
const mutation = useMutation<
AxiosResponse<IrregularityResponse>,
AxiosError<ApiErrors>,
IrregularityInput
>({
mutationFn: createIrregularity,
})
const fileUploadMutation = useMutation<
AxiosResponse<IrregularityUploadImage[]>,
AxiosError<ApiErrors>,
UploadIrregularityFiles
>({
mutationFn: uploadIrregularityFiles(),
})
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1)
}
const handleSubmit = async (
values: IrregularityFormData,
actions: FormikHelpers<IrregularityFormData>,
) => {
if (isLastStep(activeStep, steps)) {
const errors = await actions.validateForm(values)
const hasErrors = !!Object.keys(errors).length
if (hasErrors) {
setFailedStep(Steps.INFO)
return
}
setActiveStep((prevActiveStep) => prevActiveStep + 1)
setFailedStep(Steps.NONE)
try {
const data: IrregularityInput = {
campaignId: values.info.campaignId,
description: values.info.description,
notifierType: values.info.notifierType,
reason: values.info.reason,
status: values.status,
person: {
firstName: values.person.firstName,
lastName: values.person.lastName,
email: values.person.email,
phone: values.person.phone,
},
}
const response = await mutation.mutateAsync(data)
await fileUploadMutation.mutateAsync({
files,
irregularityId: response.data.id,
})
actions.resetForm()
setSuccess(true)
} catch (error) {
console.error(error)
setFail(true)
if (isAxiosError(error)) {
const { response } = error as AxiosError<ApiErrors>
response?.data.message.map(({ property, constraints }) => {
actions.setFieldError(property, t(matchValidator(constraints)))
})
}
}
return
}
actions.setTouched({})
actions.setSubmitting(false)
initialValues.info.campaignId = campaign.id
initialValues.person.firstName = person?.firstName || ''
initialValues.person.lastName = person?.lastName || ''
initialValues.person.email = person?.email || ''
initialValues.person.phone = person?.phone || ''
await stepsHandler({ actions, activeStep, setActiveStep, setFailedStep })
}
if (success) {
return <Success />
}
if (fail) {
return <Fail setFail={setFail} setActiveStep={setActiveStep} />
}
return (
<>
<GenericForm<IrregularityFormData>
onSubmit={handleSubmit}
initialValues={initialValues}
validationSchema={validationSchema[activeStep]}
innerRef={formRef}>
<Stepper
alternativeLabel
activeStep={activeStep}
className={classes.stepper}
connector={<ColorlibConnector />}>
{steps.map((step, index) => (
<Step key={index}>
<StepLabel error={isStepFailed(index)} StepIconComponent={StepIcon} />
</Step>
))}
</Stepper>
<div className={classes.content}>
<Grid container spacing={5} justifyContent="center" className={classes.instructions}>
<Grid container item xs={12}>
{activeStep < steps.length && steps[activeStep].component}
</Grid>
<Grid container item spacing={3}>
<Actions
activeStep={activeStep}
disableBack={activeStep === 0}
onBack={handleBack}
loading={mutation.isLoading}
campaign={campaign}
nextLabel={isLastStep(activeStep, steps) ? 'cta.submit' : 'cta.next'}
backLabel={isFirstStep(activeStep, steps) ? 'cta.back-to-campaign' : 'cta.back'}
/>
</Grid>
</Grid>
</div>
</GenericForm>
{activeStep === Steps.GREETING && <Remark text={t('steps.greeting.remark')} />}
{activeStep === Steps.CONTACTS && <Remark text={t('steps.contacts.remark')} />}
</>
)
}
Example #22
Source File: SupportForm.tsx From frontend with MIT License | 4 votes |
export default function SupportForm() {
const { t } = useTranslation()
const formRef = useRef<FormikProps<SupportFormData>>(null)
const [activeStep, setActiveStep] = useState<Steps>(Steps.ROLES)
const [failedStep, setFailedStep] = useState<Steps>(Steps.NONE)
const mutation = useMutation<
AxiosResponse<SupportRequestResponse>,
AxiosError<ApiErrors>,
SupportRequestInput
>({
mutationFn: createSupportRequest,
onError: () => AlertStore.show(t('common:alerts.error'), 'error'),
onSuccess: () => AlertStore.show(t('common:alerts.message-sent'), 'success'),
})
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1)
}
const handleSubmit = async (values: SupportFormData, actions: FormikHelpers<SupportFormData>) => {
if (isLastStep(activeStep, steps)) {
const errors = await actions.validateForm()
const hasErrors = !!Object.keys(errors).length
if (hasErrors) {
setFailedStep(Steps.PERSON)
return
}
setActiveStep((prevActiveStep) => prevActiveStep + 1)
setFailedStep(Steps.NONE)
try {
const { person, ...supportData } = values
await mutation.mutateAsync({ person, supportData })
actions.resetForm()
if (window) {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
} catch (error) {
console.error(error)
if (isAxiosError(error)) {
const { response } = error as AxiosError<ApiErrors>
response?.data.message.map(({ property, constraints }) => {
actions.setFieldError(property, t(matchValidator(constraints)))
})
}
}
return
}
actions.setTouched({})
actions.setSubmitting(false)
switch (activeStep) {
case Steps.ROLES:
{
const errors = await actions.validateForm()
if (errors.roles) {
setFailedStep(Steps.ROLES)
return
}
setActiveStep((prevActiveStep) => prevActiveStep + 1)
setFailedStep(Steps.NONE)
}
break
case Steps.QUESTIONS:
{
const errors = await actions.validateForm()
let hasErrors = false
const questions = Object.entries(values.roles)
.filter(([, value]) => value)
.map(([key]) => key)
Object.keys(errors).forEach((error) => {
if (questions.includes(error)) {
hasErrors = true
}
})
if (hasErrors) {
setFailedStep(Steps.QUESTIONS)
return
}
setActiveStep((prevActiveStep) => prevActiveStep + 1)
setFailedStep(Steps.NONE)
}
break
case Steps.PERSON:
{
const errors = await actions.validateForm()
if (errors.person) {
setFailedStep(Steps.PERSON)
return
}
setActiveStep((prevActiveStep) => prevActiveStep + 1)
setFailedStep(Steps.NONE)
}
break
default:
return 'Unknown step'
}
}
const isStepFailed = (step: Steps | number): boolean => {
return step === failedStep
}
const isLastStep = (activeStep: number, steps: StepType[]): boolean => {
return activeStep === steps.length - 2
}
const isThankYouStep = (activeStep: number, steps: StepType[]): boolean => {
return activeStep === steps.length - 1
}
return (
<GenericForm<SupportFormData>
onSubmit={handleSubmit}
initialValues={initialValues}
validationSchema={validationSchema[activeStep]}
innerRef={formRef}>
<Hidden mdDown>
<Stepper
alternativeLabel
activeStep={activeStep}
connector={<StepConnector sx={{ mt: 1.5 }} />}>
{steps.map((step, index) => (
<Step key={index}>
<StepLabel error={isStepFailed(index)} StepIconComponent={StepIcon}>
{t(step.label)}
</StepLabel>
</Step>
))}
</Stepper>
</Hidden>
{isThankYouStep(activeStep, steps) ? (
steps[activeStep].component
) : (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Grid container justifyContent="center">
<Grid item xs={12} sx={{ mt: 1, mb: 5 }}>
{steps[activeStep].component}
</Grid>
<Grid item xs={12} sx={(theme) => ({ '& button': { minWidth: theme.spacing(12) } })}>
<Actions
disableBack={activeStep === 0}
onBack={handleBack}
loading={mutation.isLoading}
nextLabel={
isLastStep(activeStep, steps) ? 'support:cta.submit' : 'support:cta.next'
}
/>
</Grid>
</Grid>
</Box>
)}
</GenericForm>
)
}
Example #23
Source File: UploadMetaForm.tsx From gear-js with GNU General Public License v3.0 | 4 votes |
UploadMetaForm = ({ programId, programName }: Props) => {
const alert = useAlert();
const { account } = useAccount();
const [isFileUpload, setFileUpload] = useState(true);
const [meta, setMeta] = useState<Metadata | null>(null);
const [metaFile, setMetaFile] = useState<File | null>(null);
const [metaBuffer, setMetaBuffer] = useState<string | null>(null);
const [fieldsFromFile, setFieldFromFile] = useState<string[] | null>(null);
const [initialValues, setInitialValues] = useState<FormValues>({
name: programName,
...INITIAL_VALUES,
});
const handleUploadMetaFile = async (file: File) => {
try {
const fileBuffer = (await readFileAsync(file)) as Buffer;
const currentMetaWasm = await getWasmMetadata(fileBuffer);
if (!currentMetaWasm) {
return;
}
const valuesFromFile = getMetaValues(currentMetaWasm);
const currentMetaBuffer = Buffer.from(new Uint8Array(fileBuffer)).toString('base64');
setMeta(currentMetaWasm);
setMetaBuffer(currentMetaBuffer);
setFieldFromFile(Object.keys(valuesFromFile));
setInitialValues({
...INITIAL_VALUES,
...valuesFromFile,
name: currentMetaWasm.title ?? programName,
});
} catch (error) {
alert.error(`${error}`);
} finally {
setMetaFile(file);
}
};
const resetForm = () => {
setMetaFile(null);
setMeta(null);
setMetaBuffer(null);
setFieldFromFile(null);
setInitialValues({
name: programName,
...INITIAL_VALUES,
});
};
const handleSubmit = (values: FormValues, actions: FormikHelpers<FormValues>) => {
if (!account) {
alert.error(`WALLET NOT CONNECTED`);
return;
}
const { name, ...formMeta } = values;
if (isFileUpload) {
if (meta) {
addMetadata(meta, metaBuffer, account, programId, name, alert);
} else {
alert.error(`ERROR: metadata not loaded`);
}
} else {
addMetadata(formMeta, null, account, programId, name, alert);
}
actions.setSubmitting(false);
resetForm();
};
const fields = isFileUpload ? fieldsFromFile : META_FIELDS;
return (
<Formik
initialValues={initialValues}
validateOnBlur
validationSchema={Schema}
enableReinitialize
onSubmit={handleSubmit}
>
{({ isValid, isSubmitting }: FormikProps<FormValues>) => {
const emptyFile = isFileUpload && !meta;
const disabledBtn = emptyFile || !isValid || isSubmitting;
return (
<Form className={styles.uploadMetaForm}>
<MetaSwitch isMetaFromFile={isFileUpload} onChange={setFileUpload} className={styles.formField} />
<FormInput name="name" label="Program name:" className={styles.formField} />
{fields?.map((field) => {
const MetaField = field === 'types' ? FormTextarea : FormInput;
return (
<MetaField
key={field}
name={field}
label={`${field}:`}
disabled={isFileUpload}
className={styles.formField}
/>
);
})}
{isFileUpload && (
<MetaFile
file={metaFile}
className={styles.formField}
onUpload={handleUploadMetaFile}
onDelete={resetForm}
/>
)}
<div className={styles.formBtnWrapper}>
<Button type="submit" text="Upload metadata" className={styles.formSubmitBtn} disabled={disabledBtn} />
</div>
</Form>
);
}}
</Formik>
);
}
Example #24
Source File: address-amount-form.tsx From marina with MIT License | 4 votes |
AddressAmountForm = (props: FormikProps<AddressAmountFormValues>) => {
const {
errors,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
touched,
values,
setFieldValue,
setFieldTouched,
} = props;
const setMaxAmount = () => {
const maxAmount = values.balance;
setFieldValue('amount', maxAmount);
setFieldTouched('amount', true, false);
};
return (
<form onSubmit={handleSubmit} className="mt-10">
<div className={cx({ 'mb-12': !errors.address || !touched.address })}>
<label className="block">
<p className="mb-2 text-base font-medium text-left">Address</p>
<input
className={cx(
'border-2 focus:ring-primary focus:border-primary placeholder-grayLight block w-full rounded-md',
{
'border-red': errors.address && touched.address,
'border-grayLight': !errors.address || !touched.address,
}
)}
id="address"
name="address"
onChange={handleChange}
onBlur={handleBlur}
placeholder=""
type="text"
value={values.address}
/>
</label>
{errors.address && touched.address && (
<p className="text-red h-10 mt-2 text-xs font-medium text-left">{errors.address}</p>
)}
</div>
<div className={cx({ 'mb-12': !errors.amount || !touched.amount })}>
<label className="block">
<p className="mb-2 text-base font-medium text-left">Amount</p>
<div
className={cx('focus-within:text-grayDark text-grayLight relative w-full', {
'text-grayDark': touched.amount,
})}
>
<input
className={cx(
'border-2 focus:ring-primary focus:border-primary placeholder-grayLight block w-full rounded-md',
{
'border-red': errors.amount && touched.amount,
'border-grayLight': !errors.amount || !touched.amount,
}
)}
id="amount"
name="amount"
onChange={handleChange}
onBlur={handleBlur}
placeholder="0"
type="number"
lang="en"
value={values.amount}
/>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 text-base font-medium">
{values.assetTicker}
</span>
</div>
</label>
<p className="text-primary text-right">
<button
onClick={setMaxAmount}
className="background-transparent focus:outline-none px-3 py-1 mt-1 mb-1 mr-1 text-xs font-bold uppercase transition-all duration-150 ease-linear outline-none"
type="button"
>
SEND ALL
</button>
</p>
{errors.amount && touched.amount && (
<p className="text-red h-10 mt-1 text-xs font-medium text-left">{errors.amount}</p>
)}
</div>
<div className="text-right">
<Button
className="w-2/5 -mt-2 text-base"
disabled={
isSubmitting ||
!!((errors.address && touched.address) || (errors.amount && touched.amount))
}
type="submit"
>
Verify
</Button>
</div>
</form>
);
}
Example #25
Source File: PopupFormField.tsx From firecms with MIT License | 4 votes |
export function PopupFormField<M extends { [Key: string]: any }>({
tableKey,
entity,
customFieldValidator,
name,
schemaResolver,
path,
cellRect,
setPreventOutsideClick,
open,
onClose,
columnIndex,
onCellValueChange
}: PopupFormFieldProps<M>) {
const [savingError, setSavingError] = React.useState<any>();
const [popupLocation, setPopupLocation] = useState<{ x: number, y: number }>();
const [internalValue, setInternalValue] = useState<EntityValues<M> | undefined>(entity?.values);
const classes = useStyles();
const windowSize = useWindowSize();
const ref = React.useRef<HTMLDivElement>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
const initialPositionSet = React.useRef<boolean>(false);
const draggableBoundingRect = ref.current?.getBoundingClientRect();
useDraggable({
containerRef,
ref,
x: popupLocation?.x,
y: popupLocation?.y,
onMove: (x, y) => onMove({ x, y })
});
useEffect(
() => {
initialPositionSet.current = false;
},
[name, entity]
);
const getInitialLocation = useCallback(() => {
if (!cellRect) throw Error("getInitialLocation error");
return {
x: cellRect.left < windowSize.width - cellRect.right
? cellRect.x + cellRect.width / 2
: cellRect.x - cellRect.width / 2,
y: cellRect.top < windowSize.height - cellRect.bottom
? cellRect.y + cellRect.height / 2
: cellRect.y - cellRect.height / 2
};
}, [cellRect, windowSize.height, windowSize.width]);
const normalizePosition = useCallback(({
x,
y
}: { x: number, y: number }) => {
if (!draggableBoundingRect)
throw Error("normalizePosition called before draggableBoundingRect is set");
return {
x: Math.max(0, Math.min(x, windowSize.width - draggableBoundingRect.width)),
y: Math.max(0, Math.min(y, windowSize.height - draggableBoundingRect.height))
};
}, [draggableBoundingRect, windowSize]);
const updatePopupLocation = useCallback((position?: { x: number, y: number }) => {
if (!cellRect || !draggableBoundingRect) return;
const newPosition = normalizePosition(position ?? getInitialLocation());
if (!popupLocation || newPosition.x !== popupLocation.x || newPosition.y !== popupLocation.y)
setPopupLocation(newPosition);
}, [cellRect, draggableBoundingRect, getInitialLocation, normalizePosition, popupLocation]);
useEffect(
() => {
if (!cellRect || !draggableBoundingRect || initialPositionSet.current) return;
initialPositionSet.current = true;
updatePopupLocation(getInitialLocation());
},
[cellRect, draggableBoundingRect, getInitialLocation, updatePopupLocation]
);
useLayoutEffect(
() => {
updatePopupLocation(popupLocation);
},
[windowSize, cellRect]
);
useEffect(
() => {
setPreventOutsideClick(open);
},
[open]
);
const validationSchema = useMemo(() => {
if (!schemaResolver) return;
const schema = computeSchema({
schemaOrResolver: schemaResolver,
path,
values: internalValue,
previousValues: entity?.values
});
return getYupEntitySchema(
name && schema.properties[name]
? { [name]: schema.properties[name] } as Properties<any>
: {} as Properties<any>,
customFieldValidator);
}, [path, internalValue, name]);
const adaptResize = () => {
if (!draggableBoundingRect) return;
return updatePopupLocation(popupLocation);
};
const onMove = (position: { x: number, y: number }) => {
if (!draggableBoundingRect) return;
return updatePopupLocation(position);
};
const saveValue = async (values: M) => {
setSavingError(null);
if (entity && onCellValueChange && name) {
return onCellValueChange({
value: values[name as string],
name: name as string,
entity,
setError: setSavingError,
setSaved: () => {
}
});
}
return Promise.resolve();
};
if (!entity)
return <></>;
const form = entity && (
<div
key={`popup_form_${tableKey}_${entity.id}_${columnIndex}`}
style={{
width: 520,
maxWidth: "100vw",
maxHeight: "85vh"
}}>
<Formik
initialValues={entity.values}
validationSchema={validationSchema}
validate={(values) => console.debug("Validating", values)}
onSubmit={(values, actions) => {
saveValue(values)
.then(() => onClose())
.finally(() => actions.setSubmitting(false));
}}
>
{({
handleChange,
values,
errors,
touched,
dirty,
setFieldValue,
setFieldTouched,
handleSubmit,
isSubmitting
}: FormikProps<EntityValues<M>>) => {
if (!isEqual(values, internalValue)) {
setInternalValue(values);
}
if (!entity)
return <ErrorView
error={"PopupFormField misconfiguration"}/>;
if (!schemaResolver)
return <></>;
const disabled = isSubmitting;
const schema = computeSchema({
schemaOrResolver: schemaResolver,
path,
values,
previousValues: entity?.values
});
const context: FormContext<M> = {
schema,
entityId: entity.id,
values
};
const property: Property<any> | undefined = schema.properties[name];
return <Form
className={classes.form}
onSubmit={handleSubmit}
noValidate>
{name && property && buildPropertyField<any, M>({
name: name as string,
disabled: isSubmitting || isReadOnly(property) || !!property.disabled,
property,
includeDescription: false,
underlyingValueHasChanged: false,
context,
tableMode: true,
partOfArray: false,
autoFocus: open,
shouldAlwaysRerender: true
})}
<Button
className={classes.button}
variant="contained"
color="primary"
type="submit"
disabled={disabled}
>
Save
</Button>
</Form>;
}}
</Formik>
{savingError &&
<Typography color={"error"}>
{savingError.message}
</Typography>
}
</div>
);
const draggable = (
<div
key={`draggable_${String(name)}_${entity.id}`}
className={clsx(classes.popup,
{ [classes.hidden]: !open }
)}
ref={containerRef}>
<ElementResizeListener onResize={adaptResize}/>
<div className={classes.popupInner}
ref={ref}>
{form}
<IconButton
size={"small"}
style={{
position: "absolute",
top: -14,
right: -14,
backgroundColor: "#666"
}}
onClick={(event) => {
event.stopPropagation();
onClose();
}}>
<ClearIcon style={{ color: "white" }}
fontSize={"small"}/>
</IconButton>
</div>
</div>
);
return (
<Portal container={document.body}>
{draggable}
</Portal>
);
}
Example #26
Source File: Metrics.tsx From abacus with GNU General Public License v2.0 | 4 votes |
MetricsIndexPage = (): JSX.Element => {
debug('MetricsIndexPage#render')
const classes = useStyles()
const { isLoading, data: metrics, error, reloadRef } = useDataSource(() => MetricsApi.findAll(), [])
useDataLoadingError(error, 'Metrics')
const debugMode = isDebugMode()
const { enqueueSnackbar } = useSnackbar()
// Edit Metric Modal
const [editMetricMetricId, setEditMetricMetricId] = useState<number | null>(null)
const isEditingMetric = editMetricMetricId !== null
const {
isLoading: editMetricIsLoading,
data: editMetricInitialMetric,
error: editMetricError,
} = useDataSource(async () => {
return editMetricMetricId === null ? null : await MetricsApi.findById(editMetricMetricId)
}, [editMetricMetricId])
useDataLoadingError(editMetricError, 'Metric to edit')
const onEditMetric = (metricId: number) => {
setEditMetricMetricId(metricId)
}
const onCancelEditMetric = () => {
setEditMetricMetricId(null)
}
const onSubmitEditMetric = async ({ metric }: { metric: MetricFormData }) => {
try {
if (!editMetricMetricId) {
throw new Error(`Missing metricId, this shouldn't happen.`)
}
await MetricsApi.put(editMetricMetricId, metric as unknown as MetricNew)
enqueueSnackbar('Metric Edited!', { variant: 'success' })
reloadRef.current()
setEditMetricMetricId(null)
} catch (e) /* istanbul ignore next; Shouldn't happen */ {
console.error(e)
enqueueSnackbar(`Oops! Something went wrong while trying to update your metric. ${serverErrorMessage(e)}`, {
variant: 'error',
})
}
}
// Add Metric Modal
const [isAddingMetric, setIsAddingMetric] = useState<boolean>(false)
const onAddMetric = () => setIsAddingMetric(true)
const onCancelAddMetric = () => {
setIsAddingMetric(false)
}
const onSubmitAddMetric = async ({ metric }: { metric: MetricFormData }) => {
try {
await MetricsApi.create(metric as unknown as MetricNew)
enqueueSnackbar('Metric Added!', { variant: 'success' })
reloadRef.current()
setIsAddingMetric(false)
} catch (e) /* istanbul ignore next; Shouldn't happen */ {
console.error(e)
enqueueSnackbar(`Oops! Something went wrong while trying to add your metric. ${serverErrorMessage(e)}`, {
variant: 'error',
})
}
}
return (
<Layout title='Metrics'>
{isLoading && <LinearProgress />}
{metrics && (
<>
<MetricsTable metrics={metrics || []} onEditMetric={debugMode ? onEditMetric : undefined} />
{debugMode && (
<div className={classes.actions}>
<Button variant='contained' color='secondary' onClick={onAddMetric}>
Add Metric
</Button>
</div>
)}
</>
)}
<Dialog open={isEditingMetric} fullWidth aria-labelledby='edit-metric-form-dialog-title'>
<DialogTitle id='edit-metric-form-dialog-title'>Edit Metric</DialogTitle>
{editMetricIsLoading && <LinearProgress />}
{editMetricInitialMetric && (
<Formik
initialValues={{ metric: metricToFormData(editMetricInitialMetric) }}
onSubmit={onSubmitEditMetric}
validationSchema={yup.object({ metric: metricNewSchema })}
>
{(formikProps) => (
<form onSubmit={formikProps.handleSubmit} noValidate>
<DialogContent>
<MetricFormFields formikProps={formikProps as FormikProps<{ metric: MetricFormData }>} />
</DialogContent>
<DialogActions>
<Button onClick={onCancelEditMetric} color='primary'>
Cancel
</Button>
<LoadingButtonContainer isLoading={formikProps.isSubmitting}>
<Button
type='submit'
variant='contained'
color='secondary'
disabled={formikProps.isSubmitting || !formikProps.isValid}
>
Save
</Button>
</LoadingButtonContainer>
</DialogActions>
</form>
)}
</Formik>
)}
</Dialog>
<Dialog open={isAddingMetric} fullWidth aria-labelledby='add-metric-form-dialog-title'>
<DialogTitle id='add-metric-form-dialog-title'>Add Metric</DialogTitle>
<Formik
initialValues={{ metric: metricToFormData({}) }}
onSubmit={onSubmitAddMetric}
validationSchema={yup.object({ metric: metricNewSchema })}
>
{(formikProps) => (
<form onSubmit={formikProps.handleSubmit} noValidate>
<DialogContent>
<MetricFormFields formikProps={formikProps as FormikProps<{ metric: MetricFormData }>} />
</DialogContent>
<DialogActions>
<Button onClick={onCancelAddMetric} color='primary'>
Cancel
</Button>
<LoadingButtonContainer isLoading={formikProps.isSubmitting}>
<Button
type='submit'
variant='contained'
color='secondary'
disabled={formikProps.isSubmitting || !formikProps.isValid}
>
Add
</Button>
</LoadingButtonContainer>
</DialogActions>
</form>
)}
</Formik>
</Dialog>
</Layout>
)
}
Example #27
Source File: MetricFormFields.tsx From abacus with GNU General Public License v2.0 | 4 votes |
MetricFormFields = ({ formikProps }: { formikProps: FormikProps<{ metric: MetricFormData }> }): JSX.Element => {
const classes = useStyles()
// Here we reset the params field after parameterType changes
useEffect(() => {
if (formikProps.values.metric.parameterType === MetricParameterType.Conversion) {
const eventParams = formikProps.values.metric.eventParams
formikProps.setValues({
...formikProps.values,
metric: {
...formikProps.values.metric,
revenueParams: undefined,
eventParams: eventParams ?? '[]',
},
})
} else {
const revenueParams = formikProps.values.metric.revenueParams
formikProps.setValues({
...formikProps.values,
metric: {
...formikProps.values.metric,
revenueParams: revenueParams ?? '{}',
eventParams: undefined,
},
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formikProps.values.metric.parameterType])
return (
<>
<div className={classes.row}>
<Field
component={TextField}
name='metric.name'
id='metric.name'
label='Metric name'
placeholder='metric_name'
helperText='Use snake_case.'
variant='outlined'
fullWidth
required
InputLabelProps={{
shrink: true,
}}
/>
</div>
<div className={classes.row}>
<Field
component={TextField}
name='metric.description'
id='metric.description'
label='Metric description'
placeholder='Put your Metric description here!'
variant='outlined'
fullWidth
required
multiline
rows={4}
InputLabelProps={{
shrink: true,
}}
/>
</div>
<div className={classes.row}>
<FormControlLabel
label='Higher is better'
control={
<Field
component={Switch}
name='metric.higherIsBetter'
id='metric.higherIsBetter'
label='Higher is better'
type='checkbox'
aria-label='Higher is better'
variant='outlined'
/>
}
/>
</div>
<div className={classes.row}>
<FormControl component='fieldset'>
<FormLabel required id='metric-form-radio-metric-type-label'>
Metric Type
</FormLabel>
<Field
component={RadioGroup}
name='metric.parameterType'
required
aria-labelledby='metric-form-radio-metric-type-label'
>
<FormControlLabel
value={MetricParameterType.Conversion}
label='Conversion'
aria-label='Conversion'
control={<Radio disabled={formikProps.isSubmitting} />}
disabled={formikProps.isSubmitting}
/>
<FormControlLabel
value={MetricParameterType.Revenue}
label='Revenue'
aria-label='Revenue'
control={<Radio disabled={formikProps.isSubmitting} />}
disabled={formikProps.isSubmitting}
/>
</Field>
</FormControl>
</div>
<div className={classes.row}>
{formikProps.values.metric.parameterType === MetricParameterType.Conversion && (
<Field
component={JsonTextField}
name='metric.eventParams'
id='metric.eventParams'
label='Event Parameters'
variant='outlined'
fullWidth
required
multiline
rows={8}
InputLabelProps={{
shrink: true,
}}
/>
)}
</div>
<div className={classes.row}>
{formikProps.values.metric.parameterType === MetricParameterType.Revenue && (
<Field
component={JsonTextField}
name='metric.revenueParams'
id='metric.revenueParams'
label='Revenue Parameters'
variant='outlined'
fullWidth
required
multiline
rows={8}
InputLabelProps={{
shrink: true,
}}
/>
)}
</div>
<DebugOutput label='Validation Errors' content={formikProps.errors} />
</>
)
}
Example #28
Source File: Metrics.tsx From abacus with GNU General Public License v2.0 | 4 votes |
Metrics = ({
indexedMetrics,
completionBag,
formikProps,
}: {
indexedMetrics: Record<number, Metric>
completionBag: ExperimentFormCompletionBag
formikProps: FormikProps<{ experiment: ExperimentFormData }>
}): JSX.Element => {
const classes = useStyles()
const metricEditorClasses = useMetricEditorStyles()
const decorationClasses = useDecorationStyles()
// Metric Assignments
const [metricAssignmentsField, _metricAssignmentsFieldMetaProps, metricAssignmentsFieldHelperProps] =
useField<MetricAssignment[]>('experiment.metricAssignments')
const [selectedMetric, setSelectedMetric] = useState<Metric | null>(null)
const onChangeSelectedMetricOption = (_event: unknown, value: Metric | null) => setSelectedMetric(value)
const makeMetricAssignmentPrimary = (indexToSet: number) => {
metricAssignmentsFieldHelperProps.setValue(
metricAssignmentsField.value.map((metricAssignment, index) => ({
...metricAssignment,
isPrimary: index === indexToSet,
})),
)
}
// This picks up the no metric assignments validation error
const metricAssignmentsError =
formikProps.touched.experiment?.metricAssignments &&
_.isString(formikProps.errors.experiment?.metricAssignments) &&
formikProps.errors.experiment?.metricAssignments
// ### Exposure Events
const [exposureEventsField, _exposureEventsFieldMetaProps, _exposureEventsFieldHelperProps] =
useField<EventNew[]>('experiment.exposureEvents')
return (
<div className={classes.root}>
<Typography variant='h4' gutterBottom>
Assign Metrics
</Typography>
<FieldArray
name='experiment.metricAssignments'
render={(arrayHelpers) => {
const onAddMetric = () => {
if (selectedMetric) {
const metricAssignment = createMetricAssignment(selectedMetric)
arrayHelpers.push({
...metricAssignment,
isPrimary: metricAssignmentsField.value.length === 0,
})
}
setSelectedMetric(null)
}
return (
<>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Metric</TableCell>
<TableCell>Attribution Window</TableCell>
<TableCell>Change Expected?</TableCell>
<TableCell>Minimum Practical Difference</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{metricAssignmentsField.value.map((metricAssignment, index) => {
const onRemoveMetricAssignment = () => {
arrayHelpers.remove(index)
}
const onMakePrimary = () => {
makeMetricAssignmentPrimary(index)
}
const attributionWindowError =
(_.get(
formikProps.touched,
`experiment.metricAssignments[${index}].attributionWindowSeconds`,
) as boolean | undefined) &&
(_.get(
formikProps.errors,
`experiment.metricAssignments[${index}].attributionWindowSeconds`,
) as string | undefined)
return (
<TableRow key={index}>
<TableCell className={classes.metricNameCell}>
<Tooltip arrow title={indexedMetrics[metricAssignment.metricId].description}>
<span className={clsx(classes.metricName, decorationClasses.tooltipped)}>
{indexedMetrics[metricAssignment.metricId].name}
</span>
</Tooltip>
<br />
{metricAssignment.isPrimary && <Attribute name='primary' className={classes.monospaced} />}
</TableCell>
<TableCell>
<Field
className={classes.attributionWindowSelect}
component={Select}
name={`experiment.metricAssignments[${index}].attributionWindowSeconds`}
labelId={`experiment.metricAssignments[${index}].attributionWindowSeconds`}
size='small'
variant='outlined'
autoWidth
displayEmpty
error={!!attributionWindowError}
SelectDisplayProps={{
'aria-label': 'Attribution Window',
}}
>
<MenuItem value=''>-</MenuItem>
{Object.entries(AttributionWindowSecondsToHuman).map(
([attributionWindowSeconds, attributionWindowSecondsHuman]) => (
<MenuItem value={attributionWindowSeconds} key={attributionWindowSeconds}>
{attributionWindowSecondsHuman}
</MenuItem>
),
)}
</Field>
{_.isString(attributionWindowError) && (
<FormHelperText error>{attributionWindowError}</FormHelperText>
)}
</TableCell>
<TableCell className={classes.changeExpected}>
<Field
component={Switch}
name={`experiment.metricAssignments[${index}].changeExpected`}
id={`experiment.metricAssignments[${index}].changeExpected`}
type='checkbox'
aria-label='Change Expected'
variant='outlined'
/>
</TableCell>
<TableCell>
<MetricDifferenceField
className={classes.minDifferenceField}
name={`experiment.metricAssignments[${index}].minDifference`}
id={`experiment.metricAssignments[${index}].minDifference`}
metricParameterType={indexedMetrics[metricAssignment.metricId].parameterType}
/>
</TableCell>
<TableCell>
<MoreMenu>
<MenuItem onClick={onMakePrimary}>Set as Primary</MenuItem>
<MenuItem onClick={onRemoveMetricAssignment}>Remove</MenuItem>
</MoreMenu>
</TableCell>
</TableRow>
)
})}
{metricAssignmentsField.value.length === 0 && (
<TableRow>
<TableCell colSpan={5}>
<Typography variant='body1' align='center'>
You don't have any metric assignments yet.
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<div className={metricEditorClasses.addMetric}>
<Add className={metricEditorClasses.addMetricAddSymbol} />
<FormControl className={classes.addMetricSelect}>
<MetricAutocomplete
id='add-metric-select'
value={selectedMetric}
onChange={onChangeSelectedMetricOption}
options={Object.values(indexedMetrics)}
error={metricAssignmentsError}
fullWidth
/>
</FormControl>
<Button variant='contained' disableElevation size='small' onClick={onAddMetric} aria-label='Add metric'>
Assign
</Button>
</div>
</>
)
}}
/>
<Alert severity='info' className={classes.metricsInfo}>
<Link
underline='always'
href="https://github.com/Automattic/experimentation-platform/wiki/Experimenter's-Guide#how-do-i-choose-a-primary-metric"
target='_blank'
>
How do I choose a Primary Metric?
</Link>
<Link
underline='always'
href="https://github.com/Automattic/experimentation-platform/wiki/Experimenter's-Guide#what-does-change-expected-mean-for-a-metric"
target='_blank'
>
What is Change Expected?
</Link>
</Alert>
<CollapsibleAlert
id='attr-window-panel'
severity='info'
className={classes.attributionWindowInfo}
summary={'What is an Attribution Window?'}
>
<Link
underline='always'
href="https://github.com/Automattic/experimentation-platform/wiki/Experimenter's-Guide#what-is-an-attribution-window-for-a-metric"
target='_blank'
>
An Attribution Window
</Link>{' '}
is the window of time after exposure to an experiment that we capture metric events for a participant (exposure
can be from either assignment or specified exposure events). The refund window is the window of time after a
purchase event. Revenue metrics will automatically deduct transactions that have been refunded within the
metric’s refund window.
<br />
<div className={classes.attributionWindowDiagram}>
<AttributionWindowDiagram />
<RefundWindowDiagram />
</div>
</CollapsibleAlert>
<CollapsibleAlert
id='min-diff-panel'
severity='info'
className={classes.minDiffInfo}
summary={'How do I choose a Minimum Difference?'}
>
<Link
underline='always'
href="https://github.com/Automattic/experimentation-platform/wiki/Experimenter's-Guide#how-do-i-choose-a-minimum-difference-practically-equivalent-value-for-my-metrics"
target='_blank'
>
Minimum Practical Difference values
</Link>{' '}
are absolute differences from the baseline (not relative). For example, if the baseline conversion rate is 5%, a
minimum difference of 0.5 pp is equivalent to a 10% relative change.
<br />
<div className={classes.minDiffDiagram}>
<MinDiffDiagram />
</div>
</CollapsibleAlert>
<Alert severity='info' className={classes.requestMetricInfo}>
<Link underline='always' href='https://betterexperiments.wordpress.com/?start=metric-request' target='_blank'>
{"Can't find a metric? Request one!"}
</Link>
</Alert>
<Typography variant='h4' className={classes.exposureEventsTitle}>
Exposure Events
</Typography>
<FieldArray
name='experiment.exposureEvents'
render={(arrayHelpers) => {
const onAddExposureEvent = () => {
arrayHelpers.push({
event: '',
props: [],
})
}
return (
<>
<TableContainer>
<Table>
<TableBody>
{exposureEventsField.value.map((exposureEvent, index) => (
<EventEditor
key={index}
{...{ arrayHelpers, index, classes, completionBag, exposureEvent }}
onRemoveExposureEvent={() => arrayHelpers.remove(index)}
/>
))}
{exposureEventsField.value.length === 0 && (
<TableRow>
<TableCell colSpan={1}>
<Typography variant='body1' align='center'>
You don't have any exposure events.
{}
<br />
{}
We strongly suggest considering adding one to improve the accuracy of your metrics.
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<div className={metricEditorClasses.addMetric}>
<Add className={metricEditorClasses.addMetricAddSymbol} />
<Button
variant='contained'
disableElevation
size='small'
onClick={onAddExposureEvent}
aria-label='Add exposure event'
>
Add Event
</Button>
</div>
</>
)
}}
/>
<Alert severity='info' className={classes.exposureEventsInfo}>
<Link
underline='always'
href="https://github.com/Automattic/experimentation-platform/wiki/Experimenter's-Guide#what-is-an-exposure-event-and-when-do-i-need-it"
target='_blank'
>
What is an Exposure Event? And when do I need it?
</Link>
<br />
<span>Only validated events can be used as exposure events.</span>
</Alert>
<Alert severity='info' className={classes.multipleExposureEventsInfo}>
If you have multiple exposure events, then participants will be considered exposed if they trigger{' '}
<strong>any</strong> of the exposure events.
</Alert>
</div>
)
}
Example #29
Source File: Audience.tsx From abacus with GNU General Public License v2.0 | 4 votes |
Audience = ({
indexedSegments,
formikProps,
completionBag,
}: {
indexedSegments: Record<number, Segment>
formikProps: FormikProps<{ experiment: ExperimentFormData }>
completionBag: ExperimentFormCompletionBag
}): JSX.Element => {
const classes = useStyles()
// The segmentExclusion code is currently split between here and SegmentAutocomplete
// An improvement might be to have SegmentAutocomplete only handle Segment[] and for code here
// to translate Segment <-> SegmentAssignment
const [segmentAssignmentsField, _segmentAssignmentsFieldMeta, segmentAssignmentsFieldHelper] = useField(
'experiment.segmentAssignments',
)
const [segmentExclusionState, setSegmentExclusionState] = useState<SegmentExclusionState>(() => {
// We initialize the segmentExclusionState from existing data if there is any
const firstSegmentAssignment = (segmentAssignmentsField.value as SegmentAssignmentNew[])[0]
return firstSegmentAssignment && firstSegmentAssignment.isExcluded
? SegmentExclusionState.Exclude
: SegmentExclusionState.Include
})
const onChangeSegmentExclusionState = (event: React.SyntheticEvent<HTMLInputElement>, value: string) => {
setSegmentExclusionState(value as SegmentExclusionState)
segmentAssignmentsFieldHelper.setValue(
(segmentAssignmentsField.value as SegmentAssignmentNew[]).map((segmentAssignment: SegmentAssignmentNew) => {
return {
...segmentAssignment,
isExcluded: value === SegmentExclusionState.Exclude,
}
}),
)
}
const platformError = formikProps.touched.experiment?.platform && formikProps.errors.experiment?.platform
const variationsError =
formikProps.touched.experiment?.variations && _.isString(formikProps.errors.experiment?.variations)
? formikProps.errors.experiment?.variations
: undefined
return (
<div className={classes.root}>
<Typography variant='h4' gutterBottom>
Define Your Audience
</Typography>
<div className={classes.row}>
<FormControl component='fieldset'>
<FormLabel required>Platform</FormLabel>
<Field component={Select} name='experiment.platform' displayEmpty error={!!platformError}>
<MenuItem value='' disabled>
Select a Platform
</MenuItem>
{Object.values(Platform).map((platform) => (
<MenuItem key={platform} value={platform}>
{platform}: {PlatformToHuman[platform]}
</MenuItem>
))}
</Field>
<FormHelperText error={!!platformError}>
{_.isString(platformError) ? platformError : undefined}
</FormHelperText>
</FormControl>
</div>
<div className={classes.row}>
<FormControl component='fieldset'>
<FormLabel required>User type</FormLabel>
<FormHelperText>Types of users to include in experiment</FormHelperText>
<Field component={FormikMuiRadioGroup} name='experiment.existingUsersAllowed' required>
<FormControlLabel
value='true'
label='All users (new + existing + anonymous)'
control={<Radio disabled={formikProps.isSubmitting} />}
disabled={formikProps.isSubmitting}
/>
<FormControlLabel
value='false'
label='Filter for newly signed up users (they must be also logged in)'
control={<Radio disabled={formikProps.isSubmitting} />}
disabled={formikProps.isSubmitting}
/>
</Field>
</FormControl>
</div>
<div className={classes.row}>
<FormControl component='fieldset' className={classes.segmentationFieldSet}>
<FormLabel htmlFor='segments-select'>Targeting</FormLabel>
<FormHelperText className={classes.segmentationHelperText}>
Who should see this experiment? <br /> Add optional filters to include or exclude specific target audience
segments.
</FormHelperText>
<MuiRadioGroup
aria-label='include-or-exclude-segments'
className={classes.segmentationExclusionState}
value={segmentExclusionState}
onChange={onChangeSegmentExclusionState}
>
<FormControlLabel
value={SegmentExclusionState.Include}
control={<Radio />}
label='Include'
name='non-formik-segment-exclusion-state-include'
/>
<FormControlLabel
value={SegmentExclusionState.Exclude}
control={<Radio />}
label='Exclude'
name='non-formik-segment-exclusion-state-exclude'
/>
</MuiRadioGroup>
<Field
name='experiment.segmentAssignments'
component={SegmentsAutocomplete}
options={Object.values(indexedSegments)}
// TODO: Error state, see https://stackworx.github.io/formik-material-ui/docs/api/material-ui-lab
renderInput={(params: AutocompleteRenderInputParams) => (
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
<MuiTextField
{...params}
variant='outlined'
placeholder={segmentAssignmentsField.value.length === 0 ? 'Search and select to customize' : undefined}
/>
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
)}
segmentExclusionState={segmentExclusionState}
indexedSegments={indexedSegments}
fullWidth
id='segments-select'
/>
</FormControl>
</div>
<div className={classes.row}>
<FormControl component='fieldset' className={classes.segmentationFieldSet}>
<FormLabel htmlFor='variations-select'>Variations</FormLabel>
<FormHelperText className={classes.segmentationHelperText}>
Set the percentage of traffic allocated to each variation. Percentages may sum to less than 100 to avoid
allocating the entire userbase. <br /> Use “control” for the default (fallback) experience.
</FormHelperText>
{variationsError && <FormHelperText error>{variationsError}</FormHelperText>}
<TableContainer>
<Table className={classes.variants}>
<TableHead>
<TableRow>
<TableCell> Name </TableCell>
<TableCell> Allocated Percentage </TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
<FieldArray
name={`experiment.variations`}
render={(arrayHelpers) => {
const onAddVariation = () => {
arrayHelpers.push({
name: ``,
isDefault: false,
allocatedPercentage: '',
})
}
const onRemoveVariation = (index: number) => arrayHelpers.remove(index)
const variations = formikProps.values.experiment.variations
return (
<>
{variations.map((variation, index) => {
return (
// The key here needs to be changed for variable variations
<TableRow key={index}>
<TableCell>
{variation.isDefault ? (
variation.name
) : (
<Field
component={FormikMuiTextField}
name={`experiment.variations[${index}].name`}
size='small'
variant='outlined'
required
inputProps={{
'aria-label': 'Variation Name',
}}
/>
)}
</TableCell>
<TableCell>
<Field
className={classes.variationAllocatedPercentage}
component={FormikMuiTextField}
name={`experiment.variations[${index}].allocatedPercentage`}
type='number'
size='small'
variant='outlined'
inputProps={{ min: 1, max: 99, 'aria-label': 'Allocated Percentage' }}
required
InputProps={{
endAdornment: <InputAdornment position='end'>%</InputAdornment>,
}}
/>
</TableCell>
<TableCell>
{!variation.isDefault && 2 < variations.length && (
<IconButton onClick={() => onRemoveVariation(index)} aria-label='Remove variation'>
<Clear />
</IconButton>
)}
</TableCell>
</TableRow>
)
})}
<TableRow>
<TableCell colSpan={3}>
<Alert severity='warning' className={classes.abnWarning}>
<strong> Manual analysis only A/B/n </strong>
<br />
<p>
Experiments with more than a single treatment variation are in an early alpha stage.
</p>
<p>No results will be displayed.</p>
<p>
Please do not set up such experiments in production without consulting the ExPlat team
first.
</p>
<div className={classes.addVariation}>
<Add className={classes.addVariationIcon} />
<Button
variant='contained'
onClick={onAddVariation}
disableElevation
size='small'
aria-label='Add Variation'
>
Add Variation
</Button>
</div>
</Alert>
</TableCell>
</TableRow>
</>
)
}}
/>
</TableBody>
</Table>
</TableContainer>
</FormControl>
</div>
{isDebugMode() && (
<div className={classes.row}>
<FormControl component='fieldset'>
<FormLabel htmlFor='experiment.exclusionGroupTagIds'>Exclusion Groups</FormLabel>
<FormHelperText>Optionally add this experiment to a mutually exclusive experiment group.</FormHelperText>
<br />
<Field
component={AbacusAutocomplete}
name='experiment.exclusionGroupTagIds'
id='experiment.exclusionGroupTagIds'
fullWidth
options={
// istanbul ignore next; trivial
completionBag.exclusionGroupCompletionDataSource.data ?? []
}
loading={completionBag.exclusionGroupCompletionDataSource.isLoading}
multiple
renderOption={(option: AutocompleteItem) => <Chip label={option.name} />}
renderInput={(params: AutocompleteRenderInputParams) => (
<MuiTextField
{...params}
variant='outlined'
InputProps={{
...autocompleteInputProps(params, completionBag.exclusionGroupCompletionDataSource.isLoading),
}}
InputLabelProps={{
shrink: true,
}}
/>
)}
/>
</FormControl>
</div>
)}
</div>
)
}