@material-ui/core#FormLabel TypeScript Examples
The following examples show how to use
@material-ui/core#FormLabel.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: Label.tsx From firetable with Apache License 2.0 | 6 votes |
export default function Label({
label,
children,
hint,
...props
}: ILabelProps) {
const classes = useStyles();
return (
<FormLabel className={classes.root} {...props}>
{label || children}
{hint && (
<Tooltip title={hint}>
<IconButton aria-label="delete">
<HelpIcon />
</IconButton>
</Tooltip>
)}
</FormLabel>
);
}
Example #2
Source File: Settings.tsx From backstage with Apache License 2.0 | 6 votes |
Settings = () => {
const { type, handleChangeType } = useRandomJoke();
const JOKE_TYPES: JokeType[] = ['any' as JokeType, 'programming' as JokeType];
return (
<FormControl component="fieldset">
<FormLabel component="legend">Joke Type</FormLabel>
<RadioGroup
aria-label="joke type"
value={type}
onChange={e => handleChangeType(e.target.value)}
>
{JOKE_TYPES.map(t => (
<FormControlLabel
key={t}
value={t}
control={<Radio />}
label={upperFirst(t)}
/>
))}
</RadioGroup>
</FormControl>
);
}
Example #3
Source File: LabeledRadio.tsx From UsTaxes with GNU Affero General Public License v3.0 | 6 votes |
export function LabeledRadio<A>(props: LabeledRadioProps<A>): ReactElement {
const { label, name, values, useGrid = true, sizes = { xs: 12 } } = props
const classes = useStyles()
const { control } = useFormContext<A>()
return (
<ConditionallyWrap
condition={useGrid}
wrapper={(children) => (
<Grid item {...sizes}>
{children}
</Grid>
)}
>
<Controller
name={name}
render={({ field: { value, onChange } }) => (
<div className={classes.root}>
<FormControl component="fieldset">
<FormLabel>{label}</FormLabel>
<RadioGroup name={name} value={value} onChange={onChange}>
{values.map(([rowLabel, rowValue], i) => (
<FormControlLabel
key={i}
value={rowValue}
control={<Radio color="primary" />}
label={rowLabel}
/>
))}
</RadioGroup>
</FormControl>
</div>
)}
control={control}
/>
</ConditionallyWrap>
)
}
Example #4
Source File: CreateRoom.tsx From cards-against-formality-pwa with BSD 2-Clause "Simplified" License | 5 votes |
function DeckSelector({ decks, onChange }: { decks: any[], onChange: (decks: string[]) => void }) {
const [deckOptions, setDeckOptions] = useState<{ name: string; _id: string, value?: boolean }[]>([]);
const [isExpanded, setIsExpanded] = useState(false);
const [isAllSelected, setIsAllSelected] = useState(false);
const toggleSelectAll = useCallback(() => {
setDeckOptions(prevDeck => {
prevDeck.forEach(deck => deck.value = !isAllSelected);
return [...prevDeck];
});
setIsAllSelected(!isAllSelected);
}, [isAllSelected])
useEffect(() => {
if (decks) {
setDeckOptions(decks.map(deck => {
return { value: deck.name.includes('Base'), ...deck }
}));
}
}, [decks]);
useEffect(() => {
onChange(deckOptions.filter(deck => deck.value).map(deck => deck._id));
}, [deckOptions, onChange]);
function _onChange(e: React.ChangeEvent<HTMLInputElement>) {
setDeckOptions(prevDeck => {
const deck = prevDeck.find(deck => deck._id === e.target.name);
if (deck) {
deck.value = e.target.checked;
}
return [...prevDeck];
});
}
if (!decks?.length) {
return null;
}
return <ExpansionPanel expanded={isExpanded} onChange={() => { setIsExpanded(prev => !prev) }}>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1bh-content"
id="panel1bh-header"
>
<Typography>Available Decks!</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<FormControl required component="fieldset" error={!deckOptions.some(deck => deck.value)}>
<FormControlLabel
control={<Checkbox checked={isAllSelected} onChange={toggleSelectAll} name="Select all" />}
label="Select all"
/>
<Divider />
<FormLabel component="legend">Select which decks you would like to play with</FormLabel>
<FormGroup className="deck-checkbox-group">
{deckOptions.map(deck => {
return <FormControlLabel
key={deck._id}
control={<Checkbox checked={deck.value} onChange={_onChange} name={deck._id} />}
label={deck.name}
/>
})}
</FormGroup>
<FormHelperText>You must select at least one</FormHelperText>
</FormControl>
</ExpansionPanelDetails>
</ExpansionPanel>
}
Example #5
Source File: SQFormRadioButtonGroup.tsx From SQForm with MIT License | 5 votes |
function SQFormRadioButtonGroup({
name,
onChange,
shouldDisplayInRow = false,
size = 'auto',
groupLabel,
children,
}: SQFormRadioButtonGroupProps): React.ReactElement {
const {
fieldState: {isFieldError, isFieldRequired},
formikField: {field},
fieldHelpers: {handleChange, handleBlur, HelperTextComponent},
} = useForm<
RadioButtonInputItemProps['value'],
React.ChangeEvent<HTMLInputElement>
>({
name,
onChange,
});
const childrenToRadioGroupItems = () => {
return children.map((radioOption) => {
const {label, value, isDisabled, InputProps} = radioOption;
return (
<SQFormRadioButtonGroupItem
label={label}
value={value}
isDisabled={isDisabled}
isRowDisplay={shouldDisplayInRow}
InputProps={InputProps}
key={`SQFormRadioButtonGroupItem_${value}`}
/>
);
});
};
return (
<Grid item sm={size}>
<FormControl
component="fieldset"
required={isFieldRequired}
error={isFieldError}
onBlur={handleBlur}
>
<FormLabel
component="legend"
classes={{
root: 'MuiInputLabel-root',
asterisk: 'MuiInputLabel-asterisk',
}}
>
{groupLabel}
</FormLabel>
<RadioGroup
value={field.value}
row={shouldDisplayInRow}
aria-label={`SQFormRadioButtonGroup_${name}`}
name={name}
onChange={handleChange}
>
{childrenToRadioGroupItems()}
</RadioGroup>
<FormHelperText>{HelperTextComponent}</FormHelperText>
</FormControl>
</Grid>
);
}
Example #6
Source File: VersioningStrategy.tsx From backstage with Apache License 2.0 | 5 votes |
export function VersioningStrategy() {
const navigate = useNavigate();
const { project } = useProjectContext();
const { getParsedQuery, getQueryParamsWithUpdates } = useQueryHandler();
useEffect(() => {
const { parsedQuery } = getParsedQuery();
if (!parsedQuery.versioningStrategy && !project.isProvidedViaProps) {
const { queryParams } = getQueryParamsWithUpdates({
updates: [
{ key: 'versioningStrategy', value: project.versioningStrategy },
],
});
navigate(`?${queryParams}`, { replace: true });
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<FormControl
component="fieldset"
required
disabled={project.isProvidedViaProps}
>
<FormLabel component="legend">Versioning strategy</FormLabel>
<RadioGroup
data-testid={TEST_IDS.form.versioningStrategy.radioGroup}
aria-label="calendar-strategy"
name="calendar-strategy"
value={project.versioningStrategy}
onChange={event => {
const { queryParams } = getQueryParamsWithUpdates({
updates: [{ key: 'versioningStrategy', value: event.target.value }],
});
navigate(`?${queryParams}`, { replace: true });
}}
>
<FormControlLabel
value={VERSIONING_STRATEGIES.semver}
control={<Radio />}
label="Semantic versioning"
/>
<FormControlLabel
value={VERSIONING_STRATEGIES.calver}
control={<Radio />}
label="Calendar versioning"
/>
</RadioGroup>
</FormControl>
);
}
Example #7
Source File: SearchFilter.tsx From backstage with Apache License 2.0 | 5 votes |
CheckboxFilter = (props: SearchFilterComponentProps) => {
const {
className,
defaultValue,
label,
name,
values: givenValues = [],
valuesDebounceMs,
} = props;
const classes = useStyles();
const { filters, setFilters } = useSearch();
useDefaultFilterValue(name, defaultValue);
const asyncValues =
typeof givenValues === 'function' ? givenValues : undefined;
const defaultValues =
typeof givenValues === 'function' ? undefined : givenValues;
const { value: values = [], loading } = useAsyncFilterValues(
asyncValues,
'',
defaultValues,
valuesDebounceMs,
);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const {
target: { value, checked },
} = e;
setFilters(prevFilters => {
const { [name]: filter, ...others } = prevFilters;
const rest = ((filter as string[]) || []).filter(i => i !== value);
const items = checked ? [...rest, value] : rest;
return items.length ? { ...others, [name]: items } : others;
});
};
return (
<FormControl
className={className}
disabled={loading}
fullWidth
data-testid="search-checkboxfilter-next"
>
{label ? <FormLabel className={classes.label}>{label}</FormLabel> : null}
{values.map((value: string) => (
<FormControlLabel
key={value}
control={
<Checkbox
color="primary"
tabIndex={-1}
inputProps={{ 'aria-labelledby': value }}
value={value}
name={value}
onChange={handleChange}
checked={((filters[name] as string[]) ?? []).includes(value)}
/>
}
label={value}
/>
))}
</FormControl>
);
}
Example #8
Source File: RadioInput.tsx From glific-frontend with GNU Affero General Public License v3.0 | 5 votes |
RadioInput: React.SFC<RadioInputProps> = ({
labelYes = 'Yes',
labelNo = 'No',
row = true,
field,
form: { touched, errors, setFieldValue, values },
radioTitle,
handleChange,
}) => {
const selectedValue = values[field.name];
const isChecked = (value: any) => selectedValue === value;
const handleRadioChange = (value: boolean) => {
setFieldValue(field.name, value);
if (handleChange) {
handleChange(value);
}
};
let radioGroupLabel: any;
if (radioTitle) {
radioGroupLabel = <FormLabel component="legend">{radioTitle}</FormLabel>;
}
return (
<FormControl component="fieldset">
{radioGroupLabel}
<RadioGroup row={row} name="radio-buttons">
<FormControlLabel
value={1}
control={
<Radio
color="primary"
onClick={() => handleRadioChange(true)}
checked={isChecked(true)}
/>
}
label={labelYes}
className={styles.Label}
/>
<FormControlLabel
value={0}
control={
<Radio
color="primary"
onClick={() => handleRadioChange(false)}
checked={isChecked(false)}
/>
}
label={labelNo}
className={styles.Label}
/>
</RadioGroup>
{errors[field.name] && touched[field.name] ? (
<FormHelperText className={styles.DangerText}>{errors[field.name]}</FormHelperText>
) : null}
</FormControl>
);
}
Example #9
Source File: Content.tsx From Demae with MIT License | 4 votes |
Form = ({ provider }: { provider: ProviderDraft }) => {
const classes = useStyles()
const [setProcessing] = useProcessing()
const [setMessage] = useSnackbar()
const [thumbnail, setThumbnail] = useState<File | undefined>()
const [cover, setCover] = useState<File | undefined>()
const [name] = useTextField(provider.name)
const [caption] = useTextField(provider.caption)
const [description] = useTextField(provider.description)
const providerCapabilities = provider.capabilities || []
const [capabilities, setCapabilities] = useState<{ [key in Capability]: boolean }>({
"download": providerCapabilities.includes("download"),
"instore": providerCapabilities.includes("instore"),
"online": providerCapabilities.includes("online"),
"pickup": providerCapabilities.includes("pickup")
})
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setCapabilities({ ...capabilities, [event.target.name]: event.target.checked });
};
const capabilitiesError = Object.values(capabilities).filter((v) => v).length < 1;
const uploadThumbnail = (file: File): Promise<StorageFile | undefined> => {
const ref = firebase.storage().ref(provider.documentReference.path + "/thumbnail.jpg")
return new Promise((resolve, reject) => {
ref.put(file).then(async (snapshot) => {
if (snapshot.state === "success") {
const storageFile = new StorageFile()
if (snapshot.metadata.contentType) {
storageFile.mimeType = snapshot.metadata.contentType
}
storageFile.path = ref.fullPath
resolve(storageFile)
} else {
reject(undefined)
}
})
})
}
const uploadCover = (file: File): Promise<StorageFile | undefined> => {
const ref = firebase.storage().ref(provider.documentReference.path + "/cover.jpg")
return new Promise((resolve, reject) => {
ref.put(file).then(async (snapshot) => {
if (snapshot.state === "success") {
const storageFile = new StorageFile()
if (snapshot.metadata.contentType) {
storageFile.mimeType = snapshot.metadata.contentType
}
storageFile.path = ref.fullPath
resolve(storageFile)
} else {
reject(undefined)
}
})
})
}
const [isEditing, setEdit] = useEdit(async (event) => {
event.preventDefault()
if (!provider) return
setProcessing(true)
if (thumbnail) {
const thumbnailImage = await uploadThumbnail(thumbnail)
if (thumbnailImage) {
provider.thumbnailImage = thumbnailImage
}
}
if (cover) {
const coverImage = await uploadCover(cover)
if (coverImage) {
provider.coverImage = coverImage
}
}
try {
provider.name = name.value as string
provider.caption = caption.value as string
provider.description = description.value as string
provider.capabilities = Object.keys(capabilities).filter(value => capabilities[value]) as Capability[]
await provider.save()
} catch (error) {
console.log(error)
}
setProcessing(false)
setMessage("success", "Change your provider informations.")
setEdit(false)
})
useContentToolbar(() => {
if (!provider) return <></>
if (isEditing) {
return (
<Box display="flex" flexGrow={1} justifyContent="space-between" paddingX={1}>
<Button variant="outlined" color="primary" size="small" onClick={() => setEdit(false)}>Cancel</Button>
<Button variant="contained" color="primary" size="small" type="submit" disabled={capabilitiesError}
>Save</Button>
</Box>
)
}
return (
<Box display="flex" flexGrow={1} justifyContent="space-between" paddingX={1}>
<Box display="flex" flexGrow={1} justifyContent="flex-end">
<Button variant="outlined" color="primary" size="small" onClick={() => setEdit(true)}>Edit</Button>
</Box>
</Box>
)
})
if (isEditing) {
return (
<Container maxWidth="sm">
<Box padding={2}>
<Typography variant="h1" gutterBottom>Shop</Typography>
<Paper>
<Box display="flex" position="relative" flexGrow={1}>
<Box display="flex" flexGrow={1} height={300}>
<DndCard
url={provider?.coverImageURL()}
onDrop={(files) => {
const file = files[0] as File
setCover(file)
}} />
</Box>
<Box display="flex" position="absolute" zIndex={1050} flexGrow={1} width={120} height={120} border={2} borderColor="white" borderRadius="50%" bottom={-16} left={16} style={{ overflow: "hidden" }}>
<DndCard
url={provider?.thumbnailImageURL()}
onDrop={(files) => {
const file = files[0] as File
setThumbnail(file)
}} />
</Box>
</Box>
<Box padding={2} paddingTop={5}>
<Box paddingBottom={2}>
<Typography variant="subtitle1" gutterBottom>Name</Typography>
<TextField variant="outlined" margin="dense" required {...name} fullWidth />
</Box>
<Box paddingBottom={2}>
<Typography variant="subtitle1" gutterBottom>Caption</Typography>
<TextField variant="outlined" margin="dense" required {...caption} fullWidth />
</Box>
<Box paddingBottom={2}>
<Typography variant="subtitle1" gutterBottom>Description</Typography>
<TextField variant="outlined" margin="dense" required fullWidth multiline rows={8} {...description} />
</Box>
</Box>
<Box paddingX={2} paddingBottom={1}>
<Typography variant="subtitle1" gutterBottom>Sales method</Typography>
<FormControl required error={capabilitiesError} component="fieldset">
<FormLabel component="legend">Please select at least one selling method</FormLabel>
<FormGroup>
<FormControlLabel
control={<Checkbox checked={capabilities.download} onChange={handleChange} name="download" />}
label="Download"
/>
<FormControlLabel
control={<Checkbox checked={capabilities.instore} onChange={handleChange} name="instore" />}
label="In-Store Sales"
/>
<FormControlLabel
control={<Checkbox checked={capabilities.online} onChange={handleChange} name="online" />}
label="Online Sales"
/>
<FormControlLabel
control={<Checkbox checked={capabilities.pickup} onChange={handleChange} name="pickup" />}
label="Pickup"
/>
</FormGroup>
<FormHelperText>You can choose multiple sales methods.</FormHelperText>
</FormControl>
</Box>
</Paper>
<Box padding={1}>
<Typography variant="body2" color="textSecondary" gutterBottom>ID: {provider.id}</Typography>
</Box>
</Box>
</Container>
)
}
return (
<Container maxWidth="sm">
<Box padding={2}>
<Typography variant="h1" gutterBottom>Shop</Typography>
<Paper>
<Box display="flex" position="relative" flexGrow={1}>
<Box display="flex" flexGrow={1} height={300}>
<Avatar variant="square" src={provider.coverImageURL()} style={{
minHeight: "300px",
width: "100%"
}}>
<ImageIcon />
</Avatar>
</Box>
<Box display="flex" position="absolute" zIndex={1050} flexGrow={1} width={120} height={120} border={2} borderColor="white" borderRadius="50%" bottom={-16} left={16} style={{ overflow: "hidden" }}>
<Avatar variant="square" src={provider.thumbnailImageURL()} style={{
minHeight: "64px",
height: "100%",
width: "100%"
}}>
<ImageIcon />
</Avatar>
</Box>
</Box>
<Box padding={2} paddingTop={5}>
<Box paddingBottom={2}>
<Typography variant="subtitle1" gutterBottom>Name</Typography>
<Typography variant="body1" gutterBottom>{provider.name}</Typography>
</Box>
<Box paddingBottom={2}>
<Typography variant="subtitle1" gutterBottom>Caption</Typography>
<Typography variant="body1" gutterBottom>{provider.caption}</Typography>
</Box>
<Box paddingBottom={2}>
<Typography variant="subtitle1" gutterBottom>Description</Typography>
<Typography variant="body1" gutterBottom>{provider.description}</Typography>
</Box>
</Box>
<Box paddingX={2} paddingBottom={1}>
<Typography variant="subtitle1" gutterBottom>Sales method</Typography>
<FormControl disabled component="fieldset">
<FormGroup>
<FormControlLabel
control={<Checkbox checked={capabilities.download} onChange={handleChange} name="download" />}
label="Download"
/>
<FormControlLabel
control={<Checkbox checked={capabilities.instore} onChange={handleChange} name="instore" />}
label="In-Store Sales"
/>
<FormControlLabel
control={<Checkbox checked={capabilities.online} onChange={handleChange} name="online" />}
label="Online Sales"
/>
<FormControlLabel
control={<Checkbox checked={capabilities.pickup} onChange={handleChange} name="pickup" />}
label="Pickup"
/>
</FormGroup>
</FormControl>
</Box>
</Paper>
<Box padding={1}>
<Typography variant="body2" color="textSecondary" gutterBottom>ID: {provider.id}</Typography>
</Box>
</Box>
</Container>
)
}
Example #10
Source File: ConclusionsPanel.tsx From abacus with GNU General Public License v2.0 | 4 votes |
/**
* Renders the conclusion information of an experiment in a panel component.
*
* @param experiment - The experiment with the conclusion information.
* @param experimentReloadRef - A ref for reloading the experiment.
*/
function ConclusionsPanel({
className,
experiment,
experimentReloadRef,
}: {
className?: string
experiment: ExperimentFull
experimentReloadRef: React.MutableRefObject<() => void>
}): JSX.Element {
const classes = useStyles()
const deployedVariation = Experiments.getDeployedVariation(experiment)
const data = [
{ label: 'Reason the experiment ended', value: experiment.endReason },
{
label: 'Conclusion URL',
value: !!experiment.conclusionUrl && (
<a href={experiment.conclusionUrl} rel='noopener noreferrer' target='_blank'>
{experiment.conclusionUrl}
</a>
),
},
{ label: 'Deployed variation', value: deployedVariation?.name },
]
// Edit Modal
const { enqueueSnackbar } = useSnackbar()
const [isEditing, setIsEditing] = useState<boolean>(false)
const editInitialValues = {
endReason: experiment.endReason ?? '',
conclusionUrl: experiment.conclusionUrl ?? '',
deployedVariationId: String(experiment.deployedVariationId ?? ''),
}
const onEdit = () => setIsEditing(true)
const onCancelEdit = () => setIsEditing(false)
const onSubmitEdit = async ({ experiment: formValues }: { experiment: typeof editInitialValues }) => {
try {
await ExperimentsApi.patch(experiment.experimentId, {
endReason: formValues.endReason,
conclusionUrl: formValues.conclusionUrl === '' ? undefined : formValues.conclusionUrl,
deployedVariationId: formValues.deployedVariationId ? Number(formValues.deployedVariationId) : undefined,
})
enqueueSnackbar('Experiment Updated!', { variant: 'success' })
experimentReloadRef.current()
setIsEditing(false)
} catch (e) {
// istanbul ignore next; shouldn't happen
enqueueSnackbar(`Oops! Something went wrong while trying to update your experiment. ${serverErrorMessage(e)}`, {
variant: 'error',
})
}
}
return (
<Paper className={className}>
<Toolbar>
<Typography className={classes.title} color='textPrimary' variant='h3'>
Conclusions
</Typography>
<Button onClick={onEdit} variant='outlined' aria-label='Edit Conclusion'>
<Edit />
Edit
</Button>
</Toolbar>
<LabelValueTable data={data} />
<Dialog open={isEditing} fullWidth aria-labelledby='edit-experiment-conclusions-dialog-title'>
<DialogTitle id='edit-experiment-conclusions-dialog-title'>Edit Experiment: Conclusions</DialogTitle>
<Formik
initialValues={{ experiment: editInitialValues }}
validationSchema={yup.object({ experiment: yupPick(experimentFullSchema, Object.keys(editInitialValues)) })}
onSubmit={onSubmitEdit}
>
{(formikProps) => (
<form onSubmit={formikProps.handleSubmit}>
<DialogContent>
<div className={classes.row}>
<Field
component={TextField}
name='experiment.endReason'
id='experiment.endReason'
label='Reason the experiment ended'
placeholder='Completed successfully'
variant='outlined'
fullWidth
required
multiline
rows={2}
InputLabelProps={{
shrink: true,
}}
/>
</div>
<div className={classes.row}>
<Field
component={TextField}
id='experiment.conclusionUrl'
name='experiment.conclusionUrl'
placeholder='https://your-p2-post-here/#conclusion-comment'
label='Conclusion URL'
variant='outlined'
fullWidth
InputLabelProps={{
shrink: true,
}}
/>
</div>
<div className={classes.row}>
<FormControl component='fieldset'>
<FormLabel component='legend'>Deployed variation</FormLabel>
<Field component={FormikMuiRadioGroup} name='experiment.deployedVariationId'>
{experiment.variations.map((variation) => (
<FormControlLabel
key={variation.variationId}
value={String(variation.variationId)}
control={<Radio />}
label={variation.name}
/>
))}
</Field>
</FormControl>
</div>
</DialogContent>
<DialogActions>
<Button onClick={onCancelEdit} color='primary'>
Cancel
</Button>
<LoadingButtonContainer isLoading={formikProps.isSubmitting}>
<Button
type='submit'
variant='contained'
color='secondary'
disabled={formikProps.isSubmitting || !formikProps.isValid}
>
Save
</Button>
</LoadingButtonContainer>
</DialogActions>
</form>
)}
</Formik>
</Dialog>
</Paper>
)
}
Example #11
Source File: MetricAssignmentsPanel.tsx From abacus with GNU General Public License v2.0 | 4 votes |
/** * Renders the assigned metric information of an experiment in a panel component. * * @param experiment - The experiment with the metric assignment information. * @param experimentReloadRef - Trigger a reload of the experiment. * @param metrics - The metrics to look up (aka resolve) the metric IDs of the * experiment's metric assignments. */ function MetricAssignmentsPanel({ experiment, experimentReloadRef, metrics, }: { experiment: ExperimentFull experimentReloadRef: React.MutableRefObject<() => void> metrics: Metric[] }): JSX.Element { const classes = useStyles() const resolvedMetricAssignments = useMemo( () => resolveMetricAssignments(MetricAssignments.sort(experiment.metricAssignments), metrics), [experiment, metrics], ) // TODO: Normalize this higher up const indexedMetrics = indexMetrics(metrics) // Assign Metric Modal const { enqueueSnackbar } = useSnackbar() const canAssignMetric = experiment.status !== Status.Staging const [isAssigningMetric, setIsAssigningMetric] = useState<boolean>(false) const assignMetricInitialAssignMetric = { metricId: '', attributionWindowSeconds: '', changeExpected: false, isPrimary: false, minDifference: '', } const onAssignMetric = () => setIsAssigningMetric(true) const onCancelAssignMetric = () => { setIsAssigningMetric(false) } const onSubmitAssignMetric = async (formData: { metricAssignment: typeof assignMetricInitialAssignMetric }) => { try { await ExperimentsApi.assignMetric(experiment, formData.metricAssignment as unknown as MetricAssignmentNew) enqueueSnackbar('Metric Assigned Successfully!', { variant: 'success' }) experimentReloadRef.current() setIsAssigningMetric(false) } catch (e) /* istanbul ignore next; Shouldn't happen */ { console.error(e) enqueueSnackbar( `Oops! Something went wrong while trying to assign a metric to your experiment. ${serverErrorMessage(e)}`, { variant: 'error', }, ) } } return ( <Paper> <Toolbar> <Typography className={classes.title} color='textPrimary' variant='h3'> Metrics </Typography> <Tooltip title={canAssignMetric ? '' : 'Use "Edit in Wizard" for staging experiments.'}> <div> <Button onClick={onAssignMetric} variant='outlined' disabled={!canAssignMetric}> <Add /> Assign Metric </Button> </div> </Tooltip> </Toolbar> <Table className={classes.metricsTable}> <TableHead> <TableRow> <TableCell component='th' variant='head'> Name </TableCell> <TableCell component='th' variant='head' className={classes.smallColumn}> Attribution Window </TableCell> <TableCell component='th' variant='head' className={classes.smallColumn}> Changes Expected </TableCell> <TableCell component='th' variant='head' className={classes.smallColumn}> Minimum Difference </TableCell> </TableRow> </TableHead> <TableBody> {resolvedMetricAssignments.map((resolvedMetricAssignment) => ( <TableRow key={resolvedMetricAssignment.metricAssignmentId}> <TableCell> <Tooltip title={resolvedMetricAssignment.metric.name}> <strong className={clsx(classes.monospace, classes.metricName)}> {resolvedMetricAssignment.metric.name} </strong> </Tooltip> <br /> <small className={classes.monospace}>{resolvedMetricAssignment.metric.description}</small> <br /> {resolvedMetricAssignment.isPrimary && <Attribute name='primary' />} </TableCell> <TableCell className={classes.monospace}> {AttributionWindowSecondsToHuman[resolvedMetricAssignment.attributionWindowSeconds]} </TableCell> <TableCell className={classes.monospace}> {formatBoolean(resolvedMetricAssignment.changeExpected)} </TableCell> <TableCell className={classes.monospace}> <MetricValue value={resolvedMetricAssignment.minDifference} metricParameterType={resolvedMetricAssignment.metric.parameterType} isDifference={true} /> </TableCell> </TableRow> ))} </TableBody> </Table> <Dialog open={isAssigningMetric} aria-labelledby='assign-metric-form-dialog-title'> <DialogTitle id='assign-metric-form-dialog-title'>Assign Metric</DialogTitle> <Formik initialValues={{ metricAssignment: assignMetricInitialAssignMetric }} onSubmit={onSubmitAssignMetric} validationSchema={yup.object({ metricAssignment: metricAssignmentNewSchema })} > {(formikProps) => { const metricAssignmentsError = formikProps.touched.metricAssignment?.metricId && formikProps.errors.metricAssignment?.metricId const onMetricChange = (_event: unknown, metric: Metric | null) => formikProps.setFieldValue('metricAssignment.metricId', metric?.metricId) return ( <form onSubmit={formikProps.handleSubmit} noValidate> <DialogContent> <div className={classes.row}> <FormControl component='fieldset' fullWidth> <FormLabel required className={classes.label} htmlFor={`metricAssignment.metricId`}> Metric </FormLabel> <MetricAutocomplete id={`metricAssignment.metricId`} value={indexedMetrics[Number(formikProps.values.metricAssignment.metricId)] ?? null} onChange={onMetricChange} options={Object.values(indexedMetrics)} error={metricAssignmentsError} fullWidth /> {formikProps.errors.metricAssignment?.metricId && ( <FormHelperText error={true}> <ErrorMessage name={`metricAssignment.metricId`} /> </FormHelperText> )} </FormControl> </div> <div className={classes.row}> <FormControl component='fieldset' fullWidth> <FormLabel required className={classes.label} id={`metricAssignment.attributionWindowSeconds-label`} > Attribution Window </FormLabel> <Field component={Select} name={`metricAssignment.attributionWindowSeconds`} labelId={`metricAssignment.attributionWindowSeconds-label`} id={`metricAssignment.attributionWindowSeconds`} variant='outlined' error={ // istanbul ignore next; trivial, not-critical, pain to test. !!formikProps.errors.metricAssignment?.attributionWindowSeconds && !!formikProps.touched.metricAssignment?.attributionWindowSeconds } displayEmpty > <MenuItem value=''>-</MenuItem> {Object.entries(AttributionWindowSecondsToHuman).map( ([attributionWindowSeconds, attributionWindowSecondsHuman]) => ( <MenuItem value={attributionWindowSeconds} key={attributionWindowSeconds}> {attributionWindowSecondsHuman} </MenuItem> ), )} </Field> {formikProps.errors.metricAssignment?.attributionWindowSeconds && ( <FormHelperText error={true}> <ErrorMessage name={`metricAssignment.attributionWindowSeconds`} /> </FormHelperText> )} </FormControl> </div> <div className={classes.row}> <FormControl component='fieldset' fullWidth> <FormLabel required className={classes.label}> Change Expected </FormLabel> <Field component={Switch} label='Change Expected' name={`metricAssignment.changeExpected`} id={`metricAssignment.changeExpected`} inputProps={{ 'aria-label': 'Change Expected', }} variant='outlined' type='checkbox' /> </FormControl> </div> <div className={classes.row}> <FormControl component='fieldset' fullWidth> <FormLabel required className={classes.label} id={`metricAssignment.minDifference-label`}> Minimum Difference </FormLabel> <MetricDifferenceField name={`metricAssignment.minDifference`} id={`metricAssignment.minDifference`} metricParameterType={ (formikProps.values.metricAssignment.metricId && indexedMetrics[formikProps.values.metricAssignment.metricId as unknown as number] .parameterType) || MetricParameterType.Conversion } /> </FormControl> </div> </DialogContent> <DialogActions> <Button onClick={onCancelAssignMetric} color='primary'> Cancel </Button> <LoadingButtonContainer isLoading={formikProps.isSubmitting}> <Button type='submit' variant='contained' color='secondary' disabled={formikProps.isSubmitting || !formikProps.isValid} > Assign </Button> </LoadingButtonContainer> </DialogActions> </form> ) }} </Formik> </Dialog> </Paper> ) }
Example #12
Source File: Audience.tsx From abacus with GNU General Public License v2.0 | 4 votes |
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 #13
Source File: MetricFormFields.tsx From abacus with GNU General Public License v2.0 | 4 votes |
MetricFormFields = ({ formikProps }: { formikProps: FormikProps<{ metric: MetricFormData }> }): JSX.Element => {
const classes = useStyles()
// Here we reset the params field after parameterType changes
useEffect(() => {
if (formikProps.values.metric.parameterType === MetricParameterType.Conversion) {
const eventParams = formikProps.values.metric.eventParams
formikProps.setValues({
...formikProps.values,
metric: {
...formikProps.values.metric,
revenueParams: undefined,
eventParams: eventParams ?? '[]',
},
})
} else {
const revenueParams = formikProps.values.metric.revenueParams
formikProps.setValues({
...formikProps.values,
metric: {
...formikProps.values.metric,
revenueParams: revenueParams ?? '{}',
eventParams: undefined,
},
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formikProps.values.metric.parameterType])
return (
<>
<div className={classes.row}>
<Field
component={TextField}
name='metric.name'
id='metric.name'
label='Metric name'
placeholder='metric_name'
helperText='Use snake_case.'
variant='outlined'
fullWidth
required
InputLabelProps={{
shrink: true,
}}
/>
</div>
<div className={classes.row}>
<Field
component={TextField}
name='metric.description'
id='metric.description'
label='Metric description'
placeholder='Put your Metric description here!'
variant='outlined'
fullWidth
required
multiline
rows={4}
InputLabelProps={{
shrink: true,
}}
/>
</div>
<div className={classes.row}>
<FormControlLabel
label='Higher is better'
control={
<Field
component={Switch}
name='metric.higherIsBetter'
id='metric.higherIsBetter'
label='Higher is better'
type='checkbox'
aria-label='Higher is better'
variant='outlined'
/>
}
/>
</div>
<div className={classes.row}>
<FormControl component='fieldset'>
<FormLabel required id='metric-form-radio-metric-type-label'>
Metric Type
</FormLabel>
<Field
component={RadioGroup}
name='metric.parameterType'
required
aria-labelledby='metric-form-radio-metric-type-label'
>
<FormControlLabel
value={MetricParameterType.Conversion}
label='Conversion'
aria-label='Conversion'
control={<Radio disabled={formikProps.isSubmitting} />}
disabled={formikProps.isSubmitting}
/>
<FormControlLabel
value={MetricParameterType.Revenue}
label='Revenue'
aria-label='Revenue'
control={<Radio disabled={formikProps.isSubmitting} />}
disabled={formikProps.isSubmitting}
/>
</Field>
</FormControl>
</div>
<div className={classes.row}>
{formikProps.values.metric.parameterType === MetricParameterType.Conversion && (
<Field
component={JsonTextField}
name='metric.eventParams'
id='metric.eventParams'
label='Event Parameters'
variant='outlined'
fullWidth
required
multiline
rows={8}
InputLabelProps={{
shrink: true,
}}
/>
)}
</div>
<div className={classes.row}>
{formikProps.values.metric.parameterType === MetricParameterType.Revenue && (
<Field
component={JsonTextField}
name='metric.revenueParams'
id='metric.revenueParams'
label='Revenue Parameters'
variant='outlined'
fullWidth
required
multiline
rows={8}
InputLabelProps={{
shrink: true,
}}
/>
)}
</div>
<DebugOutput label='Validation Errors' content={formikProps.errors} />
</>
)
}
Example #14
Source File: SQFormCheckboxGroup.tsx From SQForm with MIT License | 4 votes |
function SQFormCheckboxGroup({
name,
groupLabel,
onChange,
shouldDisplayInRow = false,
shouldUseSelectAll = false,
size = 'auto',
children,
}: SQFormCheckboxGroupProps): JSX.Element {
const {
fieldState: {isFieldError, isFieldRequired},
fieldHelpers: {handleChange, handleBlur, HelperTextComponent},
} = useForm<CheckboxOption['value'][], React.ChangeEvent<HTMLInputElement>>({
name,
onChange,
});
const {setFieldValue} = useFormikContext();
const handleSelectAllChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (!event.target.checked) {
setFieldValue(name, []);
return;
}
const enabledGroupValues = children.reduce(
(acc: string[], checkboxOption: CheckboxOption) => {
const {value, isDisabled} = checkboxOption;
if (!isDisabled) {
return [...acc, String(value)];
}
return acc;
},
[]
);
setFieldValue(name, enabledGroupValues);
};
const childrenToCheckboxGroupItems = () => {
const providedCheckboxItems = children.map((checkboxOption) => {
const {label, value, isDisabled, inputProps} = checkboxOption;
return (
<SQFormCheckboxGroupItem
groupName={name}
label={label}
value={value}
isRowDisplay={shouldDisplayInRow}
onChange={handleChange}
isDisabled={isDisabled}
inputProps={inputProps}
key={`SQFormCheckboxGroupItem_${value}`}
/>
);
});
if (shouldUseSelectAll) {
return [
<SQFormCheckbox
name={`${name}SelectAll`}
label="All"
onChange={handleSelectAllChange}
key={`${name}_selectAll`}
/>,
...providedCheckboxItems,
];
}
return providedCheckboxItems;
};
return (
<Grid item sm={size}>
<FormControl
component="fieldset"
required={isFieldRequired}
error={isFieldError}
onBlur={handleBlur}
>
<FormLabel
component="legend"
classes={{
root: 'MuiInputLabel-root',
asterisk: 'MuiInputLabel-asterisk',
}}
>
{groupLabel}
</FormLabel>
<FormGroup row={shouldDisplayInRow}>
{childrenToCheckboxGroupItems()}
</FormGroup>
<FormHelperText>{HelperTextComponent}</FormHelperText>
</FormControl>
</Grid>
);
}
Example #15
Source File: Organization.tsx From crossfeed with Creative Commons Zero v1.0 Universal | 4 votes |
Organization: React.FC = () => {
const {
apiGet,
apiPut,
apiPost,
user,
setFeedbackMessage
} = useAuthContext();
const { organizationId } = useParams<{ organizationId: string }>();
const [organization, setOrganization] = useState<OrganizationType>();
const [tags, setTags] = useState<AutocompleteType[]>([]);
const [userRoles, setUserRoles] = useState<Role[]>([]);
const [scanTasks, setScanTasks] = useState<ScanTask[]>([]);
const [scans, setScans] = useState<Scan[]>([]);
const [scanSchema, setScanSchema] = useState<ScanSchema>({});
const [newUserValues, setNewUserValues] = useState<{
firstName: string;
lastName: string;
email: string;
organization?: OrganizationType;
role: string;
}>({
firstName: '',
lastName: '',
email: '',
role: ''
});
const classes = useStyles();
const [tagValue, setTagValue] = React.useState<AutocompleteType | null>(null);
const [inputValue, setInputValue] = React.useState('');
const [dialog, setDialog] = React.useState<{
open: boolean;
type?: 'rootDomains' | 'ipBlocks' | 'tags';
label?: string;
}>({ open: false });
const dateAccessor = (date?: string) => {
return !date || new Date(date).getTime() === new Date(0).getTime()
? 'None'
: `${formatDistanceToNow(parseISO(date))} ago`;
};
const userRoleColumns: Column<Role>[] = [
{
Header: 'Name',
accessor: ({ user }) => user.fullName,
width: 200,
disableFilters: true,
id: 'name'
},
{
Header: 'Email',
accessor: ({ user }) => user.email,
width: 150,
minWidth: 150,
id: 'email',
disableFilters: true
},
{
Header: 'Role',
accessor: ({ approved, role, user }) => {
if (approved) {
if (user.invitePending) {
return 'Invite pending';
} else if (role === 'admin') {
return 'Administrator';
} else {
return 'Member';
}
}
return 'Pending approval';
},
width: 50,
minWidth: 50,
id: 'approved',
disableFilters: true
},
{
Header: () => {
return (
<div style={{ justifyContent: 'flex-center' }}>
<Button color="secondary" onClick={() => setDialog({ open: true })}>
<ControlPoint style={{ marginRight: '10px' }}></ControlPoint>
Add member
</Button>
</div>
);
},
id: 'action',
Cell: ({ row }: { row: { index: number } }) => {
const isApproved =
!organization?.userRoles[row.index] ||
organization?.userRoles[row.index].approved;
return (
<>
{isApproved ? (
<Button
onClick={() => {
removeUser(row.index);
}}
color="secondary"
>
<p>Remove</p>
</Button>
) : (
<Button
onClick={() => {
approveUser(row.index);
}}
color="secondary"
>
<p>Approve</p>
</Button>
)}
</>
);
},
disableFilters: true
}
];
const scanColumns: Column<Scan>[] = [
{
Header: 'Name',
accessor: 'name',
width: 150,
id: 'name',
disableFilters: true
},
{
Header: 'Description',
accessor: ({ name }) => scanSchema[name] && scanSchema[name].description,
width: 200,
minWidth: 200,
id: 'description',
disableFilters: true
},
{
Header: 'Mode',
accessor: ({ name }) =>
scanSchema[name] && scanSchema[name].isPassive ? 'Passive' : 'Active',
width: 150,
minWidth: 150,
id: 'mode',
disableFilters: true
},
{
Header: 'Action',
id: 'action',
maxWidth: 100,
Cell: ({ row }: { row: { index: number } }) => {
if (!organization) return;
const enabled = organization.granularScans.find(
(scan) => scan.id === scans[row.index].id
);
return (
<Button
type="button"
onClick={() => {
updateScan(scans[row.index], !enabled);
}}
>
{enabled ? 'Disable' : 'Enable'}
</Button>
);
},
disableFilters: true
}
];
const scanTaskColumns: Column<ScanTask>[] = [
{
Header: 'ID',
accessor: 'id',
disableFilters: true
},
{
Header: 'Status',
accessor: 'status',
disableFilters: true
},
{
Header: 'Type',
accessor: 'type',
disableFilters: true
},
{
Header: 'Name',
accessor: ({ scan }) => scan?.name,
disableFilters: true
},
{
Header: 'Created At',
accessor: ({ createdAt }) => dateAccessor(createdAt),
disableFilters: true,
disableSortBy: true
},
{
Header: 'Requested At',
accessor: ({ requestedAt }) => dateAccessor(requestedAt),
disableFilters: true,
disableSortBy: true
},
{
Header: 'Started At',
accessor: ({ startedAt }) => dateAccessor(startedAt),
disableFilters: true,
disableSortBy: true
},
{
Header: 'Finished At',
accessor: ({ finishedAt }) => dateAccessor(finishedAt),
disableFilters: true,
disableSortBy: true
},
{
Header: 'Output',
accessor: 'output',
disableFilters: true
}
];
const fetchOrganization = useCallback(async () => {
try {
const organization = await apiGet<OrganizationType>(
`/organizations/${organizationId}`
);
organization.scanTasks.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
setOrganization(organization);
setUserRoles(organization.userRoles);
setScanTasks(organization.scanTasks);
const tags = await apiGet<OrganizationTag[]>(`/organizations/tags`);
setTags(tags);
} catch (e) {
console.error(e);
}
}, [apiGet, setOrganization, organizationId]);
const fetchScans = useCallback(async () => {
try {
const response = await apiGet<{
scans: Scan[];
schema: ScanSchema;
}>('/granularScans/');
let { scans } = response;
const { schema } = response;
if (user?.userType !== 'globalAdmin')
scans = scans.filter(
(scan) =>
scan.name !== 'censysIpv4' && scan.name !== 'censysCertificates'
);
setScans(scans);
setScanSchema(schema);
} catch (e) {
console.error(e);
}
}, [apiGet, user]);
const approveUser = async (user: number) => {
try {
await apiPost(
`/organizations/${organization?.id}/roles/${organization?.userRoles[user].id}/approve`,
{ body: {} }
);
const copy = userRoles.map((role, id) =>
id === user ? { ...role, approved: true } : role
);
setUserRoles(copy);
} catch (e) {
console.error(e);
}
};
const removeUser = async (user: number) => {
try {
await apiPost(
`/organizations/${organization?.id}/roles/${userRoles[user].id}/remove`,
{ body: {} }
);
const copy = userRoles.filter((_, ind) => ind !== user);
setUserRoles(copy);
} catch (e) {
console.error(e);
}
};
const updateOrganization = async (body: any) => {
try {
const org = await apiPut('/organizations/' + organization?.id, {
body: organization
});
setOrganization(org);
setFeedbackMessage({
message: 'Organization successfully updated',
type: 'success'
});
} catch (e) {
setFeedbackMessage({
message:
e.status === 422
? 'Error updating organization'
: e.message ?? e.toString(),
type: 'error'
});
console.error(e);
}
};
const updateScan = async (scan: Scan, enabled: boolean) => {
try {
if (!organization) return;
await apiPost(
`/organizations/${organization?.id}/granularScans/${scan.id}/update`,
{
body: {
enabled
}
}
);
setOrganization({
...organization,
granularScans: enabled
? organization.granularScans.concat([scan])
: organization.granularScans.filter(
(granularScan) => granularScan.id !== scan.id
)
});
} catch (e) {
setFeedbackMessage({
message:
e.status === 422 ? 'Error updating scan' : e.message ?? e.toString(),
type: 'error'
});
console.error(e);
}
};
useEffect(() => {
fetchOrganization();
}, [fetchOrganization]);
const onInviteUserSubmit = async () => {
try {
const body = {
firstName: newUserValues.firstName,
lastName: newUserValues.lastName,
email: newUserValues.email,
organization: organization?.id,
organizationAdmin: newUserValues.role === 'admin'
};
const user: User = await apiPost('/users/', {
body
});
const newRole = user.roles[user.roles.length - 1];
newRole.user = user;
if (userRoles.find((role) => role.user.id === user.id)) {
setUserRoles(
userRoles.map((role) => (role.user.id === user.id ? newRole : role))
);
} else {
setUserRoles(userRoles.concat([newRole]));
}
} catch (e) {
setFeedbackMessage({
message:
e.status === 422 ? 'Error inviting user' : e.message ?? e.toString(),
type: 'error'
});
console.log(e);
}
};
const onInviteUserTextChange: React.ChangeEventHandler<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
> = (e) => onInviteUserChange(e.target.name, e.target.value);
const onInviteUserChange = (name: string, value: any) => {
setNewUserValues((values) => ({
...values,
[name]: value
}));
};
const filter = createFilterOptions<AutocompleteType>();
const ListInput = (props: {
type: 'rootDomains' | 'ipBlocks' | 'tags';
label: string;
}) => {
if (!organization) return null;
const elements: (string | OrganizationTag)[] = organization[props.type];
return (
<div className={classes.headerRow}>
<label>{props.label}</label>
<span>
{elements &&
elements.map((value: string | OrganizationTag, index: number) => (
<Chip
className={classes.chip}
key={index}
label={typeof value === 'string' ? value : value.name}
onDelete={() => {
organization[props.type].splice(index, 1);
setOrganization({ ...organization });
}}
></Chip>
))}
<Chip
label="ADD"
variant="outlined"
color="secondary"
onClick={() => {
setDialog({
open: true,
type: props.type,
label: props.label
});
}}
/>
</span>
</div>
);
};
if (!organization) return null;
const views = [
<Paper className={classes.settingsWrapper} key={0}>
<Dialog
open={dialog.open}
onClose={() => setDialog({ open: false })}
aria-labelledby="form-dialog-title"
maxWidth="xs"
fullWidth
>
<DialogTitle id="form-dialog-title">
Add {dialog.label && dialog.label.slice(0, -1)}
</DialogTitle>
<DialogContent>
{dialog.type === 'tags' ? (
<>
<DialogContentText>
Select an existing tag or add a new one.
</DialogContentText>
<Autocomplete
value={tagValue}
onChange={(event, newValue) => {
if (typeof newValue === 'string') {
setTagValue({
name: newValue
});
} else {
setTagValue(newValue);
}
}}
filterOptions={(options, params) => {
const filtered = filter(options, params);
// Suggest the creation of a new value
if (
params.inputValue !== '' &&
!filtered.find(
(tag) =>
tag.name?.toLowerCase() ===
params.inputValue.toLowerCase()
)
) {
filtered.push({
name: params.inputValue,
title: `Add "${params.inputValue}"`
});
}
return filtered;
}}
selectOnFocus
clearOnBlur
handleHomeEndKeys
options={tags}
getOptionLabel={(option) => {
return option.name ?? '';
}}
renderOption={(option) => {
if (option.title) return option.title;
return option.name ?? '';
}}
fullWidth
freeSolo
renderInput={(params) => (
<TextField {...params} variant="outlined" />
)}
/>
</>
) : (
<TextField
autoFocus
margin="dense"
id="name"
label={dialog.label && dialog.label.slice(0, -1)}
type="text"
fullWidth
onChange={(e) => setInputValue(e.target.value)}
/>
)}
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={() => setDialog({ open: false })}>
Cancel
</Button>
<Button
variant="contained"
color="primary"
onClick={() => {
if (dialog.type && dialog.type !== 'tags') {
if (inputValue) {
organization[dialog.type].push(inputValue);
setOrganization({ ...organization });
}
} else {
if (tagValue) {
if (!organization.tags) organization.tags = [];
organization.tags.push(tagValue as any);
setOrganization({ ...organization });
}
}
setDialog({ open: false });
setInputValue('');
setTagValue(null);
}}
>
Add
</Button>
</DialogActions>
</Dialog>
<TextField
value={organization.name}
disabled
variant="filled"
InputProps={{
className: classes.orgName
}}
></TextField>
<ListInput label="Root Domains" type="rootDomains"></ListInput>
<ListInput label="IP Blocks" type="ipBlocks"></ListInput>
<ListInput label="Tags" type="tags"></ListInput>
<div className={classes.headerRow}>
<label>Passive Mode</label>
<span>
<SwitchInput
checked={organization.isPassive}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setOrganization({
...organization,
isPassive: event.target.checked
});
}}
color="primary"
/>
</span>
</div>
<div className={classes.buttons}>
<Link to={`/organizations`}>
<Button
variant="outlined"
style={{ marginRight: '10px', color: '#565C65' }}
>
Cancel
</Button>
</Link>
<Button
variant="contained"
onClick={updateOrganization}
style={{ background: '#565C65', color: 'white' }}
>
Save
</Button>
</div>
</Paper>,
<React.Fragment key={1}>
<Table<Role> columns={userRoleColumns} data={userRoles} />
<Dialog
open={dialog.open}
onClose={() => setDialog({ open: false })}
aria-labelledby="form-dialog-title"
maxWidth="xs"
fullWidth
>
<DialogTitle id="form-dialog-title">Add Member</DialogTitle>
<DialogContent>
<p style={{ color: '#3D4551' }}>
Organization members can view Organization-specific vulnerabilities,
domains, and notes. Organization administrators can additionally
manage members and update the organization.
</p>
<TextField
margin="dense"
id="firstName"
name="firstName"
label="First Name"
type="text"
fullWidth
value={newUserValues.firstName}
onChange={onInviteUserTextChange}
variant="filled"
InputProps={{
className: classes.textField
}}
/>
<TextField
margin="dense"
id="lastName"
name="lastName"
label="Last Name"
type="text"
fullWidth
value={newUserValues.lastName}
onChange={onInviteUserTextChange}
variant="filled"
InputProps={{
className: classes.textField
}}
/>
<TextField
margin="dense"
id="email"
name="email"
label="Email"
type="text"
fullWidth
value={newUserValues.email}
onChange={onInviteUserTextChange}
variant="filled"
InputProps={{
className: classes.textField
}}
/>
<br></br>
<br></br>
<FormLabel component="legend">Role</FormLabel>
<RadioGroup
aria-label="role"
name="role"
value={newUserValues.role}
onChange={onInviteUserTextChange}
>
<FormControlLabel
value="standard"
control={<Radio color="primary" />}
label="Standard"
/>
<FormControlLabel
value="admin"
control={<Radio color="primary" />}
label="Administrator"
/>
</RadioGroup>
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={() => setDialog({ open: false })}>
Cancel
</Button>
<Button
variant="contained"
color="primary"
onClick={async () => {
onInviteUserSubmit();
setDialog({ open: false });
}}
>
Add
</Button>
</DialogActions>
</Dialog>
</React.Fragment>,
<React.Fragment key={2}>
<OrganizationList parent={organization}></OrganizationList>
</React.Fragment>,
<React.Fragment key={3}>
<Table<Scan> columns={scanColumns} data={scans} fetchData={fetchScans} />
<h2>Organization Scan History</h2>
<Table<ScanTask> columns={scanTaskColumns} data={scanTasks} />
</React.Fragment>
];
let navItems = [
{
title: 'Settings',
path: `/organizations/${organizationId}`,
exact: true
},
{
title: 'Members',
path: `/organizations/${organizationId}/members`
}
];
if (!organization.parent) {
navItems = navItems.concat([
// { title: 'Teams', path: `/organizations/${organizationId}/teams` },
{ title: 'Scans', path: `/organizations/${organizationId}/scans` }
]);
}
return (
<div>
<div className={classes.header}>
<h1 className={classes.headerLabel}>
<Link to="/organizations">Organizations</Link>
{organization.parent && (
<>
<ChevronRight></ChevronRight>
<Link to={'/organizations/' + organization.parent.id}>
{organization.parent.name}
</Link>
</>
)}
<ChevronRight
style={{
verticalAlign: 'middle',
lineHeight: '100%',
fontSize: '26px'
}}
></ChevronRight>
<span style={{ color: '#07648D' }}>{organization.name}</span>
</h1>
<Subnav
items={navItems}
styles={{
background: '#F9F9F9'
}}
></Subnav>
</div>
<div className={classes.root}>
<Switch>
<Route
path="/organizations/:organizationId"
exact
render={() => views[0]}
/>
<Route
path="/organizations/:organizationId/members"
render={() => views[1]}
/>
<Route
path="/organizations/:organizationId/teams"
render={() => views[2]}
/>
<Route
path="/organizations/:organizationId/scans"
render={() => views[3]}
/>
</Switch>
</div>
</div>
);
}