@material-ui/lab#Autocomplete TypeScript Examples

The following examples show how to use @material-ui/lab#Autocomplete. 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: SearchBox.tsx    From posso-uscire with GNU General Public License v3.0 7 votes vote down vote up
export default function SearchBox() {
  const [language] = useLanguage();
  const router = useRouter();
  return (
    <Box display="flex" justifyContent="center">
      <Autocomplete
        id="province"
        onChange={(_, value) => router.push("/" + (value as Province).urlName)}
        options={regions}
        getOptionLabel={(option) => option.nome}
        style={{ width: "80%", marginTop: 20 }}
        renderInput={(params) => (
          <TextField
            {...params}
            label={i18n.CITY[language]}
            variant="outlined"
          />
        )}
      />
    </Box>
  );
}
Example #2
Source File: MetricAutocomplete.tsx    From abacus with GNU General Public License v2.0 6 votes vote down vote up
/**
 * An Autocomplete just for Metrics
 */
export default function MetricAutocomplete<
  Multiple extends boolean | undefined = undefined,
  DisableClearable extends boolean | undefined = undefined,
  FreeSolo extends boolean | undefined = undefined,
>(
  props: Omit<AutocompleteProps<Metric, Multiple, DisableClearable, FreeSolo>, 'renderInput'> & {
    error?: string | false
  },
): ReturnType<typeof Autocomplete> {
  const processedOptions = props.options
    .filter((a) => !a.name.startsWith('archived_'))
    .sort((a, b) => a.name.localeCompare(b.name, 'en'))
  return (
    <Autocomplete<Metric, Multiple, DisableClearable, FreeSolo>
      aria-label='Select a metric'
      fullWidth
      options={processedOptions}
      noOptionsText='No metrics found'
      getOptionLabel={(metric: Metric) => metric.name}
      getOptionSelected={(metricA: Metric, metricB: Metric) => metricA.metricId === metricB.metricId}
      renderOption={(option: Metric) => (
        <div>
          <Typography variant='body1'>
            <strong>{option.name}</strong>
          </Typography>
          <Typography variant='body1'>
            <small>{option.description}</small>
          </Typography>
        </div>
      )}
      renderInput={(params: AutocompleteRenderInputParams) => (
        <TextField
          {...params}
          placeholder='Select a metric'
          error={!!props.error}
          helperText={_.isString(props.error) ? props.error : undefined}
          required
          InputProps={{
            ...autocompleteInputProps(params, false),
          }}
          InputLabelProps={{
            shrink: true,
          }}
        />
      )}
      {..._.omit(props, ['options', 'error'])}
    />
  )
}
Example #3
Source File: NewTreasuryProposalDialog.tsx    From homebase-app with MIT License 6 votes vote down vote up
AutoCompleteField = styled(Autocomplete)({
  "& .MuiInputLabel-root": {
    display: "none",
  },
  "& .MuiAutocomplete-inputRoot": {
    padding: 0
  },
  "& label + .MuiInput-formControl": {
    marginTop: "0"
  },

  '& .MuiAutocomplete-inputRoot[class*="MuiInput-root"] .MuiAutocomplete-input:first-child': {
    padding: 0
  }
})
Example #4
Source File: NFTTransfer.tsx    From homebase-app with MIT License 6 votes vote down vote up
AutoCompleteField = styled(Autocomplete)({
  "& .MuiInputLabel-root": {
    display: "none",
  },
  "& .MuiAutocomplete-inputRoot": {
    padding: 0
  },
  "& label + .MuiInput-formControl": {
    marginTop: "0"
  },

  '& .MuiAutocomplete-inputRoot[class*="MuiInput-root"] .MuiAutocomplete-input:first-child': {
    padding: 0
  }
})
Example #5
Source File: ProjectSelector.tsx    From backstage with Apache License 2.0 6 votes vote down vote up
ProjectSelector = ({
  catalogEntities,
  onChange,
  disableClearable,
  defaultValue,
  label,
}: Props) => {
  const classes = useStyles();
  return (
    <div className={classes.container}>
      <Autocomplete
        className={classes.autocomplete}
        fullWidth
        disableClearable={disableClearable}
        defaultValue={defaultValue}
        options={catalogEntities}
        getOptionLabel={option => option?.metadata?.name}
        renderOption={option => <span>{option?.metadata?.name}</span>}
        renderInput={params => <TextField {...params} label={label} />}
        onChange={(_, data) => {
          onChange(data!);
        }}
      />
    </div>
  );
}
Example #6
Source File: RepoSearchView.tsx    From github-deploy-center with MIT License 6 votes vote down vote up
RepoSearchBox: FC<RepoSearchBoxProps> = ({
  isLoading,
  options,
  selectedRepo,
  setSelectedRepo,
}) => (
  <Autocomplete
    loading={isLoading}
    options={options}
    id="search-repos"
    renderInput={(params) => (
      <TextField
        variant="outlined"
        label="Search"
        {...params}
        InputProps={{
          ...params.InputProps,
          endAdornment:
            isLoading && !selectedRepo ? (
              <Box
                maxWidth={24}
                maxHeight={24}
                ml={1}
                component={CircularProgress}></Box>
            ) : null,
        }}
      />
    )}
    groupBy={(r) => r.owner}
    getOptionLabel={(r) => r.name}
    getOptionSelected={(first, second) => first.id === second.id}
    value={selectedRepo}
    autoHighlight
    onChange={(_, value) => setSelectedRepo(value)}
  />
)
Example #7
Source File: AssigneeAutoComplete.tsx    From knboard with MIT License 6 votes vote down vote up
AssigneeAutoComplete = ({
  controlId,
  dataTestId,
  members,
  assignee,
  setAssignee,
}: Props) => {
  return (
    <Autocomplete
      multiple
      openOnFocus
      filterSelectedOptions
      disableClearable
      disableCloseOnSelect
      id={controlId}
      data-testid={dataTestId}
      size="small"
      options={members}
      getOptionLabel={(option) => option.username}
      value={assignee}
      onChange={(_event, value) => setAssignee(value)}
      renderOption={(option) => <AvatarOption option={option} />}
      renderInput={(params) => (
        <TextField {...params} autoFocus label="Assignees" variant="outlined" />
      )}
      renderTags={(value, getTagProps) =>
        value.map((option, index) => (
          <AvatarTag
            key={option.id}
            option={option}
            {...getTagProps({ index })}
          />
        ))
      }
    />
  );
}
Example #8
Source File: SelectedKindsFilter.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
SelectedKindsFilter = ({ value, onChange }: Props) => {
  const classes = useStyles();
  const alertApi = useApi(alertApiRef);
  const catalogApi = useApi(catalogApiRef);

  const { error, value: kinds } = useAsync(async () => {
    return await catalogApi
      .getEntityFacets({ facets: ['kind'] })
      .then(response => response.facets.kind?.map(f => f.value).sort() || []);
  });

  useEffect(() => {
    if (error) {
      alertApi.post({
        message: `Failed to load entity kinds`,
        severity: 'error',
      });
    }
  }, [error, alertApi]);

  const normalizedKinds = useMemo(
    () => (kinds ? kinds.map(k => k.toLocaleLowerCase('en-US')) : kinds),
    [kinds],
  );

  const handleChange = useCallback(
    (_: unknown, v: string[]) => {
      onChange(
        normalizedKinds && normalizedKinds.every(r => v.includes(r))
          ? undefined
          : v,
      );
    },
    [normalizedKinds, onChange],
  );

  const handleEmpty = useCallback(() => {
    onChange(value?.length ? value : undefined);
  }, [value, onChange]);

  if (!kinds?.length || !normalizedKinds?.length || error) {
    return <></>;
  }

  return (
    <Box pb={1} pt={1}>
      <Typography variant="button">Kinds</Typography>
      <Autocomplete
        className={classes.formControl}
        multiple
        limitTags={4}
        disableCloseOnSelect
        aria-label="Kinds"
        options={normalizedKinds}
        value={value ?? normalizedKinds}
        getOptionLabel={k => kinds[normalizedKinds.indexOf(k)] ?? k}
        onChange={handleChange}
        onBlur={handleEmpty}
        renderOption={(option, { selected }) => (
          <FormControlLabel
            control={
              <Checkbox
                icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
                checkedIcon={<CheckBoxIcon fontSize="small" />}
                checked={selected}
              />
            }
            label={kinds[normalizedKinds.indexOf(option)] ?? option}
          />
        )}
        size="small"
        popupIcon={<ExpandMoreIcon data-testid="selected-kinds-expand" />}
        renderInput={params => <TextField {...params} variant="outlined" />}
      />
    </Box>
  );
}
Example #9
Source File: SearchFilter.Autocomplete.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
AutocompleteFilter = (props: SearchAutocompleteFilterProps) => {
  const {
    className,
    defaultValue,
    name,
    values: givenValues,
    valuesDebounceMs,
    label,
    filterSelectedOptions,
    limitTags,
    multiple,
  } = props;
  const [inputValue, setInputValue] = useState<string>('');
  useDefaultFilterValue(name, defaultValue);
  const asyncValues =
    typeof givenValues === 'function' ? givenValues : undefined;
  const defaultValues =
    typeof givenValues === 'function' ? undefined : givenValues;
  const { value: values, loading } = useAsyncFilterValues(
    asyncValues,
    inputValue,
    defaultValues,
    valuesDebounceMs,
  );
  const { filters, setFilters } = useSearch();
  const filterValue =
    (filters[name] as string | string[] | undefined) || (multiple ? [] : null);

  // Set new filter values on input change.
  const handleChange = (
    _: ChangeEvent<{}>,
    newValue: string | string[] | null,
  ) => {
    setFilters(prevState => {
      const { [name]: filter, ...others } = prevState;

      if (newValue) {
        return { ...others, [name]: newValue };
      }
      return { ...others };
    });
  };

  // Provide the input field.
  const renderInput = (params: AutocompleteRenderInputParams) => (
    <TextField
      {...params}
      name="search"
      variant="outlined"
      label={label}
      fullWidth
    />
  );

  // Render tags as primary-colored chips.
  const renderTags = (
    tagValue: string[],
    getTagProps: AutocompleteGetTagProps,
  ) =>
    tagValue.map((option: string, index: number) => (
      <Chip label={option} color="primary" {...getTagProps({ index })} />
    ));

  return (
    <Autocomplete
      filterSelectedOptions={filterSelectedOptions}
      limitTags={limitTags}
      multiple={multiple}
      className={className}
      id={`${multiple ? 'multi-' : ''}select-filter-${name}--select`}
      options={values || []}
      loading={loading}
      value={filterValue}
      onChange={handleChange}
      onInputChange={(_, newValue) => setInputValue(newValue)}
      renderInput={renderInput}
      renderTags={renderTags}
    />
  );
}
Example #10
Source File: CategoryPicker.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
CategoryPicker = () => {
  const alertApi = useApi(alertApiRef);
  const { error, loading, availableTypes, selectedTypes, setSelectedTypes } =
    useEntityTypeFilter();

  if (loading) return <Progress />;

  if (error) {
    alertApi.post({
      message: `Failed to load entity types with error: ${error}`,
      severity: 'error',
    });
    return null;
  }

  if (!availableTypes) return null;

  return (
    <Box pb={1} pt={1}>
      <Typography variant="button">Categories</Typography>
      <Autocomplete
        multiple
        aria-label="Categories"
        options={availableTypes}
        value={selectedTypes}
        onChange={(_: object, value: string[]) => setSelectedTypes(value)}
        renderOption={(option, { selected }) => (
          <FormControlLabel
            control={
              <Checkbox
                icon={icon}
                checkedIcon={checkedIcon}
                checked={selected}
              />
            }
            label={capitalize(option)}
          />
        )}
        size="small"
        popupIcon={<ExpandMoreIcon />}
        renderInput={params => <TextField {...params} variant="outlined" />}
      />
    </Box>
  );
}
Example #11
Source File: EntityTagsPicker.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
EntityTagsPicker = (
  props: FieldExtensionComponentProps<string[], EntityTagsPickerUiOptions>,
) => {
  const { formData, onChange, uiSchema } = props;
  const catalogApi = useApi(catalogApiRef);
  const [inputValue, setInputValue] = useState('');
  const [inputError, setInputError] = useState(false);
  const tagValidator = makeValidator().isValidTag;
  const kinds = uiSchema['ui:options']?.kinds;

  const { loading, value: existingTags } = useAsync(async () => {
    const tagsRequest: GetEntitiesRequest = { fields: ['metadata.tags'] };
    if (kinds) {
      tagsRequest.filter = { kind: kinds };
    }

    const entities = await catalogApi.getEntities(tagsRequest);

    return [
      ...new Set(
        entities.items
          .flatMap((e: Entity) => e.metadata?.tags)
          .filter(Boolean) as string[],
      ),
    ].sort();
  });

  const setTags = (_: React.ChangeEvent<{}>, values: string[] | null) => {
    // Reset error state in case all tags were removed
    let hasError = false;
    let addDuplicate = false;
    const currentTags = formData || [];

    // If adding a new tag
    if (values?.length && currentTags.length < values.length) {
      const newTag = (values[values.length - 1] = values[values.length - 1]
        .toLocaleLowerCase('en-US')
        .trim());
      hasError = !tagValidator(newTag);
      addDuplicate = currentTags.indexOf(newTag) !== -1;
    }

    setInputError(hasError);
    setInputValue(!hasError ? '' : inputValue);
    if (!hasError && !addDuplicate) {
      onChange(values || []);
    }
  };

  // Initialize field to always return an array
  useEffectOnce(() => onChange(formData || []));

  return (
    <FormControl margin="normal">
      <Autocomplete
        multiple
        freeSolo
        filterSelectedOptions
        onChange={setTags}
        value={formData || []}
        inputValue={inputValue}
        loading={loading}
        options={existingTags || []}
        ChipProps={{ size: 'small' }}
        renderInput={params => (
          <TextField
            {...params}
            label="Tags"
            onChange={e => setInputValue(e.target.value)}
            error={inputError}
            helperText="Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters"
          />
        )}
      />
    </FormControl>
  );
}
Example #12
Source File: TemplateTypePicker.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
TemplateTypePicker = () => {
  const alertApi = useApi(alertApiRef);
  const { error, loading, availableTypes, selectedTypes, setSelectedTypes } =
    useEntityTypeFilter();

  if (loading) return <Progress />;

  if (!availableTypes) return null;

  if (error) {
    alertApi.post({
      message: `Failed to load entity types`,
      severity: 'error',
    });
    return null;
  }

  return (
    <Box pb={1} pt={1}>
      <Typography variant="button">Categories</Typography>
      <Autocomplete
        multiple
        aria-label="Categories"
        options={availableTypes}
        value={selectedTypes}
        onChange={(_: object, value: string[]) => setSelectedTypes(value)}
        renderOption={(option, { selected }) => (
          <FormControlLabel
            control={
              <Checkbox
                icon={icon}
                checkedIcon={checkedIcon}
                checked={selected}
              />
            }
            label={capitalize(option)}
          />
        )}
        size="small"
        popupIcon={<ExpandMoreIcon data-testid="categories-picker-expand" />}
        renderInput={params => <TextField {...params} variant="outlined" />}
      />
    </Box>
  );
}
Example #13
Source File: EntityTagPicker.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
EntityTagPicker = () => {
  const classes = useStyles();
  const {
    updateFilters,
    filters,
    queryParameters: { tags: tagsParameter },
  } = useEntityList();

  const catalogApi = useApi(catalogApiRef);
  const { value: availableTags } = useAsync(async () => {
    const facet = 'metadata.tags';
    const { facets } = await catalogApi.getEntityFacets({
      facets: [facet],
      filter: filters.kind?.getCatalogFilters(),
    });

    return facets[facet].map(({ value }) => value);
  }, [filters.kind]);

  const queryParamTags = useMemo(
    () => [tagsParameter].flat().filter(Boolean) as string[],
    [tagsParameter],
  );

  const [selectedTags, setSelectedTags] = useState(
    queryParamTags.length ? queryParamTags : filters.tags?.values ?? [],
  );

  // Set selected tags on query parameter updates; this happens at initial page load and from
  // external updates to the page location.
  useEffect(() => {
    if (queryParamTags.length) {
      setSelectedTags(queryParamTags);
    }
  }, [queryParamTags]);

  useEffect(() => {
    updateFilters({
      tags: selectedTags.length ? new EntityTagFilter(selectedTags) : undefined,
    });
  }, [selectedTags, updateFilters]);

  if (!availableTags?.length) return null;

  return (
    <Box pb={1} pt={1}>
      <Typography variant="button" component="label">
        Tags
        <Autocomplete
          multiple
          options={availableTags}
          value={selectedTags}
          onChange={(_: object, value: string[]) => setSelectedTags(value)}
          renderOption={(option, { selected }) => (
            <FormControlLabel
              control={
                <Checkbox
                  icon={icon}
                  checkedIcon={checkedIcon}
                  checked={selected}
                />
              }
              label={option}
            />
          )}
          size="small"
          popupIcon={<ExpandMoreIcon data-testid="tag-picker-expand" />}
          renderInput={params => (
            <TextField
              {...params}
              className={classes.input}
              variant="outlined"
            />
          )}
        />
      </Typography>
    </Box>
  );
}
Example #14
Source File: AutocompleteTextField.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
AutocompleteTextField = <TFieldValue extends string>(
  props: AutocompleteTextFieldProps<TFieldValue>,
) => {
  const {
    name,
    options,
    required,
    errors,
    rules,
    loading = false,
    loadingText,
    helperText,
    errorHelperText,
    textFieldProps = {},
  } = props;

  return (
    <Controller
      name={name}
      rules={rules}
      render={({ field: { onChange } }) => (
        <Autocomplete
          loading={loading}
          loadingText={loadingText}
          options={options || []}
          autoSelect
          freeSolo
          onChange={(_event: React.ChangeEvent<{}>, value: string | null) =>
            onChange(value)
          }
          renderInput={params => (
            <TextField
              {...params}
              helperText={(errors?.[name] && errorHelperText) || helperText}
              error={Boolean(errors?.[name])}
              margin="normal"
              variant="outlined"
              required={required}
              InputProps={{
                ...params.InputProps,
                endAdornment: (
                  <React.Fragment>
                    {loading ? (
                      <CircularProgress color="inherit" size="1em" />
                    ) : null}
                    {params.InputProps.endAdornment}
                  </React.Fragment>
                ),
              }}
              {...textFieldProps}
            />
          )}
        />
      )}
    />
  );
}
Example #15
Source File: SelectedRelationsFilter.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
SelectedRelationsFilter = ({
  relationPairs,
  value,
  onChange,
}: Props) => {
  const classes = useStyles();
  const relations = useMemo(() => relationPairs.flat(), [relationPairs]);

  const handleChange = useCallback(
    (_: unknown, v: string[]) => {
      onChange(relations.every(r => v.includes(r)) ? undefined : v);
    },
    [relations, onChange],
  );

  const handleEmpty = useCallback(() => {
    onChange(value?.length ? value : undefined);
  }, [value, onChange]);

  return (
    <Box pb={1} pt={1}>
      <Typography variant="button">Relations</Typography>
      <Autocomplete
        className={classes.formControl}
        multiple
        limitTags={4}
        disableCloseOnSelect
        aria-label="Relations"
        options={relations}
        value={value ?? relations}
        onChange={handleChange}
        onBlur={handleEmpty}
        renderOption={(option, { selected }) => (
          <FormControlLabel
            control={
              <Checkbox
                icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
                checkedIcon={<CheckBoxIcon fontSize="small" />}
                checked={selected}
              />
            }
            label={option}
          />
        )}
        size="small"
        popupIcon={<ExpandMoreIcon data-testid="selected-relations-expand" />}
        renderInput={params => <TextField {...params} variant="outlined" />}
      />
    </Box>
  );
}
Example #16
Source File: TestVariationMergeForm.tsx    From frontend with Apache License 2.0 5 votes vote down vote up
TestVariationMergeForm: React.FunctionComponent<IProps> = ({
  projectId,
  items,
}) => {
  const navigate = useNavigate();
  const buildDispatch = useBuildDispatch();
  const { enqueueSnackbar } = useSnackbar();
  const [fromBranch, setFromBranch] = React.useState("");
  const [toBranch, setToBranch] = React.useState("");

  const handleSubmit = (event: React.FormEvent) => {
    event.preventDefault();
    testVariationService
      .merge(projectId, fromBranch, toBranch)
      .then((build) => {
        enqueueSnackbar(`Merge started in build: ${build.id}`, {
          variant: "success",
        });
        navigate({
          pathname: buildProjectPageUrl(projectId),
          ...buildTestRunLocation(build.id),
        });
        selectBuild(buildDispatch, build.id);
      })
      .catch((err) =>
        enqueueSnackbar(err, {
          variant: "error",
        })
      );
  };

  return (
    <form onSubmit={handleSubmit}>
      <Grid container spacing={2} alignItems="flex-end">
        <Grid item xs>
          <Select
            required
            fullWidth
            displayEmpty
            value={fromBranch}
            onChange={(event) => setFromBranch(event.target.value as string)}
          >
            <MenuItem value="">
              <em>From branch</em>
            </MenuItem>
            {items.map((i) => (
              <MenuItem key={i} value={i}>
                {i}
              </MenuItem>
            ))}
          </Select>
        </Grid>
        <Grid item xs>
          <Autocomplete
            id="toBranch"
            options={items.map((i) => ({ title: i }))}
            getOptionLabel={(option) => option.title}
            freeSolo
            fullWidth
            renderInput={(params) => (
              <TextField {...params} required label="To branch" />
            )}
            onInputChange={(_, value) => {
              setToBranch(value);
            }}
          />
        </Grid>
        <Grid item>
          <Button type="submit" color="primary" variant="contained" id={LOCATOR_TEST_VARIATION_SELECT_BRANCH}>
            Merge
          </Button>
        </Grid>
      </Grid>
    </form>
  );
}
Example #17
Source File: DeploymentDialog.tsx    From github-deploy-center with MIT License 4 votes vote down vote up
DeploymentDialog = () => {
  const { deploymentDialog } = useAppState()
  const { updateDeployWorkflowDialog, cancelEditDeployment, saveDeployment } =
    useActions()

  const valid = Boolean(
    deploymentDialog &&
      deploymentDialog.workflowId &&
      deploymentDialog.releaseKey &&
      deploymentDialog.ref
  )
  return (
    <Dialog open={!!deploymentDialog} fullWidth onClose={cancelEditDeployment}>
      {deploymentDialog ? (
        <form
          onSubmit={(event) => {
            event.preventDefault()
            if (valid) {
              saveDeployment()
            }
          }}>
          <DialogTitle>Deploy workflow settings</DialogTitle>
          <DialogContent
            style={{ display: 'flex', gap: '1rem', flexDirection: 'column' }}>
            <SelectWorkflow
              workflowId={deploymentDialog.workflowId}
              onChange={(id) =>
                updateDeployWorkflowDialog((state) => (state.workflowId = id))
              }
            />
            <TextField
              label="Release input name"
              value={deploymentDialog.releaseKey}
              onChange={(e) =>
                updateDeployWorkflowDialog(
                  (settings) => (settings.releaseKey = e.target.value)
                )
              }
            />
            <TextField
              label="Environment input name (optional)"
              value={deploymentDialog.environmentKey}
              onChange={(e) =>
                updateDeployWorkflowDialog(
                  (settings) => (settings.environmentKey = e.target.value)
                )
              }
            />
            <TextField
              label="Run workflow from branch"
              value={deploymentDialog.ref}
              onChange={(e) =>
                updateDeployWorkflowDialog(
                  (settings) => (settings.ref = e.target.value)
                )
              }
            />
            <Autocomplete
              style={{ gridColumn: '1 / span 5' }}
              multiple
              options={[]}
              freeSolo
              value={Object.entries(deploymentDialog.extraArgs).map(
                ([key, value]) => `${key}=${value}`
              )}
              renderInput={(params) => (
                <TextField
                  label="Extra workflow args (press Enter to add)"
                  placeholder="key=value"
                  {...params}
                />
              )}
              onChange={(_, newValue) => {
                const pairs = newValue
                  .filter((x): x is string => typeof x === 'string')
                  .map((x) => x.split('='))
                  .filter(([key, value]) => key && value)
                const newArgs = fromPairs(pairs)
                updateDeployWorkflowDialog(
                  (settings) => (settings.extraArgs = newArgs)
                )
              }}
            />
          </DialogContent>
          <DialogActions style={{ padding: '2rem' }}>
            <Button
              type="submit"
              disabled={!valid}
              variant="contained"
              color="primary">
              Save
            </Button>
            <Button onClick={cancelEditDeployment}>Cancel</Button>
          </DialogActions>
        </form>
      ) : null}
    </Dialog>
  )
}
Example #18
Source File: EnvironmentDialog.tsx    From github-deploy-center with MIT License 4 votes vote down vote up
EnvironmentDialog: FC<{
  dialogState?: EnvironmentDialogState
  updateDialogState: (update: (state: EnvironmentDialogState) => void) => void
  title: string
  onSave: (settings: EnvironmentSettings) => void
  onCancel: () => void
}> = ({ dialogState, onSave, onCancel, title, updateDialogState }) => {
  const { data, isLoading, error } = useFetchEnvironments()
  const { selectedApplication } = useAppState()
  const filteredEnvironments = orderBy(
    (data || []).filter((d) => d.name !== 'github-pages'),
    [
      (e) =>
        e.name
          .toLowerCase()
          .includes(selectedApplication?.name.split(' ')[0].toLowerCase() || '')
          ? 1
          : 2,
      (e) => e.name,
    ]
  )
  return (
    <Dialog open={!!dialogState} fullWidth onClose={onCancel}>
      {dialogState ? (
        <form
          onSubmit={(event) => {
            event.preventDefault()
            const { environmentName, workflowInputValue } = dialogState
            environmentName &&
              onSave({
                workflowInputValue,
                name: environmentName,
              })
          }}>
          <DialogTitle>{title}</DialogTitle>
          <DialogContent style={{display: "flex", flexDirection: "column"}}>
            {error instanceof Error ? (
                <>
                  <Box mb={2}>
                    <Alert severity="warning">Could not fetch environments: {error.message}</Alert>
                  </Box>
                  <DialogContentText>Enter environment manually:</DialogContentText>
                  <TextField
                      label="Environment name"
                      value={dialogState.environmentName}
                      onChange={(e) =>
                      updateDialogState((state) => (state.environmentName = e.target.value))}
                  />
                  <TextField
                      label="Workflow input value"
                      value={dialogState.workflowInputValue}
                      onChange={(e) =>
                      updateDialogState((state) => (state.workflowInputValue = e.target.value))}
                  />
                </>
            ) : (
              <>
                <DialogContentText>Select environment</DialogContentText>
                <Autocomplete
                  freeSolo
                  loading={isLoading}
                  options={filteredEnvironments.map<Option>(identity)}
                  value={dialogState.environmentName}
                  openOnFocus
                  onChange={(_, value) =>
                    updateDialogState(
                      (state) =>
                        (state.environmentName =
                          typeof value === 'string'
                            ? value
                            : value?.inputValue ?? value?.name ?? '')
                    )
                  }
                  getOptionLabel={(option) =>
                    typeof option === 'string' ? option : option.name
                  }
                  getOptionSelected={(first, second) =>
                    first.name === second.name
                  }
                  filterOptions={(options, params) => {
                    const filtered = filter(options, params)

                    // Suggest the creation of a new value
                    if (params.inputValue !== '') {
                      filtered.push({
                        inputValue: params.inputValue,
                        name: `Add "${params.inputValue}"`,
                      })
                    }

                    return filtered
                  }}
                  renderInput={(params) => (
                    <TextField
                      autoFocus
                      variant="outlined"
                      label="Search"
                      {...params}
                      inputProps={{
                        ...params.inputProps,
                        'data-lpignore': true,
                      }}
                      InputProps={{
                        ...params.InputProps,
                        endAdornment:
                          isLoading && !dialogState.environmentName ? (
                            <Box
                              maxWidth={24}
                              maxHeight={24}
                              ml={1}
                              component={CircularProgress}></Box>
                          ) : null,
                      }}
                    />
                  )}
                />
                {selectedApplication?.deploySettings.type === 'workflow' &&
                  selectedApplication.deploySettings.environmentKey && (
                    <TextField
                      style={{ marginTop: '1rem' }}
                      label="Workflow input value (defaults to environment name)"
                      fullWidth
                      variant="outlined"
                      value={dialogState.workflowInputValue}
                      onChange={(event) =>
                        updateDialogState(
                          (state) =>
                            (state.workflowInputValue = event.target.value)
                        )
                      }
                    />
                  )}
              </>
            )}
          </DialogContent>
          <Box p={2} pt={1}>
            <DialogActions>
              <Button
                type="submit"
                disabled={!dialogState.environmentName}
                variant="contained"
                color="primary">
                Save
              </Button>
              <Button onClick={onCancel}>Cancel</Button>
            </DialogActions>
          </Box>
        </form>
      ) : null}
    </Dialog>
  )
}
Example #19
Source File: index.tsx    From TidGi-Desktop with Mozilla Public License 2.0 4 votes vote down vote up
export default function EditWorkspace(): JSX.Element {
  const { t } = useTranslation();
  const originalWorkspace = useWorkspaceObservable(workspaceID);
  const [workspace, workspaceSetter, onSave] = useForm(originalWorkspace);
  const {
    backupOnInterval,
    disableAudio,
    disableNotifications,
    gitUrl,
    hibernateWhenUnused,
    homeUrl,
    isSubWiki,
    mainWikiToLink,
    name,
    order,
    picturePath,
    port,
    storageService,
    syncOnInterval,
    syncOnStartup,
    tagName,
    transparentBackground,
    userName,
    wikiFolderLocation,
  } = (workspace ?? {}) as unknown as IWorkspace;
  const fileSystemPaths = usePromiseValue<ISubWikiPluginContent[]>(
    async () => (mainWikiToLink ? await window.service.wiki.getSubWikiPluginContent(mainWikiToLink) : []),
    [],
    [mainWikiToLink],
  ) as ISubWikiPluginContent[];
  const fallbackUserName = usePromiseValue<string>(async () => (await window.service.auth.get('userName')) as string, '');

  const [requestRestartCountDown, RestartSnackbar] = useRestartSnackbar();
  const requestSaveAndRestart = useCallback(async () => {
    if (!isEqual(workspace, originalWorkspace)) {
      await onSave();
      requestRestartCountDown();
    }
  }, [onSave, requestRestartCountDown, workspace, originalWorkspace]);

  const actualIP = usePromiseValue<string | undefined>(
    async () => (homeUrl ? await window.remote.getLocalHostUrlWithActualIP(homeUrl) : await Promise.resolve(undefined)),
    undefined,
    [homeUrl],
  );
  if (workspaceID === undefined) {
    return <Root>Error {workspaceID ?? '-'} not exists</Root>;
  }
  if (workspace === undefined) {
    return <Root>{t('Loading')}</Root>;
  }
  const isCreateSyncedWorkspace = storageService !== SupportedStorageServices.local;
  return (
    <Root>
      <div id="test" data-usage="For spectron automating testing" />
      {RestartSnackbar}
      <Helmet>
        <title>
          {t('WorkspaceSelector.EditWorkspace')} {String(order ?? 1)} {name}
        </title>
      </Helmet>
      <FlexGrow>
        <TextField
          id="outlined-full-width"
          label={t('EditWorkspace.Name')}
          helperText={t('EditWorkspace.NameDescription')}
          placeholder="Optional"
          value={name}
          onChange={(event) => workspaceSetter({ ...workspace, name: event.target.value })}
        />
        <TextField
          id="outlined-full-width"
          label={t('EditWorkspace.Path')}
          helperText={t('EditWorkspace.PathDescription')}
          placeholder="Optional"
          disabled
          value={wikiFolderLocation}
          onChange={(event) => workspaceSetter({ ...workspace, wikiFolderLocation: event.target.value })}
        />
        <TextField
          helperText={t('AddWorkspace.WorkspaceUserNameDetail')}
          fullWidth
          onChange={(event) => {
            workspaceSetter({ ...workspace, userName: event.target.value });
            void requestSaveAndRestart();
          }}
          label={t('AddWorkspace.WorkspaceUserName')}
          placeholder={fallbackUserName}
          value={userName}
        />
        {!isSubWiki && (
          <TextField
            id="outlined-full-width"
            label={t('EditWorkspace.Port')}
            helperText={
              <span>
                {t('EditWorkspace.URL')}{' '}
                <Link onClick={async () => actualIP && (await window.service.native.open(actualIP))} style={{ cursor: 'pointer' }}>
                  {actualIP}
                </Link>
              </span>
            }
            placeholder="Optional"
            value={port}
            onChange={async (event) => {
              if (!Number.isNaN(Number.parseInt(event.target.value))) {
                workspaceSetter({
                  ...workspace,
                  port: Number(event.target.value),
                  homeUrl: await window.remote.getLocalHostUrlWithActualIP(`http://${defaultServerIP}:${event.target.value}/`),
                });
                void requestSaveAndRestart();
              }
            }}
          />
        )}
        {isSubWiki && (
          <Autocomplete
            freeSolo
            options={fileSystemPaths?.map((fileSystemPath) => fileSystemPath.tagName)}
            value={tagName}
            onInputChange={(_, value) => {
              workspaceSetter({ ...workspace, tagName: value });
              void requestSaveAndRestart();
            }}
            renderInput={(parameters) => <TextField {...parameters} label={t('AddWorkspace.TagName')} helperText={t('AddWorkspace.TagNameHelp')} />}
          />
        )}
        <AvatarFlex>
          <AvatarLeft>
            <Avatar transparentBackground={transparentBackground}>
              <AvatarPicture alt="Icon" src={getValidIconPath(picturePath)} />
            </Avatar>
          </AvatarLeft>
          <AvatarRight>
            <Tooltip title={wikiPictureExtensions.join(', ')} placement="top">
              <PictureButton
                variant="outlined"
                size="small"
                onClick={async () => {
                  const filePaths = await window.service.native.pickFile([{ name: 'Images', extensions: wikiPictureExtensions }]);
                  if (filePaths.length > 0) {
                    await window.service.workspace.update(workspaceID, { picturePath: filePaths[0] });
                  }
                }}>
                {t('EditWorkspace.SelectLocal')}
              </PictureButton>
            </Tooltip>

            <Tooltip title={t('EditWorkspace.NoRevert') ?? ''} placement="bottom">
              <PictureButton onClick={() => workspaceSetter({ ...workspace, picturePath: null })} disabled={!picturePath}>
                {t('EditWorkspace.ResetDefaultIcon')}
              </PictureButton>
            </Tooltip>
          </AvatarRight>
        </AvatarFlex>
        <SyncedWikiDescription
          isCreateSyncedWorkspace={isCreateSyncedWorkspace}
          isCreateSyncedWorkspaceSetter={(isSynced: boolean) => {
            workspaceSetter({ ...workspace, storageService: isSynced ? SupportedStorageServices.github : SupportedStorageServices.local });
            // requestRestartCountDown();
          }}
        />
        {isCreateSyncedWorkspace && (
          <TokenForm
            storageProvider={storageService}
            storageProviderSetter={(nextStorageService: SupportedStorageServices) => {
              workspaceSetter({ ...workspace, storageService: nextStorageService });
              // requestRestartCountDown();
            }}
          />
        )}
        {storageService !== SupportedStorageServices.local && (
          <GitRepoUrlForm
            storageProvider={storageService}
            gitRepoUrl={gitUrl ?? ''}
            gitRepoUrlSetter={(nextGitUrl: string) => {
              workspaceSetter({ ...workspace, gitUrl: nextGitUrl });
            }}
            isCreateMainWorkspace={!isSubWiki}
          />
        )}
        {storageService !== SupportedStorageServices.local && (
          <>
            <Divider />
            <List>
              <ListItem disableGutters>
                <ListItemText primary={t('EditWorkspace.SyncOnInterval')} secondary={t('EditWorkspace.SyncOnIntervalDescription')} />
                <ListItemSecondaryAction>
                  <Switch
                    edge="end"
                    color="primary"
                    checked={syncOnInterval}
                    onChange={(event) => workspaceSetter({ ...workspace, syncOnInterval: event.target.checked })}
                  />
                </ListItemSecondaryAction>
              </ListItem>
              <ListItem disableGutters>
                <ListItemText primary={t('EditWorkspace.SyncOnStartup')} secondary={t('EditWorkspace.SyncOnStartupDescription')} />
                <ListItemSecondaryAction>
                  <Switch
                    edge="end"
                    color="primary"
                    checked={syncOnStartup}
                    onChange={(event) => workspaceSetter({ ...workspace, syncOnStartup: event.target.checked })}
                  />
                </ListItemSecondaryAction>
              </ListItem>
            </List>
          </>
        )}
        {storageService === SupportedStorageServices.local && (
          <>
            <Divider />
            <List>
              <Divider />
              <ListItem disableGutters>
                <ListItemText primary={t('EditWorkspace.BackupOnInterval')} secondary={t('EditWorkspace.BackupOnIntervalDescription')} />
                <ListItemSecondaryAction>
                  <Switch
                    edge="end"
                    color="primary"
                    checked={backupOnInterval}
                    onChange={(event) => workspaceSetter({ ...workspace, backupOnInterval: event.target.checked })}
                  />
                </ListItemSecondaryAction>
              </ListItem>
            </List>
          </>
        )}
        {!isSubWiki && (
          <List>
            <Divider />
            <ListItem disableGutters>
              <ListItemText primary={t('EditWorkspace.HibernateTitle')} secondary={t('EditWorkspace.HibernateDescription')} />
              <ListItemSecondaryAction>
                <Switch
                  edge="end"
                  color="primary"
                  checked={hibernateWhenUnused}
                  onChange={(event) => workspaceSetter({ ...workspace, hibernateWhenUnused: event.target.checked })}
                />
              </ListItemSecondaryAction>
            </ListItem>
            <ListItem disableGutters>
              <ListItemText primary={t('EditWorkspace.DisableNotificationTitle')} secondary={t('EditWorkspace.DisableNotification')} />
              <ListItemSecondaryAction>
                <Switch
                  edge="end"
                  color="primary"
                  checked={disableNotifications}
                  onChange={(event) => workspaceSetter({ ...workspace, disableNotifications: event.target.checked })}
                />
              </ListItemSecondaryAction>
            </ListItem>
            <ListItem disableGutters>
              <ListItemText primary={t('EditWorkspace.DisableAudioTitle')} secondary={t('EditWorkspace.DisableAudio')} />
              <ListItemSecondaryAction>
                <Switch
                  edge="end"
                  color="primary"
                  checked={disableAudio}
                  onChange={(event) => workspaceSetter({ ...workspace, disableAudio: event.target.checked })}
                />
              </ListItemSecondaryAction>
            </ListItem>
          </List>
        )}
      </FlexGrow>
      {!isEqual(workspace, originalWorkspace) && (
        <div>
          <Button color="primary" variant="contained" disableElevation onClick={requestSaveAndRestart}>
            {t('EditWorkspace.Save')}
          </Button>
          <Button variant="contained" disableElevation onClick={() => void window.remote.closeCurrentWindow()}>
            {t('EditWorkspace.Cancel')}
          </Button>
        </div>
      )}
    </Root>
  );
}
Example #20
Source File: CardSettingsContentPropertySelect.tsx    From neodash with Apache License 2.0 4 votes vote down vote up
NeoCardSettingsContentPropertySelect = ({ type, database, settings, onReportSettingUpdate, onQueryUpdate }) => {
    const { driver } = useContext<Neo4jContextState>(Neo4jContext);
    if (!driver) throw new Error('`driver` not defined. Have you added it into your app as <Neo4jContext.Provider value={{driver}}> ?')

    const debouncedRunCypherQuery = useCallback(
        debounce(runCypherQuery, RUN_QUERY_DELAY_MS),
        [],
    );

    const manualPropertyNameSpecification = settings['manualPropertyNameSpecification'];
    const [labelInputText, setLabelInputText] = React.useState(settings['entityType']);
    const [labelRecords, setLabelRecords] = React.useState([]);
    const [propertyInputText, setPropertyInputText] = React.useState(settings['propertyType']);
    const [propertyRecords, setPropertyRecords] = React.useState([]);
    var parameterName = settings['parameterName'];

    if (settings["type"] == undefined) {
        onReportSettingUpdate("type", "Node Property");
    }
    if (!parameterName && settings['entityType'] && settings['propertyType']) {
        const id = settings['id'] ? settings['id'] : "";
        onReportSettingUpdate("parameterName", "neodash_" + (settings['entityType'] + "_" + settings['propertyType'] + (id == "" || id.startsWith("_") ? id : "_" + id)).toLowerCase().replaceAll(" ", "_").replaceAll("-", "_"));
    }
    // Define query callback to allow reports to get extra data on interactions.
    const queryCallback = useCallback(
        (query, parameters, setRecords) => {
            debouncedRunCypherQuery(driver, database, query, parameters, {}, [], 10,
                (status) => { status == QueryStatus.NO_DATA ? setRecords([]) : null },
                (result => setRecords(result)),
                () => { return }, false,
                false, false,
                [], [], [], [], null);
        },
        [],
    );

    function handleParameterTypeUpdate(newValue) {
        onReportSettingUpdate('entityType', undefined);
        onReportSettingUpdate('propertyType', undefined);
        onReportSettingUpdate('id', undefined);
        onReportSettingUpdate('parameterName', undefined);
        onReportSettingUpdate("type", newValue);
    }

    function handleNodeLabelSelectionUpdate(newValue) {
        setPropertyInputText("");
        onReportSettingUpdate('entityType', newValue);
        onReportSettingUpdate('propertyType', undefined);
        onReportSettingUpdate('parameterName', undefined);
    }

    function handleFreeTextNameSelectionUpdate(newValue) {
        if (newValue) {
            const new_parameter_name = ("neodash_" +  newValue).toLowerCase().replaceAll(" ", "_").replaceAll("-", "_");
            handleReportQueryUpdate(new_parameter_name, newValue, undefined);
        } else {
            onReportSettingUpdate('parameterName', undefined);
        }
    }

    function handlePropertyNameSelectionUpdate(newValue) {
        onReportSettingUpdate('propertyType', newValue);
        if (newValue && settings['entityType']) {
            const id = settings['id'] ? settings['id'] : "";
            const new_parameter_name = "neodash_" + (settings['entityType'] + "_" + newValue + (id == "" || id.startsWith("_") ? id : "_" + id)).toLowerCase().replaceAll(" ", "_").replaceAll("-", "_");
            handleReportQueryUpdate(new_parameter_name, settings['entityType'], newValue);
        } else {
            onReportSettingUpdate('parameterName', undefined);
        }
    }

    function handleIdSelectionUpdate(value) {
        const newValue = value ? value : "";
        onReportSettingUpdate('id', "" + newValue);
        if (settings['propertyType'] && settings['entityType']) {
            const id = value ? "_" + value : "";
            const new_parameter_name = "neodash_" + (settings['entityType'] + "_" + settings['propertyType'] + (id == "" || id.startsWith("_") ? id : "_" + id)).toLowerCase().replaceAll(" ", "_").replaceAll("-", "_");
            handleReportQueryUpdate(new_parameter_name, settings['entityType'], settings['propertyType']);
        }
    }

    function handleReportQueryUpdate(new_parameter_name, entityType, propertyType) {
        // Set query based on whether we are selecting a node or relationship property.
        onReportSettingUpdate('parameterName', new_parameter_name);
        if (settings['type'] == "Node Property") {
            const newQuery = "MATCH (n:`" + entityType + "`) \nWHERE toLower(toString(n.`" + propertyType + "`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`" + propertyType + "` as value LIMIT 5";
            onQueryUpdate(newQuery);
        } else if (settings['type'] == "Relationship Property"){
            const newQuery = "MATCH ()-[n:`" + entityType + "`]->() \nWHERE toLower(toString(n.`" + propertyType + "`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`" + propertyType + "` as value LIMIT 5";
            onQueryUpdate(newQuery);
        } else {
            const newQuery = "RETURN true";
            onQueryUpdate(newQuery);
        }
    }

    const parameterSelectTypes = ["Node Property", "Relationship Property", "Free Text"]

    return <div>
        <p style={{ color: "grey", fontSize: 12, paddingLeft: "5px", border: "1px solid lightgrey", marginTop: "0px" }}>
            {REPORT_TYPES[type].helperText}
        </p>
        <TextField select={true} autoFocus id="type" value={settings["type"] ? settings["type"] : "Node Property"}
            onChange={(e) => {
                handleParameterTypeUpdate(e.target.value);
            }}
            style={{ width: "25%" }} label="Selection Type"
            type="text"
            style={{ width: 335, marginLeft: "5px", marginTop: "0px" }}>
            {parameterSelectTypes.map((option) => (
                <MenuItem key={option} value={option}>
                    {option}
                </MenuItem>
            ))}
        </TextField>

        {settings.type == "Free Text" ?
          <NeoField
            label={"Name"} 
            key={"freetext"}
            value={settings["entityType"] ? settings["entityType"] : ""}
            defaultValue={""}
            placeholder={"Enter a parameter name here..."}
            style={{ width: 335, marginLeft: "5px", marginTop: "0px" }}
            onChange={(value) => {
                setLabelInputText(value);
                handleNodeLabelSelectionUpdate(value);
                handleFreeTextNameSelectionUpdate(value);
            }}
            />
            :
            <>
                <Autocomplete
                    id="autocomplete-label-type"
                    options={manualPropertyNameSpecification ? [settings['entityType']] : labelRecords.map(r => r["_fields"] ? r["_fields"][0] : "(no data)")}
                    getOptionLabel={(option) => option ? option : ""}
                    style={{ width: 335, marginLeft: "5px", marginTop: "5px" }}
                    inputValue={labelInputText}
                    onInputChange={(event, value) => {
                        setLabelInputText(value);
                        if (manualPropertyNameSpecification) {
                            handleNodeLabelSelectionUpdate(value);
                        } else {
                            if (settings["type"] == "Node Property") {
                                queryCallback("CALL db.labels() YIELD label WITH label as nodeLabel WHERE toLower(nodeLabel) CONTAINS toLower($input) RETURN DISTINCT nodeLabel LIMIT 5", { input: value }, setLabelRecords);
                            } else {
                                queryCallback("CALL db.relationshipTypes() YIELD relationshipType WITH relationshipType as relType WHERE toLower(relType) CONTAINS toLower($input) RETURN DISTINCT relType LIMIT 5", { input: value }, setLabelRecords);
                            }
                        }
                    }}
                    value={settings['entityType'] ? settings['entityType'] : undefined}
                    onChange={(event, newValue) => handleNodeLabelSelectionUpdate(newValue)}
                    renderInput={(params) => <TextField {...params} placeholder="Start typing..." InputLabelProps={{ shrink: true }} label={settings["type"] == "Node Property" ? "Node Label" : "Relationship Type"} />}
                />
                {/* Draw the property name & id selectors only after a label/type has been selected. */}
                {settings['entityType'] ?
                    <>
                        <Autocomplete
                            id="autocomplete-property"
                            options={manualPropertyNameSpecification ? [settings['propertyType']] : propertyRecords.map(r => r["_fields"] ? r["_fields"][0] : "(no data)")}
                            getOptionLabel={(option) => option ? option : ""}
                            style={{ display: "inline-block", width: 185, marginLeft: "5px", marginTop: "5px" }}
                            inputValue={propertyInputText}
                            onInputChange={(event, value) => {
                                setPropertyInputText(value);
                                if (manualPropertyNameSpecification) {
                                    handlePropertyNameSelectionUpdate(value);
                                } else {
                                    queryCallback("CALL db.propertyKeys() YIELD propertyKey as propertyName WITH propertyName WHERE toLower(propertyName) CONTAINS toLower($input) RETURN DISTINCT propertyName LIMIT 5", { input: value }, setPropertyRecords);
                                }
                            }}
                            value={settings['propertyType']}
                            onChange={(event, newValue) => handlePropertyNameSelectionUpdate(newValue)}
                            renderInput={(params) => <TextField {...params} placeholder="Start typing..." InputLabelProps={{ shrink: true }} label={"Property Name"} />}
                        />
                        <NeoField placeholder='number'
                            label="Number (optional)" disabled={!settings['propertyType']} value={settings['id']}
                            style={{ width: "135px", marginTop: "5px", marginLeft: "10px" }}
                            onChange={(value) => {
                                handleIdSelectionUpdate(value);
                            }} />
                    </> : <></>}
            </>}
        {parameterName ? <p>Use <b>${parameterName}</b> in a query to use the parameter.</p> : <></>}
    </div>;
}
Example #21
Source File: CustomReportStyleModal.tsx    From neodash with Apache License 2.0 4 votes vote down vote up
NeoCustomReportStyleModal = ({ customReportStyleModalOpen, settingName, settingValue, type, fields, setCustomReportStyleModalOpen, onReportSettingUpdate }) => {

    // The rule set defined in this modal is updated whenever the setting value is externally changed.
    const [rules, setRules] = React.useState([]);
    useEffect(() => {
        if (settingValue) {
            setRules(settingValue);
        }
    }, [settingValue])

    const handleClose = () => {
        // If no rules are specified, clear the special report setting that holds the customization rules.
        if (rules.length == 0) {
            onReportSettingUpdate(settingName, undefined);
        } else {
            onReportSettingUpdate(settingName, rules);
        }
        setCustomReportStyleModalOpen(false);
    };


    // Update a single field in one of the rules in the rule array.
    const updateRuleField = (ruleIndex, ruleField, ruleFieldValue) => {
        var newRules = [...rules];  // Deep copy
        newRules[ruleIndex][ruleField] = ruleFieldValue;
        setRules(newRules);
    }

    /**
     * Create the list of suggestions used in the autocomplete box of the rule specification window.
     * This will be dynamic based on the type of report we are customizing.
     */
    const createFieldVariableSuggestions = () => {
        if (!fields) {
            return [];
        }
        if (type == "graph" || type == "map") {
            return fields.map((node, index) => {
                if (!Array.isArray(node)) {
                    return undefined;
                }
                return fields[index].map((property, propertyIndex) => {
                    if (propertyIndex == 0) {
                        return undefined;
                    }
                    return fields[index][0] + "." + property;
                })
            }).flat().filter(e => e !== undefined);
        }
        if (type == "bar" || type == "line" || type == "pie" || type == "table" || type == "value") {
            return fields;
        }
        return [];
    }


    return (
        <div>
            {customReportStyleModalOpen ?
                <Dialog maxWidth={"xl"} open={customReportStyleModalOpen == true}
                    PaperProps={{
                        style: {
                            overflow: 'inherit'
                        },
                    }}
                    style={{ overflow: "inherit", overflowY: "inherit" }}
                    aria-labelledby="form-dialog-title">
                    <DialogTitle id="form-dialog-title">
                        <TuneIcon style={{
                            height: "30px",
                            paddingTop: "4px",
                            marginBottom: "-8px",
                            marginRight: "5px",
                            paddingBottom: "5px"
                        }} />
                        Rule-Based Styling
                        <IconButton onClick={handleClose} style={{ padding: "3px", float: "right" }}>
                            <Badge badgeContent={""} >
                                <CloseIcon />
                            </Badge>
                        </IconButton>
                    </DialogTitle>
                    <div>
                        <DialogContent style={{ overflow: "inherit" }}>
                            <p>You can define rule-based styling for the report here. <br />
                                Style rules are checked in-order and override the default behaviour - if no rules are valid, no style is applied.<br />
                                {(type == "graph" || type == "map") ? <p>For <b>{type}</b> reports, the field name should be specified in the format <code>label.name</code>, for example: <code>Person.age</code>. This is case-sensentive.</p> : <></>}
                                {(type == "line" || type == "value" || type == "bar" || type == "pie" || type == "table") ? <p>For <b>{type}</b> reports, the field name should be the exact name of the returned field. <br />For example, if your query is <code>MATCH (n:Movie) RETURN n.rating as Rating</code>, your field name is <code>Rating</code>.</p> : <></>}
                            </p>
                            <div>

                                <hr></hr>

                                <table>
                                    {rules.map((rule, index) => {
                                        return <>
                                            <tr>

                                       
                                            <td style={{ paddingLeft: "2px", paddingRight: "2px" }}><span style={{ color: "black", width: "50px" }}>{index+1}.</span></td>
                                                <td style={{ paddingLeft: "20px", paddingRight: "20px" }}><span style={{ fontWeight: "bold", color: "black", width: "50px" }}> IF</span></td>
                                                <div style={{ border: "2px dashed grey" }}>
                                                    <td style={{ paddingLeft: "5px", paddingRight: "5px", paddingTop: "5px", paddingBottom: "5px" }}>
                                                        <Autocomplete
                                                            disableClearable={true}
                                                            id="autocomplete-label-type"
                                                            noOptionsText="*Specify an exact field name"
                                                            options={createFieldVariableSuggestions().filter(e => e.toLowerCase().includes(rule['field'].toLowerCase()))}
                                                            value={rule['field'] ? rule['field'] : ""}
                                                            inputValue={rule['field'] ? rule['field'] : ""}
                                                            popupIcon={<></>}
                                                            style={{ display: "inline-block", width: 185, marginLeft: "5px", marginTop: "5px" }}
                                                            onInputChange={(event, value) => {
                                                                updateRuleField(index, 'field', value)
                                                            }}
                                                            onChange={(event, newValue) => {
                                                                updateRuleField(index, 'field', newValue)
                                                            }}
                                                            renderInput={(params) => <TextField {...params} placeholder="Field name..." InputLabelProps={{ shrink: true }} />}
                                                        />
                                                    </td>
                                                    <td style={{ paddingLeft: "5px", paddingRight: "5px" }}>
                                                        <TextField select value={rule['condition']}
                                                            onChange={(e) => updateRuleField(index, 'condition', e.target.value)}>
                                                            {RULE_CONDITIONS.map((option) => (
                                                                <MenuItem key={option.value} value={option.value}>
                                                                    {option.label}
                                                                </MenuItem>
                                                            ))}
                                                        </TextField></td>
                                                    <td style={{ paddingLeft: "5px", paddingRight: "5px" }}><TextField placeholder="Value..." value={rule['value']}
                                                        onChange={(e) => updateRuleField(index, 'value', e.target.value)}></TextField></td>
                                                </div>
                                                <td style={{ paddingLeft: "20px", paddingRight: "20px" }}><span style={{ fontWeight: "bold", color: "black", width: "50px" }}>THEN</span></td>
                                                <div style={{ border: "2px dashed grey", marginBottom: "5px" }}>
                                                    <td style={{ paddingLeft: "5px", paddingRight: "5px", paddingTop: "5px", paddingBottom: "5px" }}>
                                                        <TextField select value={rule['customization']}
                                                            onChange={(e) => updateRuleField(index, 'customization', e.target.value)}>
                                                            {RULE_BASED_REPORT_CUSTOMIZATIONS[type] && RULE_BASED_REPORT_CUSTOMIZATIONS[type].map((option) => (
                                                                <MenuItem key={option.value} value={option.value}>
                                                                    {option.label}
                                                                </MenuItem>
                                                            ))}
                                                        </TextField></td>
                                                    <td style={{ paddingLeft: "5px", paddingRight: "5px", paddingTop: "5px", paddingBottom: "5px" }}>
                                                        <TextField style={{ width: "20px", color: "black" }} disabled={true} value={'='}></TextField>
                                                    </td>
                                                    <td style={{ paddingLeft: "5px", paddingRight: "5px" }}><NeoColorPicker label="" defaultValue="black" key={undefined} style={undefined} value={rule['customizationValue']} onChange={(value) => updateRuleField(index, 'customizationValue', value)} ></NeoColorPicker></td>
                                                </div>
                                                <td>
                                                    <Fab size="small" aria-label="add" style={{ background: "black", color: "white", marginTop: "-6px", marginLeft: "20px" }}
                                                        onClick={() => {
                                                            setRules([...rules.slice(0, index), ...rules.slice(index + 1)])
                                                        }} >
                                                        <CloseIcon />
                                                    </Fab>
                                                </td>
                                                <hr />
                                            </tr></>
                                    })}

                                    <tr >
                                        <td style={{ borderBottom: "1px solid grey", width: "750px" }} colSpan={5}>
                                            <Typography variant="h3" color="primary" style={{ textAlign: "center", marginBottom: "5px" }}>
                                                <Fab size="small" aria-label="add" style={{ background: "white", color: "black" }}
                                                    onClick={() => {
                                                        const newRule = getDefaultRule(RULE_BASED_REPORT_CUSTOMIZATIONS[type][0]['value']);
                                                        setRules(rules.concat(newRule));
                                                    }} >
                                                    <AddIcon />
                                                </Fab>
                                            </Typography>

                                        </td>
                                    </tr>
                                </table>

                            </div>

                            <Button
                                style={{ float: "right", marginTop: "20px", marginBottom: "20px", backgroundColor: "white" }}
                                color="default"
                                variant="contained"
                                size="large"
                                onClick={(e) => {
                                    handleClose();
                                }}>
                                Save</Button>
                        </DialogContent>
                    </div>
                </Dialog> : <></>}
        </div>
    );
}
Example #22
Source File: CreateTaskDialog.tsx    From knboard with MIT License 4 votes vote down vote up
CreateTaskDialog = () => {
  const theme = useTheme();
  const dispatch = useDispatch();
  const labelsOptions = useSelector(selectAllLabels);
  const members = useSelector(selectAllMembers);
  const open = useSelector((state: RootState) => state.task.createDialogOpen);
  const columnId = useSelector(
    (state: RootState) => state.task.createDialogColumn
  );
  const createLoading = useSelector(
    (state: RootState) => state.task.createLoading
  );
  const [titleTouched, setTitleTouched] = useState<boolean>(false);
  const [title, setTitle] = useState<string>("");
  const [description, setDescription] = useState<string>("");
  const [assignees, setAssignees] = useState<BoardMember[]>([]);
  const [priority, setPriority] = useState<Priority | null>({
    value: "M",
    label: "Medium",
  });
  const [labels, setLabels] = useState<Label[]>([]);
  const xsDown = useMediaQuery(theme.breakpoints.down("xs"));

  const handleEditorChange = ({ text }: any) => {
    setDescription(text);
  };

  const setInitialValues = () => {
    if (columnId) {
      setTitleTouched(false);
      setTitle("");
      setDescription("");
      setAssignees([]);
      setPriority(PRIORITY_2);
      setLabels([]);
    }
  };

  useEffect(() => {
    setInitialValues();
  }, [open]);

  const handleClose = () => {
    if (window.confirm("Are you sure? Any progress made will be lost.")) {
      dispatch(setCreateDialogOpen(false));
    }
  };

  const handleCreate = async () => {
    setTitleTouched(true);
    if (columnId && priority) {
      const newTask = {
        title,
        description,
        column: columnId,
        labels: labels.map((l) => l.id),
        assignees: assignees.map((a) => a.id),
        priority: priority.value,
      };
      dispatch(createTask(newTask));
    }
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.keyCode == Key.Enter && e.metaKey) {
      handleCreate();
    }
  };

  return (
    <Dialog
      open={open}
      onClose={handleClose}
      maxWidth="sm"
      fullWidth
      keepMounted={false}
      fullScreen={xsDown}
    >
      <Content onKeyDown={handleKeyDown}>
        <DialogTitle>New issue</DialogTitle>

        <TextField
          autoFocus
          id="create-task-title"
          data-testid="create-task-title"
          label="Title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          variant="outlined"
          fullWidth
          size="small"
          onBlur={() => setTitleTouched(true)}
          error={titleTouched && !title}
        />

        <EditorWrapper>
          <MdEditor
            plugins={MD_EDITOR_PLUGINS}
            config={MD_EDITOR_CONFIG}
            value={description}
            renderHTML={(text) => mdParser.render(text)}
            onChange={handleEditorChange}
            placeholder="Describe the issue..."
          />
        </EditorWrapper>

        <Autocomplete
          multiple
          filterSelectedOptions
          disableClearable
          openOnFocus
          id="create-assignee-select"
          size="small"
          options={members}
          getOptionLabel={(option) => option.username}
          value={assignees}
          onChange={(_event, value) => setAssignees(value)}
          renderOption={(option) => <AvatarOption option={option} />}
          renderInput={(params) => (
            <TextField {...params} label="Assignees" variant="outlined" />
          )}
          renderTags={(value, getTagProps) =>
            value.map((option, index) => (
              <AvatarTag
                key={option.id}
                option={option}
                {...getTagProps({ index })}
              />
            ))
          }
          css={css`
            width: 100%;
            margin-top: 1rem;
          `}
        />

        <Autocomplete
          id="create-priority-select"
          size="small"
          autoHighlight
          options={PRIORITY_OPTIONS}
          getOptionLabel={(option) => option.label}
          value={priority}
          onChange={(_: any, value: Priority | null) => setPriority(value)}
          renderOption={(option) => <PriorityOption option={option} />}
          renderInput={(params) => (
            <TextField {...params} label="Priority" variant="outlined" />
          )}
          openOnFocus
          disableClearable
          css={css`
            width: 100%;
            margin-top: 1rem;
          `}
        />

        <Autocomplete
          multiple
          id="create-labels-select"
          size="small"
          filterSelectedOptions
          autoHighlight
          openOnFocus
          options={labelsOptions}
          getOptionLabel={(option) => option.name}
          value={labels}
          onChange={(_, newLabels) => setLabels(newLabels)}
          renderInput={(params) => (
            <TextField {...params} label="Labels" variant="outlined" />
          )}
          renderTags={(value, getTagProps) =>
            value.map((option, index) => (
              <LabelChip
                key={option.id}
                label={option}
                size="small"
                {...getTagProps({ index })}
              />
            ))
          }
          renderOption={(option) => <LabelChip label={option} size="small" />}
          css={css`
            margin-top: 1rem;
            width: 100%;
          `}
        />
      </Content>

      <Footer theme={theme}>
        <Button
          startIcon={
            createLoading ? (
              <CircularProgress color="inherit" size={16} />
            ) : (
              <FontAwesomeIcon icon={faRocket} />
            )
          }
          variant="contained"
          color="primary"
          size="small"
          onClick={handleCreate}
          disabled={createLoading}
          data-testid="task-create"
          css={css`
            ${theme.breakpoints.down("xs")} {
              flex-grow: 1;
            }
          `}
        >
          Create issue ({getMetaKey()}+⏎)
        </Button>
        <Button
          css={css`
            margin-left: 1rem;
          `}
          onClick={handleClose}
        >
          Cancel (Esc)
        </Button>
      </Footer>
    </Dialog>
  );
}
Example #23
Source File: EditTaskDialog.tsx    From knboard with MIT License 4 votes vote down vote up
EditTaskDialog = () => {
  const theme = useTheme();
  const dispatch = useDispatch();
  const columns = useSelector(selectAllColumns);
  const labels = useSelector(selectAllLabels);
  const labelsById = useSelector(selectLabelEntities);
  const columnsById = useSelector(selectColumnsEntities);
  const tasksByColumn = useSelector((state: RootState) => state.task.byColumn);
  const taskId = useSelector((state: RootState) => state.task.editDialogOpen);
  const tasksById = useSelector((state: RootState) => state.task.byId);
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");
  const [editingDescription, setEditingDescription] = useState(false);
  const titleTextAreaRef = useRef<HTMLTextAreaElement>(null);
  const wrapperRef = useRef<HTMLDivElement>(null);
  const editorRef = useRef<MdEditor>(null);
  const cancelRef = useRef<HTMLButtonElement>(null);
  const xsDown = useMediaQuery(theme.breakpoints.down("xs"));
  const open = taskId !== null;

  useEffect(() => {
    if (taskId && tasksById[taskId]) {
      setDescription(tasksById[taskId].description);
      setTitle(tasksById[taskId].title);
    }
  }, [open, taskId]);

  const handleSaveTitle = () => {
    if (taskId) {
      dispatch(patchTask({ id: taskId, fields: { title } }));
    }
  };

  const handleSaveDescription = () => {
    if (taskId) {
      dispatch(patchTask({ id: taskId, fields: { description } }));
      setEditingDescription(false);
    }
  };

  const handleCancelDescription = () => {
    if (taskId && tasksById[taskId]) {
      setDescription(tasksById[taskId].description);
      setEditingDescription(false);
    }
  };

  useEffect(() => {
    const handleClickOutside = (event: any) => {
      if (
        wrapperRef.current &&
        !wrapperRef.current.contains(event.target) &&
        cancelRef.current &&
        !cancelRef.current?.contains(event.target)
      ) {
        handleSaveDescription();
      }
    };

    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [wrapperRef, taskId, description]);

  useEffect(() => {
    if (editingDescription && editorRef && editorRef.current) {
      editorRef.current.setSelection({
        start: 0,
        end: description.length,
      });
    }
  }, [editingDescription]);

  const findTaskColumnId = () => {
    for (const columnId in tasksByColumn) {
      for (const id of tasksByColumn[columnId]) {
        if (id === taskId) {
          return columnId;
        }
      }
    }
    return null;
  };

  const columnId = findTaskColumnId();

  if (!taskId || !tasksById[taskId] || !columnId) {
    return null;
  }

  const task = tasksById[taskId];
  const column = columnsById[columnId];

  const handleEditorKeyDown = (e: React.KeyboardEvent) => {
    if (e.keyCode == Key.Enter && e.metaKey) {
      handleSaveDescription();
    }
    if (e.keyCode === Key.Escape) {
      // Prevent propagation from reaching the Dialog
      e.stopPropagation();
      handleCancelDescription();
    }
  };

  const handleTitleKeyDown = (e: React.KeyboardEvent) => {
    if (e.keyCode === Key.Enter) {
      e.preventDefault();
      titleTextAreaRef?.current?.blur();
    }
    if (e.keyCode === Key.Escape) {
      // Prevent propagation from reaching the Dialog
      e.stopPropagation();
    }
  };

  const handleClose = () => {
    dispatch(setEditDialogOpen(null));
    setEditingDescription(false);
  };

  const handleTitleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setTitle(e.target.value);
  };

  const handleColumnChange = (_: any, value: IColumn | null) => {
    if (!column || !value || column.id === value.id) {
      return;
    }
    const current: Id[] = [...tasksByColumn[column.id]];
    const next: Id[] = [...tasksByColumn[value.id]];

    const currentId = current.indexOf(task.id);
    const newPosition = 0;

    // remove from original
    current.splice(currentId, 1);
    // insert into next
    next.splice(newPosition, 0, task.id);

    const updatedTasksByColumn: TasksByColumn = {
      ...tasksByColumn,
      [column.id]: current,
      [value.id]: next,
    };
    dispatch(updateTasksByColumn(updatedTasksByColumn));
    handleClose();
  };

  const handlePriorityChange = (_: any, priority: Priority | null) => {
    if (priority) {
      dispatch(patchTask({ id: taskId, fields: { priority: priority.value } }));
    }
  };

  const handleNotImplemented = () => {
    dispatch(createInfoToast("Not implemented yet ?"));
  };

  const handleDelete = () => {
    if (window.confirm("Are you sure? Deleting a task cannot be undone.")) {
      dispatch(deleteTask(task.id));
      handleClose();
    }
  };

  const handleDescriptionClick = () => {
    setEditingDescription(true);
  };

  const handleEditorChange = ({ text }: any) => {
    setDescription(text);
  };

  const handleLabelsChange = (newLabels: Label[]) => {
    dispatch(
      patchTask({
        id: taskId,
        fields: { labels: newLabels.map((label) => label.id) },
      })
    );
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    // don't listen for input when inputs are focused
    if (
      document.activeElement instanceof HTMLInputElement ||
      document.activeElement instanceof HTMLTextAreaElement
    ) {
      return;
    }

    if (e.key === "Backspace" && e.metaKey) {
      handleDelete();
    }

    if (e.key === "Escape" && e.metaKey) {
      handleClose();
    }

    if (e.key === "l" && e.metaKey) {
      e.preventDefault();
      handleNotImplemented();
    }
  };

  return (
    <Dialog
      open={open}
      onClose={handleClose}
      onKeyDown={handleKeyDown}
      fullWidth
      keepMounted={false}
      fullScreen={xsDown}
      css={css`
        .MuiDialog-paper {
          max-width: 920px;
        }
      `}
    >
      <Content theme={theme}>
        <Close onClose={handleClose} />
        <Main>
          <Header>id: {task.id}</Header>
          <Title>
            <FontAwesomeIcon icon={faArrowUp} />
            <TextareaAutosize
              ref={titleTextAreaRef}
              value={title}
              onChange={handleTitleChange}
              onBlur={handleSaveTitle}
              onKeyDown={handleTitleKeyDown}
              data-testid="task-title"
            />
          </Title>
          <DescriptionHeader>
            <FontAwesomeIcon icon={faAlignLeft} />
            <h3>Description</h3>
          </DescriptionHeader>
          <Description
            key={`${taskId}${editingDescription}`}
            data-testid="task-description"
          >
            <EditorWrapper
              onDoubleClick={
                editingDescription ? undefined : handleDescriptionClick
              }
              editing={editingDescription}
              ref={wrapperRef}
              theme={theme}
              onKeyDown={handleEditorKeyDown}
            >
              <MdEditor
                ref={editorRef}
                plugins={MD_EDITOR_PLUGINS}
                config={
                  editingDescription ? MD_EDITING_CONFIG : MD_READ_ONLY_CONFIG
                }
                value={
                  editingDescription
                    ? description
                    : description || DESCRIPTION_PLACEHOLDER
                }
                renderHTML={(text) => mdParser.render(text)}
                onChange={handleEditorChange}
                placeholder={DESCRIPTION_PLACEHOLDER}
              />
            </EditorWrapper>
            {editingDescription && (
              <DescriptionActions>
                <Button
                  variant="contained"
                  data-testid="save-description"
                  onClick={handleSaveDescription}
                  color="primary"
                  size="small"
                >
                  Save ({getMetaKey()}+⏎)
                </Button>
                <Button
                  variant="outlined"
                  data-testid="cancel-description"
                  onClick={handleCancelDescription}
                  ref={cancelRef}
                  size="small"
                  css={css`
                    margin-left: 0.5rem;
                  `}
                >
                  Cancel (Esc)
                </Button>
              </DescriptionActions>
            )}
          </Description>
          <CommentSection taskId={task.id} />
        </Main>
        <Side theme={theme}>
          <TaskAssignees task={task} />
          <Autocomplete
            id="column-select"
            size="small"
            options={columns}
            getOptionLabel={(option) => option.title}
            renderInput={(params) => (
              <TextField {...params} label="Column" variant="outlined" />
            )}
            value={column}
            onChange={handleColumnChange}
            disableClearable
            openOnFocus
            data-testid="edit-column"
            css={css`
              width: 100%;
            `}
          />
          <Autocomplete
            id="priority-select"
            size="small"
            blurOnSelect
            autoHighlight
            options={PRIORITY_OPTIONS}
            getOptionLabel={(option) => option.label}
            value={PRIORITY_MAP[task.priority]}
            onChange={handlePriorityChange}
            renderInput={(params) => (
              <TextField {...params} label="Priority" variant="outlined" />
            )}
            renderOption={(option) => <PriorityOption option={option} />}
            openOnFocus
            disableClearable
            data-testid="edit-priority"
            css={css`
              width: 100%;
              margin-top: 1rem;
            `}
          />
          <Autocomplete
            multiple
            id="labels-select"
            data-testid="edit-labels"
            size="small"
            filterSelectedOptions
            autoHighlight
            openOnFocus
            blurOnSelect
            disableClearable
            options={labels}
            getOptionLabel={(option) => option.name}
            value={
              tasksById[taskId].labels.map(
                (labelId) => labelsById[labelId]
              ) as Label[]
            }
            onChange={(_, newLabels) => handleLabelsChange(newLabels)}
            renderInput={(params) => (
              <TextField {...params} label="Labels" variant="outlined" />
            )}
            renderTags={(value, getTagProps) =>
              value.map((option, index) => (
                <LabelChip
                  key={option.id}
                  label={option}
                  size="small"
                  {...getTagProps({ index })}
                />
              ))
            }
            renderOption={(option) => <LabelChip label={option} size="small" />}
            css={css`
              width: 100%;
              margin-top: 1rem;
              margin-bottom: 2rem;
            `}
          />
          <ButtonsContainer>
            <Button
              startIcon={<FontAwesomeIcon fixedWidth icon={faLock} />}
              onClick={handleNotImplemented}
              size="small"
              css={css`
                font-size: 12px;
                font-weight: bold;
                color: ${TASK_G};
              `}
            >
              Lock task ({getMetaKey()}+L)
            </Button>
            <Button
              startIcon={<FontAwesomeIcon fixedWidth icon={faTrash} />}
              onClick={handleDelete}
              data-testid="delete-task"
              size="small"
              css={css`
                font-size: 12px;
                font-weight: bold;
                color: ${TASK_G};
                margin-bottom: 2rem;
              `}
            >
              Delete task ({getMetaKey()}+⌫)
            </Button>
          </ButtonsContainer>
          <Text>
            Updated {formatDistanceToNow(new Date(task.modified))} ago
          </Text>
          <Text
            css={css`
              margin-bottom: 1rem;
            `}
          >
            Created {formatDistanceToNow(new Date(task.created))} ago
          </Text>
        </Side>
      </Content>
    </Dialog>
  );
}
Example #24
Source File: SearchBox.tsx    From log4brains with Apache License 2.0 4 votes vote down vote up
export function SearchBox(props: SearchBoxProps) {
  const classes = useStyles();

  const {
    onOpen,
    onClose,
    open: openProp,
    onQueryChange,
    query,
    results,
    loading = false,
    ...otherProps
  } = props;

  const [open, setOpenState] = useControlled({
    controlled: openProp,
    default: false,
    name: "SearchBox",
    state: "open"
  });

  const handleOpen = (event: React.ChangeEvent<{}>) => {
    if (open) {
      return;
    }
    setOpenState(true);
    if (onOpen) {
      onOpen(event);
    }
  };

  const router = useRouter();

  const handleClose = (
    event: React.ChangeEvent<{}>,
    reason: AutocompleteCloseReason
  ) => {
    if (!open) {
      return;
    }
    setOpenState(false);
    if (onClose) {
      onClose(event, reason);
    }
  };

  let noOptionsText: React.ReactNode = "Type to start searching";
  if (loading) {
    noOptionsText = (
      <div style={{ textAlign: "center" }}>
        <CircularProgress size={20} />
      </div>
    );
  } else if (query) {
    noOptionsText = "No matching documents";
  }

  return (
    <Autocomplete
      {...otherProps}
      classes={{ paper: classes.acPaper }}
      options={results ?? []}
      getOptionLabel={(result) => result.title}
      renderInput={(params) => (
        <SearchBar
          {...params}
          open={open}
          onClear={(event) =>
            onQueryChange && onQueryChange(event, "", "clear")
          }
          className={classes.searchBar}
        />
      )}
      inputValue={query}
      onInputChange={(event, value, reason) => {
        // We don't want to replace the inputValue by the selected value
        if (reason !== "reset" && onQueryChange) {
          onQueryChange(event, value, reason);
        }
      }}
      open={open}
      onOpen={handleOpen}
      onClose={handleClose}
      filterOptions={(r) => r} // We hijack Autocomplete's behavior to display search results as options
      renderOption={(result) => (
        <>
          <SvgIcon fontSize="small">
            <AdrIcon />
          </SvgIcon>
          <Typography className={classes.resultTitle}>
            {result.title}
          </Typography>
        </>
      )}
      noOptionsText={noOptionsText}
      onChange={async (_, result) => {
        if (result) {
          await router.push(result.href);
        }
      }}
    />
  );
}
Example #25
Source File: TagsMenuPopover.tsx    From vscode-crossnote with GNU Affero General Public License v3.0 4 votes vote down vote up
export function TagsMenuPopover(props: Props) {
  const classes = useStyles(props);
  const { t } = useTranslation();
  const [tagName, setTagName] = useState<string>("");
  const [options, setOptions] = useState<string[]>([]);

  const addTag = useCallback(
    (tagName: string) => {
      if (!tagName || tagName.trim().length === 0) {
        return;
      }
      props.addTag(tagName);
      setTagName("");
    },
    [props]
  );

  useEffect(() => {
    setTagName("");
  }, [props.anchorElement]);

  useEffect(() => {
    if (!props.anchorElement) {
      return;
    }
    let options: string[] = [];
    const helper = (children: TagNode[]) => {
      if (!children || !children.length) {
        return;
      }
      for (let i = 0; i < children.length; i++) {
        const tag = children[i].path;
        options.push(tag);
        helper(children[i].children);
      }
    };
    helper(props.notebookTagNode.children);
    setOptions(options);
  }, [props.notebookTagNode, props]);

  return (
    <Popover
      open={Boolean(props.anchorElement)}
      anchorEl={props.anchorElement}
      keepMounted
      onClose={props.onClose}
    >
      <List>
        <ListItem
          className={clsx(classes.menuItemOverride, classes.menuItemTextField)}
        >
          <Autocomplete
            inputValue={tagName}
            onInputChange={(event, newInputValue) => {
              setTagName(newInputValue);
            }}
            options={(tagName.trim().length > 0 &&
            options.findIndex((x) => x === tagName.trim()) < 0
              ? [tagName, ...options]
              : options
            ).map((opt) => "+  " + opt)}
            style={{ width: 300, maxWidth: "100%" }}
            value={""}
            onChange={(event: any, newValue: string = "") => {
              if (newValue) {
                addTag(newValue.replace(/^+/, "").trim());
              }
            }}
            renderInput={(params) => (
              <TextField
                placeholder={t("general/add-a-tag")}
                fullWidth={true}
                autoFocus={true}
                onKeyUp={(event) => {
                  if (event.which === 13) {
                    addTag(tagName);
                  }
                }}
                {...params}
              ></TextField>
            )}
            noOptionsText={t("general/no-tags")}
            openText={t("general/Open")}
            closeText={t("general/close")}
            loadingText={t("general/loading")}
            clearText={t("general/clear-all")}
          ></Autocomplete>
        </ListItem>
        {props.tagNames.length > 0 ? (
          props.tagNames.map((tagName) => {
            return (
              <ListItem
                key={tagName}
                className={clsx(classes.menuItemOverride)}
              >
                <Box
                  style={{
                    display: "flex",
                    flexDirection: "row",
                    alignItems: "center",
                    justifyContent: "space-between",
                    width: "100%",
                  }}
                >
                  <Typography>{tagName}</Typography>
                  <IconButton onClick={() => props.deleteTag(tagName)}>
                    <TrashCan></TrashCan>
                  </IconButton>
                </Box>
              </ListItem>
            );
          })
        ) : (
          <ListItem className={clsx(classes.menuItemOverride)}>
            <Typography style={{ margin: "8px 0" }}>
              {t("general/no-tags")}
            </Typography>
          </ListItem>
        )}
      </List>
    </Popover>
  );
}
Example #26
Source File: index.tsx    From frontegg-react with MIT License 4 votes vote down vote up
Select: FC<SelectProps> = (props) => {
  const styles = useStyles();
  const { t } = useT();
  const p = mapper(props);
  const [open, setOpen] = useState(false);

  const {
    size,
    value,
    loading,
    onChange,
    options,
    onOpen,
    onBlur,
    onClose,
    multiple,
    fullWidth,
    loadingText,
    renderOption,
    noOptionsText,
    getOptionLabel,
    open: propOpen,
  } = p;
  const color: any = p.color;
  const [remountCount, setRemountCount] = useState(0);
  const refresh = () => setRemountCount(remountCount + 1);

  const handleChange = useCallback(
    (e, newValue, reson) => {
      onChange?.(e, newValue, reson);
    },
    [onChange]
  );

  const renderTag = useCallback(
    (option, getTagProps, index) => {
      const state = { ...getTagProps({ index }) };
      return renderOption ? (
        <React.Fragment key={index}>{renderOption(option, state)}</React.Fragment>
      ) : (
        <Chip size={size} disabled={true} label={option?.label} {...getTagProps({ index })} />
      );
    },
    [renderOption]
  );

  return (
    <Autocomplete
      {...p}
      className={classNames({ [styles.inForm]: props.inForm })}
      multiple={multiple ?? false}
      options={options}
      size={size}
      value={value}
      loading={loading}
      {...(multiple && { disableCloseOnSelect: true })}
      filterSelectedOptions
      open={propOpen ?? open}
      noOptionsText={noOptionsText ?? t('common.empty-items')}
      loadingText={loadingText ?? `${t('common.loading')}...`}
      onOpen={(e) => (onOpen ? onOpen(e) : setOpen(true))}
      onClose={(e, reson) => (onClose ? onClose(e, reson) : setOpen(false))}
      onChange={(e, newValue, reson) => {
        handleChange(e, newValue, reson);
        setTimeout(() => refresh());
      }}
      getOptionSelected={(option: any, value: any) => option.value === value.value}
      getOptionLabel={(option: any) => (getOptionLabel ? getOptionLabel(option) : option.label)}
      renderTags={(tagValue, getTagProps) => tagValue.map((option, index) => renderTag(option, getTagProps, index))}
      renderInput={(params) => (
        <TextField
          {...params}
          name={props.name}
          label={props.label}
          fullWidth={fullWidth ?? true}
          style={{ minWidth: `${fullWidth ? '100%' : '14em'}` }}
          variant='outlined'
          color={color}
          InputProps={{
            ...params.InputProps,
            endAdornment: (
              <React.Fragment>
                {loading ? <CircularProgress color='inherit' size={20} /> : null}
                {params.InputProps.endAdornment}
              </React.Fragment>
            ),
          }}
        />
      )}
    />
  );
}
Example #27
Source File: SelectionPicker.tsx    From clearflask with Apache License 2.0 4 votes vote down vote up
render() {
    const DropdownIcon = this.props.dropdownIcon || ArrowDropDownIcon;
    const onInputChange = (e, val: string, reason: AutocompleteInputChangeReason) => {
      if (reason === 'reset') return; // Prevent setting text value in textfield
      if (this.props.onInputChange) {
        this.props.onInputChange(val, reason);
      }
      if (this.props.inputValue === undefined) {
        this.setState({ inputValue: val });
      }
    };
    const renderTags = (value: Label[], getTagProps?: AutocompleteGetTagProps) => this.props.showTags === false ? null : value.map((option, index) => (
      <Fade key={option.value} in={true}>
        {this.props.bareTags ? (
          <div
            className={classNames(
              this.props.classes.chip,
              this.props.classes.chipBare,
            )}
            style={{
              color: option.color,
            }}
            {...(getTagProps ? getTagProps({ index }) : {})}
          >{option.label}</div>
        ) : (
          <Chip
            className={this.props.classes.chip}
            variant='outlined'
            label={option.label}
            size='small'
            style={{
              color: option.color,
            }}
            {...(getTagProps ? getTagProps({ index }) : {})}
          />
        )}
      </Fade>
    ));
    return (
      <Autocomplete<LabelInternal, boolean, boolean, boolean>
        freeSolo={!this.props.disableInput}
        autoHighlight
        multiple={!!this.props.isMulti}
        value={this.props.isMulti ? this.props.value : (this.props.value[0] || null)}
        onChange={(e, val, reason) => {
          // Convert create label to create-option
          var createLabel: LabelInternal | undefined;
          if (this.props.isMulti && val instanceof Array) {
            createLabel = (val as LabelInternal[]).find(label => label.isCreateOption);
          } else if (!this.props.isMulti && val instanceof Object && (val as LabelInternal).isCreateOption) {
            createLabel = (val as LabelInternal);
          }
          if (createLabel) {
            reason = 'create-option';
            val = createLabel.value;
          }

          if (reason === 'create-option') {
            var createdText: string = '';
            if (typeof val === 'string') {
              createdText = val;
            } else if (Array.isArray(val)) {
              const valParsed = val[val.length - 1];
              if (typeof valParsed === 'string') {
                createdText = valParsed;
              }
            }
            !!createdText && this.props.onValueCreate && this.props.onValueCreate(createdText);
            onInputChange(undefined, '', 'clear');
          } else if (reason === 'clear' || reason === 'blur') {
            this.props.onValueChange([]);
          } else if (reason === 'select-option' || reason === 'remove-option') {
            this.props.onValueChange(!val ? [] : (this.props.isMulti ? val as Label[] : [val as Label]));
            if (!this.props.disableClearOnValueChange) {
              onInputChange(undefined, '', 'clear');
            }
          }
        }}
        disableCloseOnSelect={this.props.disableCloseOnSelect}
        filterSelectedOptions={true}
        filterOptions={(options, params) => {
          // Sometimes autocomplete decides to pre-filter, so use options from props
          var filtered: LabelInternal[] = [...this.props.options];

          if (!this.props.disableFilter) {
            filtered = filterOptions(options, params);
          }

          // Suggest the creation of a new value
          if (!!this.props.onValueCreate && params.inputValue !== '') {
            const createLabel = {
              label: this.props.formatCreateLabel
                ? this.props.formatCreateLabel(params.inputValue)
                : `Add "${params.inputValue}"`,
              value: params.inputValue,
              groupBy: '__EMPTY__',
              isCreateOption: true,
            };
            if (this.props.showCreateAtTop) {
              filtered.unshift(createLabel);
            } else {
              filtered.push(createLabel);
            }
          }

          // Header
          if (!!this.props.formatHeader) {
            const header = this.props.formatHeader(params.inputValue);
            if (header) {
              filtered.unshift({
                label: header,
                value: '__HEADER__',
                groupBy: '__HEADER__',
                disabled: true,
              });
            }
          }

          // Add loading
          if (this.props.loading) {
            filtered.push({
              label: 'Loading…',
              value: '__HEADER__',
              groupBy: '__HEADER__',
              disabled: true,
            });
          }

          if (this.props.noOptionsMessage && filtered.length === 0) {
            filtered.push({
              label: this.props.noOptionsMessage,
              value: '__HEADER__',
              groupBy: '__HEADER__',
              disabled: true,
            });
          }

          return filtered;
        }}
        popupIcon={DropdownIcon === null ? null : (
          <DropdownIcon
            fontSize='small'
            color='inherit'
            className={this.props.value.length > 0
              ? this.props.classes.dropdownIconWithTags
              : this.props.classes.dropdownIconWithoutTags}
          />
        )}
        forcePopupIcon={this.props.forceDropdownIcon !== undefined ? this.props.forceDropdownIcon : (!!this.props.dropdownIcon || 'auto')}
        options={this.props.options}
        getOptionLabel={option => option.filterString || option.value}
        getOptionSelected={(option, value) => option.value === value.value}
        inputValue={this.props.inputValue !== undefined
          ? this.props.inputValue
          : (this.state.inputValue || '')}
        onInputChange={onInputChange}
        style={this.props.style}
        className={this.props.className}
        limitTags={this.props.limitTags}
        disabled={this.props.disabled}
        getOptionDisabled={option => !!option.disabled}
        groupBy={this.props.group ? (label: Label) => label.groupBy || label.value[0] : undefined}
        renderGroup={this.props.group ? (params: AutocompleteRenderGroupParams) => (
          <div className={classNames(this.props.classes.group, params.group === '__HEADER__' && this.props.classes.header)}>
            {params.group && params.group !== '__EMPTY__' && params.group !== '__HEADER__' && (
              <Typography key={params.key} variant='overline' className={this.props.classes.menuHeader}>{params.group}</Typography>
            )}
            {params.children}
          </div>
        ) : undefined}
        getLimitTagsText={this.props.limitTags === 0 ? more => null : undefined}
        handleHomeEndKeys
        openOnFocus
        onFocus={this.props.onFocus}
        onBlur={e => {
          if (this.props.clearOnBlur) {
            onInputChange(undefined, '', 'clear');
          }
        }}
        renderOption={(option: Label, { selected }) => (
          <Typography
            noWrap
            style={{
              fontWeight: selected ? 'bold' : undefined,
              color: option.color,
            }}
            component='div'
          >
            {(option.value !== '__HEADER__' && this.props.renderOption)
              ? this.props.renderOption(option, selected)
              : option.label}
          </Typography>
        )}
        renderTags={renderTags}
        open={this.props.menuIsOpen}
        disableClearable={this.props.disableClearable || this.props.value.length === 0}
        onOpen={this.props.menuOnChange ? () => this.props.menuOnChange && this.props.menuOnChange(true) : undefined}
        onClose={this.props.menuOnChange ? () => this.props.menuOnChange && this.props.menuOnChange(false) : undefined}
        classes={{
          ...this.props.autocompleteClasses,
          root: classNames(this.props.classes.autocomplete, this.props.autocompleteClasses?.root),
          focused: classNames(this.props.classes.autocompleteFocused, this.props.autocompleteClasses?.focused),
          popupIndicatorOpen: classNames(!!this.props.dropdownIcon && this.props.classes.dropdownIconDontFlip, this.props.autocompleteClasses?.popupIndicator),
          endAdornment: classNames(this.props.classes.endAdornment, this.props.autocompleteClasses?.endAdornment),
          input: classNames(this.props.classes.input, this.props.autocompleteClasses?.input),
          inputRoot: classNames(this.props.classes.inputRoot, this.props.autocompleteClasses?.inputRoot),
          popper: classNames(this.props.classes.popper, this.props.autocompleteClasses?.popper),
          listbox: classNames(this.props.classes.popperListbox, this.props.autocompleteClasses?.listbox),
          tag: classNames(this.props.classes.tag, this.props.autocompleteClasses?.tag),
          clearIndicator: classNames(!!this.props.clearIndicatorNeverHide && this.props.classes.clearIndicatorNeverHide, this.props.autocompleteClasses?.clearIndicator),
        }}
        PopperComponent={getSelectionPopper(this.props.PopperProps)}
        renderInput={(params) => {
          // Remove limitTags span element since it's just taking up space
          const paramsStartAdornment = (params.InputProps.startAdornment as Array<any>);
          if (this.props.limitTags === 0
            && paramsStartAdornment
            && paramsStartAdornment[0]['type'] === 'span') {
            paramsStartAdornment.shift();
          }
          const TextFieldCmpt = this.props.TextFieldComponent || TextField;
          return (
            <TextFieldCmpt
              label={this.props.label}
              helperText={this.props.errorMsg || this.props.helperText}
              placeholder={(!!this.props.bareTags && this.props.value.length > 0)
                ? undefined
                : this.props.placeholder}
              error={!!this.props.errorMsg}
              {...params}
              {...this.props.TextFieldProps}
              InputLabelProps={{
                ...params.InputLabelProps,
                ...this.props.TextFieldProps?.InputLabelProps,
              }}
              inputProps={{
                ...params.inputProps,
                ...this.props.TextFieldProps?.inputProps,
                style: {
                  minWidth: this.props.inputMinWidth === undefined
                    ? (this.props.disableInput ? 0 : 50)
                    : this.props.inputMinWidth,
                  ...this.props.TextFieldProps?.inputProps?.style,
                  maxHeight: this.props.disableInput ? 20 : undefined,
                },
              }}
              InputProps={{
                ...params.InputProps,
                ...this.props.TextFieldProps?.InputProps,
                readOnly: this.props.disableInput || this.props.TextFieldProps?.InputProps?.readOnly,
                startAdornment: (
                  <>
                    {this.props.TextFieldProps?.InputProps?.endAdornment || null}
                    {!!this.props.showTags
                      && !this.props.isMulti
                      && this.props.value.length > 0
                      && renderTags(this.props.value)}
                    {params.InputProps.startAdornment}
                    {!!this.props.alwaysWrapChipsInput && !!paramsStartAdornment?.length && (
                      <div className={this.props.classes.flexWrapBreak} />
                    )}
                  </>
                ),
                endAdornment: (
                  <>
                    {this.props.TextFieldProps?.InputProps?.endAdornment || null}
                    {params.InputProps.endAdornment}
                  </>
                ),
              }}
            />
          );
        }}
      />
    );
  }
