components#Table TypeScript Examples

The following examples show how to use components#Table. 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: AdminPage.tsx    From solo with MIT License 6 votes vote down vote up
AdminPage: React.FC = () => {
  const {
    //loadingStatus,
    modifyWarehouseUser,
    updateUserPermissons,
    // pageCount,
    users
  } = useWarehouseUsers();

  const tableColumns = useMemo(
    () => createColumns(modifyWarehouseUser, updateUserPermissons),
    [modifyWarehouseUser, updateUserPermissons]
  );

  return (
    <div className="tablet:margin-x-8 overflow-x-auto">
      <Title>User Administration</Title>
      <Table<WarehouseUser> columns={tableColumns} data={users} />
    </div>
  );
}
Example #2
Source File: Domains.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 5 votes vote down vote up
Domains: React.FC = () => {
  const { showAllOrganizations } = useAuthContext();
  const tableRef = useRef<TableInstance<Domain>>(null);
  const columns = useMemo(() => createColumns(), []);
  const [domains, setDomains] = useState<Domain[]>([]);
  const [totalResults, setTotalResults] = useState(0);

  const { listDomains } = useDomainApi(showAllOrganizations);

  const fetchDomains = useCallback(
    async (q: Query<Domain>) => {
      try {
        const { domains, count } = await listDomains(q);
        setDomains(domains);
        setTotalResults(count);
      } catch (e) {
        console.error(e);
      }
    },
    [listDomains]
  );

  const fetchDomainsExport = async (): Promise<string> => {
    const { sortBy, filters } = tableRef.current?.state ?? {};
    try {
      const { url } = await listDomains(
        {
          sort: sortBy ?? [],
          page: 1,
          pageSize: -1,
          filters: filters ?? []
        },
        true
      );
      return url!;
    } catch (e) {
      console.error(e);
      return '';
    }
  };

  const renderPagination = (table: TableInstance<Domain>) => (
    <Paginator
      table={table}
      totalResults={totalResults}
      export={{
        name: 'domains',
        getDataToExport: fetchDomainsExport
      }}
    />
  );

  return (
    <div className={classes.root}>
      <Subnav
        items={[
          { title: 'Search Results', path: '/inventory', exact: true },
          { title: 'All Domains', path: '/inventory/domains' },
          { title: 'All Vulnerabilities', path: '/inventory/vulnerabilities' }
        ]}
      ></Subnav>
      <br></br>
      <Table<Domain>
        renderPagination={renderPagination}
        tableRef={tableRef}
        columns={columns}
        data={domains}
        pageCount={Math.ceil(totalResults / PAGE_SIZE)}
        fetchData={fetchDomains}
        pageSize={PAGE_SIZE}
      />
    </div>
  );
}
Example #3
Source File: ConfirmationOfReceiptPage.tsx    From solo with MIT License 5 votes vote down vote up
ConfirmationOfReceiptPage: React.FC = () => {
  const [bulkReceivedBy, setBulkReceivedBy] = useState("");
  const {
    docs,
    updateDocuments,
    pageCount,
    submitCOR,
    submitBulkCOR,
    bulkSubmitStatus,
    resetBulkSubmitStatus
  } = useCORDocuments();
  const columns = useMemo(
    () =>
      createColumns({
        onSubmitCOR: submitCOR
      }),
    [submitCOR]
  );

  const onSelectedRowsChange = useCallback(
    ({ toggleHideColumn, selectedFlatRows }: TableInstance<Document>) => {
      // show individual row submit forms only when no rows are selected
      resetBulkSubmitStatus();
      toggleHideColumn("submitCOR", selectedFlatRows.length > 0);
    },
    [resetBulkSubmitStatus]
  );

  const renderPagination = (table: TableInstance<Document>) => (
    <>{table.selectedFlatRows.length === 0 && <Paginator table={table} />}</>
  );

  const renderFilterControls = (table: TableInstance<Document>) => {
    const { setGlobalFilter, selectedFlatRows } = table;
    return selectedFlatRows.length > 0 ? (
      <CORInputForm
        value={bulkReceivedBy}
        onReceivedByChange={setBulkReceivedBy}
        onSubmitCOR={() =>
          submitBulkCOR(
            selectedFlatRows.map(({ original }) => original.sdn),
            bulkReceivedBy
          )
        }
        actionText={`Submit ${selectedFlatRows.length} Cors`}
        className="margin-left-2 padding-y-2 flex-justify-center"
        {...bulkSubmitStatus}
      />
    ) : (
      <SelectFilterControls options={filterable} onSubmit={setGlobalFilter} />
    );
  };

  return (
    <div className="tablet:margin-x-8 overflow-x-auto">
      <Title>Confirmation of Receipt</Title>
      <Table<Document>
        columns={columns}
        data={docs}
        onSelectedRowsChange={onSelectedRowsChange}
        renderFilterControls={renderFilterControls}
        renderPagination={renderPagination}
        pageCount={pageCount}
        fetchData={updateDocuments}
      />
    </div>
  );
}
Example #4
Source File: EnterReceiptPage.tsx    From solo with MIT License 5 votes vote down vote up
EnterReceiptPage: React.FC = () => {
  const {
    docs,
    addSdn,
    submitAllLoadingStatus,
    submitAll,
    modifyDocument,
    removeDocument
  } = useEnterReceiptDocuments();

  const columns = useMemo(() => createColumns(modifyDocument, removeDocument), [
    modifyDocument,
    removeDocument
  ]);

  const [duplicateSdn, setDuplicateSdn] = useState(false);
  const onAddSdn = (sdn: string) => {
    setDuplicateSdn(false);
    const isDuplicate = docs.some(doc => doc.sdn === sdn);
    if (isDuplicate) {
      setDuplicateSdn(true);
    } else if (sdn !== "") {
      addSdn(sdn);
    }
  };

  return (
    <div className="tablet:margin-x-8 overflow-x-auto">
      <Title>Enter Receipt</Title>
      <EnterReceiptStatusIndicator {...submitAllLoadingStatus} />
      <Table<Document>
        columns={columns}
        data={docs}
        manualPagination={false}
        manualSortBy={false}
      />
      <DuplicateSdnIndicator isDuplicate={duplicateSdn} />
      <div className="grid-row flex-align-start flex-justify margin-bottom-1em">
        <SdnInputForm
          onSubmit={onAddSdn}
          disabled={submitAllLoadingStatus.loading}
        />
        <Button
          onClick={submitAll}
          className="margin-top-1"
          disabled={submitAllLoadingStatus.loading || docs.length < 1}
        >
          Submit All
        </Button>
      </div>
    </div>
  );
}
Example #5
Source File: StatusPage.tsx    From solo with MIT License 5 votes vote down vote up
StatusPage: React.FC = () => {
  const { docs, updateDocuments, pageCount } = useDocumentSet();
  const tableColumns = useMemo(createColumns, []);

  const renderSubComponent = ({
    original: { shipTo, holder, part, statuses }
  }: Row<Document>) => (
    <>
      <DocumentStepper statuses={statuses} />
      <DocumentDetails
        shipper={holder}
        receiver={shipTo}
        part={part}
        statuses={statuses}
      />
    </>
  );

  const renderPagination = (table: TableInstance<Document>) => (
    <Paginator table={table} />
  );
  const renderFilterControls = ({
    setGlobalFilter
  }: TableInstance<Document>) => (
    <SelectFilterControls options={filterable} onSubmit={setGlobalFilter} />
  );

  return (
    <div className="tablet:margin-x-8 overflow-x-auto">
      <Title>Status</Title>
      <Table<Document>
        columns={tableColumns}
        data={docs}
        renderSubComponent={renderSubComponent}
        renderPagination={renderPagination}
        renderFilterControls={renderFilterControls}
        fetchData={updateDocuments}
        pageCount={pageCount}
      />
    </div>
  );
}
Example #6
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 #7
Source File: ScanTasksView.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
ScanTasksView: React.FC = () => {
  const { apiPost, token } = useAuthContext();
  const [scanTasks, setScanTasks] = useState<ScanTask[]>([]);
  const [totalResults, setTotalResults] = useState(0);
  const [errors, setErrors] = useState<Errors>({});

  const killScanTask = async (index: number) => {
    try {
      const row = scanTasks[index];
      await apiPost(`/scan-tasks/${row.id}/kill`, { body: {} });
      setScanTasks(
        Object.assign([], scanTasks, {
          [index]: {
            ...row,
            status: 'failed'
          }
        })
      );
    } catch (e) {
      setErrors({
        global:
          e.status === 422 ? 'Unable to kill scan' : e.message ?? e.toString()
      });
      console.log(e);
    }
  };

  const renderExpanded = (row: Row<ScanTask>) => {
    const { original } = row;
    return (
      <div className={classes.expandedRoot}>
        {original.fargateTaskArn && (
          <>
            <h4>
              Logs
              {original.fargateTaskArn?.match('.*/(.*)') && (
                <a
                  target="_blank"
                  rel="noopener noreferrer"
                  href={`https://us-east-1.console.aws.amazon.com/cloudwatch/home?region=us-east-1#logsV2:log-groups/log-group/${process
                    .env
                    .REACT_APP_FARGATE_LOG_GROUP!}/log-events/worker$252Fmain$252F${
                    (original.fargateTaskArn.match('.*/(.*)') || [])[1]
                  }`}
                >
                  {' '}
                  (View all on CloudWatch)
                </a>
              )}
            </h4>

            <Log
              token={token ?? ''}
              url={`${process.env.REACT_APP_API_URL}/scan-tasks/${original.id}/logs`}
            />
          </>
        )}

        <h4>Input</h4>
        <small>
          <pre>{JSON.stringify(JSON.parse(original.input), null, 2)}</pre>
        </small>
        <h4>Output</h4>
        <small>
          <pre>{original.output || 'None'}</pre>
        </small>

        {row.original.status !== 'finished' &&
          row.original.status !== 'failed' && (
            <>
              <h4>Actions</h4>
              <a
                href="# "
                onClick={(e) => {
                  e.preventDefault();
                  killScanTask(row.index);
                }}
              >
                Kill
              </a>
            </>
          )}
      </div>
    );
  };

  const columns: Column<ScanTask>[] = [
    {
      Header: 'ID',
      accessor: 'id',
      Filter: ColumnFilter,
      disableSortBy: true,
      disableFilters: true
    },
    {
      Header: 'Status',
      accessor: 'status',
      Filter: selectFilter([
        'created',
        'queued',
        'requested',
        'started',
        'finished',
        'failed'
      ]),
      disableSortBy: true
    },
    {
      Header: 'Name',
      id: 'name',
      accessor: ({ scan }) => scan?.name,
      Filter: selectFilter([
        // TODO: sync this with the SCAN_SCHEMA
        'censys',
        'amass',
        'findomain',
        'portscanner',
        'wappalyzer',
        'censysIpv4',
        'censysCertificates',
        'sslyze',
        'searchSync',
        'cve',
        'dotgov',
        'webscraper',
        'intrigueIdent',
        'shodan',
        'hibp',
        'lookingGlass',
        'dnstwist',
        'peCybersixgill',
        'peHibpSync',
        'peShodan',
        'peDomMasq',
        'rootDomainSync'
      ]),
      disableSortBy: true
    },
    {
      Header: 'Created At',
      id: 'createdAt',
      accessor: ({ createdAt }) => dateAccessor(createdAt),
      disableFilters: true
    },
    {
      Header: 'Finished At',
      id: 'finishedAt',
      accessor: ({ finishedAt }) => dateAccessor(finishedAt),
      disableFilters: true
    },
    {
      Header: 'Details',
      Cell: ({ row }: CellProps<ScanTask>) => (
        <span
          {...row.getToggleRowExpandedProps()}
          className="text-center display-block"
        >
          {row.isExpanded ? <FaMinus /> : <FaPlus />}
        </span>
      ),
      disableFilters: true
    }
  ];
  const PAGE_SIZE = 25;

  const fetchScanTasks = useCallback(
    async (query: Query<ScanTask>) => {
      const { page, sort, filters } = query;
      try {
        const { result, count } = await apiPost<ApiResponse>(
          '/scan-tasks/search',
          {
            body: {
              page,
              sort: sort[0]?.id ?? 'createdAt',
              order: sort[0]?.desc ? 'DESC' : 'ASC',
              filters: filters
                .filter((f) => Boolean(f.value))
                .reduce(
                  (accum, next) => ({
                    ...accum,
                    [next.id]: next.value
                  }),
                  {}
                )
            }
          }
        );
        setScanTasks(result);
        setTotalResults(count);
      } catch (e) {
        console.error(e);
      }
    },
    [apiPost]
  );

  const renderPagination = (table: TableInstance<ScanTask>) => (
    <Paginator table={table} totalResults={totalResults} />
  );

  return (
    <>
      {errors.global && <p className={classes.error}>{errors.global}</p>}
      <Table<ScanTask>
        renderPagination={renderPagination}
        columns={columns}
        data={scanTasks}
        pageCount={Math.ceil(totalResults / PAGE_SIZE)}
        fetchData={fetchScanTasks}
        pageSize={PAGE_SIZE}
        initialSortBy={[
          {
            id: 'createdAt',
            desc: true
          }
        ]}
        renderExpanded={renderExpanded}
      />
    </>
  );
}
Example #8
Source File: ScansView.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
ScansView: React.FC = () => {
  const { apiGet, apiPost, apiDelete } = useAuthContext();
  const [showModal, setShowModal] = useState<Boolean>(false);
  const [selectedRow, setSelectedRow] = useState<number>(0);
  const [scans, setScans] = useState<Scan[]>([]);
  const [organizationOptions, setOrganizationOptions] = useState<
    OrganizationOption[]
  >([]);
  const [tags, setTags] = useState<OrganizationTag[]>([]);
  const [scanSchema, setScanSchema] = useState<ScanSchema>({});

  const columns: Column<Scan>[] = [
    {
      Header: 'Run',
      id: 'run',
      Cell: ({ row }: { row: { index: number } }) => (
        <div
          style={{ textAlign: 'center' }}
          onClick={() => {
            runScan(row.index);
          }}
        >
          <FaPlayCircle />
        </div>
      ),
      disableFilters: true
    },
    {
      Header: 'Name',
      accessor: 'name',
      width: 200,
      id: 'name',
      disableFilters: true
    },
    {
      Header: 'Tags',
      accessor: ({ tags }) => tags.map((tag) => tag.name).join(', '),
      width: 150,
      minWidth: 150,
      id: 'tags',
      disableFilters: true
    },
    {
      Header: 'Mode',
      accessor: ({ name }) =>
        scanSchema[name] && scanSchema[name].isPassive ? 'Passive' : 'Active',
      width: 150,
      minWidth: 150,
      id: 'mode',
      disableFilters: true
    },
    {
      Header: 'Frequency',
      accessor: ({ frequency, isSingleScan }) => {
        let val, unit;
        if (frequency < 60 * 60) {
          val = frequency / 60;
          unit = 'minute';
        } else if (frequency < 60 * 60 * 24) {
          val = frequency / (60 * 60);
          unit = 'hour';
        } else {
          val = frequency / (60 * 60 * 24);
          unit = 'day';
        }
        if (isSingleScan) {
          return 'Single Scan';
        }
        return `Every ${val} ${unit}${val === 1 ? '' : 's'}`;
      },
      width: 200,
      id: 'frequency',
      disableFilters: true
    },
    {
      Header: 'Last Run',
      accessor: (args: Scan) => {
        return !args.lastRun ||
          new Date(args.lastRun).getTime() === new Date(0).getTime()
          ? 'None'
          : `${formatDistanceToNow(parseISO(args.lastRun))} ago`;
      },
      width: 200,
      id: 'lastRun',
      disableFilters: true
    },
    {
      Header: 'Edit',
      id: 'edit',
      Cell: ({ row }: CellProps<Scan>) => (
        <Link to={`/scans/${row.original.id}`} style={{ color: 'black' }}>
          <FaEdit />
        </Link>
      ),
      disableFilters: true
    },
    {
      Header: 'Delete',
      id: 'delete',
      Cell: ({ row }: { row: { index: number } }) => (
        <span
          onClick={() => {
            setShowModal(true);
            setSelectedRow(row.index);
          }}
        >
          <FaTimes />
        </span>
      ),
      disableFilters: true
    },
    {
      Header: 'Description',
      accessor: ({ name }) => scanSchema[name]?.description,
      width: 200,
      maxWidth: 200,
      id: 'description',
      disableFilters: true
    }
  ];
  const [errors, setErrors] = useState<Errors>({});

  const [values] = useState<ScanFormValues>({
    name: 'censys',
    arguments: '{}',
    organizations: [],
    frequency: 1,
    frequencyUnit: 'minute',
    isGranular: false,
    isUserModifiable: false,
    isSingleScan: false,
    tags: []
  });

  React.useEffect(() => {
    document.addEventListener('keyup', (e) => {
      //Escape
      if (e.keyCode === 27) {
        setShowModal(false);
      }
    });
  }, [apiGet]);

  const fetchScans = useCallback(async () => {
    try {
      const { scans, organizations, schema } = await apiGet<{
        scans: Scan[];
        organizations: Organization[];
        schema: ScanSchema;
      }>('/scans/');
      const tags = await apiGet<OrganizationTag[]>(`/organizations/tags`);
      setScans(scans);
      setScanSchema(schema);
      setOrganizationOptions(
        organizations.map((e) => ({ label: e.name, value: e.id }))
      );
      setTags(tags);
    } catch (e) {
      console.error(e);
    }
  }, [apiGet]);

  const deleteRow = async (index: number) => {
    try {
      const row = scans[index];
      await apiDelete(`/scans/${row.id}`, { body: {} });
      setScans(scans.filter((scan) => scan.id !== row.id));
    } catch (e) {
      setErrors({
        global:
          e.status === 422 ? 'Unable to delete scan' : e.message ?? e.toString()
      });
      console.log(e);
    }
  };

  const onSubmit = async (body: ScanFormValues) => {
    try {
      // For now, parse the arguments as JSON. We'll want to add a GUI for this in the future
      body.arguments = JSON.parse(body.arguments);
      setFrequency(body);

      const scan = await apiPost('/scans/', {
        body: {
          ...body,
          organizations: body.organizations
            ? body.organizations.map((e) => e.value)
            : [],
          tags: body.tags ? body.tags.map((e) => ({ id: e.value })) : []
        }
      });
      setScans(scans.concat(scan));
    } catch (e) {
      setErrors({
        global: e.message ?? e.toString()
      });
      console.log(e);
    }
  };

  const invokeScheduler = async () => {
    setErrors({ ...errors, scheduler: '' });
    try {
      await apiPost('/scheduler/invoke', { body: {} });
    } catch (e) {
      console.error(e);
      setErrors({ ...errors, scheduler: 'Invocation failed.' });
    }
  };

  /**
   * Manually runs a single scan, then immediately invokes the
   * scheduler so the scan is run.
   * @param index Row index
   */
  const runScan = async (index: number) => {
    const row = scans[index];
    try {
      await apiPost(`/scans/${row.id}/run`, { body: {} });
    } catch (e) {
      console.error(e);
      setErrors({ ...errors, scheduler: 'Run failed.' });
    }
    await invokeScheduler();
  };

  return (
    <>
      <Table<Scan> columns={columns} data={scans} fetchData={fetchScans} />
      <br></br>
      <Button type="submit" outline onClick={invokeScheduler}>
        Manually run scheduler
      </Button>
      {errors.scheduler && <p className={classes.error}>{errors.scheduler}</p>}
      <h2>Add a scan</h2>
      {errors.global && <p className={classes.error}>{errors.global}</p>}
      <ScanForm
        organizationOption={organizationOptions}
        tags={tags}
        propValues={values}
        onSubmit={onSubmit}
        type="create"
        scanSchema={scanSchema}
      ></ScanForm>
      <ImportExport<Scan>
        name="scans"
        fieldsToExport={['name', 'arguments', 'frequency']}
        onImport={async (results) => {
          // TODO: use a batch call here instead.
          const createdScans = [];
          for (const result of results) {
            createdScans.push(
              await apiPost('/scans/', {
                body: {
                  ...result,
                  // These fields are initially parsed as strings, so they need
                  // to be converted to objects.
                  arguments: JSON.parse(
                    ((result.arguments as unknown) as string) || ''
                  )
                }
              })
            );
          }
          setScans(scans.concat(...createdScans));
        }}
        getDataToExport={() =>
          scans.map((scan) => ({
            ...scan,
            arguments: JSON.stringify(scan.arguments)
          }))
        }
      />

      {showModal && (
        <div>
          <Overlay />
          <ModalContainer>
            <Modal
              actions={
                <>
                  <Button
                    outline
                    type="button"
                    onClick={() => {
                      setShowModal(false);
                    }}
                  >
                    Cancel
                  </Button>
                  <Button
                    type="button"
                    onClick={() => {
                      deleteRow(selectedRow);
                      setShowModal(false);
                    }}
                  >
                    Delete
                  </Button>
                </>
              }
              title={<h2>Delete scan?</h2>}
            >
              <p>
                Are you sure you would like to delete the{' '}
                <code>{scans[selectedRow].name}</code> scan?
              </p>
            </Modal>
          </ModalContainer>
        </div>
      )}
    </>
  );
}
Example #9
Source File: Settings.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
Settings: React.FC = () => {
  const { logout, user, setUser, apiPost, apiDelete } = useAuthContext();
  const [showModal, setShowModal] = useState<Boolean>(false);
  const [apiKey, setApiKey] = useState<string>('');

  const generateApiKey = async () => {
    if (!user) return;
    const apiKey = await apiPost<
      ApiKey & {
        key: string;
      }
    >('/api-keys');
    setUser({ ...user, apiKeys: user.apiKeys.concat([apiKey]) });
    setApiKey(apiKey.key);
    setShowModal(true);
  };

  const deleteApiKey = async (key: string) => {
    if (!user) return;
    await apiDelete<ApiKey>('/api-keys/' + key);
    setUser({
      ...user,
      apiKeys: user.apiKeys.filter((k) => k.id !== key)
    });
  };

  const columns: Column<ApiKey>[] = [
    {
      Header: 'Key',
      accessor: ({ lastFour }) => '*'.repeat(12) + lastFour,
      width: 200,
      disableFilters: true,
      id: 'key'
    },
    {
      Header: 'Date Created',
      accessor: ({ createdAt }) =>
        `${formatDistanceToNow(parseISO(createdAt))} ago`,
      width: 50,
      minWidth: 50,
      id: 'createdAt',
      disableFilters: true
    },
    {
      Header: 'Last Used',
      accessor: ({ lastUsed }) =>
        lastUsed ? `${formatDistanceToNow(parseISO(lastUsed))} ago` : 'None',
      width: 50,
      minWidth: 50,
      id: 'lastUsed',
      disableFilters: true
    },
    {
      Header: 'Delete',
      id: 'delete',
      Cell: ({ row }: { row: { index: number } }) => (
        <span
          onClick={() => {
            if (!user) return;
            deleteApiKey(user.apiKeys[row.index].id);
          }}
        >
          <FaTimes />
        </span>
      ),
      disableFilters: true
    }
  ];

  return (
    <div className={classes.root}>
      <h1>My Account</h1>
      <h2>Name: {user && user.fullName}</h2>
      <h2>Email: {user && user.email}</h2>
      <h2>
        Member of:{' '}
        {user &&
          (user.roles || [])
            .filter((role) => role.approved)
            .map((role) => role.organization.name)
            .join(', ')}
      </h2>
      <h2>API Keys:</h2>
      <p>
        <a
          href="https://docs.crossfeed.cyber.dhs.gov/api-reference"
          rel="noopener noreferrer"
          target="_blank"
        >
          Read API documentation
        </a>
      </p>
      {(!user?.apiKeys || user.apiKeys.length === 0) && <p>No API Keys</p>}
      {user?.apiKeys && user.apiKeys.length > 0 && (
        <Table<ApiKey> columns={columns} data={user?.apiKeys} />
      )}
      <br></br>
      <Button type="button" onClick={generateApiKey}>
        Generate API Key
      </Button>
      <br></br>
      <br></br>

      {showModal && (
        <div>
          <Overlay />
          <ModalContainer>
            <Modal
              actions={
                <>
                  <Button
                    type="button"
                    onClick={() => {
                      setShowModal(false);
                    }}
                  >
                    Ok
                  </Button>
                </>
              }
              title={<h2>Copy API Key</h2>}
            >
              <p>
                Please copy your API key now, as you will not be able to see it
                again:
              </p>
              <code>{apiKey}</code>
            </Modal>
          </ModalContainer>
        </div>
      )}
      {user?.userType === 'globalAdmin' && (
        <>
          <a href={`${process.env.REACT_APP_API_URL}/matomo/index.php`}>
            <Button type="button">Matomo</Button>
          </a>
          <br />
          <br />
        </>
      )}
      <Button type="button" onClick={logout}>
        Logout
      </Button>
    </div>
  );
}
Example #10
Source File: Users.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
Users: React.FC = () => {
  const { apiGet, apiPost, apiDelete } = useAuthContext();
  const [showModal, setShowModal] = useState<Boolean>(false);
  const [selectedRow, setSelectedRow] = useState<number>(0);
  const [users, setUsers] = useState<User[]>([]);

  const columns: Column<User>[] = [
    {
      Header: 'Name',
      accessor: 'fullName',
      width: 200,
      disableFilters: true,
      id: 'name'
    },
    {
      Header: 'Email',
      accessor: 'email',
      width: 150,
      minWidth: 150,
      id: 'email',
      disableFilters: true
    },
    {
      Header: 'Organizations',
      accessor: ({ roles }) =>
        roles &&
        roles
          .filter((role) => role.approved)
          .map((role) => role.organization.name)
          .join(', '),
      id: 'organizations',
      width: 200,
      disableFilters: true
    },
    {
      Header: 'User type',
      accessor: ({ userType }) =>
        userType === 'standard'
          ? 'Standard'
          : userType === 'globalView'
          ? 'Global View'
          : 'Global Admin',
      width: 50,
      minWidth: 50,
      id: 'userType',
      disableFilters: true
    },
    {
      Header: 'Date ToU Signed',
      accessor: ({ dateAcceptedTerms }) =>
        dateAcceptedTerms
          ? `${formatDistanceToNow(parseISO(dateAcceptedTerms))} ago`
          : 'None',
      width: 50,
      minWidth: 50,
      id: 'dateAcceptedTerms',
      disableFilters: true
    },
    {
      Header: 'ToU Version',
      accessor: 'acceptedTermsVersion',
      width: 50,
      minWidth: 50,
      id: 'acceptedTermsVersion',
      disableFilters: true
    },
    {
      Header: 'Last Logged In',
      accessor: ({ lastLoggedIn }) =>
        lastLoggedIn
          ? `${formatDistanceToNow(parseISO(lastLoggedIn))} ago`
          : 'None',
      width: 50,
      minWidth: 50,
      id: 'lastLoggedIn',
      disableFilters: true
    },
    {
      Header: 'Delete',
      id: 'delete',
      Cell: ({ row }: { row: { index: number } }) => (
        <span
          onClick={() => {
            setShowModal(true);
            setSelectedRow(row.index);
          }}
        >
          <FaTimes />
        </span>
      ),
      disableFilters: true
    }
  ];
  const [errors, setErrors] = useState<Errors>({});

  const [values, setValues] = useState<{
    firstName: string;
    lastName: string;
    email: string;
    organization?: Organization;
    userType: string;
  }>({
    firstName: '',
    lastName: '',
    email: '',
    userType: ''
  });

  const fetchUsers = useCallback(async () => {
    try {
      const rows = await apiGet<User[]>('/users/');
      setUsers(rows);
    } catch (e) {
      console.error(e);
    }
  }, [apiGet]);

  const deleteRow = async (index: number) => {
    try {
      const row = users[index];
      await apiDelete(`/users/${row.id}`, { body: {} });
      setUsers(users.filter((user) => user.id !== row.id));
    } catch (e) {
      setErrors({
        global:
          e.status === 422 ? 'Unable to delete user' : e.message ?? e.toString()
      });
      console.log(e);
    }
  };

  const onSubmit: React.FormEventHandler = async (e) => {
    e.preventDefault();
    try {
      const body = {
        firstName: values.firstName,
        lastName: values.lastName,
        email: values.email,
        userType: values.userType
      };
      const user = await apiPost('/users/', {
        body
      });
      setUsers(users.concat(user));
    } catch (e) {
      setErrors({
        global:
          e.status === 422
            ? 'Error when submitting user entry.'
            : e.message ?? e.toString()
      });
      console.log(e);
    }
  };

  const onTextChange: React.ChangeEventHandler<
    HTMLInputElement | HTMLSelectElement
  > = (e) => onChange(e.target.name, e.target.value);

  const onChange = (name: string, value: any) => {
    setValues((values) => ({
      ...values,
      [name]: value
    }));
  };

  React.useEffect(() => {
    document.addEventListener('keyup', (e) => {
      //Escape
      if (e.keyCode === 27) {
        setShowModal(false);
      }
    });
  }, [apiGet]);

  return (
    <div className={classes.root}>
      <h1>Users</h1>
      <Table<User> columns={columns} data={users} fetchData={fetchUsers} />
      <h2>Invite a user</h2>
      <form onSubmit={onSubmit} className={classes.form}>
        {errors.global && <p className={classes.error}>{errors.global}</p>}
        <Label htmlFor="firstName">First Name</Label>
        <TextInput
          required
          id="firstName"
          name="firstName"
          className={classes.textField}
          type="text"
          value={values.firstName}
          onChange={onTextChange}
        />
        <Label htmlFor="lastName">Last Name</Label>
        <TextInput
          required
          id="lastName"
          name="lastName"
          className={classes.textField}
          type="text"
          value={values.lastName}
          onChange={onTextChange}
        />
        <Label htmlFor="email">Email</Label>
        <TextInput
          required
          id="email"
          name="email"
          className={classes.textField}
          type="text"
          value={values.email}
          onChange={onTextChange}
        />
        <Label htmlFor="userType">User Type</Label>
        <RadioGroup
          aria-label="User Type"
          name="userType"
          value={values.userType}
          onChange={onTextChange}
        >
          <FormControlLabel
            value="standard"
            control={<Radio color="primary" />}
            label="Standard"
          />
          <FormControlLabel
            value="globalView"
            control={<Radio color="primary" />}
            label="Global View"
          />
          <FormControlLabel
            value="globalAdmin"
            control={<Radio color="primary" />}
            label="Global Administrator"
          />
        </RadioGroup>
        <br></br>
        <Button type="submit">Invite User</Button>
      </form>
      <ImportExport<
        | User
        | {
            roles: string;
          }
      >
        name="users"
        fieldsToExport={['firstName', 'lastName', 'email', 'roles', 'userType']}
        onImport={async (results) => {
          // TODO: use a batch call here instead.
          const createdUsers = [];
          for (const result of results) {
            const parsedRoles: {
              organization: string;
              role: string;
            }[] = JSON.parse(result.roles as string);
            const body: any = result;
            // For now, just create role with the first organization
            if (parsedRoles.length > 0) {
              body.organization = parsedRoles[0].organization;
              body.organizationAdmin = parsedRoles[0].role === 'admin';
            }
            try {
              createdUsers.push(
                await apiPost('/users/', {
                  body
                })
              );
            } catch (e) {
              // Just continue when an error occurs
              console.error(e);
            }
          }
          setUsers(users.concat(...createdUsers));
        }}
        getDataToExport={() =>
          users.map((user) => ({
            ...user,
            roles: JSON.stringify(
              user.roles.map((role) => ({
                organization: role.organization.id,
                role: role.role
              }))
            )
          }))
        }
      />

      {showModal && (
        <div>
          <Overlay />
          <ModalContainer>
            <Modal
              actions={
                <>
                  <Button
                    outline
                    type="button"
                    onClick={() => {
                      setShowModal(false);
                    }}
                  >
                    Cancel
                  </Button>
                  <Button
                    type="button"
                    onClick={() => {
                      deleteRow(selectedRow);
                      setShowModal(false);
                    }}
                  >
                    Delete
                  </Button>
                </>
              }
              title={<h2>Delete user?</h2>}
            >
              <p>
                Are you sure you would like to delete{' '}
                <code>{users[selectedRow].fullName}</code>?
              </p>
            </Modal>
          </ModalContainer>
        </div>
      )}
    </div>
  );
}
Example #11
Source File: Vulnerabilities.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
Vulnerabilities: React.FC<{ groupBy?: string }> = ({
  groupBy = undefined
}: {
  children?: React.ReactNode;
  groupBy?: string;
}) => {
  const {
    currentOrganization,
    apiPost,
    apiPut,
    showAllOrganizations
  } = useAuthContext();
  const [vulnerabilities, setVulnerabilities] = useState<Vulnerability[]>([]);
  const [totalResults, setTotalResults] = useState(0);
  const tableRef = useRef<TableInstance<Vulnerability>>(null);
  const listClasses = useStyles();
  const [noResults, setNoResults] = useState(false);

  const updateVulnerability = useCallback(
    async (index: number, body: { [key: string]: string }) => {
      try {
        const res = await apiPut<Vulnerability>(
          '/vulnerabilities/' + vulnerabilities[index].id,
          {
            body: body
          }
        );
        const vulnCopy = [...vulnerabilities];
        vulnCopy[index].state = res.state;
        vulnCopy[index].substate = res.substate;
        vulnCopy[index].actions = res.actions;
        setVulnerabilities(vulnCopy);
      } catch (e) {
        console.error(e);
      }
    },
    [setVulnerabilities, apiPut, vulnerabilities]
  );
  const columns = useMemo(() => createColumns(updateVulnerability), [
    updateVulnerability
  ]);
  const groupedColumns = useMemo(() => createGroupedColumns(), []);

  const vulnerabilitiesSearch = useCallback(
    async ({
      filters,
      sort,
      page,
      pageSize = PAGE_SIZE,
      doExport = false,
      groupBy = undefined
    }: {
      filters: Filters<Vulnerability>;
      sort: SortingRule<Vulnerability>[];
      page: number;
      pageSize?: number;
      doExport?: boolean;
      groupBy?: string;
    }): Promise<ApiResponse | undefined> => {
      try {
        const tableFilters: {
          [key: string]: string | boolean | undefined;
        } = filters
          .filter((f) => Boolean(f.value))
          .reduce(
            (accum, next) => ({
              ...accum,
              [next.id]: next.value
            }),
            {}
          );
        // If not open or closed, substitute for appropriate substate
        if (
          tableFilters['state'] &&
          !['open', 'closed'].includes(tableFilters['state'] as string)
        ) {
          const substate = (tableFilters['state'] as string)
            .match(/\((.*)\)/)
            ?.pop();
          if (substate)
            tableFilters['substate'] = substate.toLowerCase().replace(' ', '-');
          delete tableFilters['state'];
        }
        if (!showAllOrganizations && currentOrganization) {
          if ('rootDomains' in currentOrganization)
            tableFilters['organization'] = currentOrganization.id;
          else tableFilters['tag'] = currentOrganization.id;
        }
        if (tableFilters['isKev']) {
          // Convert string to boolean filter.
          tableFilters['isKev'] = tableFilters['isKev'] === 'true';
        }
        return await apiPost<ApiResponse>(
          doExport ? '/vulnerabilities/export' : '/vulnerabilities/search',
          {
            body: {
              page,
              sort: sort[0]?.id ?? 'createdAt',
              order: sort[0]?.desc ? 'DESC' : 'ASC',
              filters: tableFilters,
              pageSize,
              groupBy
            }
          }
        );
      } catch (e) {
        console.error(e);
        return;
      }
    },
    [apiPost, currentOrganization, showAllOrganizations]
  );

  const fetchVulnerabilities = useCallback(
    async (query: Query<Vulnerability>) => {
      const resp = await vulnerabilitiesSearch({
        filters: query.filters,
        sort: query.sort,
        page: query.page,
        groupBy
      });
      if (!resp) return;
      const { result, count } = resp;
      setVulnerabilities(result);
      setTotalResults(count);
      setNoResults(count === 0);
    },
    [vulnerabilitiesSearch, groupBy]
  );

  const fetchVulnerabilitiesExport = async (): Promise<string> => {
    const { sortBy, filters } = tableRef.current?.state ?? {};
    const { url } = (await vulnerabilitiesSearch({
      filters: filters!,
      sort: sortBy!,
      page: 1,
      pageSize: -1,
      doExport: true
    })) as ApiResponse;
    return url!;
  };

  const renderPagination = (table: TableInstance<Vulnerability>) => (
    <Paginator
      table={table}
      totalResults={totalResults}
      export={{
        name: 'vulnerabilities',
        getDataToExport: fetchVulnerabilitiesExport
      }}
    />
  );

  const initialFilterBy: Filters<Vulnerability> = [];
  let initialSortBy: SortingRule<Vulnerability>[] = [];
  const params = parse(window.location.search);
  if (!('state' in params)) params['state'] = 'open';
  for (const param of Object.keys(params)) {
    if (param === 'sort') {
      initialSortBy = [
        {
          id: params[param] as string,
          desc: 'desc' in params ? params['desc'] === 'true' : true
        }
      ];
    } else if (param !== 'desc') {
      initialFilterBy.push({
        id: param,
        value: params[param] as string
      });
    }
  }

  return (
    <div>
      <div className={listClasses.contentWrapper}>
        <Subnav
          items={[
            { title: 'Search Results', path: '/inventory', exact: true },
            { title: 'All Domains', path: '/inventory/domains' },
            { title: 'All Vulnerabilities', path: '/inventory/vulnerabilities' }
          ]}
        ></Subnav>
        <br></br>
        <div className={classes.root}>
          <Table<Vulnerability>
            renderPagination={renderPagination}
            columns={groupBy ? groupedColumns : columns}
            data={vulnerabilities}
            pageCount={Math.ceil(totalResults / PAGE_SIZE)}
            fetchData={fetchVulnerabilities}
            tableRef={tableRef}
            initialFilterBy={initialFilterBy}
            initialSortBy={initialSortBy}
            noResults={noResults}
            pageSize={PAGE_SIZE}
            noResultsMessage={
              "We don't see any vulnerabilities that match your criteria."
            }
          />
        </div>
      </div>
    </div>
  );
}
Example #12
Source File: index.test.tsx    From geist-ui with MIT License 4 votes vote down vote up
describe('Table', () => {
  it('should render correctly', () => {
    const wrapper = mount(
      <Table data={data}>
        <Table.Column prop="property" label="property" />
        <Table.Column prop="description" label="description" />
        <Table.Column prop="default" label="default" />
      </Table>,
    )
    expect(wrapper.html()).toMatchSnapshot()
    expect(() => wrapper.unmount()).not.toThrow()
  })

  it('should work correctly with multiple identical props', () => {
    const wrapper = mount(
      <Table data={data}>
        <Table.Column prop="property" label="property" />
        <Table.Column prop="description" label="description" />
        <Table.Column prop="property" label="property2" />
        <Table.Column prop="property" label="property3" />
        <Table.Column prop="description" label="description2" />
      </Table>,
    )
    expect(wrapper.html()).toMatchSnapshot()
    expect(() => wrapper.unmount()).not.toThrow()
  })

  it('should re-render when data changed', async () => {
    const wrapper = mount(
      <Table data={data}>
        <Table.Column prop="property" label="property" />
        <Table.Column prop="description" label="description" />
        <Table.Column prop="default" label="default" />
      </Table>,
    )
    expect(wrapper.find('tbody').find('tr').length).toBe(data.length)
    wrapper.setProps({ data: [] })
    await updateWrapper(wrapper, 350)
    expect(wrapper.find('tbody').find('tr').length).toBe(0)
  })

  it('should set width automatically', () => {
    window.getComputedStyle = jest.fn().mockImplementation(() => ({
      width: '100px',
    }))
    const wrapper = mount(
      <Table data={data}>
        <Table.Column prop="property" label="property" />
        <Table.Column prop="description" label="description" />
        <Table.Column prop="default" label="default" width={50} />
      </Table>,
    )
    expect(wrapper.html()).toMatchSnapshot()
    expect(() => wrapper.unmount()).not.toThrow()
    ;(window.getComputedStyle as jest.Mock).mockClear()
  })

  it('should be no erros when width is too large', () => {
    window.getComputedStyle = jest.fn().mockImplementation(() => ({
      width: '10px',
    }))
    const wrapper = mount(
      <Table data={data}>
        <Table.Column prop="property" label="property" />
        <Table.Column prop="description" label="description" />
        <Table.Column prop="default" label="default" width={50} />
      </Table>,
    )
    expect(wrapper.html()).toMatchSnapshot()
    expect(() => wrapper.unmount()).not.toThrow()
    ;(window.getComputedStyle as jest.Mock).mockClear()
  })

  it('should work with other components', () => {
    const dataWithNodes = [
      ...data,
      { property: 'bold', description: <Code>boolean</Code>, default: 'true' },
    ]
    const wrapper = mount(
      <Table data={dataWithNodes}>
        <Table.Column prop="property" label="property" />
        <Table.Column prop="description" label="description" />
        <Table.Column prop="default" label="default" />
      </Table>,
    )
    expect(wrapper.html()).toMatchSnapshot()
    expect(wrapper.find('code').length).not.toBe(0)
    expect(() => wrapper.unmount()).not.toThrow()
  })

  it('should work without hover effect', () => {
    const wrapper = mount(
      <Table data={data} hover={false}>
        <Table.Column prop="property" label="property" />
        <Table.Column prop="description" label="description" />
        <Table.Column prop="default" label="default" />
      </Table>,
    )
    expect(wrapper.html()).toMatchSnapshot()
    expect(() => wrapper.unmount()).not.toThrow()
  })

  it('should be render specified elements', async () => {
    type Item = {
      property: string
      description: string
      operation: string
    }
    const renderAction: TableColumnRender<Item> = (value, rowData, index) => {
      return (
        <div>
          <button id="test-btn">Remove</button>
          <div id="value">{value}</div>
          <div id="row-data">{rowData.description}</div>
          <div id="row-index">{index}</div>
        </div>
      )
    }
    const operation = Math.random().toString(16).slice(-10)
    const data = [{ property: 'bold', description: 'boolean', operation }]
    const wrapper = mount(
      <Table<Item> data={data}>
        <Table.Column<Item> prop="property" label="property" />
        <Table.Column<Item> prop="description" label="description" />
        <Table.Column<Item> prop="operation" label="operation" render={renderAction} />
      </Table>,
    )
    const buttons = wrapper.find('tbody').find('#test-btn')
    expect(buttons.length).not.toEqual(0)
    const value = wrapper.find('tbody').find('#value').html()
    expect(value).toMatch(operation)
    const rowData = wrapper.find('tbody').find('#row-data').html()
    expect(rowData).toMatch(`${data[0].description}`)
    const rowIndex = wrapper.find('tbody').find('#row-index').html()
    expect(rowIndex).toMatch(`0`)
  })

  it('should render emptyText when data missing', () => {
    const data = [{ property: 'bold', description: 'boolean' }]
    const wrapper = mount(
      <Table data={data} emptyText="test-not-found">
        <Table.Column prop="property" label="property" />
        <Table.Column prop="description" label="description" />
        <Table.Column prop="operation" label="operation" />
      </Table>,
    )
    expect(wrapper.find('tbody').text()).toContain('test-not-found')
  })

  it('should trigger events when cell clicked', () => {
    const rowHandler = jest.fn()
    const cellHandler = jest.fn()
    const data = [{ property: 'bold', description: 'boolean' }]
    const wrapper = mount(
      <Table
        data={data}
        emptyText="test-not-found"
        onRow={rowHandler}
        onCell={cellHandler}>
        <Table.Column prop="property" label="property" />
        <Table.Column prop="description" label="description" />
      </Table>,
    )
    wrapper.find('tbody').find('tr').find('td').at(0).simulate('click', nativeEvent)
    expect(rowHandler).toHaveBeenCalled()
    expect(cellHandler).toHaveBeenCalled()
  })

  it('should wraning when prop missing', () => {
    const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
    mount(
      <Table data={data}>
        <Table.Column prop="" label="property" />
        <Table.Column prop="description" label="description" />
      </Table>,
    )
    expect(errorSpy).toHaveBeenCalled()
    errorSpy.mockRestore()
  })

  it('should render children for table head', () => {
    const wrapper = mount(
      <Table data={data}>
        <Table.Column prop="property">
          <Code>property</Code>
        </Table.Column>
      </Table>,
    )
    expect(wrapper.find('thead').find('code').length).not.toBe(0)
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('the changes of column should be tracked', () => {
    const Mock = ({ label }: { label: string }) => {
      return (
        <Table data={data}>
          <Table.Column prop="description" label={label} />
        </Table>
      )
    }
    const wrapper = mount(<Mock label="test1" />)
    expect(wrapper.find('thead').find('tr').at(0).text()).toBe('test1')

    act(() => {
      wrapper.setProps({ label: 'test2' })
    })
    expect(wrapper.find('thead').find('tr').at(0).text()).toBe('test2')
    expect(() => wrapper.unmount()).not.toThrow()
  })

  it('the changes of children should be tracked', () => {
    const Mock = ({ label }: { label: string }) => {
      return (
        <Table data={data}>
          <Table.Column prop="description">{label}</Table.Column>
        </Table>
      )
    }
    const wrapper = mount(<Mock label="test1" />)
    expect(wrapper.find('thead').find('tr').at(0).text()).toBe('test1')

    act(() => {
      wrapper.setProps({ label: 'test2' })
    })
    expect(wrapper.find('thead').find('tr').at(0).text()).toBe('test2')
    expect(() => wrapper.unmount()).not.toThrow()
  })
})