@material-ui/core#FormLabel TypeScript Examples

The following examples show how to use @material-ui/core#FormLabel. 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: Label.tsx    From firetable with Apache License 2.0 6 votes vote down vote up
export default function Label({
  label,
  children,
  hint,
  ...props
}: ILabelProps) {
  const classes = useStyles();

  return (
    <FormLabel className={classes.root} {...props}>
      {label || children}

      {hint && (
        <Tooltip title={hint}>
          <IconButton aria-label="delete">
            <HelpIcon />
          </IconButton>
        </Tooltip>
      )}
    </FormLabel>
  );
}
Example #2
Source File: Settings.tsx    From backstage with Apache License 2.0 6 votes vote down vote up
Settings = () => {
  const { type, handleChangeType } = useRandomJoke();
  const JOKE_TYPES: JokeType[] = ['any' as JokeType, 'programming' as JokeType];
  return (
    <FormControl component="fieldset">
      <FormLabel component="legend">Joke Type</FormLabel>
      <RadioGroup
        aria-label="joke type"
        value={type}
        onChange={e => handleChangeType(e.target.value)}
      >
        {JOKE_TYPES.map(t => (
          <FormControlLabel
            key={t}
            value={t}
            control={<Radio />}
            label={upperFirst(t)}
          />
        ))}
      </RadioGroup>
    </FormControl>
  );
}
Example #3
Source File: LabeledRadio.tsx    From UsTaxes with GNU Affero General Public License v3.0 6 votes vote down vote up
export function LabeledRadio<A>(props: LabeledRadioProps<A>): ReactElement {
  const { label, name, values, useGrid = true, sizes = { xs: 12 } } = props

  const classes = useStyles()
  const { control } = useFormContext<A>()

  return (
    <ConditionallyWrap
      condition={useGrid}
      wrapper={(children) => (
        <Grid item {...sizes}>
          {children}
        </Grid>
      )}
    >
      <Controller
        name={name}
        render={({ field: { value, onChange } }) => (
          <div className={classes.root}>
            <FormControl component="fieldset">
              <FormLabel>{label}</FormLabel>
              <RadioGroup name={name} value={value} onChange={onChange}>
                {values.map(([rowLabel, rowValue], i) => (
                  <FormControlLabel
                    key={i}
                    value={rowValue}
                    control={<Radio color="primary" />}
                    label={rowLabel}
                  />
                ))}
              </RadioGroup>
            </FormControl>
          </div>
        )}
        control={control}
      />
    </ConditionallyWrap>
  )
}
Example #4
Source File: CreateRoom.tsx    From cards-against-formality-pwa with BSD 2-Clause "Simplified" License 5 votes vote down vote up
function DeckSelector({ decks, onChange }: { decks: any[], onChange: (decks: string[]) => void }) {
  const [deckOptions, setDeckOptions] = useState<{ name: string; _id: string, value?: boolean }[]>([]);
  const [isExpanded, setIsExpanded] = useState(false);
  const [isAllSelected, setIsAllSelected] = useState(false);
  const toggleSelectAll = useCallback(() => {
    setDeckOptions(prevDeck => {
      prevDeck.forEach(deck => deck.value = !isAllSelected);
      return [...prevDeck];
    });

    setIsAllSelected(!isAllSelected);
  }, [isAllSelected])

  useEffect(() => {
    if (decks) {
      setDeckOptions(decks.map(deck => {
        return { value: deck.name.includes('Base'), ...deck }
      }));
    }
  }, [decks]);

  useEffect(() => {
    onChange(deckOptions.filter(deck => deck.value).map(deck => deck._id));
  }, [deckOptions, onChange]);


  function _onChange(e: React.ChangeEvent<HTMLInputElement>) {
    setDeckOptions(prevDeck => {
      const deck = prevDeck.find(deck => deck._id === e.target.name);
      if (deck) {
        deck.value = e.target.checked;
      }
      return [...prevDeck];
    });
  }

  if (!decks?.length) {
    return null;
  }

  return <ExpansionPanel expanded={isExpanded} onChange={() => { setIsExpanded(prev => !prev) }}>
    <ExpansionPanelSummary
      expandIcon={<ExpandMoreIcon />}
      aria-controls="panel1bh-content"
      id="panel1bh-header"
    >
      <Typography>Available Decks!</Typography>
    </ExpansionPanelSummary>
    <ExpansionPanelDetails>
      <FormControl required component="fieldset" error={!deckOptions.some(deck => deck.value)}>
        <FormControlLabel
          control={<Checkbox checked={isAllSelected} onChange={toggleSelectAll} name="Select all" />}
          label="Select all"
        />
        <Divider />
        <FormLabel component="legend">Select which decks you would like to play with</FormLabel>
        <FormGroup className="deck-checkbox-group">
          {deckOptions.map(deck => {
            return <FormControlLabel
              key={deck._id}
              control={<Checkbox checked={deck.value} onChange={_onChange} name={deck._id} />}
              label={deck.name}
            />
          })}
        </FormGroup>
        <FormHelperText>You must select at least one</FormHelperText>
      </FormControl>
    </ExpansionPanelDetails>
  </ExpansionPanel>
}
Example #5
Source File: SQFormRadioButtonGroup.tsx    From SQForm with MIT License 5 votes vote down vote up
function SQFormRadioButtonGroup({
  name,
  onChange,
  shouldDisplayInRow = false,
  size = 'auto',
  groupLabel,
  children,
}: SQFormRadioButtonGroupProps): React.ReactElement {
  const {
    fieldState: {isFieldError, isFieldRequired},
    formikField: {field},
    fieldHelpers: {handleChange, handleBlur, HelperTextComponent},
  } = useForm<
    RadioButtonInputItemProps['value'],
    React.ChangeEvent<HTMLInputElement>
  >({
    name,
    onChange,
  });

  const childrenToRadioGroupItems = () => {
    return children.map((radioOption) => {
      const {label, value, isDisabled, InputProps} = radioOption;
      return (
        <SQFormRadioButtonGroupItem
          label={label}
          value={value}
          isDisabled={isDisabled}
          isRowDisplay={shouldDisplayInRow}
          InputProps={InputProps}
          key={`SQFormRadioButtonGroupItem_${value}`}
        />
      );
    });
  };

  return (
    <Grid item sm={size}>
      <FormControl
        component="fieldset"
        required={isFieldRequired}
        error={isFieldError}
        onBlur={handleBlur}
      >
        <FormLabel
          component="legend"
          classes={{
            root: 'MuiInputLabel-root',
            asterisk: 'MuiInputLabel-asterisk',
          }}
        >
          {groupLabel}
        </FormLabel>
        <RadioGroup
          value={field.value}
          row={shouldDisplayInRow}
          aria-label={`SQFormRadioButtonGroup_${name}`}
          name={name}
          onChange={handleChange}
        >
          {childrenToRadioGroupItems()}
        </RadioGroup>
        <FormHelperText>{HelperTextComponent}</FormHelperText>
      </FormControl>
    </Grid>
  );
}
Example #6
Source File: VersioningStrategy.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
export function VersioningStrategy() {
  const navigate = useNavigate();
  const { project } = useProjectContext();
  const { getParsedQuery, getQueryParamsWithUpdates } = useQueryHandler();

  useEffect(() => {
    const { parsedQuery } = getParsedQuery();

    if (!parsedQuery.versioningStrategy && !project.isProvidedViaProps) {
      const { queryParams } = getQueryParamsWithUpdates({
        updates: [
          { key: 'versioningStrategy', value: project.versioningStrategy },
        ],
      });

      navigate(`?${queryParams}`, { replace: true });
    }
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <FormControl
      component="fieldset"
      required
      disabled={project.isProvidedViaProps}
    >
      <FormLabel component="legend">Versioning strategy</FormLabel>

      <RadioGroup
        data-testid={TEST_IDS.form.versioningStrategy.radioGroup}
        aria-label="calendar-strategy"
        name="calendar-strategy"
        value={project.versioningStrategy}
        onChange={event => {
          const { queryParams } = getQueryParamsWithUpdates({
            updates: [{ key: 'versioningStrategy', value: event.target.value }],
          });

          navigate(`?${queryParams}`, { replace: true });
        }}
      >
        <FormControlLabel
          value={VERSIONING_STRATEGIES.semver}
          control={<Radio />}
          label="Semantic versioning"
        />

        <FormControlLabel
          value={VERSIONING_STRATEGIES.calver}
          control={<Radio />}
          label="Calendar versioning"
        />
      </RadioGroup>
    </FormControl>
  );
}
Example #7
Source File: SearchFilter.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
CheckboxFilter = (props: SearchFilterComponentProps) => {
  const {
    className,
    defaultValue,
    label,
    name,
    values: givenValues = [],
    valuesDebounceMs,
  } = props;
  const classes = useStyles();
  const { filters, setFilters } = useSearch();
  useDefaultFilterValue(name, defaultValue);
  const asyncValues =
    typeof givenValues === 'function' ? givenValues : undefined;
  const defaultValues =
    typeof givenValues === 'function' ? undefined : givenValues;
  const { value: values = [], loading } = useAsyncFilterValues(
    asyncValues,
    '',
    defaultValues,
    valuesDebounceMs,
  );

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const {
      target: { value, checked },
    } = e;

    setFilters(prevFilters => {
      const { [name]: filter, ...others } = prevFilters;
      const rest = ((filter as string[]) || []).filter(i => i !== value);
      const items = checked ? [...rest, value] : rest;
      return items.length ? { ...others, [name]: items } : others;
    });
  };

  return (
    <FormControl
      className={className}
      disabled={loading}
      fullWidth
      data-testid="search-checkboxfilter-next"
    >
      {label ? <FormLabel className={classes.label}>{label}</FormLabel> : null}
      {values.map((value: string) => (
        <FormControlLabel
          key={value}
          control={
            <Checkbox
              color="primary"
              tabIndex={-1}
              inputProps={{ 'aria-labelledby': value }}
              value={value}
              name={value}
              onChange={handleChange}
              checked={((filters[name] as string[]) ?? []).includes(value)}
            />
          }
          label={value}
        />
      ))}
    </FormControl>
  );
}
Example #8
Source File: RadioInput.tsx    From glific-frontend with GNU Affero General Public License v3.0 5 votes vote down vote up
RadioInput: React.SFC<RadioInputProps> = ({
  labelYes = 'Yes',
  labelNo = 'No',
  row = true,
  field,
  form: { touched, errors, setFieldValue, values },
  radioTitle,
  handleChange,
}) => {
  const selectedValue = values[field.name];

  const isChecked = (value: any) => selectedValue === value;

  const handleRadioChange = (value: boolean) => {
    setFieldValue(field.name, value);
    if (handleChange) {
      handleChange(value);
    }
  };

  let radioGroupLabel: any;
  if (radioTitle) {
    radioGroupLabel = <FormLabel component="legend">{radioTitle}</FormLabel>;
  }

  return (
    <FormControl component="fieldset">
      {radioGroupLabel}
      <RadioGroup row={row} name="radio-buttons">
        <FormControlLabel
          value={1}
          control={
            <Radio
              color="primary"
              onClick={() => handleRadioChange(true)}
              checked={isChecked(true)}
            />
          }
          label={labelYes}
          className={styles.Label}
        />
        <FormControlLabel
          value={0}
          control={
            <Radio
              color="primary"
              onClick={() => handleRadioChange(false)}
              checked={isChecked(false)}
            />
          }
          label={labelNo}
          className={styles.Label}
        />
      </RadioGroup>
      {errors[field.name] && touched[field.name] ? (
        <FormHelperText className={styles.DangerText}>{errors[field.name]}</FormHelperText>
      ) : null}
    </FormControl>
  );
}
Example #9
Source File: Content.tsx    From Demae with MIT License 4 votes vote down vote up
Form = ({ provider }: { provider: ProviderDraft }) => {
	const classes = useStyles()
	const [setProcessing] = useProcessing()
	const [setMessage] = useSnackbar()
	const [thumbnail, setThumbnail] = useState<File | undefined>()
	const [cover, setCover] = useState<File | undefined>()
	const [name] = useTextField(provider.name)
	const [caption] = useTextField(provider.caption)
	const [description] = useTextField(provider.description)
	const providerCapabilities = provider.capabilities || []
	const [capabilities, setCapabilities] = useState<{ [key in Capability]: boolean }>({
		"download": providerCapabilities.includes("download"),
		"instore": providerCapabilities.includes("instore"),
		"online": providerCapabilities.includes("online"),
		"pickup": providerCapabilities.includes("pickup")
	})

	const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
		setCapabilities({ ...capabilities, [event.target.name]: event.target.checked });
	};

	const capabilitiesError = Object.values(capabilities).filter((v) => v).length < 1;

	const uploadThumbnail = (file: File): Promise<StorageFile | undefined> => {
		const ref = firebase.storage().ref(provider.documentReference.path + "/thumbnail.jpg")
		return new Promise((resolve, reject) => {
			ref.put(file).then(async (snapshot) => {
				if (snapshot.state === "success") {
					const storageFile = new StorageFile()
					if (snapshot.metadata.contentType) {
						storageFile.mimeType = snapshot.metadata.contentType
					}
					storageFile.path = ref.fullPath
					resolve(storageFile)
				} else {
					reject(undefined)
				}
			})
		})
	}

	const uploadCover = (file: File): Promise<StorageFile | undefined> => {
		const ref = firebase.storage().ref(provider.documentReference.path + "/cover.jpg")
		return new Promise((resolve, reject) => {
			ref.put(file).then(async (snapshot) => {
				if (snapshot.state === "success") {
					const storageFile = new StorageFile()
					if (snapshot.metadata.contentType) {
						storageFile.mimeType = snapshot.metadata.contentType
					}
					storageFile.path = ref.fullPath
					resolve(storageFile)
				} else {
					reject(undefined)
				}
			})
		})
	}

	const [isEditing, setEdit] = useEdit(async (event) => {
		event.preventDefault()
		if (!provider) return
		setProcessing(true)
		if (thumbnail) {
			const thumbnailImage = await uploadThumbnail(thumbnail)
			if (thumbnailImage) {
				provider.thumbnailImage = thumbnailImage
			}
		}
		if (cover) {
			const coverImage = await uploadCover(cover)
			if (coverImage) {
				provider.coverImage = coverImage
			}
		}
		try {
			provider.name = name.value as string
			provider.caption = caption.value as string
			provider.description = description.value as string
			provider.capabilities = Object.keys(capabilities).filter(value => capabilities[value]) as Capability[]
			await provider.save()
		} catch (error) {
			console.log(error)
		}
		setProcessing(false)
		setMessage("success", "Change your provider informations.")
		setEdit(false)
	})

	useContentToolbar(() => {
		if (!provider) return <></>
		if (isEditing) {
			return (
				<Box display="flex" flexGrow={1} justifyContent="space-between" paddingX={1}>
					<Button variant="outlined" color="primary" size="small" onClick={() => setEdit(false)}>Cancel</Button>
					<Button variant="contained" color="primary" size="small" type="submit" disabled={capabilitiesError}
					>Save</Button>
				</Box>
			)
		}
		return (
			<Box display="flex" flexGrow={1} justifyContent="space-between" paddingX={1}>
				<Box display="flex" flexGrow={1} justifyContent="flex-end">
					<Button variant="outlined" color="primary" size="small" onClick={() => setEdit(true)}>Edit</Button>
				</Box>
			</Box>
		)
	})

	if (isEditing) {
		return (
			<Container maxWidth="sm">
				<Box padding={2}>
					<Typography variant="h1" gutterBottom>Shop</Typography>
					<Paper>
						<Box display="flex" position="relative" flexGrow={1}>
							<Box display="flex" flexGrow={1} height={300}>
								<DndCard
									url={provider?.coverImageURL()}
									onDrop={(files) => {
										const file = files[0] as File
										setCover(file)
									}} />
							</Box>
							<Box display="flex" position="absolute" zIndex={1050} flexGrow={1} width={120} height={120} border={2} borderColor="white" borderRadius="50%" bottom={-16} left={16} style={{ overflow: "hidden" }}>
								<DndCard
									url={provider?.thumbnailImageURL()}
									onDrop={(files) => {
										const file = files[0] as File
										setThumbnail(file)
									}} />
							</Box>
						</Box>
						<Box padding={2} paddingTop={5}>
							<Box paddingBottom={2}>
								<Typography variant="subtitle1" gutterBottom>Name</Typography>
								<TextField variant="outlined" margin="dense" required {...name} fullWidth />
							</Box>
							<Box paddingBottom={2}>
								<Typography variant="subtitle1" gutterBottom>Caption</Typography>
								<TextField variant="outlined" margin="dense" required {...caption} fullWidth />
							</Box>
							<Box paddingBottom={2}>
								<Typography variant="subtitle1" gutterBottom>Description</Typography>
								<TextField variant="outlined" margin="dense" required fullWidth multiline rows={8} {...description} />
							</Box>
						</Box>

						<Box paddingX={2} paddingBottom={1}>
							<Typography variant="subtitle1" gutterBottom>Sales method</Typography>
							<FormControl required error={capabilitiesError} component="fieldset">
								<FormLabel component="legend">Please select at least one selling method</FormLabel>
								<FormGroup>
									<FormControlLabel
										control={<Checkbox checked={capabilities.download} onChange={handleChange} name="download" />}
										label="Download"
									/>
									<FormControlLabel
										control={<Checkbox checked={capabilities.instore} onChange={handleChange} name="instore" />}
										label="In-Store Sales"
									/>
									<FormControlLabel
										control={<Checkbox checked={capabilities.online} onChange={handleChange} name="online" />}
										label="Online Sales"
									/>
									<FormControlLabel
										control={<Checkbox checked={capabilities.pickup} onChange={handleChange} name="pickup" />}
										label="Pickup"
									/>
								</FormGroup>
								<FormHelperText>You can choose multiple sales methods.</FormHelperText>
							</FormControl>
						</Box>
					</Paper>
					<Box padding={1}>
						<Typography variant="body2" color="textSecondary" gutterBottom>ID: {provider.id}</Typography>
					</Box>
				</Box>
			</Container>
		)
	}

	return (
		<Container maxWidth="sm">
			<Box padding={2}>
				<Typography variant="h1" gutterBottom>Shop</Typography>
				<Paper>
					<Box display="flex" position="relative" flexGrow={1}>
						<Box display="flex" flexGrow={1} height={300}>
							<Avatar variant="square" src={provider.coverImageURL()} style={{
								minHeight: "300px",
								width: "100%"
							}}>
								<ImageIcon />
							</Avatar>
						</Box>
						<Box display="flex" position="absolute" zIndex={1050} flexGrow={1} width={120} height={120} border={2} borderColor="white" borderRadius="50%" bottom={-16} left={16} style={{ overflow: "hidden" }}>
							<Avatar variant="square" src={provider.thumbnailImageURL()} style={{
								minHeight: "64px",
								height: "100%",
								width: "100%"
							}}>
								<ImageIcon />
							</Avatar>
						</Box>
					</Box>

					<Box padding={2} paddingTop={5}>
						<Box paddingBottom={2}>
							<Typography variant="subtitle1" gutterBottom>Name</Typography>
							<Typography variant="body1" gutterBottom>{provider.name}</Typography>
						</Box>
						<Box paddingBottom={2}>
							<Typography variant="subtitle1" gutterBottom>Caption</Typography>
							<Typography variant="body1" gutterBottom>{provider.caption}</Typography>
						</Box>
						<Box paddingBottom={2}>
							<Typography variant="subtitle1" gutterBottom>Description</Typography>
							<Typography variant="body1" gutterBottom>{provider.description}</Typography>
						</Box>
					</Box>

					<Box paddingX={2} paddingBottom={1}>
						<Typography variant="subtitle1" gutterBottom>Sales method</Typography>
						<FormControl disabled component="fieldset">
							<FormGroup>
								<FormControlLabel
									control={<Checkbox checked={capabilities.download} onChange={handleChange} name="download" />}
									label="Download"
								/>
								<FormControlLabel
									control={<Checkbox checked={capabilities.instore} onChange={handleChange} name="instore" />}
									label="In-Store Sales"
								/>
								<FormControlLabel
									control={<Checkbox checked={capabilities.online} onChange={handleChange} name="online" />}
									label="Online Sales"
								/>
								<FormControlLabel
									control={<Checkbox checked={capabilities.pickup} onChange={handleChange} name="pickup" />}
									label="Pickup"
								/>
							</FormGroup>
						</FormControl>
					</Box>
				</Paper>
				<Box padding={1}>
					<Typography variant="body2" color="textSecondary" gutterBottom>ID: {provider.id}</Typography>
				</Box>
			</Box>
		</Container>
	)
}
Example #10
Source File: ConclusionsPanel.tsx    From abacus with GNU General Public License v2.0 4 votes vote down vote up
/**
 * Renders the conclusion information of an experiment in a panel component.
 *
 * @param experiment - The experiment with the conclusion information.
 * @param experimentReloadRef - A ref for reloading the experiment.
 */
