date-fns#formatDistanceToNow TypeScript Examples

The following examples show how to use date-fns#formatDistanceToNow. 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: JobDetails.tsx    From norfolkdevelopers-website with MIT License 6 votes vote down vote up
export default function JobDetails({ job }: Props) {
  return (
    <ol className="mt-2 text-foreground-secondary text-sm md:flex md:m-0">
      <li className="mb-2 md:mb-0 md:mr-2">
        <span className="mr-2" role="img" aria-label="Published at">?</span>
        {formatDistanceToNow(new Date(job.date))} ago
      </li>
      <li className="mb-2 md:mb-0 md:mr-2">
        <span className="mr-2" role="img" aria-label="Expiration date">⏱️</span>
        {job.expiryDate ? `${dateFormat(new Date(job.expiryDate))}` : 'No expiry date.'}
      </li>
      <li className="mb-2 md:mb-0 md:mr-2">
        <span className="mr-2" role="img" aria-label="Job Role">?</span>
        {job.role}
      </li>
    </ol>
  );
}
Example #2
Source File: CommentItem.tsx    From knboard with MIT License 6 votes vote down vote up
CommentItem = ({ comment }: Props) => {
  const memberEntities = useSelector(selectMembersEntities);
  const author = memberEntities[comment.author];

  if (!author) {
    return null;
  }

  return (
    <Box display="flex" mb={2}>
      <Box marginRight={2} mt={0.25}>
        <MemberAvatar member={author} />
      </Box>
      <Box>
        <Box display="flex">
          <Name>{author.first_name || author.username}</Name>
          <TimeAgo>
            {formatDistanceToNow(new Date(comment.created), {
              addSuffix: true,
            })}
          </TimeAgo>
        </Box>
        <Text>{comment.text}</Text>
        {CommentActionRow({ comment })}
      </Box>
    </Box>
  );
}
Example #3
Source File: columns.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 6 votes vote down vote up
createColumns: CreateColumns = () => [
  {
    Header: 'Details',
    Cell: ({ row: { original } }: CellProps<Domain>) => (
      <Link to={`/inventory/domain/${original.id}`}>
        <FaSearch className="margin-x-auto display-block" />
      </Link>
    )
  },
  {
    Header: 'Organization',
    accessor: (e) => e.organization.name,
    id: 'organizationName',
    Filter: ColumnFilter
  },
  {
    Header: 'Domain',
    accessor: 'name',
    id: 'reverseName',
    Filter: ColumnFilter
  },
  {
    Header: 'IP',
    accessor: 'ip',
    Filter: ColumnFilter
  },
  {
    Header: 'Ports',
    id: 'port',
    disableSortBy: true,
    accessor: ({ services }) =>
      services.map((service) => service.port).join(', '),
    Filter: ColumnFilter
  },
  {
    Header: 'Services',
    id: 'service',
    disableSortBy: true,
    accessor: (domain) => getServiceNames(domain),
    Filter: ColumnFilter
  },
  {
    Header: 'Vulnerabilities',
    id: 'vulnerability',
    accessor: (domain) =>
      domain.vulnerabilities &&
      domain.vulnerabilities
        .map((vulnerability) => vulnerability.cve)
        .join(', '),
    Filter: ColumnFilter
  },
  {
    Header: 'Last Seen',
    id: 'updatedAt',
    accessor: ({ updatedAt }) =>
      `${formatDistanceToNow(parseISO(updatedAt))} ago`,
    disableFilters: true
  },
  {
    Header: 'First Seen',
    id: 'createdAt',
    accessor: ({ createdAt }) =>
      `${formatDistanceToNow(parseISO(createdAt))} ago`,
    disableFilters: true
  }
]
Example #4
Source File: QuestProgress.tsx    From mStable-apps with GNU Lesser General Public License v3.0 6 votes vote down vote up
QuestTimeRemaining: FC<{ expiry?: number }> = ({ expiry }) => {
  const expired = expiry && expiry > 0 && getUnixTime(Date.now()) > expiry
  return (
    <Container progressType={ProgressType.TimeRemaining} questType={QuestType.Seasonal}>
      <div>
        {typeof expiry !== 'number' ? (
          <ThemedSkeleton height={20} />
        ) : expired ? (
          <span>Expired</span>
        ) : (
          <Typist>
            Time remaining
            <span>{formatDistanceToNow(expiry * 1000)}</span>
          </Typist>
        )}
      </div>
    </Container>
  )
}
Example #5
Source File: DocumentStepperStep.tsx    From solo with MIT License 6 votes vote down vote up
Step: React.FC<StepProps> = ({
  first = false,
  last = false,
  complete = false,
  title,
  occuredAt
}) => (
  <div
    className={classNames(classes.step, {
      [classes.stepComplete]: complete,
      [classes.stepIncomplete]: !complete
    })}
  >
    <StatusIcon className={classes.stepIcon} complete={Boolean(complete)} />
    <div>{title}</div>
    {occuredAt && (
      <div>{`${formatDistanceToNow(parseISO(occuredAt))} ago `}</div>
    )}
    {!first && <div className={classes.stepLeft} />}
    {!last && <div className={classes.stepRight} />}
  </div>
)
Example #6
Source File: TimeSince.tsx    From amplication with Apache License 2.0 5 votes vote down vote up
function formatTimeToNow(time: Date | null): string | null {
  return (
    time &&
    formatDistanceToNow(new Date(time), {
      addSuffix: true,
    })
  );
}
Example #7
Source File: StatsPageContent.tsx    From disco-cube-admin with MIT License 5 votes vote down vote up
StatsPageContent: React.FC<Props> = ({
  isConnected,
  statusChangedAt,
  cpuLoadsPercent,
  allSystemInfo,
  memUsagePercent,
  cpuTemperature,
}) => {
  return (
    <Segment spacing={10} width="100%" maxWidth={500} height={"100%"} scroll="vertical">
      <h1>Disco Cube</h1>
      <Vertical spacing={30}>
        <Horizontal spacing={40}>
          <Stat label="Status">
            <div>
              <span style={{ color: isConnected ? "#3f8600" : "#cf1322" }}>
                {isConnected ? "Connected" : "Disconnected"}
              </span>
              <span style={{ color: "rgba(0, 0, 0, 0.45)", fontSize: "0.75em" }}>
                {statusChangedAt ? " " + formatDistanceToNow(statusChangedAt) + " ago" : ""}
              </span>
            </div>
          </Stat>
          <Stat label="CPU temperature">
            <span>{cpuTemperature}</span>
          </Stat>
        </Horizontal>
        <Horizontal spacing={40}>
          <Stat label="CPU LOADS">
            <Vertical>
              {cpuLoadsPercent.map((c, i) => (
                <Progress
                  key={i}
                  percent={Math.round(c)}
                  strokeColor={{
                    from: getProgressStrokeColorFromPercentage(c),
                    to: getProgressStrokeColorFromPercentage(c),
                  }}
                />
              ))}
            </Vertical>
          </Stat>
          <Stat label="Mem usage">
            <Progress
              type="circle"
              width={80}
              percent={Math.round(memUsagePercent)}
              strokeColor={{
                from: getProgressStrokeColorFromPercentage(memUsagePercent),
                to: getProgressStrokeColorFromPercentage(memUsagePercent),
              }}
            />
          </Stat>
          <Stretch />
        </Horizontal>

        <Horizontal spacing={40}>
          <Stat label="All System Info" width={360}>
            <SystemInfoTree info={allSystemInfo} />
          </Stat>
        </Horizontal>
      </Vertical>
    </Segment>
  );
}
Example #8
Source File: tableColumns.tsx    From solo with MIT License 5 votes vote down vote up
createColumns: CreateColumns = () => [
  {
    Header: "Details",
    Cell: ({ row }) => (
      <span {...row.getToggleRowExpandedProps()}>
        <FontAwesomeIcon icon={row.isExpanded ? faMinus : faPlus} />
      </span>
    )
  },
  {
    Header: "SDN",
    accessor: "sdn"
  },
  {
    Header: "Service Request #",
    accessor: "serviceRequest",
    id: "service_request"
  },
  {
    Header: "Commodity",
    id: "suppadd__code",
    accessor: "commodityName"
  },
  {
    Header: "Status",
    disableSortBy: true,
    id: "currentStatus",
    accessor: ({ mostRecentStatusIdx, statuses }) =>
      statuses[mostRecentStatusIdx].dic
  },
  {
    Header: "Nomenclature",
    id: "part__nomen",
    accessor: "part.nomen"
  },
  {
    Header: "Last Updated",
    id: "statuses__status_date",
    disableSortBy: true,
    accessor: ({ mostRecentStatusIdx, statuses }) =>
      `${formatDistanceToNow(
        parseISO(statuses[mostRecentStatusIdx].status_date)
      )} ago`
  }
]
Example #9
Source File: DocumentDetails.tsx    From solo with MIT License 5 votes vote down vote up
DocumentDetails: React.FC<DocumentDetailDataProps> = ({
  statuses,
  part,
  shipper,
  receiver
}) => {
  return (
    <div className="grid-row flex-justify flex-align-start padding-5">
      <div className="flex-col">
        <div className="text-bold">DIC</div>
        {statuses.map(({ status_date, dic }) => {
          const formattedDate = formatDistanceToNow(parseISO(status_date));
          return (
            <div
              className="grid-row flex-nowrap flex-col flex-align-center"
              key={dic}
            >
              <div className="width-5">{dic}</div>
              <div>{`${formattedDate} ago`}</div>
            </div>
          );
        })}
      </div>
      <div className="flex-col">
        <div className="text-bold">Quantity</div>
        {statuses.map(({ id, projected_qty, received_qty }) => (
          <div
            className="grid-row flex-nowrap flex-col flex-align-center"
            key={id}
          >
            {`${received_qty}/${projected_qty}`}
          </div>
        ))}
      </div>
      {part && (
        <>
          <div className="flex-col">
            <div className="text-bold">NIIN</div>
            <div>{part.nsn}</div>
          </div>
          <div className="flex-col">
            <div className="text-bold">Unit of Measure</div>
            <div>{part.uom}</div>
          </div>
        </>
      )}
      <div className="flex-col">
        <div className="text-bold">Shipped From</div>
        <div>{shipper?.name || ""}</div>
      </div>
      <div className="flex-col">
        <div className="text-bold">Shipped To</div>
        <div>{receiver?.name || ""}</div>
      </div>
    </div>
  );
}
Example #10
Source File: ScanTasksView.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 5 votes vote down vote up
dateAccessor = (date?: string) => {
  return !date || new Date(date).getTime() === new Date(0).getTime()
    ? 'None'
    : `${formatDistanceToNow(parseISO(date))} ago`;
}
Example #11
Source File: UserAndTime.tsx    From amplication with Apache License 2.0 5 votes vote down vote up
function formatTimeToNow(time: Date | null): string | null {
  return (
    time &&
    formatDistanceToNow(new Date(time), {
      addSuffix: true,
    })
  );
}
Example #12
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 #13
Source File: Vulnerability.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
Vulnerability: React.FC = () => {
  const { vulnerabilityId } = useParams();
  const { apiGet, apiPut } = useAuthContext();
  const [vulnerability, setVulnerability] = useState<VulnerabilityType>();
  const [comment, setComment] = useState<string>('');
  const [showCommentForm, setShowCommentForm] = useState<boolean>(false);
  const [menuAnchor, setMenuAnchor] = React.useState<null | HTMLElement>(null);
  const classes = useStyles();
  const history = useHistory();

  const formatDate = (date: string) => {
    return format(parseISO(date), 'MM-dd-yyyy');
  };

  const fetchVulnerability = useCallback(async () => {
    try {
      const result = await apiGet<VulnerabilityType>(
        `/vulnerabilities/${vulnerabilityId}`
      );
      setVulnerability(result);
    } catch (e) {
      console.error(e);
    }
  }, [vulnerabilityId, apiGet]);

  const updateVulnerability = async (body: { [key: string]: string }) => {
    try {
      if (!vulnerability) return;
      const res = await apiPut<VulnerabilityType>(
        '/vulnerabilities/' + vulnerability.id,
        {
          body: body
        }
      );
      setVulnerability({
        ...vulnerability,
        state: res.state,
        substate: res.substate,
        actions: res.actions
      });
    } catch (e) {
      console.error(e);
    }
  };

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

  if (!vulnerability) return <></>;

  const references = vulnerability.references.map((ref) => ref);
  if (vulnerability.cve)
    references.unshift({
      name: 'NIST National Vulnerability Database',
      url: `https://nvd.nist.gov/vuln/detail/${vulnerability.cve}`,
      source: '',
      tags: []
    });

  const states = [
    'unconfirmed',
    'exploitable',
    'false-positive',
    'accepted-risk',
    'remediated'
  ];
  interface dnstwist {
    'domain-name': string;
    fuzzer: string;
    'dns-a'?: string;
    'dns-aaas'?: string;
    'dns-mx'?: string;
    'dns-ns'?: string;
    'date-first-observed'?: string;
  }

  return (
    <>
      {/* <Alert severity="info">
        This vulnerability is found on 17 domains you have access to.
      </Alert> */}
      <div className={classes.root}>
        <p>
          <Link
            to="# "
            onClick={() => history.goBack()}
            className={classes.backLink}
          >
            <ChevronLeft
              style={{
                height: '100%',
                verticalAlign: 'middle',
                marginTop: '-2px'
              }}
            ></ChevronLeft>
            Go back
          </Link>
        </p>

        <div className={classes.contentWrapper}>
          <div className={classes.content}>
            <div
              className={classes.panel}
              style={{
                flex: '0 0 45%'
              }}
            >
              <Paper classes={{ root: classes.cardRoot }}>
                <div className={classes.title}>
                  <h4>{vulnerability.title}</h4>
                  <Button
                    aria-haspopup="true"
                    onClick={(event: React.MouseEvent<HTMLButtonElement>) =>
                      setMenuAnchor(event.currentTarget)
                    }
                  >
                    <Flag
                      style={{
                        fontSize: '14px',
                        color: '#A9AEB1',
                        marginRight: '5px'
                      }}
                    ></Flag>
                    Mark Item <ArrowDropDown />
                  </Button>
                  <Menu
                    anchorEl={menuAnchor}
                    keepMounted
                    open={Boolean(menuAnchor)}
                    getContentAnchorEl={null}
                    onClose={() => setMenuAnchor(null)}
                    anchorOrigin={{
                      vertical: 'bottom',
                      horizontal: 'center'
                    }}
                    transformOrigin={{
                      vertical: 'top',
                      horizontal: 'center'
                    }}
                  >
                    {states.map((state) => (
                      <MenuItem
                        key={state}
                        onClick={() => {
                          updateVulnerability({
                            substate: state
                          });
                          setMenuAnchor(null);
                        }}
                        style={{ outline: 'none' }}
                      >
                        {stateMap[state]}
                      </MenuItem>
                    ))}
                  </Menu>
                </div>
                <Chip
                  style={{
                    marginLeft: '1.5rem'
                  }}
                  // icon={<Check></Check>}
                  label={`${vulnerability.state[0].toUpperCase()}${vulnerability.state.slice(
                    1
                  )} (${stateMap[vulnerability.substate]})`}
                  color={
                    vulnerability.state === 'open' ? 'secondary' : 'default'
                  }
                />
                <div className={classes.inner}>
                  <div className={classes.section}>
                    <h4 className={classes.subtitle}>Description</h4>
                    {vulnerability.description}
                  </div>
                  <div className={classes.section}>
                    <h4 className={classes.subtitle}>References</h4>
                    {references &&
                      references.map((ref, index) => (
                        <p key={index}>
                          <a
                            href={ref.url}
                            target="_blank"
                            rel="noopener noreferrer"
                          >
                            {ref.name ? ref.name : ref.url}
                          </a>
                          {ref.tags.length > 0
                            ? ' - ' + ref.tags.join(',')
                            : ''}
                        </p>
                      ))}
                  </div>
                  {vulnerability.source === 'hibp' && (
                    <div className={classes.section}>
                      <h4 className={classes.subtitle}>Data</h4>
                      <Table aria-label="simple table">
                        <TableHead>
                          <TableRow>
                            <TableCell>Exposed Emails</TableCell>
                            <TableCell align="right">Breaches</TableCell>
                          </TableRow>
                        </TableHead>
                        <TableBody>
                          {Object.keys(
                            vulnerability.structuredData['emails']
                          ).map((keyName, keyIndex) => (
                            <TableRow key={keyName}>
                              <TableCell component="th" scope="row">
                                {keyName}
                              </TableCell>
                              <TableCell align="right">
                                {vulnerability.structuredData['emails'][
                                  keyName
                                ].join(',  ')}
                              </TableCell>
                            </TableRow>
                          ))}
                        </TableBody>
                      </Table>
                    </div>
                  )}
                  {vulnerability.source === 'lookingGlass' && (
                    <div className={classes.section}>
                      <h4 className={classes.subtitle}>Data</h4>
                      <Table aria-label="simple table">
                        <TableHead>
                          <TableRow>
                            <TableCell>First Seen</TableCell>
                            <TableCell align="right">Last Seen</TableCell>
                            <TableCell align="right">Vuln Name</TableCell>
                            <TableCell align="right">Type</TableCell>
                          </TableRow>
                        </TableHead>
                        <TableBody>
                          {vulnerability.structuredData['lookingGlassData'].map(
                            (col: any) => (
                              <TableRow key={col.right_name}>
                                <TableCell component="th" scope="row">
                                  {formatDistanceToNow(
                                    parseISO(col.firstSeen)
                                  ) + ' ago'}
                                </TableCell>
                                <TableCell align="right">
                                  {formatDistanceToNow(parseISO(col.lastSeen)) +
                                    ' ago'}
                                </TableCell>
                                <TableCell align="right">
                                  {col.right_name}
                                </TableCell>
                                <TableCell align="right">
                                  {col.vulnOrMal}
                                </TableCell>
                              </TableRow>
                            )
                          )}
                        </TableBody>
                      </Table>
                    </div>
                  )}
                  {vulnerability.source === 'dnstwist' && (
                    <div className={classes.section}>
                      <h4 className={classes.subtitle}>Data</h4>
                      <TableContainer>
                        <Table aria-label="simple table">
                          <TableHead>
                            <TableRow>
                              <TableCell>Domain Name</TableCell>
                              <TableCell>IP Address / A Record</TableCell>
                              <TableCell>MX Record</TableCell>
                              <TableCell>NS Record</TableCell>
                              <TableCell>Date Observed</TableCell>
                              <TableCell>Fuzzer</TableCell>
                            </TableRow>
                          </TableHead>
                          <TableBody>
                            {vulnerability.structuredData['domains'].map(
                              (dom: dnstwist) => (
                                <TableRow key={dom['domain-name']}>
                                  <TableCell component="th" scope="row">
                                    {dom['domain-name']}
                                  </TableCell>
                                  <TableCell>{dom['dns-a']}</TableCell>
                                  <TableCell>{dom['dns-mx']}</TableCell>
                                  <TableCell>{dom['dns-ns']}</TableCell>
                                  <TableCell>
                                    {dom['date-first-observed']}
                                  </TableCell>
                                  <TableCell>{dom['fuzzer']}</TableCell>
                                </TableRow>
                              )
                            )}
                          </TableBody>
                        </Table>
                      </TableContainer>
                    </div>
                  )}
                </div>
              </Paper>
            </div>
            <div
              className={classes.panel}
              style={{
                flex: '0 0 30%'
              }}
            >
              <Paper className={classes.cardRoot}>
                <div className={classes.inner}>
                  <div className={classes.section}>
                    <h2 className={classes.subtitle}>Team notes</h2>
                    <button
                      onClick={() => {
                        setShowCommentForm(!showCommentForm);
                      }}
                      className={classes.linkSmall}
                    >
                      Add new note
                    </button>
                  </div>
                  {showCommentForm && (
                    <div>
                      <TextareaAutosize
                        style={{
                          width: '100%',
                          padding: 10,
                          marginBottom: '20px'
                        }}
                        rowsMin={4}
                        placeholder="Leave a Note"
                        onChange={(e) => setComment(e.target.value)}
                      />
                      <Button
                        onClick={() => {
                          updateVulnerability({
                            comment
                          });
                          setComment('');
                          setShowCommentForm(false);
                        }}
                        style={{
                          width: 150,
                          marginBottom: '20px'
                        }}
                        variant="contained"
                        color="secondary"
                      >
                        Save
                      </Button>
                    </div>
                  )}
                  {vulnerability.actions &&
                    vulnerability.actions
                      .filter((action) => action.type === 'comment')
                      .map((action, index) => (
                        <div className={classes.section} key={index}>
                          <h4
                            className={classes.subtitle}
                            style={{ fontSize: '16px', display: 'inline' }}
                          >
                            {action.userName}
                          </h4>
                          <span style={{ float: 'right', display: 'inline' }}>
                            {formatDistanceToNow(parseISO(action.date))} ago
                          </span>
                          <ReactMarkdown linkTarget="_blank">
                            {action.value || ''}
                          </ReactMarkdown>
                        </div>
                      ))}
                </div>
              </Paper>
              <Paper className={classes.cardRoot}>
                <div className={classes.inner}>
                  <div className={classes.section}>
                    <h2 className={classes.subtitle}>Vulnerability History</h2>
                  </div>
                  <Timeline
                    style={{
                      position: 'relative',
                      marginLeft: '-90%'
                    }}
                    align="left"
                  >
                    {vulnerability.actions &&
                      vulnerability.actions
                        .filter(
                          (action) =>
                            action.type === 'state-change' && action.substate
                        )
                        .map((action, index) => (
                          <TimelineItem key={index}>
                            <TimelineSeparator>
                              <TimelineDot />
                              <TimelineConnector />
                            </TimelineSeparator>{' '}
                            <TimelineContent>
                              State {action.automatic ? 'automatically ' : ''}
                              changed to {action.state} (
                              {stateMap[action.substate!].toLowerCase()})
                              {action.userName ? ' by ' + action.userName : ''}{' '}
                              <br></br>
                              <span
                                style={{
                                  color: '#A9AEB1'
                                }}
                              >
                                {formatDate(action.date)}
                              </span>
                            </TimelineContent>
                          </TimelineItem>
                        ))}

                    <TimelineItem>
                      <TimelineSeparator>
                        <TimelineDot />
                      </TimelineSeparator>
                      <TimelineContent>
                        Vulnerability opened<br></br>
                        <span
                          style={{
                            color: '#A9AEB1'
                          }}
                        >
                          {formatDate(vulnerability.createdAt)}
                        </span>
                      </TimelineContent>
                    </TimelineItem>
                  </Timeline>
                </div>
              </Paper>
              <Paper className={classes.cardRoot}>
                <div className={classes.inner}>
                  <div className={classes.section}>
                    <h2 className={classes.subtitle}>Provenance</h2>
                    <p>
                      <strong>Root Domain: </strong>
                      {vulnerability.domain.fromRootDomain}
                    </p>
                    <p>
                      <strong>Subdomain: </strong>
                      {vulnerability.domain.name} (
                      {vulnerability.domain.subdomainSource})
                    </p>
                    {vulnerability.service && (
                      <p>
                        <strong>Service/Port: </strong>
                        {vulnerability.service.service
                          ? vulnerability.service.service
                          : vulnerability.service.port}{' '}
                        ({vulnerability.service.serviceSource})
                      </p>
                    )}
                    {vulnerability.cpe && (
                      <>
                        <p>
                          <strong>Product: </strong>
                          {vulnerability.cpe}
                        </p>
                      </>
                    )}
                    <p>
                      <strong>Vulnerability: </strong>
                      {vulnerability.title} ({vulnerability.source})
                    </p>
                  </div>
                </div>
              </Paper>
              {vulnerability.source === 'hibp' && (
                <Paper className={classes.cardRoot}>
                  <div className={classes.inner}>
                    <div className={classes.section}>
                      <h2 className={classes.subtitle}>Breaches</h2>
                      <Table aria-label="simple table">
                        <TableHead>
                          <TableRow>
                            <TableCell>Breach Name</TableCell>
                            <TableCell align="right">Date Added</TableCell>
                          </TableRow>
                        </TableHead>
                        <TableBody>
                          {Object.keys(vulnerability.structuredData['breaches'])
                            .sort(
                              (a, b) =>
                                parseISO(
                                  vulnerability.structuredData['breaches'][b][
                                    'AddedDate'
                                  ]
                                ).getTime() -
                                parseISO(
                                  vulnerability.structuredData['breaches'][a][
                                    'AddedDate'
                                  ]
                                ).getTime()
                            )
                            .map((keyName, keyIndex) => (
                              <TableRow key={keyName}>
                                <TableCell component="th" scope="row">
                                  {keyName}
                                </TableCell>
                                <TableCell align="right">
                                  {formatDistanceToNow(
                                    parseISO(
                                      vulnerability.structuredData['breaches'][
                                        keyName
                                      ]['AddedDate']
                                    )
                                  ) + ' ago'}
                                </TableCell>
                              </TableRow>
                            ))}
                        </TableBody>
                      </Table>
                    </div>
                  </div>
                </Paper>
              )}
            </div>
          </div>
        </div>
      </div>
    </>
  );
}
Example #14
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 #15
Source File: ResultCard.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
ResultCard: React.FC<Props> = (props) => {
  const classes = useStyles(props);
  const [expanded, setExpanded] = useState<Record<string, boolean>>({});
  const {
    id,
    name,
    ip,
    updatedAt,
    services,
    vulnerabilities,
    inner_hits,
    onDomainSelected
  } = props;

  const toggleExpanded = (key: string) => {
    setExpanded((expanded) => ({
      ...expanded,
      [key]: expanded[key] ? !expanded[key] : true
    }));
  };

  let lastSeen;
  try {
    lastSeen = formatDistanceToNow(parseISO(updatedAt.raw));
  } catch (e) {
    lastSeen = '';
  }

  const onClick = () => {
    onDomainSelected(id.raw);
  };

  const ports = services.raw.reduce(
    (acc, nextService) => [...acc, nextService.port],
    []
  );

  const products = services.raw.reduce(
    (acc, nextService) => [
      ...acc,
      ...nextService.products.map(
        (p: any) => `${p.name}${p.version ? ' ' + p.version : ''}`
      )
    ],
    []
  );

  const cves = vulnerabilities.raw.reduce(
    (acc, nextVuln) => [...acc, nextVuln.cve],
    []
  );

  const data = [];
  if (products.length > 0) {
    data.push({
      label: `Product${products.length > 1 ? 's' : ''}`,
      count: products.length,
      value: filterExpanded(
        [...Array.from(new Set(products))],
        Boolean(expanded.products),
        8
      ).join(', '),
      onExpand: () => toggleExpanded('products'),
      expansionText:
        products.length <= 8 ? null : expanded.products ? 'less' : 'more'
    });
  }
  if (cves.length > 0) {
    data.push({
      label: `CVE${cves.length > 1 ? 's' : ''}`,
      count: cves.length,
      value: filterExpanded(cves, Boolean(expanded.vulns), 10).join(', '),
      onExpand: () => toggleExpanded('vulns'),
      expansionText: cves.length <= 10 ? null : expanded.vulns ? 'less' : 'more'
    });
  }
  if (inner_hits?.webpage?.hits?.hits?.length! > 0) {
    const { hits } = inner_hits!.webpage!.hits!;
    data.push({
      label: `matching webpage${hits.length > 1 ? 's' : ''}`,
      count: hits.length,
      value: hits.map((e, idx) => (
        <React.Fragment key={idx}>
          <small>
            <strong>{e._source.webpage_url}</strong>
            <br />
            {e.highlight?.webpage_body?.map((body, idx) => (
              <React.Fragment key={idx}>
                <code
                  dangerouslySetInnerHTML={{
                    __html: sanitize(body, { ALLOWED_TAGS: ['em'] })
                  }}
                />
              </React.Fragment>
            ))}
          </small>
        </React.Fragment>
      ))
    });
  }

  return (
    <Paper
      elevation={0}
      classes={{ root: classes.root }}
      aria-label="view domain details"
    >
      <div className={classes.inner} onClick={onClick}>
        <button className={classes.domainRow}>
          <h4>{name.raw}</h4>
          <div className={classes.lastSeen}>
            <span className={classes.label}>Last Seen</span>
            <span className={classes.data}>{lastSeen} ago</span>
          </div>
        </button>

        {ip.raw && (
          <div className={clsx(classes.ipRow, classes.row)}>
            <div>
              <span className={classes.label}>IP</span>
              <span className={classes.data}>{ip.raw}</span>
            </div>
            {ports.length > 0 && (
              <div className={classes.lastSeen}>
                <span className={classes.label}>
                  <span className={classes.count}>{ports.length}</span>
                  {` Port${ports.length > 1 ? 's' : ''}`}
                </span>
                <span className={classes.data}>{ports.join(', ')}</span>
              </div>
            )}
          </div>
        )}

        {data.map(({ label, value, count, onExpand, expansionText }) => (
          <p className={classes.row} key={label}>
            <span className={classes.label}>
              {count !== undefined && (
                <span className={classes.count}>{count} </span>
              )}
              {label}
            </span>
            <span className={classes.data}>
              {value}
              {expansionText && (
                <button
                  className={classes.expandMore}
                  onClick={(event) => {
                    event.stopPropagation();
                    if (onExpand) onExpand();
                  }}
                >
                  {expansionText}
                </button>
              )}
            </span>
          </p>
        ))}
      </div>
    </Paper>
  );
}
Example #16
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 #17
Source File: WorkflowManager.tsx    From legend-studio with Apache License 2.0 4 votes vote down vote up
WorkflowTreeNodeContainer: React.FC<
  TreeNodeContainerProps<
    WorkflowExplorerTreeNodeData,
    {
      workflowManagerState: WorkflowManagerState;
      workflowState: WorkflowState;
      treeData: TreeData<WorkflowExplorerTreeNodeData>;
    }
  >