Example #28
Source File: Organization.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
Organization: React.FC = () => {
  const {
    apiGet,
    apiPut,
    apiPost,
    user,
    setFeedbackMessage
  } = useAuthContext();
  const { organizationId } = useParams<{ organizationId: string }>();
  const [organization, setOrganization] = useState<OrganizationType>();
  const [tags, setTags] = useState<AutocompleteType[]>([]);
  const [userRoles, setUserRoles] = useState<Role[]>([]);
  const [scanTasks, setScanTasks] = useState<ScanTask[]>([]);
  const [scans, setScans] = useState<Scan[]>([]);
  const [scanSchema, setScanSchema] = useState<ScanSchema>({});
  const [newUserValues, setNewUserValues] = useState<{
    firstName: string;
    lastName: string;
    email: string;
    organization?: OrganizationType;
    role: string;
  }>({
    firstName: '',
    lastName: '',
    email: '',
    role: ''
  });
  const classes = useStyles();
  const [tagValue, setTagValue] = React.useState<AutocompleteType | null>(null);
  const [inputValue, setInputValue] = React.useState('');
  const [dialog, setDialog] = React.useState<{
    open: boolean;
    type?: 'rootDomains' | 'ipBlocks' | 'tags';
    label?: string;
  }>({ open: false });

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  if (!organization) return null;

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

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

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

  return (
    <div>
      <div className={classes.header}>
        <h1 className={classes.headerLabel}>
          <Link to="/organizations">Organizations</Link>
          {organization.parent && (
            <>
              <ChevronRight></ChevronRight>
              <Link to={'/organizations/' + organization.parent.id}>
                {organization.parent.name}
              </Link>
            </>
          )}
          <ChevronRight
            style={{
              verticalAlign: 'middle',
              lineHeight: '100%',
              fontSize: '26px'
            }}
          ></ChevronRight>
          <span style={{ color: '#07648D' }}>{organization.name}</span>
        </h1>
        <Subnav
          items={navItems}
          styles={{
            background: '#F9F9F9'
          }}
        ></Subnav>
      </div>
      <div className={classes.root}>
        <Switch>
          <Route
            path="/organizations/:organizationId"
            exact
            render={() => views[0]}
          />
          <Route
            path="/organizations/:organizationId/members"
            render={() => views[1]}
          />
          <Route
            path="/organizations/:organizationId/teams"
            render={() => views[2]}
          />
          <Route
            path="/organizations/:organizationId/scans"
            render={() => views[3]}
          />
        </Switch>
      </div>
    </div>
  );
}
Example #29
Source File: Header.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
HeaderNoCtx: React.FC<ContextType> = (props) => {
  const { searchTerm, setSearchTerm } = props;
  const classes = useStyles();
  const history = useHistory();
  const location = useLocation();
  const {
    currentOrganization,
    setOrganization,
    showAllOrganizations,
    setShowAllOrganizations,
    user,
    logout,
    apiGet
  } = useAuthContext();
  const [navOpen, setNavOpen] = useState(false);
  const [organizations, setOrganizations] = useState<
    (Organization | OrganizationTag)[]
  >([]);
  const theme = useTheme();
  const isSmall = useMediaQuery(theme.breakpoints.down('md'));

  let userLevel = 0;
  if (user && user.isRegistered) {
    if (user.userType === 'standard') {
      userLevel = STANDARD_USER;
    } else {
      userLevel = GLOBAL_ADMIN;
    }
  }

  const fetchOrganizations = useCallback(async () => {
    try {
      const rows = await apiGet<Organization[]>('/organizations/');
      let tags: (OrganizationTag | Organization)[] = [];
      if (userLevel === GLOBAL_ADMIN) {
        tags = await apiGet<OrganizationTag[]>('/organizations/tags');
      }
      setOrganizations(tags.concat(rows));
    } catch (e) {
      console.error(e);
    }
  }, [apiGet, setOrganizations, userLevel]);

  React.useEffect(() => {
    if (userLevel > 0) {
      fetchOrganizations();
    }
  }, [fetchOrganizations, userLevel]);

  const navItems: NavItemType[] = [
    {
      title: 'Overview',
      path: '/',
      users: ALL_USERS,
      exact: true
    },
    {
      title: 'Inventory',
      path: '/inventory',
      users: ALL_USERS,
      exact: false
    },
    { title: 'Feeds', path: '/feeds', users: ALL_USERS, exact: false },
    {
      title: 'Scans',
      path: '/scans',
      users: GLOBAL_ADMIN,
      exact: true
    }
  ].filter(({ users }) => (users & userLevel) > 0);

  const userMenu: NavItemType = {
    title: (
      <div className={classes.userLink}>
        <UserIcon /> My Account <ArrowDropDown />
      </div>
    ),
    path: '#',
    exact: false,
    nested: [
      {
        title: 'Manage Organizations',
        path: '/organizations',
        users: GLOBAL_ADMIN,
        exact: true
      },
      {
        title: 'My Organizations',
        path: '/organizations',
        users: STANDARD_USER,
        exact: true
      },
      {
        title: 'Manage Users',
        path: '/users',
        users: GLOBAL_ADMIN,
        exact: true
      },
      {
        title: 'My Settings',
        path: '/settings',
        users: ALL_USERS,
        exact: true
      },
      {
        title: 'Logout',
        path: '/settings',
        users: ALL_USERS,
        onClick: logout,
        exact: true
      }
    ].filter(({ users }) => (users & userLevel) > 0)
  };

  const userItemsSmall: NavItemType[] = [
    {
      title: 'My Account',
      path: '#',
      users: ALL_USERS,
      exact: true
    },
    {
      title: 'Manage Organizations',
      path: '/organizations',
      users: GLOBAL_ADMIN,
      exact: true
    },
    {
      title: 'My Organizations',
      path: '/organizations',
      users: STANDARD_USER,
      exact: true
    },
    {
      title: 'Manage Users',
      path: '/users',
      users: GLOBAL_ADMIN,
      exact: true
    },
    {
      title: 'My Settings',
      path: '/settings',
      users: ALL_USERS,
      exact: true
    },
    {
      title: 'Logout',
      path: '/',
      users: ALL_USERS,
      onClick: logout,
      exact: true
    }
  ].filter(({ users }) => (users & userLevel) > 0);

  const desktopNavItems: JSX.Element[] = navItems.map((item) => (
    <NavItem key={item.title.toString()} {...item} />
  ));

  const navItemsToUse = () => {
    if (isSmall) {
      return userItemsSmall;
    } else {
      return navItems;
    }
  };

  return (
    <div>
      <AppBar position="static" elevation={0}>
        <div className={classes.inner}>
          <Toolbar>
            <Link to="/">
              <img
                src={logo}
                className={classes.logo}
                alt="Crossfeed Icon Navigate Home"
              />
            </Link>
            <div className={classes.lgNav}>{desktopNavItems.slice()}</div>

            <div className={classes.spacing} />

            {userLevel > 0 && (
              <>
                <SearchBar
                  initialValue={searchTerm}
                  value={searchTerm}
                  onChange={(value) => {
                    if (location.pathname !== '/inventory')
                      history.push('/inventory?q=' + value);
                    setSearchTerm(value, {
                      shouldClearFilters: false,
                      autocompleteResults: false
                    });
                  }}
                />
                {organizations.length > 1 && (
                  <>
                    <div className={classes.spacing} />
                    <Autocomplete
                      options={[{ name: 'All Organizations' }].concat(
                        organizations
                      )}
                      autoComplete={false}
                      className={classes.selectOrg}
                      classes={{
                        option: classes.option
                      }}
                      value={
                        showAllOrganizations
                          ? { name: 'All Organizations' }
                          : currentOrganization ?? undefined
                      }
                      filterOptions={(options, state) => {
                        // If already selected, show all
                        if (
                          options.find(
                            (option) =>
                              option.name.toLowerCase() ===
                              state.inputValue.toLowerCase()
                          )
                        ) {
                          return options;
                        }
                        return options.filter((option) =>
                          option.name
                            .toLowerCase()
                            .includes(state.inputValue.toLowerCase())
                        );
                      }}
                      disableClearable
                      blurOnSelect
                      selectOnFocus
                      getOptionLabel={(option) => option.name}
                      renderOption={(option) => (
                        <React.Fragment>{option.name}</React.Fragment>
                      )}
                      onChange={(
                        event: any,
                        value:
                          | Organization
                          | {
                              name: string;
                            }
                          | undefined
                      ) => {
                        if (value && 'id' in value) {
                          setOrganization(value);
                          setShowAllOrganizations(false);
                        } else {
                          setShowAllOrganizations(true);
                        }
                      }}
                      renderInput={(params) => (
                        <TextField
                          {...params}
                          variant="outlined"
                          inputProps={{
                            ...params.inputProps,
                            id: 'autocomplete-input',
                            autoComplete: 'new-password' // disable autocomplete and autofill
                          }}
                        />
                      )}
                    />
                  </>
                )}
                {isSmall ? null : <NavItem {...userMenu} />}
              </>
            )}
            <IconButton
              edge="start"
              className={classes.menuButton}
              aria-label="toggle mobile menu"
              color="inherit"
              onClick={() => setNavOpen((open) => !open)}
            >
              <MenuIcon />
            </IconButton>
          </Toolbar>
        </div>
      </AppBar>

      <Drawer
        anchor="right"
        open={navOpen}
        onClose={() => setNavOpen(false)}
        data-testid="mobilenav"
      >
        <List className={classes.mobileNav}>
          {navItemsToUse().map(({ title, path, nested, onClick }) => (
            <React.Fragment key={title.toString()}>
              {path && (
                <ListItem
                  button
                  exact
                  component={NavLink}
                  to={path}
                  activeClassName={classes.activeMobileLink}
                  onClick={onClick ? onClick : undefined}
                >
                  {title}
                </ListItem>
              )}
              {nested?.map((nested) => (
                <ListItem
                  button
                  exact
                  key={nested.title.toString()}
                  component={NavLink}
                  to={nested.onClick ? '#' : nested.path}
                  activeClassName={classes.activeMobileLink}
                  onClick={nested.onClick ? nested.onClick : undefined}
                >
                  {nested.title}
                </ListItem>
              ))}
            </React.Fragment>
          ))}
        </List>
      </Drawer>
    </div>
  );
}