@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 |
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 |
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 |
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 |
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 |
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 |
/** * 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 |
Audience = ({
indexedSegments,
formikProps,
completionBag,
}: {
indexedSegments: Record<number, Segment>
formikProps: FormikProps<{ experiment: ExperimentFormData }>
completionBag: ExperimentFormCompletionBag
}): JSX.Element => {
const classes = useStyles()
// The segmentExclusion code is currently split between here and SegmentAutocomplete
// An improvement might be to have SegmentAutocomplete only handle Segment[] and for code here
// to translate Segment <-> SegmentAssignment
const [segmentAssignmentsField, _segmentAssignmentsFieldMeta, segmentAssignmentsFieldHelper] = useField(
'experiment.segmentAssignments',
)
const [segmentExclusionState, setSegmentExclusionState] = useState<SegmentExclusionState>(() => {
// We initialize the segmentExclusionState from existing data if there is any
const firstSegmentAssignment = (segmentAssignmentsField.value as SegmentAssignmentNew[])[0]
return firstSegmentAssignment && firstSegmentAssignment.isExcluded
? SegmentExclusionState.Exclude
: SegmentExclusionState.Include
})
const onChangeSegmentExclusionState = (event: React.SyntheticEvent<HTMLInputElement>, value: string) => {
setSegmentExclusionState(value as SegmentExclusionState)
segmentAssignmentsFieldHelper.setValue(
(segmentAssignmentsField.value as SegmentAssignmentNew[]).map((segmentAssignment: SegmentAssignmentNew) => {
return {
...segmentAssignment,
isExcluded: value === SegmentExclusionState.Exclude,
}
}),
)
}
const platformError = formikProps.touched.experiment?.platform && formikProps.errors.experiment?.platform
const variationsError =
formikProps.touched.experiment?.variations && _.isString(formikProps.errors.experiment?.variations)
? formikProps.errors.experiment?.variations
: undefined
return (
<div className={classes.root}>
<Typography variant='h4' gutterBottom>
Define Your Audience
</Typography>
<div className={classes.row}>
<FormControl component='fieldset'>
<FormLabel required>Platform</FormLabel>
<Field component={Select} name='experiment.platform' displayEmpty error={!!platformError}>
<MenuItem value='' disabled>
Select a Platform
</MenuItem>
{Object.values(Platform).map((platform) => (
<MenuItem key={platform} value={platform}>
{platform}: {PlatformToHuman[platform]}
</MenuItem>
))}
</Field>
<FormHelperText error={!!platformError}>
{_.isString(platformError) ? platformError : undefined}
</FormHelperText>
</FormControl>
</div>
<div className={classes.row}>
<FormControl component='fieldset'>
<FormLabel required>User type</FormLabel>
<FormHelperText>Types of users to include in experiment</FormHelperText>
<Field component={FormikMuiRadioGroup} name='experiment.existingUsersAllowed' required>
<FormControlLabel
value='true'
label='All users (new + existing + anonymous)'
control={<Radio disabled={formikProps.isSubmitting} />}
disabled={formikProps.isSubmitting}
/>
<FormControlLabel
value='false'
label='Filter for newly signed up users (they must be also logged in)'
control={<Radio disabled={formikProps.isSubmitting} />}
disabled={formikProps.isSubmitting}
/>
</Field>
</FormControl>
</div>
<div className={classes.row}>
<FormControl component='fieldset' className={classes.segmentationFieldSet}>
<FormLabel htmlFor='segments-select'>Targeting</FormLabel>
<FormHelperText className={classes.segmentationHelperText}>
Who should see this experiment? <br /> Add optional filters to include or exclude specific target audience
segments.
</FormHelperText>
<MuiRadioGroup
aria-label='include-or-exclude-segments'
className={classes.segmentationExclusionState}
value={segmentExclusionState}
onChange={onChangeSegmentExclusionState}
>
<FormControlLabel
value={SegmentExclusionState.Include}
control={<Radio />}
label='Include'
name='non-formik-segment-exclusion-state-include'
/>
<FormControlLabel
value={SegmentExclusionState.Exclude}
control={<Radio />}
label='Exclude'
name='non-formik-segment-exclusion-state-exclude'
/>
</MuiRadioGroup>
<Field
name='experiment.segmentAssignments'
component={SegmentsAutocomplete}
options={Object.values(indexedSegments)}
// TODO: Error state, see https://stackworx.github.io/formik-material-ui/docs/api/material-ui-lab
renderInput={(params: AutocompleteRenderInputParams) => (
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
<MuiTextField
{...params}
variant='outlined'
placeholder={segmentAssignmentsField.value.length === 0 ? 'Search and select to customize' : undefined}
/>
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
)}
segmentExclusionState={segmentExclusionState}
indexedSegments={indexedSegments}
fullWidth
id='segments-select'
/>
</FormControl>
</div>
<div className={classes.row}>
<FormControl component='fieldset' className={classes.segmentationFieldSet}>
<FormLabel htmlFor='variations-select'>Variations</FormLabel>
<FormHelperText className={classes.segmentationHelperText}>
Set the percentage of traffic allocated to each variation. Percentages may sum to less than 100 to avoid
allocating the entire userbase. <br /> Use “control” for the default (fallback) experience.
</FormHelperText>
{variationsError && <FormHelperText error>{variationsError}</FormHelperText>}
<TableContainer>
<Table className={classes.variants}>
<TableHead>
<TableRow>
<TableCell> Name </TableCell>
<TableCell> Allocated Percentage </TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
<FieldArray
name={`experiment.variations`}
render={(arrayHelpers) => {
const onAddVariation = () => {
arrayHelpers.push({
name: ``,
isDefault: false,
allocatedPercentage: '',
})
}
const onRemoveVariation = (index: number) => arrayHelpers.remove(index)
const variations = formikProps.values.experiment.variations
return (
<>
{variations.map((variation, index) => {
return (
// The key here needs to be changed for variable variations
<TableRow key={index}>
<TableCell>
{variation.isDefault ? (
variation.name
) : (
<Field
component={FormikMuiTextField}
name={`experiment.variations[${index}].name`}
size='small'
variant='outlined'
required
inputProps={{
'aria-label': 'Variation Name',
}}
/>
)}
</TableCell>
<TableCell>
<Field
className={classes.variationAllocatedPercentage}
component={FormikMuiTextField}
name={`experiment.variations[${index}].allocatedPercentage`}
type='number'
size='small'
variant='outlined'
inputProps={{ min: 1, max: 99, 'aria-label': 'Allocated Percentage' }}
required
InputProps={{
endAdornment: <InputAdornment position='end'>%</InputAdornment>,
}}
/>
</TableCell>
<TableCell>
{!variation.isDefault && 2 < variations.length && (
<IconButton onClick={() => onRemoveVariation(index)} aria-label='Remove variation'>
<Clear />
</IconButton>
)}
</TableCell>
</TableRow>
)
})}
<TableRow>
<TableCell colSpan={3}>
<Alert severity='warning' className={classes.abnWarning}>
<strong> Manual analysis only A/B/n </strong>
<br />
<p>
Experiments with more than a single treatment variation are in an early alpha stage.
</p>
<p>No results will be displayed.</p>
<p>
Please do not set up such experiments in production without consulting the ExPlat team
first.
</p>
<div className={classes.addVariation}>
<Add className={classes.addVariationIcon} />
<Button
variant='contained'
onClick={onAddVariation}
disableElevation
size='small'
aria-label='Add Variation'
>
Add Variation
</Button>
</div>
</Alert>
</TableCell>
</TableRow>
</>
)
}}
/>
</TableBody>
</Table>
</TableContainer>
</FormControl>
</div>
{isDebugMode() && (
<div className={classes.row}>
<FormControl component='fieldset'>
<FormLabel htmlFor='experiment.exclusionGroupTagIds'>Exclusion Groups</FormLabel>
<FormHelperText>Optionally add this experiment to a mutually exclusive experiment group.</FormHelperText>
<br />
<Field
component={AbacusAutocomplete}
name='experiment.exclusionGroupTagIds'
id='experiment.exclusionGroupTagIds'
fullWidth
options={
// istanbul ignore next; trivial
completionBag.exclusionGroupCompletionDataSource.data ?? []
}
loading={completionBag.exclusionGroupCompletionDataSource.isLoading}
multiple
renderOption={(option: AutocompleteItem) => <Chip label={option.name} />}
renderInput={(params: AutocompleteRenderInputParams) => (
<MuiTextField
{...params}
variant='outlined'
InputProps={{
...autocompleteInputProps(params, completionBag.exclusionGroupCompletionDataSource.isLoading),
}}
InputLabelProps={{
shrink: true,
}}
/>
)}
/>
</FormControl>
</div>
)}
</div>
)
}
Example #8
Source File: Metrics.tsx From abacus with GNU General Public License v2.0 | 4 votes |
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 |
Metrics = ({
indexedMetrics,
completionBag,
formikProps,
}: {
indexedMetrics: Record<number, Metric>
completionBag: ExperimentFormCompletionBag
formikProps: FormikProps<{ experiment: ExperimentFormData }>
}): JSX.Element => {
const classes = useStyles()
const metricEditorClasses = useMetricEditorStyles()
const decorationClasses = useDecorationStyles()
// Metric Assignments
const [metricAssignmentsField, _metricAssignmentsFieldMetaProps, metricAssignmentsFieldHelperProps] =
useField<MetricAssignment[]>('experiment.metricAssignments')
const [selectedMetric, setSelectedMetric] = useState<Metric | null>(null)
const onChangeSelectedMetricOption = (_event: unknown, value: Metric | null) => setSelectedMetric(value)
const makeMetricAssignmentPrimary = (indexToSet: number) => {
metricAssignmentsFieldHelperProps.setValue(
metricAssignmentsField.value.map((metricAssignment, index) => ({
...metricAssignment,
isPrimary: index === indexToSet,
})),
)
}
// This picks up the no metric assignments validation error
const metricAssignmentsError =
formikProps.touched.experiment?.metricAssignments &&
_.isString(formikProps.errors.experiment?.metricAssignments) &&
formikProps.errors.experiment?.metricAssignments
// ### Exposure Events
const [exposureEventsField, _exposureEventsFieldMetaProps, _exposureEventsFieldHelperProps] =
useField<EventNew[]>('experiment.exposureEvents')
return (
<div className={classes.root}>
<Typography variant='h4' gutterBottom>
Assign Metrics
</Typography>
<FieldArray
name='experiment.metricAssignments'
render={(arrayHelpers) => {
const onAddMetric = () => {
if (selectedMetric) {
const metricAssignment = createMetricAssignment(selectedMetric)
arrayHelpers.push({
...metricAssignment,
isPrimary: metricAssignmentsField.value.length === 0,
})
}
setSelectedMetric(null)
}
return (
<>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Metric</TableCell>
<TableCell>Attribution Window</TableCell>
<TableCell>Change Expected?</TableCell>
<TableCell>Minimum Practical Difference</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{metricAssignmentsField.value.map((metricAssignment, index) => {
const onRemoveMetricAssignment = () => {
arrayHelpers.remove(index)
}
const onMakePrimary = () => {
makeMetricAssignmentPrimary(index)
}
const attributionWindowError =
(_.get(
formikProps.touched,
`experiment.metricAssignments[${index}].attributionWindowSeconds`,
) as boolean | undefined) &&
(_.get(
formikProps.errors,
`experiment.metricAssignments[${index}].attributionWindowSeconds`,
) as string | undefined)
return (
<TableRow key={index}>
<TableCell className={classes.metricNameCell}>
<Tooltip arrow title={indexedMetrics[metricAssignment.metricId].description}>
<span className={clsx(classes.metricName, decorationClasses.tooltipped)}>
{indexedMetrics[metricAssignment.metricId].name}
</span>
</Tooltip>
<br />
{metricAssignment.isPrimary && <Attribute name='primary' className={classes.monospaced} />}
</TableCell>
<TableCell>
<Field
className={classes.attributionWindowSelect}
component={Select}
name={`experiment.metricAssignments[${index}].attributionWindowSeconds`}
labelId={`experiment.metricAssignments[${index}].attributionWindowSeconds`}
size='small'
variant='outlined'
autoWidth
displayEmpty
error={!!attributionWindowError}
SelectDisplayProps={{
'aria-label': 'Attribution Window',
}}
>
<MenuItem value=''>-</MenuItem>
{Object.entries(AttributionWindowSecondsToHuman).map(
([attributionWindowSeconds, attributionWindowSecondsHuman]) => (
<MenuItem value={attributionWindowSeconds} key={attributionWindowSeconds}>
{attributionWindowSecondsHuman}
</MenuItem>
),
)}
</Field>
{_.isString(attributionWindowError) && (
<FormHelperText error>{attributionWindowError}</FormHelperText>
)}
</TableCell>
<TableCell className={classes.changeExpected}>
<Field
component={Switch}
name={`experiment.metricAssignments[${index}].changeExpected`}
id={`experiment.metricAssignments[${index}].changeExpected`}
type='checkbox'
aria-label='Change Expected'
variant='outlined'
/>
</TableCell>
<TableCell>
<MetricDifferenceField
className={classes.minDifferenceField}
name={`experiment.metricAssignments[${index}].minDifference`}
id={`experiment.metricAssignments[${index}].minDifference`}
metricParameterType={indexedMetrics[metricAssignment.metricId].parameterType}
/>
</TableCell>
<TableCell>
<MoreMenu>
<MenuItem onClick={onMakePrimary}>Set as Primary</MenuItem>
<MenuItem onClick={onRemoveMetricAssignment}>Remove</MenuItem>
</MoreMenu>
</TableCell>
</TableRow>
)
})}
{metricAssignmentsField.value.length === 0 && (
<TableRow>
<TableCell colSpan={5}>
<Typography variant='body1' align='center'>
You don't have any metric assignments yet.
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<div className={metricEditorClasses.addMetric}>
<Add className={metricEditorClasses.addMetricAddSymbol} />
<FormControl className={classes.addMetricSelect}>
<MetricAutocomplete
id='add-metric-select'
value={selectedMetric}
onChange={onChangeSelectedMetricOption}
options={Object.values(indexedMetrics)}
error={metricAssignmentsError}
fullWidth
/>
</FormControl>
<Button variant='contained' disableElevation size='small' onClick={onAddMetric} aria-label='Add metric'>
Assign
</Button>
</div>
</>
)
}}
/>
<Alert severity='info' className={classes.metricsInfo}>
<Link
underline='always'
href="https://github.com/Automattic/experimentation-platform/wiki/Experimenter's-Guide#how-do-i-choose-a-primary-metric"
target='_blank'
>
How do I choose a Primary Metric?
</Link>
<Link
underline='always'
href="https://github.com/Automattic/experimentation-platform/wiki/Experimenter's-Guide#what-does-change-expected-mean-for-a-metric"
target='_blank'
>
What is Change Expected?
</Link>
</Alert>
<CollapsibleAlert
id='attr-window-panel'
severity='info'
className={classes.attributionWindowInfo}
summary={'What is an Attribution Window?'}
>
<Link
underline='always'
href="https://github.com/Automattic/experimentation-platform/wiki/Experimenter's-Guide#what-is-an-attribution-window-for-a-metric"
target='_blank'
>
An Attribution Window
</Link>{' '}
is the window of time after exposure to an experiment that we capture metric events for a participant (exposure
can be from either assignment or specified exposure events). The refund window is the window of time after a
purchase event. Revenue metrics will automatically deduct transactions that have been refunded within the
metric’s refund window.
<br />
<div className={classes.attributionWindowDiagram}>
<AttributionWindowDiagram />
<RefundWindowDiagram />
</div>
</CollapsibleAlert>
<CollapsibleAlert
id='min-diff-panel'
severity='info'
className={classes.minDiffInfo}
summary={'How do I choose a Minimum Difference?'}
>
<Link
underline='always'
href="https://github.com/Automattic/experimentation-platform/wiki/Experimenter's-Guide#how-do-i-choose-a-minimum-difference-practically-equivalent-value-for-my-metrics"
target='_blank'
>
Minimum Practical Difference values
</Link>{' '}
are absolute differences from the baseline (not relative). For example, if the baseline conversion rate is 5%, a
minimum difference of 0.5 pp is equivalent to a 10% relative change.
<br />
<div className={classes.minDiffDiagram}>
<MinDiffDiagram />
</div>
</CollapsibleAlert>
<Alert severity='info' className={classes.requestMetricInfo}>
<Link underline='always' href='https://betterexperiments.wordpress.com/?start=metric-request' target='_blank'>
{"Can't find a metric? Request one!"}
</Link>
</Alert>
<Typography variant='h4' className={classes.exposureEventsTitle}>
Exposure Events
</Typography>
<FieldArray
name='experiment.exposureEvents'
render={(arrayHelpers) => {
const onAddExposureEvent = () => {
arrayHelpers.push({
event: '',
props: [],
})
}
return (
<>
<TableContainer>
<Table>
<TableBody>
{exposureEventsField.value.map((exposureEvent, index) => (
<EventEditor
key={index}
{...{ arrayHelpers, index, classes, completionBag, exposureEvent }}
onRemoveExposureEvent={() => arrayHelpers.remove(index)}
/>
))}
{exposureEventsField.value.length === 0 && (
<TableRow>
<TableCell colSpan={1}>
<Typography variant='body1' align='center'>
You don't have any exposure events.
{}
<br />
{}
We strongly suggest considering adding one to improve the accuracy of your metrics.
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<div className={metricEditorClasses.addMetric}>
<Add className={metricEditorClasses.addMetricAddSymbol} />
<Button
variant='contained'
disableElevation
size='small'
onClick={onAddExposureEvent}
aria-label='Add exposure event'
>
Add Event
</Button>
</div>
</>
)
}}
/>
<Alert severity='info' className={classes.exposureEventsInfo}>
<Link
underline='always'
href="https://github.com/Automattic/experimentation-platform/wiki/Experimenter's-Guide#what-is-an-exposure-event-and-when-do-i-need-it"
target='_blank'
>
What is an Exposure Event? And when do I need it?
</Link>
<br />
<span>Only validated events can be used as exposure events.</span>
</Alert>
<Alert severity='info' className={classes.multipleExposureEventsInfo}>
If you have multiple exposure events, then participants will be considered exposed if they trigger{' '}
<strong>any</strong> of the exposure events.
</Alert>
</div>
)
}
Example #10
Source File: TestDetailsModal.tsx From frontend with Apache License 2.0 | 4 votes |
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 |
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 |
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 |
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 |
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>
);
}