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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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&apos;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>
        &nbsp;
        <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&apos;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 vote down vote up
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 &ldquo;control&rdquo; 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>
  )
}