function ConclusionsPanel({
  className,
  experiment,
  experimentReloadRef,
}: {
  className?: string
  experiment: ExperimentFull
  experimentReloadRef: React.MutableRefObject<() => void>
}): JSX.Element {
  const classes = useStyles()

  const deployedVariation = Experiments.getDeployedVariation(experiment)
  const data = [
    { label: 'Reason the experiment ended', value: experiment.endReason },
    {
      label: 'Conclusion URL',
      value: !!experiment.conclusionUrl && (
        <a href={experiment.conclusionUrl} rel='noopener noreferrer' target='_blank'>
          {experiment.conclusionUrl}
        </a>
      ),
    },
    { label: 'Deployed variation', value: deployedVariation?.name },
  ]

  // Edit Modal
  const { enqueueSnackbar } = useSnackbar()
  const [isEditing, setIsEditing] = useState<boolean>(false)
  const editInitialValues = {
    endReason: experiment.endReason ?? '',
    conclusionUrl: experiment.conclusionUrl ?? '',
    deployedVariationId: String(experiment.deployedVariationId ?? ''),
  }
  const onEdit = () => setIsEditing(true)
  const onCancelEdit = () => setIsEditing(false)
  const onSubmitEdit = async ({ experiment: formValues }: { experiment: typeof editInitialValues }) => {
    try {
      await ExperimentsApi.patch(experiment.experimentId, {
        endReason: formValues.endReason,
        conclusionUrl: formValues.conclusionUrl === '' ? undefined : formValues.conclusionUrl,
        deployedVariationId: formValues.deployedVariationId ? Number(formValues.deployedVariationId) : undefined,
      })
      enqueueSnackbar('Experiment Updated!', { variant: 'success' })
      experimentReloadRef.current()
      setIsEditing(false)
    } catch (e) {
      // istanbul ignore next; shouldn't happen
      enqueueSnackbar(`Oops! Something went wrong while trying to update your experiment. ${serverErrorMessage(e)}`, {
        variant: 'error',
      })
    }
  }

  return (
    <Paper className={className}>
      <Toolbar>
        <Typography className={classes.title} color='textPrimary' variant='h3'>
          Conclusions
        </Typography>
        <Button onClick={onEdit} variant='outlined' aria-label='Edit Conclusion'>
          <Edit />
          Edit
        </Button>
      </Toolbar>

      <LabelValueTable data={data} />

      <Dialog open={isEditing} fullWidth aria-labelledby='edit-experiment-conclusions-dialog-title'>
        <DialogTitle id='edit-experiment-conclusions-dialog-title'>Edit Experiment: Conclusions</DialogTitle>
        <Formik
          initialValues={{ experiment: editInitialValues }}
          validationSchema={yup.object({ experiment: yupPick(experimentFullSchema, Object.keys(editInitialValues)) })}
          onSubmit={onSubmitEdit}
        >
          {(formikProps) => (
            <form onSubmit={formikProps.handleSubmit}>
              <DialogContent>
                <div className={classes.row}>
                  <Field
                    component={TextField}
                    name='experiment.endReason'
                    id='experiment.endReason'
                    label='Reason the experiment ended'
                    placeholder='Completed successfully'
                    variant='outlined'
                    fullWidth
                    required
                    multiline
                    rows={2}
                    InputLabelProps={{
                      shrink: true,
                    }}
                  />
                </div>

                <div className={classes.row}>
                  <Field
                    component={TextField}
                    id='experiment.conclusionUrl'
                    name='experiment.conclusionUrl'
                    placeholder='https://your-p2-post-here/#conclusion-comment'
                    label='Conclusion URL'
                    variant='outlined'
                    fullWidth
                    InputLabelProps={{
                      shrink: true,
                    }}
                  />
                </div>

                <div className={classes.row}>
                  <FormControl component='fieldset'>
                    <FormLabel component='legend'>Deployed variation</FormLabel>
                    <Field component={FormikMuiRadioGroup} name='experiment.deployedVariationId'>
                      {experiment.variations.map((variation) => (
                        <FormControlLabel
                          key={variation.variationId}
                          value={String(variation.variationId)}
                          control={<Radio />}
                          label={variation.name}
                        />
                      ))}
                    </Field>
                  </FormControl>
                </div>
              </DialogContent>
              <DialogActions>
                <Button onClick={onCancelEdit} 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>
    </Paper>
  )
}
Example #11
Source File: MetricAssignmentsPanel.tsx    From abacus with GNU General Public License v2.0 4 votes vote down vote up
/**
 * Renders the assigned metric information of an experiment in a panel component.
 *
 * @param experiment - The experiment with the metric assignment information.
 * @param experimentReloadRef - Trigger a reload of the experiment.
 * @param metrics - The metrics to look up (aka resolve) the metric IDs of the
 *   experiment's metric assignments.
 */
function MetricAssignmentsPanel({
  experiment,
  experimentReloadRef,
  metrics,
}: {
  experiment: ExperimentFull
  experimentReloadRef: React.MutableRefObject<() => void>
  metrics: Metric[]
}): JSX.Element {
  const classes = useStyles()
  const resolvedMetricAssignments = useMemo(
    () => resolveMetricAssignments(MetricAssignments.sort(experiment.metricAssignments), metrics),
    [experiment, metrics],
  )

  // TODO: Normalize this higher up
  const indexedMetrics = indexMetrics(metrics)

  // Assign Metric Modal
  const { enqueueSnackbar } = useSnackbar()
  const canAssignMetric = experiment.status !== Status.Staging
  const [isAssigningMetric, setIsAssigningMetric] = useState<boolean>(false)
  const assignMetricInitialAssignMetric = {
    metricId: '',
    attributionWindowSeconds: '',
    changeExpected: false,
    isPrimary: false,
    minDifference: '',
  }
  const onAssignMetric = () => setIsAssigningMetric(true)
  const onCancelAssignMetric = () => {
    setIsAssigningMetric(false)
  }
  const onSubmitAssignMetric = async (formData: { metricAssignment: typeof assignMetricInitialAssignMetric }) => {
    try {
      await ExperimentsApi.assignMetric(experiment, formData.metricAssignment as unknown as MetricAssignmentNew)
      enqueueSnackbar('Metric Assigned Successfully!', { variant: 'success' })
      experimentReloadRef.current()
      setIsAssigningMetric(false)
    } catch (e) /* istanbul ignore next; Shouldn't happen */ {
      console.error(e)
      enqueueSnackbar(
        `Oops! Something went wrong while trying to assign a metric to your experiment. ${serverErrorMessage(e)}`,
        {
          variant: 'error',
        },
      )
    }
  }

  return (
    <Paper>
      <Toolbar>
        <Typography className={classes.title} color='textPrimary' variant='h3'>
          Metrics
        </Typography>
        <Tooltip title={canAssignMetric ? '' : 'Use "Edit in Wizard" for staging experiments.'}>
          <div>
            <Button onClick={onAssignMetric} variant='outlined' disabled={!canAssignMetric}>
              <Add />
              Assign Metric
            </Button>
          </div>
        </Tooltip>
      </Toolbar>
      <Table className={classes.metricsTable}>
        <TableHead>
          <TableRow>
            <TableCell component='th' variant='head'>
              Name
            </TableCell>
            <TableCell component='th' variant='head' className={classes.smallColumn}>
              Attribution Window
            </TableCell>
            <TableCell component='th' variant='head' className={classes.smallColumn}>
              Changes Expected
            </TableCell>
            <TableCell component='th' variant='head' className={classes.smallColumn}>
              Minimum Difference
            </TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {resolvedMetricAssignments.map((resolvedMetricAssignment) => (
            <TableRow key={resolvedMetricAssignment.metricAssignmentId}>
              <TableCell>
                <Tooltip title={resolvedMetricAssignment.metric.name}>
                  <strong className={clsx(classes.monospace, classes.metricName)}>
                    {resolvedMetricAssignment.metric.name}
                  </strong>
                </Tooltip>
                <br />
                <small className={classes.monospace}>{resolvedMetricAssignment.metric.description}</small>
                <br />
                {resolvedMetricAssignment.isPrimary && <Attribute name='primary' />}
              </TableCell>
              <TableCell className={classes.monospace}>
                {AttributionWindowSecondsToHuman[resolvedMetricAssignment.attributionWindowSeconds]}
              </TableCell>
              <TableCell className={classes.monospace}>
                {formatBoolean(resolvedMetricAssignment.changeExpected)}
              </TableCell>
              <TableCell className={classes.monospace}>
                <MetricValue
                  value={resolvedMetricAssignment.minDifference}
                  metricParameterType={resolvedMetricAssignment.metric.parameterType}
                  isDifference={true}
                />
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
      <Dialog open={isAssigningMetric} aria-labelledby='assign-metric-form-dialog-title'>
        <DialogTitle id='assign-metric-form-dialog-title'>Assign Metric</DialogTitle>
        <Formik
          initialValues={{ metricAssignment: assignMetricInitialAssignMetric }}
          onSubmit={onSubmitAssignMetric}
          validationSchema={yup.object({ metricAssignment: metricAssignmentNewSchema })}
        >
          {(formikProps) => {
            const metricAssignmentsError =
              formikProps.touched.metricAssignment?.metricId && formikProps.errors.metricAssignment?.metricId
            const onMetricChange = (_event: unknown, metric: Metric | null) =>
              formikProps.setFieldValue('metricAssignment.metricId', metric?.metricId)
            return (
              <form onSubmit={formikProps.handleSubmit} noValidate>
                <DialogContent>
                  <div className={classes.row}>
                    <FormControl component='fieldset' fullWidth>
                      <FormLabel required className={classes.label} htmlFor={`metricAssignment.metricId`}>
                        Metric
                      </FormLabel>
                      <MetricAutocomplete
                        id={`metricAssignment.metricId`}
                        value={indexedMetrics[Number(formikProps.values.metricAssignment.metricId)] ?? null}
                        onChange={onMetricChange}
                        options={Object.values(indexedMetrics)}
                        error={metricAssignmentsError}
                        fullWidth
                      />
                      {formikProps.errors.metricAssignment?.metricId && (
                        <FormHelperText error={true}>
                          <ErrorMessage name={`metricAssignment.metricId`} />
                        </FormHelperText>
                      )}
                    </FormControl>
                  </div>
                  <div className={classes.row}>
                    <FormControl component='fieldset' fullWidth>
                      <FormLabel
                        required
                        className={classes.label}
                        id={`metricAssignment.attributionWindowSeconds-label`}
                      >
                        Attribution Window
                      </FormLabel>
                      <Field
                        component={Select}
                        name={`metricAssignment.attributionWindowSeconds`}
                        labelId={`metricAssignment.attributionWindowSeconds-label`}
                        id={`metricAssignment.attributionWindowSeconds`}
                        variant='outlined'
                        error={
                          // istanbul ignore next; trivial, not-critical, pain to test.
                          !!formikProps.errors.metricAssignment?.attributionWindowSeconds &&
                          !!formikProps.touched.metricAssignment?.attributionWindowSeconds
                        }
                        displayEmpty
                      >
                        <MenuItem value=''>-</MenuItem>
                        {Object.entries(AttributionWindowSecondsToHuman).map(
                          ([attributionWindowSeconds, attributionWindowSecondsHuman]) => (
                            <MenuItem value={attributionWindowSeconds} key={attributionWindowSeconds}>
                              {attributionWindowSecondsHuman}
                            </MenuItem>
                          ),
                        )}
                      </Field>
                      {formikProps.errors.metricAssignment?.attributionWindowSeconds && (
                        <FormHelperText error={true}>
                          <ErrorMessage name={`metricAssignment.attributionWindowSeconds`} />
                        </FormHelperText>
                      )}
                    </FormControl>
                  </div>
                  <div className={classes.row}>
                    <FormControl component='fieldset' fullWidth>
                      <FormLabel required className={classes.label}>
                        Change Expected
                      </FormLabel>
                      <Field
                        component={Switch}
                        label='Change Expected'
                        name={`metricAssignment.changeExpected`}
                        id={`metricAssignment.changeExpected`}
                        inputProps={{
                          'aria-label': 'Change Expected',
                        }}
                        variant='outlined'
                        type='checkbox'
                      />
                    </FormControl>
                  </div>
                  <div className={classes.row}>
                    <FormControl component='fieldset' fullWidth>
                      <FormLabel required className={classes.label} id={`metricAssignment.minDifference-label`}>
                        Minimum Difference
                      </FormLabel>
                      <MetricDifferenceField
                        name={`metricAssignment.minDifference`}
                        id={`metricAssignment.minDifference`}
                        metricParameterType={
                          (formikProps.values.metricAssignment.metricId &&
                            indexedMetrics[formikProps.values.metricAssignment.metricId as unknown as number]
                              .parameterType) ||
                          MetricParameterType.Conversion
                        }
                      />
                    </FormControl>
                  </div>
                </DialogContent>
                <DialogActions>
                  <Button onClick={onCancelAssignMetric} color='primary'>
                    Cancel
                  </Button>
                  <LoadingButtonContainer isLoading={formikProps.isSubmitting}>
                    <Button
                      type='submit'
                      variant='contained'
                      color='secondary'
                      disabled={formikProps.isSubmitting || !formikProps.isValid}
                    >
                      Assign
                    </Button>
                  </LoadingButtonContainer>
                </DialogActions>
              </form>
            )
          }}
        </Formik>
      </Dialog>
    </Paper>
  )
}
Example #12
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>
  )
}
Example #13
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 #14
Source File: SQFormCheckboxGroup.tsx    From SQForm with MIT License 4 votes vote down vote up
function SQFormCheckboxGroup({
  name,
  groupLabel,
  onChange,
  shouldDisplayInRow = false,
  shouldUseSelectAll = false,
  size = 'auto',
  children,
}: SQFormCheckboxGroupProps): JSX.Element {
  const {
    fieldState: {isFieldError, isFieldRequired},
    fieldHelpers: {handleChange, handleBlur, HelperTextComponent},
  } = useForm<CheckboxOption['value'][], React.ChangeEvent<HTMLInputElement>>({
    name,
    onChange,
  });

  const {setFieldValue} = useFormikContext();

  const handleSelectAllChange = (
    event: React.ChangeEvent<HTMLInputElement>
  ) => {
    if (!event.target.checked) {
      setFieldValue(name, []);
      return;
    }

    const enabledGroupValues = children.reduce(
      (acc: string[], checkboxOption: CheckboxOption) => {
        const {value, isDisabled} = checkboxOption;
        if (!isDisabled) {
          return [...acc, String(value)];
        }

        return acc;
      },
      []
    );
    setFieldValue(name, enabledGroupValues);
  };

  const childrenToCheckboxGroupItems = () => {
    const providedCheckboxItems = children.map((checkboxOption) => {
      const {label, value, isDisabled, inputProps} = checkboxOption;

      return (
        <SQFormCheckboxGroupItem
          groupName={name}
          label={label}
          value={value}
          isRowDisplay={shouldDisplayInRow}
          onChange={handleChange}
          isDisabled={isDisabled}
          inputProps={inputProps}
          key={`SQFormCheckboxGroupItem_${value}`}
        />
      );
    });
    if (shouldUseSelectAll) {
      return [
        <SQFormCheckbox
          name={`${name}SelectAll`}
          label="All"
          onChange={handleSelectAllChange}
          key={`${name}_selectAll`}
        />,
        ...providedCheckboxItems,
      ];
    }

    return providedCheckboxItems;
  };

  return (
    <Grid item sm={size}>
      <FormControl
        component="fieldset"
        required={isFieldRequired}
        error={isFieldError}
        onBlur={handleBlur}
      >
        <FormLabel
          component="legend"
          classes={{
            root: 'MuiInputLabel-root',
            asterisk: 'MuiInputLabel-asterisk',
          }}
        >
          {groupLabel}
        </FormLabel>
        <FormGroup row={shouldDisplayInRow}>
          {childrenToCheckboxGroupItems()}
        </FormGroup>
        <FormHelperText>{HelperTextComponent}</FormHelperText>
      </FormControl>
    </Grid>
  );
}
Example #15
Source File: Organization.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
Organization: React.FC = () => {
  const {
    apiGet,
    apiPut,
    apiPost,
    user,
    setFeedbackMessage
  } = useAuthContext();
  const { organizationId } = useParams<{ organizationId: string }>();
  const [organization, setOrganization] = useState<OrganizationType>();
  const [tags, setTags] = useState<AutocompleteType[]>([]);
  const [userRoles, setUserRoles] = useState<Role[]>([]);
  const [scanTasks, setScanTasks] = useState<ScanTask[]>([]);
  const [scans, setScans] = useState<Scan[]>([]);
  const [scanSchema, setScanSchema] = useState<ScanSchema>({});
  const [newUserValues, setNewUserValues] = useState<{
    firstName: string;
    lastName: string;
    email: string;
    organization?: OrganizationType;
    role: string;
  }>({
    firstName: '',
    lastName: '',
    email: '',
    role: ''
  });
  const classes = useStyles();
  const [tagValue, setTagValue] = React.useState<AutocompleteType | null>(null);
  const [inputValue, setInputValue] = React.useState('');
  const [dialog, setDialog] = React.useState<{
    open: boolean;
    type?: 'rootDomains' | 'ipBlocks' | 'tags';
    label?: string;
  }>({ open: false });

  const dateAccessor = (date?: string) => {
    return !date || new Date(date).getTime() === new Date(0).getTime()
      ? 'None'
      : `${formatDistanceToNow(parseISO(date))} ago`;
  };

  const userRoleColumns: Column<Role>[] = [
    {
      Header: 'Name',
      accessor: ({ user }) => user.fullName,
      width: 200,
      disableFilters: true,
      id: 'name'
    },
    {
      Header: 'Email',
      accessor: ({ user }) => user.email,
      width: 150,
      minWidth: 150,
      id: 'email',
      disableFilters: true
    },
    {
      Header: 'Role',
      accessor: ({ approved, role, user }) => {
        if (approved) {
          if (user.invitePending) {
            return 'Invite pending';
          } else if (role === 'admin') {
            return 'Administrator';
          } else {
            return 'Member';
          }
        }
        return 'Pending approval';
      },
      width: 50,
      minWidth: 50,
      id: 'approved',
      disableFilters: true
    },
    {
      Header: () => {
        return (
          <div style={{ justifyContent: 'flex-center' }}>
            <Button color="secondary" onClick={() => setDialog({ open: true })}>
              <ControlPoint style={{ marginRight: '10px' }}></ControlPoint>
              Add member
            </Button>
          </div>
        );
      },
      id: 'action',
      Cell: ({ row }: { row: { index: number } }) => {
        const isApproved =
          !organization?.userRoles[row.index] ||
          organization?.userRoles[row.index].approved;
        return (
          <>
            {isApproved ? (
              <Button
                onClick={() => {
                  removeUser(row.index);
                }}
                color="secondary"
              >
                <p>Remove</p>
              </Button>
            ) : (
              <Button
                onClick={() => {
                  approveUser(row.index);
                }}
                color="secondary"
              >
                <p>Approve</p>
              </Button>
            )}
          </>
        );
      },
      disableFilters: true
    }
  ];

  const scanColumns: Column<Scan>[] = [
    {
      Header: 'Name',
      accessor: 'name',
      width: 150,
      id: 'name',
      disableFilters: true
    },
    {
      Header: 'Description',
      accessor: ({ name }) => scanSchema[name] && scanSchema[name].description,
      width: 200,
      minWidth: 200,
      id: 'description',
      disableFilters: true
    },
    {
      Header: 'Mode',
      accessor: ({ name }) =>
        scanSchema[name] && scanSchema[name].isPassive ? 'Passive' : 'Active',
      width: 150,
      minWidth: 150,
      id: 'mode',
      disableFilters: true
    },
    {
      Header: 'Action',
      id: 'action',
      maxWidth: 100,
      Cell: ({ row }: { row: { index: number } }) => {
        if (!organization) return;
        const enabled = organization.granularScans.find(
          (scan) => scan.id === scans[row.index].id
        );
        return (
          <Button
            type="button"
            onClick={() => {
              updateScan(scans[row.index], !enabled);
            }}
          >
            {enabled ? 'Disable' : 'Enable'}
          </Button>
        );
      },
      disableFilters: true
    }
  ];

  const scanTaskColumns: Column<ScanTask>[] = [
    {
      Header: 'ID',
      accessor: 'id',
      disableFilters: true
    },
    {
      Header: 'Status',
      accessor: 'status',
      disableFilters: true
    },
    {
      Header: 'Type',
      accessor: 'type',
      disableFilters: true
    },
    {
      Header: 'Name',
      accessor: ({ scan }) => scan?.name,
      disableFilters: true
    },
    {
      Header: 'Created At',
      accessor: ({ createdAt }) => dateAccessor(createdAt),
      disableFilters: true,
      disableSortBy: true
    },
    {
      Header: 'Requested At',
      accessor: ({ requestedAt }) => dateAccessor(requestedAt),
      disableFilters: true,
      disableSortBy: true
    },
    {
      Header: 'Started At',
      accessor: ({ startedAt }) => dateAccessor(startedAt),
      disableFilters: true,
      disableSortBy: true
    },
    {
      Header: 'Finished At',
      accessor: ({ finishedAt }) => dateAccessor(finishedAt),
      disableFilters: true,
      disableSortBy: true
    },
    {
      Header: 'Output',
      accessor: 'output',
      disableFilters: true
    }
  ];

  const fetchOrganization = useCallback(async () => {
    try {
      const organization = await apiGet<OrganizationType>(
        `/organizations/${organizationId}`
      );
      organization.scanTasks.sort(
        (a, b) =>
          new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
      );
      setOrganization(organization);
      setUserRoles(organization.userRoles);
      setScanTasks(organization.scanTasks);
      const tags = await apiGet<OrganizationTag[]>(`/organizations/tags`);
      setTags(tags);
    } catch (e) {
      console.error(e);
    }
  }, [apiGet, setOrganization, organizationId]);

  const fetchScans = useCallback(async () => {
    try {
      const response = await apiGet<{
        scans: Scan[];
        schema: ScanSchema;
      }>('/granularScans/');
      let { scans } = response;
      const { schema } = response;

      if (user?.userType !== 'globalAdmin')
        scans = scans.filter(
          (scan) =>
            scan.name !== 'censysIpv4' && scan.name !== 'censysCertificates'
        );

      setScans(scans);
      setScanSchema(schema);
    } catch (e) {
      console.error(e);
    }
  }, [apiGet, user]);

  const approveUser = async (user: number) => {
    try {
      await apiPost(
        `/organizations/${organization?.id}/roles/${organization?.userRoles[user].id}/approve`,
        { body: {} }
      );
      const copy = userRoles.map((role, id) =>
        id === user ? { ...role, approved: true } : role
      );
      setUserRoles(copy);
    } catch (e) {
      console.error(e);
    }
  };

  const removeUser = async (user: number) => {
    try {
      await apiPost(
        `/organizations/${organization?.id}/roles/${userRoles[user].id}/remove`,
        { body: {} }
      );
      const copy = userRoles.filter((_, ind) => ind !== user);
      setUserRoles(copy);
    } catch (e) {
      console.error(e);
    }
  };

  const updateOrganization = async (body: any) => {
    try {
      const org = await apiPut('/organizations/' + organization?.id, {
        body: organization
      });
      setOrganization(org);
      setFeedbackMessage({
        message: 'Organization successfully updated',
        type: 'success'
      });
    } catch (e) {
      setFeedbackMessage({
        message:
          e.status === 422
            ? 'Error updating organization'
            : e.message ?? e.toString(),
        type: 'error'
      });
      console.error(e);
    }
  };

  const updateScan = async (scan: Scan, enabled: boolean) => {
    try {
      if (!organization) return;
      await apiPost(
        `/organizations/${organization?.id}/granularScans/${scan.id}/update`,
        {
          body: {
            enabled
          }
        }
      );
      setOrganization({
        ...organization,
        granularScans: enabled
          ? organization.granularScans.concat([scan])
          : organization.granularScans.filter(
              (granularScan) => granularScan.id !== scan.id
            )
      });
    } catch (e) {
      setFeedbackMessage({
        message:
          e.status === 422 ? 'Error updating scan' : e.message ?? e.toString(),
        type: 'error'
      });
      console.error(e);
    }
  };

  useEffect(() => {
    fetchOrganization();
  }, [fetchOrganization]);

  const onInviteUserSubmit = async () => {
    try {
      const body = {
        firstName: newUserValues.firstName,
        lastName: newUserValues.lastName,
        email: newUserValues.email,
        organization: organization?.id,
        organizationAdmin: newUserValues.role === 'admin'
      };
      const user: User = await apiPost('/users/', {
        body
      });
      const newRole = user.roles[user.roles.length - 1];
      newRole.user = user;
      if (userRoles.find((role) => role.user.id === user.id)) {
        setUserRoles(
          userRoles.map((role) => (role.user.id === user.id ? newRole : role))
        );
      } else {
        setUserRoles(userRoles.concat([newRole]));
      }
    } catch (e) {
      setFeedbackMessage({
        message:
          e.status === 422 ? 'Error inviting user' : e.message ?? e.toString(),
        type: 'error'
      });
      console.log(e);
    }
  };

  const onInviteUserTextChange: React.ChangeEventHandler<
    HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
  > = (e) => onInviteUserChange(e.target.name, e.target.value);

  const onInviteUserChange = (name: string, value: any) => {
    setNewUserValues((values) => ({
      ...values,
      [name]: value
    }));
  };
  const filter = createFilterOptions<AutocompleteType>();

  const ListInput = (props: {
    type: 'rootDomains' | 'ipBlocks' | 'tags';
    label: string;
  }) => {
    if (!organization) return null;
    const elements: (string | OrganizationTag)[] = organization[props.type];
    return (
      <div className={classes.headerRow}>
        <label>{props.label}</label>
        <span>
          {elements &&
            elements.map((value: string | OrganizationTag, index: number) => (
              <Chip
                className={classes.chip}
                key={index}
                label={typeof value === 'string' ? value : value.name}
                onDelete={() => {
                  organization[props.type].splice(index, 1);
                  setOrganization({ ...organization });
                }}
              ></Chip>
            ))}
          <Chip
            label="ADD"
            variant="outlined"
            color="secondary"
            onClick={() => {
              setDialog({
                open: true,
                type: props.type,
                label: props.label
              });
            }}
          />
        </span>
      </div>
    );
  };

  if (!organization) return null;

  const views = [
    <Paper className={classes.settingsWrapper} key={0}>
      <Dialog
        open={dialog.open}
        onClose={() => setDialog({ open: false })}
        aria-labelledby="form-dialog-title"
        maxWidth="xs"
        fullWidth
      >
        <DialogTitle id="form-dialog-title">
          Add {dialog.label && dialog.label.slice(0, -1)}
        </DialogTitle>
        <DialogContent>
          {dialog.type === 'tags' ? (
            <>
              <DialogContentText>
                Select an existing tag or add a new one.
              </DialogContentText>
              <Autocomplete
                value={tagValue}
                onChange={(event, newValue) => {
                  if (typeof newValue === 'string') {
                    setTagValue({
                      name: newValue
                    });
                  } else {
                    setTagValue(newValue);
                  }
                }}
                filterOptions={(options, params) => {
                  const filtered = filter(options, params);
                  // Suggest the creation of a new value
                  if (
                    params.inputValue !== '' &&
                    !filtered.find(
                      (tag) =>
                        tag.name?.toLowerCase() ===
                        params.inputValue.toLowerCase()
                    )
                  ) {
                    filtered.push({
                      name: params.inputValue,
                      title: `Add "${params.inputValue}"`
                    });
                  }
                  return filtered;
                }}
                selectOnFocus
                clearOnBlur
                handleHomeEndKeys
                options={tags}
                getOptionLabel={(option) => {
                  return option.name ?? '';
                }}
                renderOption={(option) => {
                  if (option.title) return option.title;
                  return option.name ?? '';
                }}
                fullWidth
                freeSolo
                renderInput={(params) => (
                  <TextField {...params} variant="outlined" />
                )}
              />
            </>
          ) : (
            <TextField
              autoFocus
              margin="dense"
              id="name"
              label={dialog.label && dialog.label.slice(0, -1)}
              type="text"
              fullWidth
              onChange={(e) => setInputValue(e.target.value)}
            />
          )}
        </DialogContent>
        <DialogActions>
          <Button variant="outlined" onClick={() => setDialog({ open: false })}>
            Cancel
          </Button>
          <Button
            variant="contained"
            color="primary"
            onClick={() => {
              if (dialog.type && dialog.type !== 'tags') {
                if (inputValue) {
                  organization[dialog.type].push(inputValue);
                  setOrganization({ ...organization });
                }
              } else {
                if (tagValue) {
                  if (!organization.tags) organization.tags = [];
                  organization.tags.push(tagValue as any);
                  setOrganization({ ...organization });
                }
              }
              setDialog({ open: false });
              setInputValue('');
              setTagValue(null);
            }}
          >
            Add
          </Button>
        </DialogActions>
      </Dialog>
      <TextField
        value={organization.name}
        disabled
        variant="filled"
        InputProps={{
          className: classes.orgName
        }}
      ></TextField>
      <ListInput label="Root Domains" type="rootDomains"></ListInput>
      <ListInput label="IP Blocks" type="ipBlocks"></ListInput>
      <ListInput label="Tags" type="tags"></ListInput>
      <div className={classes.headerRow}>
        <label>Passive Mode</label>
        <span>
          <SwitchInput
            checked={organization.isPassive}
            onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
              setOrganization({
                ...organization,
                isPassive: event.target.checked
              });
            }}
            color="primary"
          />
        </span>
      </div>
      <div className={classes.buttons}>
        <Link to={`/organizations`}>
          <Button
            variant="outlined"
            style={{ marginRight: '10px', color: '#565C65' }}
          >
            Cancel
          </Button>
        </Link>
        <Button
          variant="contained"
          onClick={updateOrganization}
          style={{ background: '#565C65', color: 'white' }}
        >
          Save
        </Button>
      </div>
    </Paper>,
    <React.Fragment key={1}>
      <Table<Role> columns={userRoleColumns} data={userRoles} />
      <Dialog
        open={dialog.open}
        onClose={() => setDialog({ open: false })}
        aria-labelledby="form-dialog-title"
        maxWidth="xs"
        fullWidth
      >
        <DialogTitle id="form-dialog-title">Add Member</DialogTitle>
        <DialogContent>
          <p style={{ color: '#3D4551' }}>
            Organization members can view Organization-specific vulnerabilities,
            domains, and notes. Organization administrators can additionally
            manage members and update the organization.
          </p>
          <TextField
            margin="dense"
            id="firstName"
            name="firstName"
            label="First Name"
            type="text"
            fullWidth
            value={newUserValues.firstName}
            onChange={onInviteUserTextChange}
            variant="filled"
            InputProps={{
              className: classes.textField
            }}
          />
          <TextField
            margin="dense"
            id="lastName"
            name="lastName"
            label="Last Name"
            type="text"
            fullWidth
            value={newUserValues.lastName}
            onChange={onInviteUserTextChange}
            variant="filled"
            InputProps={{
              className: classes.textField
            }}
          />
          <TextField
            margin="dense"
            id="email"
            name="email"
            label="Email"
            type="text"
            fullWidth
            value={newUserValues.email}
            onChange={onInviteUserTextChange}
            variant="filled"
            InputProps={{
              className: classes.textField
            }}
          />
          <br></br>
          <br></br>
          <FormLabel component="legend">Role</FormLabel>
          <RadioGroup
            aria-label="role"
            name="role"
            value={newUserValues.role}
            onChange={onInviteUserTextChange}
          >
            <FormControlLabel
              value="standard"
              control={<Radio color="primary" />}
              label="Standard"
            />
            <FormControlLabel
              value="admin"
              control={<Radio color="primary" />}
              label="Administrator"
            />
          </RadioGroup>
        </DialogContent>
        <DialogActions>
          <Button variant="outlined" onClick={() => setDialog({ open: false })}>
            Cancel
          </Button>
          <Button
            variant="contained"
            color="primary"
            onClick={async () => {
              onInviteUserSubmit();
              setDialog({ open: false });
            }}
          >
            Add
          </Button>
        </DialogActions>
      </Dialog>
    </React.Fragment>,
    <React.Fragment key={2}>
      <OrganizationList parent={organization}></OrganizationList>
    </React.Fragment>,
    <React.Fragment key={3}>
      <Table<Scan> columns={scanColumns} data={scans} fetchData={fetchScans} />
      <h2>Organization Scan History</h2>
      <Table<ScanTask> columns={scanTaskColumns} data={scanTasks} />
    </React.Fragment>
  ];

  let navItems = [
    {
      title: 'Settings',
      path: `/organizations/${organizationId}`,
      exact: true
    },
    {
      title: 'Members',
      path: `/organizations/${organizationId}/members`
    }
  ];

  if (!organization.parent) {
    navItems = navItems.concat([
      // { title: 'Teams', path: `/organizations/${organizationId}/teams` },
      { title: 'Scans', path: `/organizations/${organizationId}/scans` }
    ]);
  }

  return (
    <div>
      <div className={classes.header}>
        <h1 className={classes.headerLabel}>
          <Link to="/organizations">Organizations</Link>
          {organization.parent && (
            <>
              <ChevronRight></ChevronRight>
              <Link to={'/organizations/' + organization.parent.id}>
                {organization.parent.name}
              </Link>
            </>
          )}
          <ChevronRight
            style={{
              verticalAlign: 'middle',
              lineHeight: '100%',
              fontSize: '26px'
            }}
          ></ChevronRight>
          <span style={{ color: '#07648D' }}>{organization.name}</span>
        </h1>
        <Subnav
          items={navItems}
          styles={{
            background: '#F9F9F9'
          }}
        ></Subnav>
      </div>
      <div className={classes.root}>
        <Switch>
          <Route
            path="/organizations/:organizationId"
            exact
            render={() => views[0]}
          />
          <Route
            path="/organizations/:organizationId/members"
            render={() => views[1]}
          />
          <Route
            path="/organizations/:organizationId/teams"
            render={() => views[2]}
          />
          <Route
            path="/organizations/:organizationId/scans"
            render={() => views[3]}
          />
        </Switch>
      </div>
    </div>
  );
}