> = (props) => {
  const { node, level, stepPaddingInRem, onNodeSelect } = props;
  const { workflowManagerState, treeData, workflowState } = props.innerProps;
  const expandIcon = !(node instanceof WorkflowTreeNodeData) ? (
    <div />
  ) : node.isOpen ? (
    <ChevronDownIcon />
  ) : (
    <ChevronRightIcon />
  );
  const nodeIcon =
    node instanceof WorkflowTreeNodeData
      ? getWorkflowStatusIcon(node.workflow.status)
      : getWorkflowJobStatusIcon(
          guaranteeType(node, WorkflowJobTreeNodeData).workflowJob.status,
        );
  const selectNode: React.MouseEventHandler = (event) => onNodeSelect?.(node);

  return (
    <ContextMenu
      content={
        <WorkflowExplorerContextMenu
          workflowManagerState={workflowManagerState}
          workflowState={workflowState}
          treeData={treeData}
          node={node}
        />
      }
      menuProps={{ elevation: 7 }}
    >
      <div
        className={clsx(
          'tree-view__node__container workflow-manager__explorer__workflow-tree__node__container',
        )}
        onClick={selectNode}
        style={{
          paddingLeft: `${level * (stepPaddingInRem ?? 1)}rem`,
          display: 'flex',
        }}
      >
        <div className="tree-view__node__icon workflow-manager__explorer__workflow-tree__node__icon">
          <div className="workflow-manager__explorer__workflow-tree__node__icon__expand">
            {expandIcon}
          </div>
          <div className="workflow-manager__explorer__workflow-tree__node__icon__type">
            {nodeIcon}
          </div>
        </div>
        {node instanceof WorkflowTreeNodeData && (
          <a
            className="workflow-manager__item__link"
            rel="noopener noreferrer"
            target="_blank"
            href={node.workflow.webURL}
            title={'See workflow detail'}
          >
            <div className="workflow-manager__item__link__content">
              <span className="workflow-manager__item__link__content__id">
                #{node.label}
              </span>
              <span className="workflow-manager__item__link__content__created-at">
                created{' '}
                {formatDistanceToNow(node.workflow.createdAt, {
                  includeSeconds: true,
                  addSuffix: true,
                })}
              </span>
            </div>
          </a>
        )}
        {node instanceof WorkflowJobTreeNodeData && (
          <a
            className="workflow-manager__item__link"
            rel="noopener noreferrer"
            target="_blank"
            href={node.workflowJob.webURL}
            title={'See job detail'}
          >
            <div className="workflow-manager__item__link__content">
              <span className="workflow-manager__item__link__content__id">
                {node.workflowJob.name}
              </span>
              <span className="workflow-manager__item__link__content__created-at">
                created{' '}
                {formatDistanceToNow(node.workflowJob.createdAt, {
                  includeSeconds: true,
                  addSuffix: true,
                })}
              </span>
            </div>
          </a>
        )}
      </div>
    </ContextMenu>
  );
}
Example #18
Source File: WorkspaceReview.tsx    From legend-studio with Apache License 2.0 4 votes vote down vote up
WorkspaceReview = observer(() => {
  const editorStore = useEditorStore();
  const applicationStore = useApplicationStore<LegendStudioConfig>();
  const workspaceReviewState = editorStore.workspaceReviewState;
  const workspaceReview = workspaceReviewState.workspaceReview;
  // Review Title
  const reviewTitleInputRef = useRef<HTMLInputElement>(null);
  const editReviewTitle: React.ChangeEventHandler<HTMLInputElement> = (
    event,
  ) => {
    if (!workspaceReview) {
      workspaceReviewState.setReviewTitle(event.target.value);
    }
  };
  const isDispatchingAction =
    workspaceReviewState.isCreatingWorkspaceReview ||
    workspaceReviewState.isFetchingCurrentWorkspaceReview ||
    workspaceReviewState.isRefreshingWorkspaceChangesDetector ||
    workspaceReviewState.isCommittingWorkspaceReview ||
    workspaceReviewState.isClosingWorkspaceReview ||
    workspaceReviewState.isRecreatingWorkspaceAfterCommittingReview;
  const refresh = (): void => {
    flowResult(workspaceReviewState.refreshWorkspaceChanges()).catch(
      applicationStore.alertUnhandledError,
    );
    flowResult(workspaceReviewState.fetchCurrentWorkspaceReview()).catch(
      applicationStore.alertUnhandledError,
    );
  };
  const closeReview = (): void => {
    workspaceReviewState.setReviewTitle('');
    flowResult(workspaceReviewState.closeWorkspaceReview()).catch(
      applicationStore.alertUnhandledError,
    );
  };
  const commitReview = (): void => {
    if (workspaceReview && !isDispatchingAction) {
      const commit = (): void => {
        workspaceReviewState.setReviewTitle('');
        flowResult(
          workspaceReviewState.commitWorkspaceReview(workspaceReview),
        ).catch(applicationStore.alertUnhandledError);
      };
      if (editorStore.hasUnpushedChanges) {
        editorStore.setActionAlertInfo({
          message: 'You have unpushed changes',
          prompt:
            'This action will discard these changes and refresh the application',
          type: ActionAlertType.CAUTION,
          onEnter: (): void => editorStore.setBlockGlobalHotkeys(true),
          onClose: (): void => editorStore.setBlockGlobalHotkeys(false),
          actions: [
            {
              label: 'Proceed to commit review',
              type: ActionAlertActionType.PROCEED_WITH_CAUTION,
              handler: (): void => {
                editorStore.setIgnoreNavigationBlocking(true);
                commit();
              },
            },
            {
              label: 'Abort',
              type: ActionAlertActionType.PROCEED,
              default: true,
            },
          ],
        });
      } else {
        commit();
      }
    }
  };
  const createReview = (): void => {
    if (
      workspaceReviewState.reviewTitle &&
      !workspaceReview &&
      !isDispatchingAction
    ) {
      flowResult(
        workspaceReviewState.createWorkspaceReview(
          workspaceReviewState.reviewTitle,
        ),
      ).catch(applicationStore.alertUnhandledError);
    }
  };

  // since the review can be changed by other people, we can refresh it more proactively
  // the diffs are caused by the current user though, so we should handle that as part
  // of `syncWithWorkspace` for example; in case it is bad, user can click refresh to make it right again
  useEffect(() => {
    flowResult(workspaceReviewState.fetchCurrentWorkspaceReview()).catch(
      applicationStore.alertUnhandledError,
    );
  }, [applicationStore, workspaceReviewState]);

  useEffect(() => {
    if (editorStore.activeActivity === ACTIVITY_MODE.WORKSPACE_REVIEW) {
      reviewTitleInputRef.current?.focus();
    }
  }, [editorStore.activeActivity]);

  return (
    <div className="panel workspace-review">
      <div className="panel__header side-bar__header">
        <div className="panel__header__title workspace-review__header__title">
          <div className="panel__header__title__content side-bar__header__title__content">
            REVIEW
          </div>
        </div>
        <div className="panel__header__actions side-bar__header__actions">
          <button
            className={clsx(
              'panel__header__action side-bar__header__action workspace-review__refresh-btn',
              {
                'workspace-review__refresh-btn--loading':
                  workspaceReviewState.isRefreshingWorkspaceChangesDetector,
              },
            )}
            disabled={isDispatchingAction}
            onClick={refresh}
            tabIndex={-1}
            title="Refresh"
          >
            <RefreshIcon />
          </button>
          <button
            className="panel__header__action side-bar__header__action workspace-review__close-btn"
            disabled={!workspaceReview || isDispatchingAction}
            onClick={closeReview}
            tabIndex={-1}
            title="Close review"
          >
            <TimesIcon />
          </button>
        </div>
      </div>
      <div className="panel__content side-bar__content">
        <PanelLoadingIndicator isLoading={isDispatchingAction} />
        <div className="panel workspace-review">
          {!workspaceReview && (
            <>
              <form
                className="workspace-review__title"
                onSubmit={(e): void => {
                  e.preventDefault();
                }}
              >
                <div className="workspace-review__title__content">
                  <input
                    className="workspace-review__title__content__input input--dark"
                    ref={reviewTitleInputRef}
                    spellCheck={false}
                    value={workspaceReviewState.reviewTitle}
                    disabled={Boolean(workspaceReview)}
                    onChange={editReviewTitle}
                    placeholder={'Title'}
                  />
                </div>
                <button
                  className="btn--dark btn--sm"
                  onClick={createReview}
                  disabled={
                    isDispatchingAction ||
                    Boolean(workspaceReview) ||
                    !workspaceReviewState.reviewTitle
                  }
                  title={'Create review'}
                >
                  <PlusIcon />
                </button>
              </form>
            </>
          )}
          {workspaceReview && (
            <>
              <div className="workspace-review__title">
                <div className="workspace-review__title__content">
                  <div
                    className="workspace-review__title__content__input workspace-review__title__content__input--with-link"
                    title={'See review detail'}
                  >
                    <Link
                      className="workspace-review__title__content__input__link"
                      rel="noopener noreferrer"
                      target="_blank"
                      to={generateReviewRoute(
                        workspaceReview.projectId,
                        workspaceReview.id,
                      )}
                    >
                      <span className="workspace-review__title__content__input__link__review-name">
                        {workspaceReview.title}
                      </span>
                      <div className="workspace-review__title__content__input__link__btn">
                        <ExternalLinkSquareIcon />
                      </div>
                    </Link>
                  </div>
                </div>
                <button
                  className="btn--dark btn--sm workspace-review__merge-review-btn"
                  onClick={commitReview}
                  disabled={isDispatchingAction || Boolean(!workspaceReview)}
                  tabIndex={-1}
                  title={'Commit review'}
                >
                  <TruncatedGitMergeIcon />
                </button>
              </div>
              <div className="workspace-review__title__content__review-status">
                created{' '}
                {formatDistanceToNow(workspaceReview.createdAt, {
                  includeSeconds: true,
                  addSuffix: true,
                })}
              </div>
            </>
          )}
          <WorkspaceReviewDiffs />
        </div>
      </div>
    </div>
  );
})
Example #19
Source File: ReviewSideBar.tsx    From legend-studio with Apache License 2.0 4 votes vote down vote up
ReviewSideBar = observer(() => {
  const reviewStore = useReviewStore();
  const editorStore = useEditorStore();
  const applicationStore = useApplicationStore();
  // Review infos
  const review = reviewStore.review;
  const currentUser = editorStore.sdlcServerClient.currentUser;
  let reviewStatus = '';
  switch (review.state) {
    case ReviewState.OPEN:
      reviewStatus = `created ${formatDistanceToNow(review.createdAt, {
        includeSeconds: true,
        addSuffix: true,
      })} by ${review.author.name}`;
      break;
    case ReviewState.CLOSED:
      reviewStatus = review.closedAt
        ? `closed ${formatDistanceToNow(review.closedAt, {
            includeSeconds: true,
            addSuffix: true,
          })}`
        : 'review is closed';
      break;
    case ReviewState.COMMITTED:
      reviewStatus = review.committedAt
        ? `committed ${formatDistanceToNow(review.committedAt, {
            includeSeconds: true,
            addSuffix: true,
          })}`
        : 'review is closed';
      break;
    case ReviewState.UNKNOWN:
      reviewStatus = review.lastUpdatedAt
        ? `last updated ${formatDistanceToNow(review.lastUpdatedAt, {
            includeSeconds: true,
            addSuffix: true,
          })}`
        : 'review status is unknown';
      break;
    default:
  }
  // Actions
  const isDispatchingAction =
    reviewStore.isFetchingComparison ||
    reviewStore.isApprovingReview ||
    reviewStore.isClosingReview ||
    reviewStore.isCommittingReview ||
    reviewStore.isReopeningReview;
  const closeReview = applicationStore.guardUnhandledError(() =>
    flowResult(reviewStore.closeReview()),
  );
  const reOpenReview = applicationStore.guardUnhandledError(() =>
    flowResult(reviewStore.reOpenReview()),
  );
  const commitReview = applicationStore.guardUnhandledError(() =>
    flowResult(reviewStore.commitReview()),
  );
  const approveReview = applicationStore.guardUnhandledError(() =>
    flowResult(reviewStore.approveReview()),
  );
  // Changes
  const changes = editorStore.changeDetectionState.aggregatedWorkspaceChanges;
  const currentEditorState = editorStore.currentEditorState;
  const isSelectedDiff = (diff: EntityDiff): boolean =>
    currentEditorState instanceof EntityDiffViewState &&
    diff.oldPath === currentEditorState.fromEntityPath &&
    diff.newPath === currentEditorState.toEntityPath;
  const openChange =
    (diff: EntityDiff): (() => void) =>
    (): void =>
      editorStore.workspaceReviewState.openReviewChange(diff);

  return (
    <div className="panel review__side-bar">
      <div className="panel__header side-bar__header">
        <div className="panel__header__title review__side-bar__header__title">
          <div className="panel__header__title__content side-bar__header__title__content">
            REVIEW
          </div>
        </div>
        <div className="panel__header__actions side-bar__header__actions">
          {review.state !== ReviewState.COMMITTED && (
            <button
              className="panel__header__action side-bar__header__action review__close-btn"
              disabled={
                isDispatchingAction || review.state === ReviewState.CLOSED
              }
              onClick={closeReview}
              tabIndex={-1}
              title="Close review"
            >
              <TimesIcon />
            </button>
          )}
        </div>
      </div>
      <div className="panel__content side-bar__content">
        <PanelLoadingIndicator isLoading={isDispatchingAction} />
        <div className="panel workspace-review">
          <div className="review__side-bar__review__info">
            <div
              className={clsx(
                'review__side-bar__review__info__content',
                {
                  'review__side-bar__review__info__content--closed':
                    review.state === ReviewState.CLOSED,
                },
                {
                  'review__side-bar__review__info__content--committed':
                    review.state === ReviewState.COMMITTED,
                },
              )}
            >
              <div className="review__side-bar__review__info__content__title">
                <span className="review__side-bar__review__info__content__title__review-name">
                  {review.title}
                </span>
              </div>
            </div>
            {review.state === ReviewState.CLOSED && (
              <button
                className="review__side-bar__review__info__action btn--dark btn--sm"
                onClick={reOpenReview}
                disabled={isDispatchingAction}
                tabIndex={-1}
                title={'Re-open review'}
              >
                <ArrowUpIcon />
              </button>
            )}
            {review.state === ReviewState.OPEN && (
              <>
                <button
                  className="btn--dark btn--sm"
                  onClick={approveReview}
                  // TODO: when we improve approval APIs we can know when to hide/disable this button altogether, right now the check is just to ensure nobody can self-approve
                  disabled={
                    isDispatchingAction ||
                    currentUser?.userId === review.author.name
                  }
                  tabIndex={-1}
                  title={'Approve review'}
                >
                  <CheckIcon />
                </button>
                <button
                  className="btn--dark btn--sm review__side-bar__merge-btn"
                  onClick={commitReview}
                  // TODO: when we improve approval APIs we can know when to hide/disable this button altogether
                  disabled={isDispatchingAction}
                  tabIndex={-1}
                  title={'Commit review'}
                >
                  <TruncatedGitMergeIcon />
                </button>
              </>
            )}
          </div>
          <div className="review__side-bar__review__info__content__status">
            {reviewStatus}
          </div>
          <div className="panel side-bar__panel">
            <div className="panel__header">
              <div className="panel__header__title">
                <div className="panel__header__title__content">CHANGES</div>
                <div
                  className="side-bar__panel__title__info"
                  title="All changes made in the workspace since the revision the workspace is created"
                >
                  <InfoCircleIcon />
                </div>
              </div>
              <div
                className="side-bar__panel__header__changes-count"
                data-testid={
                  LEGEND_STUDIO_TEST_ID.SIDEBAR_PANEL_HEADER__CHANGES_COUNT
                }
              >
                {changes.length}
              </div>
            </div>
            <div className="panel__content">
              {changes
                .slice()
                .sort(entityDiffSorter)
                .map((diff) => (
                  <EntityDiffSideBarItem
                    key={diff.key}
                    diff={diff}
                    isSelected={isSelectedDiff(diff)}
                    openDiff={openChange(diff)}
                  />
                ))}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
})
Example #20
Source File: MetaSubmission.tsx    From crosshare with GNU Affero General Public License v3.0 4 votes vote down vote up
MetaSubmissionForm = (props: {
  user: firebase.User;
  revealDisabledUntil: Date | null;
  hasPrize: boolean;
  dispatch: Dispatch<ContestSubmitAction | ContestRevealAction>;
  solutions: Array<string>;
}) => {
  const [submission, setSubmission] = useState('');
  const displayName = useDisplayName();
  const [editingDisplayName, setEditingDisplayName] = useState(false);
  const [enteringForPrize, setEnteringForPrize] = useState(false);
  const { addToast } = useSnackbar();
  const disabled = props.revealDisabledUntil
    ? new Date() < props.revealDisabledUntil
    : false;

  function submitMeta(event: FormEvent) {
    event.preventDefault();
    props.dispatch({
      type: 'CONTESTSUBMIT',
      submission: submission,
      displayName: displayName || 'Anonymous Crossharer',
      ...(props.hasPrize &&
        enteringForPrize &&
        props.user.email && { email: props.user.email }),
    });
    if (isMetaSolution(submission, props.solutions)) {
      addToast('? Solved a meta puzzle!');
    }
    setSubmission('');
  }

  return (
    <>
      <form onSubmit={submitMeta}>
        <p>
          {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
          <label>
            Your meta submission:
            <br />
            <LengthLimitedInput
              placeholder="Submission (case insensitive)"
              type="text"
              value={submission}
              maxLength={MAX_META_SUBMISSION_LENGTH}
              updateValue={setSubmission}
            />
            <LengthView
              value={submission}
              maxLength={MAX_META_SUBMISSION_LENGTH}
              hideUntilWithin={30}
            />
          </label>
        </p>
        {props.hasPrize && props.user.email ? (
          <p>
            This puzzle has a prize. If you choose to enter, your email will be
            made available to the constructor so they can select a winner.
            <br />
            <label>
              <input
                css={{ marginRight: '1em' }}
                type="checkbox"
                checked={enteringForPrize}
                onChange={(e) => {
                  setEnteringForPrize(e.target.checked);
                }}
              />{' '}
              Enter my email address ({props.user.email}) for the prize
            </label>
          </p>
        ) : (
          ''
        )}
        {editingDisplayName || !displayName ? (
          ''
        ) : (
          <>
            <p>
              <Button
                type="submit"
                css={{ marginRight: '0.5em' }}
                disabled={!/\S/.test(submission)}
                text="Submit"
              />
              submitting as {displayName} (
              <ButtonAsLink
                onClick={() => setEditingDisplayName(true)}
                text="change name"
              />
              )
            </p>
          </>
        )}
      </form>
      {editingDisplayName || !displayName ? (
        <>
          <DisplayNameForm onCancel={() => setEditingDisplayName(false)} />
        </>
      ) : (
        ''
      )}
      <p>
        <Button
          css={{ marginRight: '0.5em' }}
          onClick={() => {
            props.dispatch({
              type: 'CONTESTREVEAL',
              displayName: displayName || 'Anonymous Crossharer',
            });
          }}
          disabled={disabled}
          text="Give Up / Reveal"
        />
        {disabled && props.revealDisabledUntil ? (
          <span>
            Reveal will be available{' '}
            {formatDistanceToNow(props.revealDisabledUntil, {
              addSuffix: true,
            })}
          </span>
        ) : (
          ''
        )}
      </p>
    </>
  );
}
Example #21
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 #22
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>
  );
}