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

Implement attachments #4131

Merged
merged 33 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d495481
AB#222: Ability to upload attachment
ce-simsoft Nov 3, 2022
3a28f00
AB#618: Add enumeration of possible classification to dictionary
ce-simsoft Jan 9, 2023
66d9775
AB#763: Be able to actually upload attachments
mk-simsoft Mar 27, 2023
b2b00cf
AB#823: Ability to download attachments
ce-simsoft Apr 12, 2023
81e6a51
AB#826: Attachment Engagement Report Edit Funcinality
mk-simsoft Apr 14, 2023
082b6a7
AB#763: Be able to actually download attachments
mk-simsoft Apr 17, 2023
67aec99
AB#222: Add author to attachments
ce-simsoft Apr 17, 2023
e29b2dc
AB#598: Be able to add attachment to report
mk-simsoft Apr 25, 2023
f45190c
AB#839: Be able to view image without download
ce-simsoft May 8, 2023
945267f
AB#832: Be able to zoom into a pciture (image) attachment
mk-simsoft May 8, 2023
797df93
AB#823/AB#763 Make attachment content view/download streaming
gjvoosten May 11, 2023
9a38d84
AB#832 Refactor the way background image and size are determined
gjvoosten May 17, 2023
f774a14
AB#832 Add contentLength to attachment
gjvoosten May 17, 2023
8fe38b3
AB#832: Client side comment/review fixes
mk-simsoft May 17, 2023
0c894c1
AB#853: Add server side tests
ce-simsoft May 17, 2023
0bf1fd3
AB#863: Change upload of attachment content
ce-simsoft May 29, 2023
8b0fd4c
AB864: Change functionality of upload attachment for client-side
mk-simsoft May 29, 2023
d9585cf
AB#854: Create tests for attachment client side
mk-simsoft Jun 1, 2023
5a6813b
AB#866: Restrict uploading attachments
ce-simsoft Jun 2, 2023
57281a4
AB#866 Refactor attachment content upload and download
gjvoosten Jun 7, 2023
0b57106
AB#866 Add authentication header to upload request
gjvoosten Jun 8, 2023
5b294f8
AB#866 Show upload progress indicator
gjvoosten Jun 8, 2023
027f370
AB#866 Return negative contentLength if attachment content is absent
gjvoosten Jun 8, 2023
0ff99fc
AB#866 Treat attachments without content differently
gjvoosten Jun 8, 2023
b5901da
AB#866 Set page title on attachment pages consistently
gjvoosten Jun 8, 2023
ebe2e1c
AB#866 Clearly distinguish messages when multiple uploads are in prog…
gjvoosten Jun 8, 2023
fbea6c7
AB#866 Add some more nice icons for various MIME types
gjvoosten Jun 8, 2023
a172523
AB#866: Update restriction
ce-simsoft Jun 9, 2023
a7fa7c9
AB#866 Actually store a test attachment in the base data
gjvoosten Jun 14, 2023
0b6f642
AB#866 Update comment
gjvoosten Jun 14, 2023
e7ae352
AB#866 Properly return a 204 No Content after successful content upload
gjvoosten Jun 15, 2023
7cb2c10
AB#866 Refactor checks on attachments
gjvoosten Jun 15, 2023
ed7604b
AB#866 Add sanity check on mimeType of uploaded content
gjvoosten Jun 15, 2023
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
29 changes: 29 additions & 0 deletions anet-dictionary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,36 @@ fields:
label: Red
color: '#ff8279'

attachment:
disabled: false
shortLabel: Attachment
shortName:
label: Attachment Name
placeholder: Enter an attachment name, example....
description: Attachment description
classification:
type: enum
label: UNDEFINED / NATO_UNCLASSIFIED / NATO_UNCLASSIFIED_Releasable_to_EU
choices:
UNDEFINED:
value: UNDEFINED
label: undefined
NATO_UNCLASSIFIED:
value: NATO_UNCLASSIFIED
label: NATO UNCLASSIFIED
NATO_UNCLASSIFIED_Releasable_to_EU:
value: NATO_UNCLASSIFIED_Releasable_to_EU
label: NATO UNCLASSIFIED Releasable to EU
mimeTypes:
- application/pdf
- image/jpeg
- image/jpg
- image/png
- text/plain

