Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add validations on create evidence #1142

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions frontend/src/components/binary_upload/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,28 @@ const BinaryUpload = (props: {
onChange: (newValue: File | null) => void,
isSupportedFile: (file: File ) => boolean,
value: File | null,
error: string,
}) => {
const [err, setErr] = React.useState<Error | null>(null)

const {value, isSupportedFile, label} = {...props}

React.useEffect(() => {
if (!props.error) {
setErr(null)
} else {
setErr(Error(props.error))
}
}, [props.error])

React.useEffect(() => {
const file = value
if (file == null || isSupportedFile(file)) {
setErr(null)
if (!props.error) setErr(null)
} else {
setErr(Error(`Expected a ${label.toLowerCase()}, but got ${file.type}.`))
}
}, [value, isSupportedFile, label])
}, [value, isSupportedFile, label, props.error])

const { getRootProps, getInputProps, isDragActive } = useDropzone({
multiple: false,
Expand Down Expand Up @@ -51,18 +60,20 @@ const BinaryUploadChildren = (props: {
err: Error | null,
file: File | null,
}) => {
const content = (props.file != null && props.err == null)
? <div className={cx('has-content')}>
<div>Will Upload: {props.file.name}</div>
</div>
: <div className={cx('no-content')}>
Drag {props.friendlyFileType} here or <span>Browse for one</span> to upload
{props.err &&
<div className={cx('error')}>{props.err.message}</div>
}
</div>
if (props.file !== null && props.err === null) {
return (
<div className={cx('has-content')}>
<div>Will Upload: {props.file.name}</div>
</div>
)
}

return content
return (
<div className={cx('no-content')}>
Drag {props.friendlyFileType} here or <span>Browse for one</span> to upload
{props.err && <div className={cx('error')}>{props.err.message}</div>}
</div>
)
}

export default BinaryUpload
3 changes: 3 additions & 0 deletions frontend/src/components/code_block/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import supportedLanguages from './supported_languages'
import { CodeBlock } from 'src/global_types'
import ComboBox from 'src/components/combobox'
import { useAsyncComponent } from 'src/helpers'

const cx = classnames.bind(require('./stylesheet'))
const importAceEditorAsync = () => import('./ace_editor').then(module => module.default)

export const CodeBlockEditor = (props: {
disabled?: boolean,
onChange: (newValue: CodeBlock) => void,
value: CodeBlock,
error: string
}) => {
const AceEditor = useAsyncComponent(importAceEditorAsync)

Expand Down Expand Up @@ -44,6 +46,7 @@ export const CodeBlockEditor = (props: {
readOnly={props.disabled}
/>
</div>
{props.error && <div className={cx('error')}>{props.error}</div>}
</div>
)
}
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/code_block/stylesheet.styl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
.source-none
font-style: italic

.error
margin-top: 10px
font-weight: 800
color: $error

.code-editor
.controls
display: flex
Expand Down
25 changes: 18 additions & 7 deletions frontend/src/components/image_upload/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,19 @@ export default (props: {
label: string,
onChange: (newValue: File|null) => void,
value: File | null,
error: string,
}) => {
const [imageDataUriString, setImageDataUriString] = React.useState<string|null>(null)
const [err, setErr] = React.useState<Error|null>(null)

React.useEffect(() => {
if (props.error) {
setErr(Error(props.error))
} else {
setErr(null)
}
}, [props.error])

React.useEffect(() => {
const file = props.value
if (file == null) {
Expand Down Expand Up @@ -51,13 +60,15 @@ const ImageUploadChildren = (props: {
err: Error | null,
image: string | null,
}) => {
if (props.image == null) return (
<div className={cx('no-image')}>
<img src={require('./image.svg')} />
Drag an image here or <span>Browse for an image</span> to upload
{props.err && <div className={cx('error')}>{props.err.message}</div>}
</div>
)
if (props.image == null) {
return (
<div className={cx('no-image')}>
<img src={require('./image.svg')} />
Drag an image here or <span>Browse for an image</span> to upload
{props.err && <div className={cx('error')}>{props.err.message}</div>}
</div>
)
}

return (
<div className={cx('has-images')}>
Expand Down
15 changes: 11 additions & 4 deletions frontend/src/helpers/use_form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import {Result} from 'src/global_types'
// )

export function useForm<T>(i: {
handleSubmit: () => Promise<T>,
fields?: Array<{setDisabled: (v: boolean) => void}>,
handleSubmit: () => T | Promise<T>,
fields?: Array<{setDisabled: (v: boolean) => void, setError: (error: string) => void}>,
onSuccessText?: string,
onSuccess?: () => void,
}) {
Expand Down Expand Up @@ -63,12 +63,19 @@ export function useForm<T>(i: {
}
}

return {onSubmit, loading, result}
const clear = () => {
if (i.fields) i.fields.map(field => field.setError(""))

setResult(null)
}

return {onSubmit, clear, loading, result}
}

export function useFormField<T>(initialValue: T) {
const [value, onChange] = React.useState<T>(initialValue)
const [disabled, setDisabled] = React.useState(false)
const [error, setError] = React.useState<string>('')

return {value, onChange, disabled, setDisabled}
return {value, onChange, disabled, setDisabled, error, setError}
}
52 changes: 44 additions & 8 deletions frontend/src/pages/operation_show/evidence_modals/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
SubmittableEvidence,
Operation,
TagDifference,
SupportedEvidenceType,
} from 'src/global_types'
import {
codeblockToBlob,
Expand Down Expand Up @@ -56,9 +55,16 @@ import SplitInputRow from 'src/components/split_input_row'
import WithLabel from 'src/components/with_label'
import { format, isValid } from 'date-fns'


const cx = classnames.bind(require('./stylesheet'))

type EvidenceType = 'codeblock' | 'image' | 'event' | 'terminal-recording' | 'http-request-cycle' | 'none'

interface EvidenceOption {
name: string;
value: EvidenceType;
content?: React.ReactNode;
}

export const CreateEvidenceModal = (props: {
onCreated: () => void,
onRequestClose: () => void,
Expand All @@ -73,7 +79,7 @@ export const CreateEvidenceModal = (props: {
const isATerminalRecording = (file: File) => file.type == ''
const isAnHttpRequestCycle = (file: File) => file.name.endsWith("har")

const evidenceTypeOptions: Array<{ name: string, value: SupportedEvidenceType, content?: React.ReactNode }> = [
const evidenceTypeOptions: Array<EvidenceOption> = [
{ name: 'Screenshot', value: 'image', content: <ImageUpload label='Screenshot' {...binaryBlobField} /> },
{ name: 'Code Block', value: 'codeblock', content: <CodeBlockEditor {...codeblockField} /> },
{ name: 'Event', value: 'event', content: <div /> },
Expand All @@ -88,21 +94,47 @@ export const CreateEvidenceModal = (props: {
]

const [selectedCBValue, setSelectedCBValue] = React.useState<string>(evidenceTypeOptions[0].value)
const getSelectedOption = () => evidenceTypeOptions.filter(opt => opt.value === selectedCBValue)[0]
const getSelectedOption = () => evidenceTypeOptions.find(opt => opt.value === selectedCBValue) as EvidenceOption

const formComponentProps = useForm({
const handleError = (field: ReturnType<typeof useFormField>, errorMessage: string) => {
const error = new Error(errorMessage)

field.setError(error.message)

return Promise.reject(error)
}

const { clear: clearForm, ...formComponentProps} = useForm({
fields: [descriptionField, binaryBlobField, adjustedAtField],
onSuccess: () => { props.onCreated(); props.onRequestClose() },
handleSubmit: () => {
let data: SubmittableEvidence = { type: "none" }
const selectedOption = getSelectedOption()
const fileBasedKeys = ['image', 'terminal-recording', 'http-request-cycle']

if (selectedOption.value === 'codeblock' && codeblockField.value !== null) {
if (selectedOption.value === 'codeblock') {
if (!codeblockField.value?.code) {
return handleError(codeblockField, "A codeblock must have a code written on it")
} else {
codeblockField.setError("")
}

data = { type: 'codeblock', file: codeblockToBlob(codeblockField.value) }
} else if (fileBasedKeys.includes(selectedOption.value) && binaryBlobField.value != null) {
} else if (fileBasedKeys.includes(selectedOption.value)) {
if (!binaryBlobField.value) {
return handleError(binaryBlobField, `A file is required for evidence of type ${selectedOption.name}`)
} else {
binaryBlobField.setError("")
}

data = { type: selectedOption.value, file: binaryBlobField.value }
} else if (selectedOption.value === 'event') {
if (!descriptionField.value) {
return handleError(descriptionField, "Description is required for event evidence")
} else {
descriptionField.setError("")
}

data = { type: 'event' }
}

Expand All @@ -119,12 +151,16 @@ export const CreateEvidenceModal = (props: {
return (
<ModalForm title="New Evidence" submitText="Create Evidence" onRequestClose={props.onRequestClose} {...formComponentProps}>
<TextArea label="Description" {...descriptionField} />
{descriptionField.error && <div className={cx('error')}>{descriptionField.error}</div>}
<ComboBox
label="Evidence Type"
className={cx('dropdown')}
options={evidenceTypeOptions}
value={selectedCBValue}
onChange={setSelectedCBValue}
onChange={(value) => {
clearForm()
setSelectedCBValue(value)
}}
/>
{getSelectedOption().content}
<TagChooser operationSlug={props.operationSlug} label="Tags" {...tagsField} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
@import '~src/vars'

.dropdown
width: 35%

.error
margin-top: 8px
font-weight: 800
color: $error

.view-metadata-root
display: flex
flex-direction: column
Expand Down
Loading