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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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>
);
}