report:
attachments:
label: Attachments
canUnpublishReports: true
intent: Engagement purpose
atmosphere: Atmospherics
Expand Down
17 changes: 14 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ dependencies {
implementation "io.dropwizard:dropwizard-auth:${dropwizardVersion}"
implementation "io.dropwizard:dropwizard-views-freemarker:${dropwizardVersion}"
implementation 'io.dropwizard-bundles:dropwizard-configurable-assets-bundle:1.3.5'
implementation "io.dropwizard:dropwizard-forms:${dropwizardVersion}"
implementation "org.jdbi:jdbi3-postgres:3.37.1"

implementation 'ru.vyarus.guicey:guicey-jdbi3:5.9.0'
implementation 'com.google.guava:guava:31.1-jre' // Pick the non-Android version of Guice
Expand Down Expand Up @@ -122,6 +124,8 @@ dependencies {
// used for writing Excel documents
implementation 'org.apache.poi:poi:5.2.3'
implementation 'org.apache.poi:poi-ooxml:5.2.3'
// For inferring MIME types of attachments
implementation 'org.apache.tika:tika-core:2.8.0'

// For parsing HTML to check for 'empty' input
implementation 'org.jsoup:jsoup:1.16.1'
Expand Down Expand Up @@ -394,16 +398,23 @@ task pushPsqlBaseData(type: DockerCopyFileToContainer) {
hostPath = "${projectDir}/insertBaseData-psql.sql"
remotePath = psqlPushDataPath
}

pushPsqlBaseData.mustRunAfter('dockerStartDB')

task dbLoad(dependsOn: pushPsqlBaseData, type: DockerExecContainer) {
task pushPsqlImageData(type: DockerCopyFileToContainer) {
group = "database runtime"
description = "Push SQL base data to PostgreSQL"
containerId = dbContainerName
hostPath = "${projectDir}/src/test/resources/assets/default_avatar.png"
remotePath = psqlPushDataPath
}
pushPsqlImageData.mustRunAfter('dockerStartDB')

task dbLoad(dependsOn: [pushPsqlBaseData, pushPsqlImageData], type: DockerExecContainer) {
group = "database runtime"
description = "Runs the ANET database load command for PostgreSQL; loads demo data."
containerId = dbContainerName
commands = [ ["psql", "-U", run.environment["ANET_DB_USERNAME"], "-d", run.environment["ANET_DB_NAME"], "-f", "${psqlPushDataPath}/insertBaseData-psql.sql"] as String[] ]
}

dbLoad.mustRunAfter('dbMigrate')

task deleteDbDump(type: DockerExecContainer) {
Expand Down
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"@fullcalendar/react": "6.1.7",
"@fullcalendar/timegrid": "6.1.7",
"@projectstorm/react-diagrams": "7.0.2",
"axios": "1.4.0",
"bootstrap": "5.1.3",
"change-case": "4.1.2",
"classnames": "2.3.2",
Expand Down
110 changes: 110 additions & 0 deletions client/src/components/Attachment/Attachment.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/******** FILE UPLOAD AREA ********/
.file-upload-container {
margin: 10px 0;
display: flex;
position: relative;
padding: 35px 20px;
border-radius: 6px;
align-items: center;
flex-direction: column;
background-color: #f2f2f2;
border: 2px dotted lightgray;
}

.file-upload-container:hover {
background-color: #dddbdb;
}

.form-field {
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
width: 100%;
border: none;
display: block;
cursor: pointer;
font-size: 18px;
position: absolute;
text-transform: none;
}

.drag-drop-text {
display: flex;
margin-top: 0;
text-align: center;
align-items: center;
flex-direction: column;
letter-spacing: 2.2px;
}

.drag-drop-text > img {
margin-bottom: 10px;
width: fit-content;
}

.image-preview {
width: 100%;
height: 100%;
border-radius: 6px;
}

.file-info {
display: none;
}
.info-show:hover .file-info {
height: 100%;
display: flex;
flex-direction: column;
background: #ffffff73;
justify-content: space-between;
}
.info-line {
width: 100%;
display: flex;
justify-content: space-between;
}
.file-info > .bp4-popover2-target {
height: 100%;
width: 100%;
}
.detail-btn {
display: grid;
width: 100%;
height: 120px;
place-content: center;
}
.icon:hover {
cursor: pointer;
}
.edit:hover {
color: blue;
}
.delete:hover {
color: #cd2222;
}

/******** ATTACHMENT CARD ********/
.card-image {
width: 100%;
height: 120px;
background-position: center;
background-repeat: no-repeat;
border-radius: 0.10rem 0.10rem 0 0;
}

/******** ATTACHMENT SHOW PAGE ********/
.img-preview {
height: 100%;
overflow: hidden;
}
.attachment-image {
width: 100%;
border-radius: 5px;
transition: transform .5s ease;
}
.img-hover-zoom:hover .attachment-image {
cursor: zoom-in;
transform: scale(1.5);
}
127 changes: 127 additions & 0 deletions client/src/components/Attachment/AttachmentCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { gql } from "@apollo/client"
import { Icon } from "@blueprintjs/core"
import { IconNames } from "@blueprintjs/icons"
import API from "api"
import ConfirmDestructive from "components/ConfirmDestructive"
import LinkTo from "components/LinkTo"
import PropTypes from "prop-types"
import React from "react"
import { Card } from "react-bootstrap"
import { toast } from "react-toastify"
import utils from "utils"
import "./Attachment.css"

const GQL_DELETE_ATTACHMENT = gql`
mutation ($uuid: String!) {
deleteAttachment(uuid: $uuid)
}
`

const AttachmentCard = ({
attachment,
edit,
remove,
setError,
setRemove,
uploadedList,
setUploadedList
}) => {
const { backgroundSize, backgroundImage } = utils.getAttachmentIconDetails(
attachment,
true
)

return (
<div id="attachmentCard" key={attachment.uuid} style={{ width: "20%" }}>
<Card>
<div
className="image-preview info-show card-image"
style={{
backgroundSize,
backgroundImage: `url(${backgroundImage})`
}}
mk-simsoft marked this conversation as resolved.
Show resolved Hide resolved
>
<div className="file-info image-info">
<div style={{ display: "grid" }}>
<LinkTo
className="detail-btn"
modelType="Attachment"
model={attachment}
>
{" "}
</LinkTo>
</div>
</div>
</div>
<Card.Body className="p-1 d-block">
<Card.Title style={{ fontSize: "15px" }} className="info-line">
{utils.ellipsize(attachment?.fileName, 8)}
<span>
{utils.humanReadableFileSize(attachment?.contentLength)}
</span>
</Card.Title>
{edit && (
<div className="info-line">
<div>
<LinkTo
modelType="Attachment"
edit
model={attachment}
button="outline-primary"
>
<Icon icon={IconNames.EDIT} className="icon edit" />
</LinkTo>
</div>
<ConfirmDestructive
onConfirm={() => deleteAttachment(attachment)}
objectType="attachment"
objectDisplay={"#" + attachment.uuid}
title="Delete attachment"
variant="outline-danger"
buttonSize="xs"
>
<Icon icon={IconNames.TRASH} />
</ConfirmDestructive>
</div>
)}
</Card.Body>
</Card>
</div>
)

function deleteAttachment(attachment) {
const newAttachments = uploadedList.filter(
item => item.uuid !== attachment.uuid
)
API.mutation(GQL_DELETE_ATTACHMENT, { uuid: attachment.uuid })
.then(data => {
setUploadedList(newAttachments)
if (!remove) {
setRemove(true)
}
toast.success(
`Your attachment ${attachment.fileName} has been successfully deleted`
)
})
.catch(error => {
setError(error)
})
}
}

AttachmentCard.propTypes = {
attachment: PropTypes.object,
edit: PropTypes.bool,
remove: PropTypes.bool,
setError: PropTypes.func,
setRemove: PropTypes.func,
uploadedList: PropTypes.array,
setUploadedList: PropTypes.func
}

AttachmentCard.defaultProps = {
edit: true,
remove: undefined
}

export default AttachmentCard
38 changes: 38 additions & 0 deletions client/src/components/Attachment/AttachmentRelatedObjectsTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import LinkTo from "components/LinkTo"
import Model from "components/Model"
import React from "react"
import { Table } from "react-bootstrap"

const AttachmentRelatedObjectsTable = ({ relatedObjects }) => {
return (
<div id="related_objects">
{!relatedObjects?.length ? (
<em>No linked objects</em>
) : (
<Table striped hover responsive className="related_objects_table">
<tbody>
{relatedObjects.map(attachRelObj => (
<tr key={attachRelObj.relatedObjectUuid}>
<td>
<LinkTo
modelType={attachRelObj.relatedObjectType}
model={{
uuid: attachRelObj.relatedObjectUuid,
...attachRelObj.relatedObject
}}
/>
</td>
</tr>
))}
</tbody>
</Table>
)}
</div>
)
}

AttachmentRelatedObjectsTable.propTypes = {
relatedObjects: Model.attachmentRelatedObjectsPropType.isRequired
}

export default AttachmentRelatedObjectsTable
Loading