@material-ui/icons#Add TypeScript Examples

The following examples show how to use @material-ui/icons#Add. 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: with-full-config.tsx    From react-component-library with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
getIcon = (icon: string): JSX.Element | undefined => {
    switch (icon) {
        case '<Add />':
            return <Add />;
        case '<PinDrop />':
            return <PinDrop />;
        case '<Remove />':
            return <Remove />;
        case '<AddAPhoto />':
            return <AddAPhoto />;
        case '<Menu />':
            return <Menu />;
        case '<FitnessCenter />':
            return <FitnessCenter />;
        case '<Dashboard />':
            return <Dashboard />;
        case 'undefined':
        default:
            return undefined;
    }
}
Example #2
Source File: with-nested-list-items.tsx    From react-component-library with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
getIcon = (icon: string): JSX.Element | undefined => {
    switch (icon) {
        case '<Add />':
            return <Add />;
        case '<PinDrop />':
            return <PinDrop />;
        case '<Remove />':
            return <Remove />;
        case '<AddAPhoto />':
            return <AddAPhoto />;
        case 'undefined':
        default:
            return undefined;
    }
}
Example #3
Source File: icon_button_menu.spec.tsx    From jupyter-extensions with Apache License 2.0 5 votes vote down vote up
describe('IconButtonMenu', () => {
  const menuItemsProp = (closeHandler: MenuCloseHandler) => (
    <React.Fragment>
      <MenuItem onClick={closeHandler}>Item 1</MenuItem>
      <MenuItem onClick={closeHandler}>Item 2</MenuItem>
      <MenuItem onClick={closeHandler}>Item 3</MenuItem>
    </React.Fragment>
  );

  it('Renders menu items', () => {
    const iconButtonMenu = shallow(
      <IconButtonMenu menuItems={menuItemsProp} />
    );

    expect(iconButtonMenu).toMatchSnapshot();
  });

  it('Renders with provided icon', () => {
    const iconButtonMenu = shallow(
      <IconButtonMenu menuItems={menuItemsProp} icon={<Add />} />
    );

    expect(iconButtonMenu).toMatchSnapshot();
  });

  it('Opens from icon button and closes when an item is clicked', () => {
    const iconButtonMenu = shallow(
      <IconButtonMenu menuItems={menuItemsProp} />
    );
    expect(iconButtonMenu.find(Menu).prop('open')).toBe(false);

    const openMenuButton = iconButtonMenu.find(IconButton).first();
    openMenuButton.simulate('click', {
      currentTarget: openMenuButton.getElement(),
    });

    expect(iconButtonMenu.find(Menu).prop('open')).toBe(true);
    const menuItems = iconButtonMenu.find(MenuItem);
    expect(menuItems.length).toBe(3);
    menuItems.first().simulate('click');
    expect(iconButtonMenu.find(Menu).prop('open')).toBe(false);
  });
});
Example #4
Source File: import-csv-dialog-each-item.tsx    From react-admin-import-csv with MIT License 5 votes vote down vote up
ImportCsvDialogEachItem = (props: ImportCsvDialogEachItemProps) => {
  const {
    disableImportNew,
    disableImportOverwrite,
    currentValue,
    resourceName,
    values,
    fileName,
    openAskDecide,
    handleClose,
    handleAskDecideReplace,
    handleAskDecideAddAsNew,
    handleAskDecideSkip,
    handleAskDecideSkipAll,
    isLoading,
    idsConflicting,
  } = props;
  const translate = translateWrapper();

  return (
    <SharedDialogWrapper
      title={translate("csv.dialogDecide.title", {
        id: currentValue && currentValue.id,
        resource: resourceName,
      })}
      subTitle={translate("csv.dialogCommon.subtitle", {
        count: values && values.length,
        fileName: fileName,
        resource: resourceName,
      })}
      open={openAskDecide}
      handleClose={handleClose}
    >
      {isLoading && <SharedLoader loadingTxt={translate("csv.loading")}></SharedLoader>}
      {!isLoading && (
        <div>
          <p
            style={{ fontFamily: "sans-serif", margin: "0" }}
            dangerouslySetInnerHTML={{
              __html: translate("csv.dialogCommon.conflictCount", {
                resource: resourceName,
                conflictingCount: idsConflicting && idsConflicting.length,
              }),
            }}
          ></p>
          <List>
            <SharedDialogButton
              disabled={disableImportOverwrite}
              onClick={handleAskDecideReplace}
              icon={<Done htmlColor="#29c130" />}
              label={translate("csv.dialogDecide.buttons.replaceRow", {
                id: currentValue && currentValue.id,
              })}
            />
            <SharedDialogButton
              disabled={disableImportNew}
              onClick={handleAskDecideAddAsNew}
              icon={<Add htmlColor="#3a88ca" />}
              label={translate("csv.dialogDecide.buttons.addAsNewRow")}
            />
            <SharedDialogButton
              onClick={handleAskDecideSkip}
              icon={<Undo htmlColor="black" />}
              label={translate("csv.dialogDecide.buttons.skipDontReplace")}
            />
            <SharedDialogButton
              onClick={handleAskDecideSkipAll}
              icon={<Clear htmlColor="#3a88ca" />}
              label={translate("csv.dialogCommon.buttons.cancel")}
            />
          </List>
        </div>
      )}
    </SharedDialogWrapper>
  );
}
Example #5
Source File: AddTodo.tsx    From max-todos with MIT License 5 votes vote down vote up
AddTodo: FC<{ addTodo: (text: string) => void }> = ({ addTodo }) => {
  const [text, setText] = useState("");
  const [open, setOpen] = useState(false);
  const handleChange = (
    e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => setText(e.target.value);
  const createTodo = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    addTodo(text);
    setText("");
    if (text.trim()) setOpen(true);
  };

  return (
    <div>
      <Container maxWidth="sm">
        <form onSubmit={createTodo} className="add-todo">
          <FormControl fullWidth={true}>
            <TextField
              label="I will do this"
              variant="standard"
              onChange={handleChange}
              required={true}
              value={text}
            />
            <Button
              variant="contained"
              color="primary"
              style={{ marginTop: 5 }}
              type="submit"
            >
              <Add />
              Add
            </Button>
          </FormControl>
        </form>
      </Container>
      <Snackbar
        open={open}
        autoHideDuration={4000}
        onClose={() => setOpen(false)}
        anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
      >
        <Alert
          // icon={<Check fontSize="inherit" />}
          elevation={6}
          variant="filled"
          onClose={() => setOpen(false)}
          severity="success"
        >
          Successfully added item!
        </Alert>
      </Snackbar>
    </div>
  );
}
Example #6
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 #7
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 #8
Source File: Metrics.tsx    From abacus with GNU General Public License v2.0 4 votes vote down vote up
EventEditor = ({
  index,
  completionBag: { eventCompletionDataSource },
  exposureEvent: { event: name, props: propList },
  onRemoveExposureEvent,
}: {
  index: number
  completionBag: ExperimentFormCompletionBag
  exposureEvent: EventNew
  onRemoveExposureEvent: () => void
}) => {
  const classes = useEventEditorStyles()
  const metricClasses = useMetricEditorStyles()
  const { isLoading, data: propCompletions } = useDataSource(async () => name && getPropNameCompletions(name), [name])

  return (
    <TableRow>
      <TableCell>
        <div className={classes.exposureEventsEventNameCell}>
          <Field
            component={AbacusAutocomplete}
            name={`experiment.exposureEvents[${index}].event`}
            className={classes.exposureEventsEventName}
            id={`experiment.exposureEvents[${index}].event`}
            options={eventCompletionDataSource.data}
            loading={eventCompletionDataSource.isLoading}
            renderInput={(params: AutocompleteRenderInputParams) => (
              <MuiTextField
                {...params}
                label='Event Name'
                placeholder='event_name'
                variant='outlined'
                InputLabelProps={{
                  shrink: true,
                }}
                InputProps={{
                  ...autocompleteInputProps(params, eventCompletionDataSource.isLoading),
                  'aria-label': 'Event Name',
                }}
              />
            )}
          />
          <IconButton
            className={classes.exposureEventsEventRemoveButton}
            onClick={onRemoveExposureEvent}
            aria-label='Remove exposure event'
          >
            <Clear />
          </IconButton>
        </div>
        <FieldArray
          name={`experiment.exposureEvents[${index}].props`}
          render={(arrayHelpers) => {
            const onAddExposureEventProperty = () => {
              arrayHelpers.push({
                key: '',
                value: '',
              })
            }

            return (
              <div>
                <div>
                  {propList &&
                    propList.map((_prop: unknown, propIndex: number) => {
                      const onRemoveExposureEventProperty = () => {
                        arrayHelpers.remove(propIndex)
                      }

                      return (
                        <div className={classes.exposureEventsEventPropertiesRow} key={propIndex}>
                          <Field
                            component={AbacusAutocomplete}
                            name={`experiment.exposureEvents[${index}].props[${propIndex}].key`}
                            id={`experiment.exposureEvents[${index}].props[${propIndex}].key`}
                            options={propCompletions || []}
                            loading={isLoading}
                            freeSolo={true}
                            className={classes.exposureEventsEventPropertiesKeyAutoComplete}
                            renderInput={(params: AutocompleteRenderInputParams) => (
                              <MuiTextField
                                {...params}
                                className={classes.exposureEventsEventPropertiesKey}
                                label='Key'
                                placeholder='key'
                                variant='outlined'
                                size='small'
                                InputProps={{
                                  ...autocompleteInputProps(params, isLoading),
                                  'aria-label': 'Property Key',
                                }}
                                InputLabelProps={{
                                  shrink: true,
                                }}
                              />
                            )}
                          />
                          <Field
                            component={TextField}
                            name={`experiment.exposureEvents[${index}].props[${propIndex}].value`}
                            id={`experiment.exposureEvents[${index}].props[${propIndex}].value`}
                            type='text'
                            variant='outlined'
                            placeholder='value'
                            label='Value'
                            size='small'
                            inputProps={{
                              'aria-label': 'Property Value',
                            }}
                            InputLabelProps={{
                              shrink: true,
                            }}
                          />
                          <IconButton
                            className={classes.exposureEventsEventRemoveButton}
                            onClick={onRemoveExposureEventProperty}
                            aria-label='Remove exposure event property'
                          >
                            <Clear />
                          </IconButton>
                        </div>
                      )
                    })}
                </div>
                <div className={metricClasses.addMetric}>
                  <Add className={metricClasses.addMetricAddSymbol} />
                  <Button
                    variant='contained'
                    onClick={onAddExposureEventProperty}
                    disableElevation
                    size='small'
                    aria-label='Add Property'
                  >
                    Add Property
                  </Button>
                </div>
              </div>
            )
          }}
        />
      </TableCell>
    </TableRow>
  )
}
Example #9
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 #10
Source File: TestDetailsModal.tsx    From frontend with Apache License 2.0 4 votes vote down vote up
TestDetailsModal: React.FunctionComponent<{
  testRun: TestRun;
  touched: boolean;
  handleClose: () => void;
}> = ({ testRun, touched, handleClose }) => {
  const classes = useStyles();
  const navigate = useNavigate();
  const { enqueueSnackbar } = useSnackbar();
  const testRunDispatch = useTestRunDispatch();

  const stageWidth = (window.innerWidth / 2) * 0.8;
  const stageHeigth = window.innerHeight * 0.6;
  const stageScaleBy = 1.2;
  const [stageScale, setStageScale] = React.useState(1);
  const [stagePos, setStagePos] = React.useState(defaultStagePos);
  const [stageInitPos, setStageInitPos] = React.useState(defaultStagePos);
  const [stageOffset, setStageOffset] = React.useState(defaultStagePos);
  const [processing, setProcessing] = React.useState(false);
  const [isDrawMode, setIsDrawMode] = useState(false);
  const [valueOfIgnoreOrCompare, setValueOfIgnoreOrCompare] = useState(
    "Ignore Areas"
  );
  const [isDiffShown, setIsDiffShown] = useState(false);
  const [selectedRectId, setSelectedRectId] = React.useState<string>();
  const [ignoreAreas, setIgnoreAreas] = React.useState<IgnoreArea[]>([]);
  const [applyIgnoreDialogOpen, setApplyIgnoreDialogOpen] = React.useState(
    false
  );

  const toggleApplyIgnoreDialogOpen = () => {
    setApplyIgnoreDialogOpen(!applyIgnoreDialogOpen);
  };

  const [image, imageStatus] = useImage(
    staticService.getImage(testRun.imageName)
  );
  const [baselineImage, baselineImageStatus] = useImage(
    staticService.getImage(testRun.baselineName)
  );
  const [diffImage, diffImageStatus] = useImage(
    staticService.getImage(testRun.diffName)
  );

  const applyIgnoreAreaText =
    "Apply selected ignore area to all images in this build.";

  React.useEffect(() => {
    fitStageToScreen();
    // eslint-disable-next-line
  }, [image]);

  React.useEffect(() => {
    setIsDiffShown(!!testRun.diffName);
  }, [testRun.diffName]);

  React.useEffect(() => {
    setIgnoreAreas(JSON.parse(testRun.ignoreAreas));
  }, [testRun]);

  const isImageSizeDiffer = React.useMemo(
    () =>
      testRun.baselineName &&
      testRun.imageName &&
      (image?.height !== baselineImage?.height ||
        image?.width !== baselineImage?.width),
    [image, baselineImage, testRun.baselineName, testRun.imageName]
  );

  const handleIgnoreAreaChange = (ignoreAreas: IgnoreArea[]) => {
    setIgnoreAreas(ignoreAreas);
    testRunDispatch({
      type: "touched",
      payload: testRun.ignoreAreas !== JSON.stringify(ignoreAreas),
    });
  };

  const removeSelection = (event: KonvaEventObject<MouseEvent>) => {
    // deselect when clicked not on Rect
    const isRectClicked = event.target.className === "Rect";
    if (!isRectClicked) {
      setSelectedRectId(undefined);
    }
  };

  const deleteIgnoreArea = (id: string) => {
    handleIgnoreAreaChange(ignoreAreas.filter((area) => area.id !== id));
    setSelectedRectId(undefined);
  };

  const saveTestRun = (ignoreAreas: IgnoreArea[], successMessage: string) => {
    testRunService
      .updateIgnoreAreas({
        ids: [testRun.id],
        ignoreAreas,
      })
      .then(() => {
        enqueueSnackbar(successMessage, {
          variant: "success",
        });
      })
      .catch((err) =>
        enqueueSnackbar(err, {
          variant: "error",
        })
      );
  };

  const saveIgnoreAreasOrCompareArea = () => {
    if (valueOfIgnoreOrCompare.includes("Ignore")) {
      saveTestRun(ignoreAreas, "Ignore areas are updated.");
    } else {
      const invertedIgnoreAreas = invertIgnoreArea(
        image!.width,
        image!.height,
        head(ignoreAreas)
      );

      handleIgnoreAreaChange(invertedIgnoreAreas);
      saveTestRun(
        invertedIgnoreAreas,
        "Selected area has been inverted to ignore areas and saved."
      );
    }
    testRunDispatch({ type: "touched", payload: false });
  };

  const onIgnoreOrCompareSelectChange = (value: string) => {
    if (value.includes("Compare")) {
      setValueOfIgnoreOrCompare("Compare Area");
    } else {
      setValueOfIgnoreOrCompare("Ignore Areas");
    }
  };

  const setOriginalSize = () => {
    setStageScale(1);
    resetPositioin();
  };

  const fitStageToScreen = () => {
    const scale = image
      ? Math.min(
          stageWidth < image.width ? stageWidth / image.width : 1,
          stageHeigth < image.height ? stageHeigth / image.height : 1
        )
      : 1;
    setStageScale(scale);
    resetPositioin();
  };

  const resetPositioin = () => {
    setStagePos(defaultStagePos);
    setStageOffset(defaultStagePos);
  };

  const applyIgnoreArea = () => {
    let newIgnoreArea = ignoreAreas.find((area) => selectedRectId! === area.id);
    if (newIgnoreArea) {
      setProcessing(true);
      testRunService
        .getList(testRun.buildId)
        .then((testRuns: TestRun[]) => {
          let allIds = testRuns.map((item) => item.id);
          let data: UpdateIgnoreAreaDto = {
            ids: allIds,
            ignoreAreas: [newIgnoreArea!],
          };
          testRunService.addIgnoreAreas(data).then(() => {
            setProcessing(false);
            setSelectedRectId(undefined);
            enqueueSnackbar(
              "Ignore areas are updated in all images in this build.",
              {
                variant: "success",
              }
            );
          });
        })
        .catch((error) => {
          enqueueSnackbar("There was an error : " + error, {
            variant: "error",
          });
          setProcessing(false);
        });
    } else {
      enqueueSnackbar(
        "There was an error determining which ignore area to apply.",
        { variant: "error" }
      );
    }
  };

  useHotkeys(
    "d",
    () => !!testRun.diffName && setIsDiffShown((isDiffShown) => !isDiffShown),
    [testRun.diffName]
  );
  useHotkeys("ESC", handleClose, [handleClose]);

  return (
    <React.Fragment>
      <AppBar position="sticky">
        <Toolbar>
          <Grid container justifyContent="space-between">
            <Grid item>
              <Typography variant="h6">{testRun.name}</Typography>
            </Grid>
            {testRun.diffName && (
              <Grid item>
                <Tooltip title={"Hotkey: D"}>
                  <Switch
                    checked={isDiffShown}
                    onChange={() => setIsDiffShown(!isDiffShown)}
                    name="Toggle diff"
                  />
                </Tooltip>
              </Grid>
            )}
            {(testRun.status === TestStatus.unresolved ||
              testRun.status === TestStatus.new) && (
              <Grid item>
                <ApproveRejectButtons testRun={testRun} />
              </Grid>
            )}
            <Grid item>
              <IconButton color="inherit" onClick={handleClose}>
                <Close />
              </IconButton>
            </Grid>
          </Grid>
        </Toolbar>
      </AppBar>
      {processing && <LinearProgress />}
      <Box m={1}>
        <Grid container alignItems="center">
          <Grid item xs={12}>
            <Grid container alignItems="center">
              <Grid item>
                <TestRunDetails testRun={testRun} />
              </Grid>
              {isImageSizeDiffer && (
                <Grid item>
                  <Tooltip
                    title={
                      "Image height/width differ from baseline! Cannot calculate diff!"
                    }
                  >
                    <IconButton>
                      <WarningRounded color="secondary" />
                    </IconButton>
                  </Tooltip>
                </Grid>
              )}
            </Grid>
          </Grid>
          <Grid item>
            <Grid container alignItems="center" spacing={2}>
              <Grid item>
                <Select
                  id="area-select"
                  labelId="areaSelect"
                  value={valueOfIgnoreOrCompare}
                  onChange={(event) =>
                    onIgnoreOrCompareSelectChange(event.target.value as string)
                  }
                >
                  {["Ignore Areas", "Compare Area"].map((eachItem) => (
                    <MenuItem key={eachItem} value={eachItem}>
                      {eachItem}
                    </MenuItem>
                  ))}
                </Select>
              </Grid>
              <Grid item>
                <ToggleButton
                  value={"drawMode"}
                  selected={isDrawMode}
                  onClick={() => {
                    setIsDrawMode(!isDrawMode);
                  }}
                >
                  <Add />
                </ToggleButton>
              </Grid>
              <Grid item>
                <IconButton
                  disabled={!selectedRectId || ignoreAreas.length === 0}
                  onClick={() =>
                    selectedRectId && deleteIgnoreArea(selectedRectId)
                  }
                >
                  <Delete />
                </IconButton>
              </Grid>
              <Tooltip title="Clears all ignore areas." aria-label="reject">
                <Grid item>
                  <IconButton
                    disabled={ignoreAreas.length === 0}
                    onClick={() => {
                      handleIgnoreAreaChange([]);
                    }}
                  >
                    <LayersClear />
                  </IconButton>
                </Grid>
              </Tooltip>
              <Tooltip
                title={applyIgnoreAreaText}
                aria-label="apply ignore area"
              >
                <Grid item>
                  <IconButton
                    disabled={!selectedRectId || ignoreAreas.length === 0}
                    onClick={() => toggleApplyIgnoreDialogOpen()}
                  >
                    <Collections />
                  </IconButton>
                </Grid>
              </Tooltip>
              <Grid item>
                <IconButton
                  disabled={!touched}
                  onClick={() => saveIgnoreAreasOrCompareArea()}
                >
                  <Save />
                </IconButton>
              </Grid>
            </Grid>
          </Grid>
          <Grid item>
            <Button
              color="primary"
              disabled={!testRun.testVariationId}
              onClick={() => {
                navigate(
                  `${routes.VARIATION_DETAILS_PAGE}/${testRun.testVariationId}`
                );
              }}
            >
              Baseline history
            </Button>
          </Grid>
          <Grid item>
            <CommentsPopper
              text={testRun.comment}
              onSave={(comment) =>
                testRunService
                  .update(testRun.id, { comment })
                  .then(() =>
                    enqueueSnackbar("Comment updated", {
                      variant: "success",
                    })
                  )
                  .catch((err) =>
                    enqueueSnackbar(err, {
                      variant: "error",
                    })
                  )
              }
            />
          </Grid>
        </Grid>
      </Box>
      <Box
        overflow="hidden"
        minHeight="65%"
        className={classes.drawAreaContainer}
      >
        <Grid container style={{ height: "100%" }}>
          <Grid item xs={6} className={classes.drawAreaItem}>
            <DrawArea
              type="Baseline"
              imageName={testRun.baselineName}
              branchName={testRun.baselineBranchName}
              imageState={[baselineImage, baselineImageStatus]}
              ignoreAreas={[]}
              tempIgnoreAreas={[]}
              setIgnoreAreas={handleIgnoreAreaChange}
              selectedRectId={selectedRectId}
              setSelectedRectId={setSelectedRectId}
              onStageClick={removeSelection}
              stageScaleState={[stageScale, setStageScale]}
              stagePosState={[stagePos, setStagePos]}
              stageInitPosState={[stageInitPos, setStageInitPos]}
              stageOffsetState={[stageOffset, setStageOffset]}
              drawModeState={[false, setIsDrawMode]}
            />
          </Grid>
          <Grid item xs={6} className={classes.drawAreaItem}>
            {isDiffShown ? (
              <DrawArea
                type="Diff"
                imageName={testRun.diffName}
                branchName={testRun.branchName}
                imageState={[diffImage, diffImageStatus]}
                ignoreAreas={ignoreAreas}
                tempIgnoreAreas={JSON.parse(testRun.tempIgnoreAreas)}
                setIgnoreAreas={handleIgnoreAreaChange}
                selectedRectId={selectedRectId}
                setSelectedRectId={setSelectedRectId}
                onStageClick={removeSelection}
                stageScaleState={[stageScale, setStageScale]}
                stagePosState={[stagePos, setStagePos]}
                stageInitPosState={[stageInitPos, setStageInitPos]}
                stageOffsetState={[stageOffset, setStageOffset]}
                drawModeState={[isDrawMode, setIsDrawMode]}
              />
            ) : (
              <DrawArea
                type="Image"
                imageName={testRun.imageName}
                branchName={testRun.branchName}
                imageState={[image, imageStatus]}
                ignoreAreas={ignoreAreas}
                tempIgnoreAreas={JSON.parse(testRun.tempIgnoreAreas)}
                setIgnoreAreas={handleIgnoreAreaChange}
                selectedRectId={selectedRectId}
                setSelectedRectId={setSelectedRectId}
                onStageClick={removeSelection}
                stageScaleState={[stageScale, setStageScale]}
                stagePosState={[stagePos, setStagePos]}
                stageInitPosState={[stageInitPos, setStageInitPos]}
                stageOffsetState={[stageOffset, setStageOffset]}
                drawModeState={[isDrawMode, setIsDrawMode]}
              />
            )}
          </Grid>
        </Grid>
      </Box>
      <ScaleActionsSpeedDial
        onZoomInClick={() => setStageScale(stageScale * stageScaleBy)}
        onZoomOutClick={() => setStageScale(stageScale / stageScaleBy)}
        onOriginalSizeClick={setOriginalSize}
        onFitIntoScreenClick={fitStageToScreen}
      />
      <BaseModal
        open={applyIgnoreDialogOpen}
        title={applyIgnoreAreaText}
        submitButtonText={"Yes"}
        onCancel={toggleApplyIgnoreDialogOpen}
        content={
          <Typography>
            {`All images in the current build will be re-compared with new ignore area taken into account. Are you sure?`}
          </Typography>
        }
        onSubmit={() => {
          toggleApplyIgnoreDialogOpen();
          applyIgnoreArea();
        }}
      />
    </React.Fragment>
  );
}
Example #11
Source File: ProjectListPage.tsx    From frontend with Apache License 2.0 4 votes vote down vote up
ProjectsListPage = () => {
  const { enqueueSnackbar } = useSnackbar();
  const projectState = useProjectState();
  const projectDispatch = useProjectDispatch();
  const helpDispatch = useHelpDispatch();

  const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
  const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false);
  const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);

  useEffect(() => {
    setHelpSteps(helpDispatch, PROJECT_LIST_PAGE_STEPS);
  });

  const toggleCreateDialogOpen = () => {
    setCreateDialogOpen(!createDialogOpen);
  };

  const toggleUpdateDialogOpen = () => {
    setUpdateDialogOpen(!updateDialogOpen);
  };

  const toggleDeleteDialogOpen = () => {
    setDeleteDialogOpen(!deleteDialogOpen);
  };

  return (
    <Box mt={2}>
      <Grid container spacing={2}>
        <Grid item xs={4}>
          <Box
            height="100%"
            alignItems="center"
            justifyContent="center"
            display="flex"
          >
            <Fab
              color="primary"
              aria-label="add"
              onClick={() => {
                toggleCreateDialogOpen();
                setProjectEditState(projectDispatch);
              }}
            >
              <Add />
            </Fab>
          </Box>

          <BaseModal
            open={createDialogOpen}
            title={"Create Project"}
            submitButtonText={"Create"}
            onCancel={toggleCreateDialogOpen}
            content={<ProjectForm />}
            onSubmit={() =>
              createProject(projectDispatch, projectState.projectEditState)
                .then((project) => {
                  toggleCreateDialogOpen();
                  enqueueSnackbar(`${project.name} created`, {
                    variant: "success",
                  });
                })
                .catch((err) =>
                  enqueueSnackbar(err, {
                    variant: "error",
                  })
                )
            }
          />

          <BaseModal
            open={updateDialogOpen}
            title={"Update Project"}
            submitButtonText={"Update"}
            onCancel={toggleUpdateDialogOpen}
            content={<ProjectForm />}
            onSubmit={() =>
              updateProject(projectDispatch, projectState.projectEditState)
                .then((project) => {
                  toggleUpdateDialogOpen();
                  enqueueSnackbar(`${project.name} updated`, {
                    variant: "success",
                  });
                })
                .catch((err) =>
                  enqueueSnackbar(err, {
                    variant: "error",
                  })
                )
            }
          />

          <BaseModal
            open={deleteDialogOpen}
            title={"Delete Project"}
            submitButtonText={"Delete"}
            onCancel={toggleDeleteDialogOpen}
            content={
              <Typography>{`Are you sure you want to delete: ${projectState.projectEditState.name}?`}</Typography>
            }
            onSubmit={() =>
              deleteProject(projectDispatch, projectState.projectEditState.id)
                .then((project) => {
                  toggleDeleteDialogOpen();
                  enqueueSnackbar(`${project.name} deleted`, {
                    variant: "success",
                  });
                })
                .catch((err) =>
                  enqueueSnackbar(err, {
                    variant: "error",
                  })
                )
            }
          />
        </Grid>
        {projectState.projectList.map((project) => (
          <Grid item xs={4} key={project.id}>
            <Card id={LOCATOR_PROJECT_LIST_PAGE_PROJECT_LIST}>
              <CardContent>
                <Typography>Id: {project.id}</Typography>
                <Typography>Name: {project.name}</Typography>
                <Typography>Main branch: {project.mainBranchName}</Typography>
                <Typography>
                  Created: {formatDateTime(project.createdAt)}
                </Typography>
              </CardContent>
              <CardActions>
                <Button color="primary" href={project.id}>
                  Builds
                </Button>
                <Button
                  color="primary"
                  href={`${routes.VARIATION_LIST_PAGE}/${project.id}`}
                >
                  Variations
                </Button>
                <IconButton
                  onClick={(event: React.MouseEvent<HTMLElement>) => {
                    toggleUpdateDialogOpen();
                    setProjectEditState(projectDispatch, project);
                  }}
                >
                  <Edit />
                </IconButton>
                <IconButton
                  onClick={(event: React.MouseEvent<HTMLElement>) => {
                    toggleDeleteDialogOpen();
                    setProjectEditState(projectDispatch, project);
                  }}
                >
                  <Delete />
                </IconButton>
              </CardActions>
            </Card>
          </Grid>
        ))}
      </Grid>
    </Box>
  );
}
Example #12
Source File: OrganizationList.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
OrganizationList: React.FC<{
  parent?: Organization;
}> = ({ parent }) => {
  const { apiPost, apiGet, setFeedbackMessage, user } = useAuthContext();
  const [organizations, setOrganizations] = useState<Organization[]>([]);
  const [dialogOpen, setDialogOpen] = useState(false);
  const history = useHistory();
  const classes = useStyles();

  const onSubmit = async (body: Object) => {
    try {
      const org = await apiPost('/organizations/', {
        body
      });
      setOrganizations(organizations.concat(org));
    } catch (e) {
      setFeedbackMessage({
        message:
          e.status === 422
            ? 'Error when submitting organization entry.'
            : e.message ?? e.toString(),
        type: 'error'
      });
      console.error(e);
    }
  };

  const fetchOrganizations = useCallback(async () => {
    try {
      const rows = await apiGet<Organization[]>('/organizations/');
      setOrganizations(rows);
    } catch (e) {
      console.error(e);
    }
  }, [apiGet]);

  React.useEffect(() => {
    if (!parent) fetchOrganizations();
    else {
      setOrganizations(parent.children);
    }
  }, [fetchOrganizations, parent]);

  return (
    <>
      <Grid
        container
        spacing={2}
        style={{ margin: '0 auto', marginTop: '1rem', maxWidth: '1000px' }}
      >
        {user?.userType === 'globalAdmin' && (
          <Grid item>
            <Paper
              elevation={0}
              classes={{ root: classes.cardRoot }}
              style={{ border: '1px dashed #C9C9C9', textAlign: 'center' }}
              onClick={() => setDialogOpen(true)}
            >
              <h1>Create New {parent ? 'Team' : 'Organization'}</h1>
              <p>
                <Add></Add>
              </p>
            </Paper>
          </Grid>
        )}
        {organizations.map((org) => (
          // TODO: Add functionality to delete organizations
          <Grid item key={org.id}>
            <Paper
              elevation={0}
              classes={{ root: classes.cardRoot }}
              onClick={() => {
                history.push('/organizations/' + org.id);
              }}
            >
              <h1>{org.name}</h1>
              <p>{org.userRoles ? org.userRoles.length : 0} members</p>
              {org.tags && org.tags.length > 0 && (
                <p>Tags: {org.tags.map((tag) => tag.name).join(', ')}</p>
              )}
            </Paper>
          </Grid>
        ))}
      </Grid>
      <OrganizationForm
        onSubmit={onSubmit}
        open={dialogOpen}
        setOpen={setDialogOpen}
        type="create"
        parent={parent}
      ></OrganizationForm>
    </>
  );
}
Example #13
Source File: Request.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
DocumentRequest = ({
  requestBody,
  defaultRequestBody,
  setRequestBody,
}: Props) => {
  const [textDocuments, setTextDocuments] = useState("")
  const [uris, setURIs] = useState<string[]>([])
  const [showCustom, setShowCustom] = useState(false)
  const [rows, setRows] = useState<string[]>([])
  const [placeholders, setPlaceholders] = useState<{ [key: string]: string }>(
    {}
  )
  const [keys, setKeys] = useState<{ [key: string]: string }>({})
  const [values, setValues] = useState<{ [key: string]: string }>({})

  const toggleShowCustom = () => setShowCustom((prev) => !prev)

  useEffect(() => {
    const {
      rows: initialRows,
      keys: initialKeys,
      values: initialValues,
      text: initialText,
      uris: initialURIs,
      placeholders: initialPlaceholders,
    } = parseDocumentRequest(requestBody, defaultRequestBody)

    setPlaceholders(initialPlaceholders)
    setTextDocuments(initialText)
    setRows(initialRows.length ? initialRows : [nanoid()])
    setValues(initialValues)
    setKeys(initialKeys)
    setURIs(initialURIs)
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  useEffect(() => {
    const handleUpdate = async () => {
      const formattedBody = await formatDocumentRequest(
        textDocuments,
        uris,
        rows,
        keys,
        values
      )
      setRequestBody(formattedBody)
    }
    handleUpdate()
  }, [textDocuments, uris, rows, keys, values, setRequestBody])

  const addRow = () => {
    const rowId = nanoid()
    setRows((prev) => {
      return [...prev, rowId]
    })
  }

  const handleFileSelect = async (files: FileList | null) => {
    const uris: string[] = []
    const filesArray = Array.from(files || [])

    for (let file of filesArray) {
      const uri = await fileToBase64(file)
      uris.push(uri)
    }

    setURIs(uris)
  }

  const removeRow = (rowId: string) => {
    if (placeholders[rowId]) return setValue(rowId, "")

    const index = rows.indexOf(rowId)
    setRows((prev) => {
      prev.splice(index, 1)
      return prev.length === 0 ? [nanoid()] : [...prev]
    })
  }

  const setKey = (rowId: string, key: string) => {
    setKeys((prev) => {
      prev[rowId] = key
      return { ...prev }
    })
  }

  const setValue = (rowId: string, value: string) => {
    setValues((prev) => {
      prev[rowId] = value
      return { ...prev }
    })
  }

  const removeFiles = () => {
    setURIs([])
  }

  const numCustomFields = reduce(
    rows,
    (acc, rowId) => {
      if (values[rowId] && keys[rowId]) return acc + 1
      return acc
    },
    0
  )

  return (
    <>
      <Box mb={2}>
        <Grid container spacing={2}>
          <Grid item xs={12}>
            <TextInput
              label="Text Documents"
              placeholder="Text Documents"
              variant="outlined"
              multiline
              minRows={3}
              maxRows={25}
              type="custom-text"
              value={textDocuments}
              onChange={(e) => setTextDocuments(e.target.value)}
            />
          </Grid>
        </Grid>
      </Box>
      <Grid container>
        <Grid item xs={6}>
          <FileInput
            type="file"
            multiple
            id="attach-files-button"
            onChange={(e) => handleFileSelect(e.target.files)}
          />
          <label htmlFor="attach-files-button">
            <Button size="large" component="span">
              Select Files
            </Button>
          </label>
          {uris?.length ? (
            <Box display="inline" marginLeft={3}>
              {uris.length} files selected{" "}
              <Button onClick={removeFiles}>Remove</Button>
            </Box>
          ) : (
            ""
          )}
        </Grid>
        <Grid item xs={6}>
          <Box textAlign="right" onClick={toggleShowCustom}>
            <Button size="large">
              {showCustom ? "Hide " : "Show "}Additional Fields
              {numCustomFields ? ` (${numCustomFields})` : ""}
            </Button>
          </Box>
        </Grid>
      </Grid>
      <Collapse in={showCustom}>
        <Box width="100%">
          <Divider />
          {rows.map((id) => (
            <Grid key={id} container spacing={2} paddingTop={3}>
              <Grid item xs={4}>
                <TextInput
                  label="Key"
                  variant="outlined"
                  type="custom-input"
                  disabled={typeof placeholders[id] === "string"}
                  value={keys[id] || ""}
                  onChange={(e) => setKey(id, e.target.value)}
                />
              </Grid>
              <Grid item xs={7}>
                <TextInput
                  label="Value"
                  variant="outlined"
                  type="custom-input"
                  value={values[id] || ""}
                  placeholder={placeholders[id] || ""}
                  onChange={(e) => setValue(id, e.target.value)}
                  InputLabelProps={{
                    shrink: true,
                  }}
                />
              </Grid>
              <Grid item xs={1}>
                {!placeholders[id] || values[id] ? (
                  <Button size="large" onClick={() => removeRow(id)}>
                    <Close />
                  </Button>
                ) : (
                  <></>
                )}
              </Grid>
            </Grid>
          ))}

          <Box paddingTop={3}>
            <Button size="large" onClick={addRow}>
              <Add /> Add Field
            </Button>
          </Box>
        </Box>
      </Collapse>
    </>
  )
}
Example #14
Source File: BroadcastList.tsx    From twilio-voice-notification-app with Apache License 2.0 4 votes vote down vote up
BroadcastList: React.FC = () => {
  const [currentPage, setCurrentPage] = useState(1);
  const classes = useStyles();

  const { data, loading, error } = useFetch(
    `/api/broadcasts?page=${currentPage - 1}`,
    {
      method: 'GET',
      data: { broadcasts: [], pageCount: 0 },
    },
    [currentPage]
  );

  const {
    broadcasts,
    pageCount,
  }: { broadcasts: Broadcast[]; pageCount: number } = data;

  const onPaginationChange = useCallback(
    (event: React.ChangeEvent<unknown>, value: number) => {
      setCurrentPage(value);
    },
    [setCurrentPage]
  );

  return (
    <StyledCard className={classes.root}>
      <Box display="flex" flexDirection="row">
        <Box flexGrow="1">
          <Typography variant="h4" component="h1">
            My Voice Notifications
          </Typography>
          <Typography className={classes.subtitle}>
            Review all of your voice notifications and access reports
          </Typography>
        </Box>
        <Box>
          <Button
            component={RouterLink}
            to="/create"
            color="primary"
            variant="contained"
            className={classes.createButton}
            startIcon={<Add />}
          >
            New Voice Notification
          </Button>
        </Box>
      </Box>
      {loading && (
        <Box data-testid={LOADER_TEST_ID}>
          <CircularProgress />
        </Box>
      )}
      {error && <Alert alert={alert} showReload={true} />}
      {!error && !loading && broadcasts?.length > 0 && (
        <>
          {broadcasts.map(
            ({
              broadcastId,
              friendlyName,
              dateCreated,
              canceled,
              completed,
            }) => (
              <NotificationItem
                key={broadcastId}
                friendlyName={friendlyName}
                broadcastId={broadcastId}
                dateCreated={dateCreated}
                canceled={canceled}
                completed={completed}
              />
            )
          )}
          {pageCount > 1 && (
            <Box
              margin={4}
              display="flex"
              flexDirection="row"
              justifyContent="center"
            >
              <Pagination
                count={pageCount}
                page={currentPage}
                onChange={onPaginationChange}
                color="primary"
              />
            </Box>
          )}
        </>
      )}
      {!error && !loading && broadcasts?.length === 0 && (
        <GreyContainer>
          You haven't created any voice notification yet. You can create your
          first voice notification in 3 easy steps, only be sure to have the
          following
          <ul>
            {REQUIREMENTS.map((requirement, i) => (
              <li key={i}>{requirement}</li>
            ))}
          </ul>
        </GreyContainer>
      )}
    </StyledCard>
  );
}