From 5596f54864499fbace8a33a244fdb43bbb3afeba Mon Sep 17 00:00:00 2001 From: Barry Low Date: Fri, 28 Sep 2018 17:31:19 -0700 Subject: [PATCH] Refactor upload-image to image-upload component --- api/src/index.js | 23 +- api/src/storage/azure.js | 2 +- client/src/views/common/ImageUpload.js | 264 ++++++++++++++++++ .../dashboard/components/ManageTeamEdit.js | 39 ++- 4 files changed, 306 insertions(+), 22 deletions(-) create mode 100644 client/src/views/common/ImageUpload.js diff --git a/api/src/index.js b/api/src/index.js index 6c61ceed8..5ec890d4b 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -13,18 +13,7 @@ const checkJwt = require('./auth/middleware/jwt') const { getMember } = require('./auth/middleware/getMember') const { validateIdToken } = require('./auth/validateIdToken') const { directiveResolvers } = require('./auth/authDirectives') -const { - createStoragePipline, - createServiceURL, - listContainers, - createContainer, - createBlob, - uploadBlob, - downloadBlob, - deleteBlob, - deleteContainer -} = require('./storage/azure') -//var azure = require('azure-storage') + const Azure = require("@azure/storage-blob") const azureAccount = process.env.AZURE_STORAGE_ACCOUNT const azureKey = process.env.AZURE_STORAGE_ACCESS_KEY @@ -160,6 +149,7 @@ const resolvers = { const containerName = teamSlug const containerURL = Azure.ContainerURL.fromServiceURL(serviceURL, containerName) + //list container and check if already exists. let marker let containerCheck = null do { @@ -167,25 +157,28 @@ const resolvers = { Azure.Aborter.None, marker, ) - console.log(`ContainerCheck marker: `, listContainersResponse) + // console.log(`ContainerCheck marker: `, listContainersResponse) marker = listContainersResponse.marker; for (const container of listContainersResponse.containerItems) { - console.log(`ContainerCheck: ${container.name} | ${containerName} | ${marker}`) + // console.log(`ContainerCheck: ${container.name} | ${containerName} | ${marker}`) if(container.name === containerName){ containerCheck = container.name } } } while (marker) + + // if container doesn't exist, create one let newContainer if(containerCheck === null){ newContainer = await containerURL.create(Azure.Aborter.None, { access: 'blob' }) } - console.log('getAzureSAS container: ', containerURL, containerCheck, newContainer) + // console.log('getAzureSAS container: ', containerURL, containerCheck, newContainer) // Set permissions for service, resource types and containers const SASServicePerms = Azure.AccountSASServices.parse('b') const SASResourceTypes = Azure.AccountSASResourceTypes.parse('co') const SASContainerPerms = Azure.ContainerSASPermissions.parse('rwl') + // Generate SAS url const SASQueryParameters = Azure.generateAccountSASQueryParameters( { diff --git a/api/src/storage/azure.js b/api/src/storage/azure.js index 324181b04..abeea558e 100644 --- a/api/src/storage/azure.js +++ b/api/src/storage/azure.js @@ -4,7 +4,7 @@ const Azure = require('@azure/storage-blob') * @accountName: Azure Blob Storage account name (required) - Environment variable * @accountKey: Azure Blob Storage storage key (required) - Environment variable */ -const createStoragePipline = async (accountNAme, accountKey) => { +const createStoragePipline = async (accountName, accountKey) => { return Azure.StorageURL.newPipeline( new Azure.SharedKeyCredential(accountName, accountKey) ) diff --git a/client/src/views/common/ImageUpload.js b/client/src/views/common/ImageUpload.js new file mode 100644 index 000000000..fd4d837bb --- /dev/null +++ b/client/src/views/common/ImageUpload.js @@ -0,0 +1,264 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { Mutation } from 'react-apollo' +import Dropzone from 'react-dropzone' +import { Cropper } from 'react-image-cropper' +import * as Azure from '@azure/storage-blob' +import readAndCompressImage from 'browser-image-resizer' +import { imgSrcToBlob } from 'blob-util' +import { withStyles } from '@material-ui/core/styles' + +import Dialog from '@material-ui/core/Dialog' +import DialogActions from '@material-ui/core/DialogActions' +import DialogContent from '@material-ui/core/DialogContent' +import Button from '@material-ui/core/Button' +import Typography from '@material-ui/core/Typography' + +const styles = theme => ({ + dropzoneContainer: { + display: 'block', + margin: '1em', + height: 200 + }, + dropzone: { + display: 'block', + padding: '3em', + height: 200, + borderWidth: 2, + borderColor: '#999', + backgroundColor: '#eee' + }, + dropzoneText: { + textAlign: 'center' + }, + existingImagePreview: { + width: 100, + height: 100 + }, + cropImageBox: { + width: 350, + height: 350 + } +}) + +// placeholder image if no existing image +const placeHolder = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22288%22%20height%3D%22225%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20288%20225%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_164edaf95ee%20text%20%7B%20fill%3A%23eceeef%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A14pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_164edaf95ee%22%3E%3Crect%20width%3D%22288%22%20height%3D%22225%22%20fill%3D%22%2355595c%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%2296.32500076293945%22%20y%3D%22118.8%22%3EAvatar%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E' + +// config for image resize and quality +const imageResizeConfig = { + quality: 0.6, + maxWidth: 200, + maxHeight: 200, + autoRotate: true, + debug: true, + mimeType: 'image/jpeg' +} + +class UploadImage extends Component { + constructor (props) { + super(props) + + this.onDrop = this.onDrop.bind(this) + this.handleCrop = this.handleCrop.bind(this) + this.handleClickOpen = this.handleClickOpen.bind(this) + this.handleClose = this.handleClose.bind(this) + this.handleChangeImage = this.handleChangeImage.bind(this) + this.cancelChangeImage = this.cancelChangeImage.bind(this) + this.saveImage = this.saveImage.bind(this) + + this.state = { + open: false, + change: false, + file: null, + src: null, + crop: { + x: 10, + y: 10, + width: 200, + height: 200 + }, + cropping: false, + cropped: null, + uploaded: false, + image: null, + sasqr: null + } + } + + render () { + const { classes, cropContainer, saveImageConfig, containerName, fileName, existingImage, handleUrl, AZURE_SAS_QUERY } = this.props + let image = null + let resizeImage = null + if (existingImage !== undefined && existingImage !== null) { + image = existingImage + } else { + image = placeHolder + } + + if (saveImageConfig !== undefined && saveImageConfig !== null) { + resizeImage = saveImageConfig + } else { + resizeImage = imageResizeConfig + } + + return ( +
+ + {(getAzureSAS, { loading, error, data }) => { + if (this.state.cropping) { + return ( + Cropping... + ) + } else { + return ( + +
+ {(this.state.change || this.state.src === null) && ( + + this.onDrop(e, this.state.edit)} + className={classes.dropzone} + multiple={false} + > +

Drag-and-drop image here or click to select
Only *.jpeg and *.png images

+
+ +
+ )} + {!this.state.change && ( + + Existing Image Preview + + + )} + {this.state.image !== null && ( + Save profile to save image changes! + )} +
+ +
+ +
+ {this.state.src !== null && ( + { this.cropper = ref }} + onImgLoad={this.onImageLoaded} + ratio={1} + /> + )} +
+
+ + + + +
+
+
+ ) + } + }} +
+
+ ) + } + + onDrop (file, editImageType) { + console.log('onDrop', file) + this.onSelectFile(file, editImageType) + this.setState({ + file + }) + } + + async handleCrop (e, getAzureSAS, resizeImage, containerName, fileName, handleUrl) { + e.preventDefault() + const data = await getAzureSAS({ variables: { containerName } }) + + const cropped = this.cropper.crop() + const croppedBlob = await imgSrcToBlob(cropped, 'image/png', 'Anonymous') + let resizedBlob = await readAndCompressImage(croppedBlob, resizeImage) + + return this.setState({ cropped: resizedBlob, cropping: true, open: false }, () => { + this.saveImage(data.getAzureSAS, containerName, fileName, handleUrl) + }) + } + + async saveImage (SASQueryParameters, containerName, fileName, handleUrl) { + const SASurl = `${process.env.AZURE_STORAGE_URL}?${this.state.sasqr}` + + // create Azure BlockBlobURL to save image to + const pipeline = Azure.StorageURL.newPipeline(new Azure.AnonymousCredential()) + const serviceURL = new Azure.ServiceURL(SASurl, pipeline) + + const containerURL = Azure.ContainerURL.fromServiceURL(serviceURL, containerName) + const blockBlobURL = Azure.BlockBlobURL.fromContainerURL(containerURL, fileName) + + await Azure.uploadBrowserDataToBlockBlob(Azure.Aborter.None, this.state.cropped, blockBlobURL) + + const image = `${process.env.AZURE_STORAGE_URL}${containerName}/${fileName}?${Math.random()}` // random number keeps browser from using cached version of existing image + handleUrl(image) // pass image URL up to be save to DB + this.setState({ cropping: false, uploaded: true, change: false, image: image }) + return image + } + + handleChangeImage (e, cropContainer) { + e.preventDefault() + let crop = cropContainer // dimensions of cropContainer passed from above + this.setState({ + change: true, + crop: crop + }) + } + + cancelChangeImage () { + this.setState({ change: false }) + } + + handleClickOpen (e) { + e.preventDefault() + this.setState({ open: true }) + } + + handleClose () { + this.setState({ open: false, src: null, change: false }) + } +} + +UploadImage.propTypes = { + classes: PropTypes.object.isRequired, + cropContainer: PropTypes.object, // { x: 10, y: 10, width: 200, height: 200 } (default) + saveImageConfig: PropTypes.object, // see imageResizeConfig above for default (default) + containerName: PropTypes.string.isRequired, // name of Azure Container image will be saved in + fileName: PropTypes.string.isRequired, // filename to save image as + existingImage: PropTypes.string, // image url of existing image (optional) + handleUrl: PropTypes.func.isRequired, // function that handles uploaded image file url + AZURE_SAS_QUERY: PropTypes.any.isRequired // GraphQL for getting Azure SAS URL +} + +export default withStyles(styles)(UploadImage) diff --git a/client/src/views/dashboard/components/ManageTeamEdit.js b/client/src/views/dashboard/components/ManageTeamEdit.js index 6cb6596b8..de5e9aa83 100644 --- a/client/src/views/dashboard/components/ManageTeamEdit.js +++ b/client/src/views/dashboard/components/ManageTeamEdit.js @@ -20,7 +20,8 @@ import GET_TEAMS_BY_OWNER from '../../../graphql/teams/GetTeamsByOwnerQuery' import { checkGraphQLError } from '../../../utils/graphql-errors' -import { UploadImage } from '../../common' +import { ImageUpload } from '../../common' +import GET_AZURE_SAS from '../../graphql/teams/GetAzureSASMutation' const styles = theme => ({ dialogContainer: { @@ -111,11 +112,37 @@ export class ManageTeamEdit extends Component { Edit Team Details - +