Skip to content
This repository has been archived by the owner on Jun 4, 2024. It is now read-only.

csv: implement file drag and drop #385

Merged
merged 11 commits into from
Feb 27, 2018
8 changes: 5 additions & 3 deletions app/components/Settings/Preview/TableTree.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ class TableTree extends Component {
getLabel(connectionObject) {
switch (connectionObject.dialect) {
case DIALECTS.SQLITE:
return BASENAME_RE.exec(connectionObject.storage)[0] || connectionObject.storage;
return BASENAME_RE.exec(connectionObject.storage)[0] || connectionObject.storage;
case DIALECTS.DATA_WORLD:
return getPathNames(connectionObject.url)[2];
return getPathNames(connectionObject.url)[2];
case DIALECTS.CSV:
return connectionObject.label || connectionObject.id || connectionObject.database;
default:
return connectionObject.database;
return connectionObject.database;
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/components/Settings/Tabs/Tab.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default class ConnectionTab extends Component {
} else if (dialect === DIALECTS.APACHE_SPARK) {
label = `Apache Spark (${connectionObject.host}:${connectionObject.port})`;
} else if (connectionObject.dialect === DIALECTS.CSV) {
label = `CSV (${connectionObject.database})`;
label = connectionObject.label || connectionObject.id || connectionObject.database;
} else if (connectionObject.dialect === DIALECTS.ELASTICSEARCH) {
label = `Elasticsearch (${connectionObject.host})`;
} else if (connectionObject.dialect === DIALECTS.SQLITE) {
Expand Down
12 changes: 12 additions & 0 deletions app/components/Settings/UserConnections/UserConnections.react.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import Filedrop from './filedrop.jsx';

import {contains} from 'ramda';

import {CONNECTION_CONFIG, SAMPLE_DBS} from '../../../constants/constants';
import {dynamicRequireElectron} from '../../../utils/utils';

Expand Down Expand Up @@ -151,6 +154,15 @@ export default class UserConnections extends Component {
</div>
</div>
);
} else if (setting.type === 'filedrop') {
input = (
<Filedrop
settings={setting}
connection={connectionObject}
updateConnection={updateConnection}
sampleCredentialsStyle={sampleCredentialsStyle}
/>
);
}

return (
Expand Down
175 changes: 175 additions & 0 deletions app/components/Settings/UserConnections/filedrop.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';

import {SAMPLE_DBS} from '../../../constants/constants';

export default class Filedrop extends Component {
static propTypes = {
settings: PropTypes.object,
connection: PropTypes.object,
updateConnection: PropTypes.func,
sampleCredentialsStyle : PropTypes.object
}

/**
* Filedrop is an input component where users can type an URL or drop a file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General Comment. I like the style how you have documented the JSDocs and React class structure. I have noticed that there seems to be some inconsistency with the React Classes. Do we have a standard format? I have noticed some classes have PropTypes at the bottom. Is there a standard? Should we document this somewhere? If so where is a good place?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shannonlal

I have noticed that there seems to be some inconsistency with the React Classes. Do we have a standard format? I have noticed some classes have PropTypes at the bottom. Is there a standard? Should we document this somewhere? If so where is a good place?

We don't have any guidelines. I'd keep them in a file: CODING_GUIDELINES.md.

I don't want the guidelines to become a burden (it's easier for us to tell contributors to use given files as models; e.g: we could chose one of the files in the project as a model for stateless components, another file for stateful components, and another for components that use redux). Also, by burden, I mean:

  • For example, the burden of prescribing that PropTypes go at the top is OK, because it's just a copy'n'paste.
  • But, for example, I don't think the burden of prescribing all React components should be class components is worth it (i.e. it's OK, and some would say better, that stateless components are written as functional components).

*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks clean and easy to understand. Do you think this component is worth having a Jest test for or is this overkill?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just added the tests

* @param {object} props - Component properties
*
* @param {object} props.settings - FileDrop settings
* @param {string} props.settings.type - Set to 'filedrop'
* @param {string} props.settings.value - Target property in the connection object
* @param {string} props.settings.inputLabel - Label for input box
* @param {string} props.settings.dropLabel - Label for drop box
* @param {string} props.settings.placeholder - Placeholder for input box
*
* @param {object} props.connection - Connection object
* @param {string} props.connection.dialect - Connection dialect
* @param {string} props.connection.label - Connection label
*
* @param {function} props.updateConnection - Callback to update the connection object
*
* @param {object} props.sampleCredentialsStyle - To control the display of sample credentials
*/
constructor(props) {
super(props);

const {
settings,
connection,
} = this.props;

const url = connection[settings.value];

/**
* @member {object} state - Component state
* @property {string} state.inputValue - Value typed into the input box
* @property {string} state.dropValue - Data URL dropped into the drop box
*/
this.state = (typeof url === 'string' && url.startsWith('data:')) ? {
inputValue: connection.label || url.slice(0, 64),
dropValue: url,
} : {
inputValue: url,
dropValue: '',
};
}


render() {
const {
settings,
connection,
updateConnection,
sampleCredentialsStyle
} = this.props;

const {
inputValue,
dropValue,
drag
} = this.state;

const setState = this.setState.bind(this);

const {
value,
inputLabel,
dropLabel,
placeholder
} = settings;

const {dialect} = connection;

const sampleCredential = (SAMPLE_DBS[dialect]) ? SAMPLE_DBS[dialect][value] : null;

return (
<div
className={'inputContainer'}
onDragOver={onDragOver}
onDrop={onDrop}
>
<label className={'label'}>
{inputLabel}
</label>
<div className={'wrapInput'}>
<input
style={{'background-color': (drag || dropValue) ? 'lightcyan' : null}}
onChange={onChange}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
value={inputValue}
placeholder={placeholder}
type={'text'}
/>
<small style={{
clear: 'both',
float: 'left',
'margin-left': '20px'
}} >
{dropLabel}
</small>
<div style={sampleCredentialsStyle}>
<code>
{sampleCredential}
</code>
</div>
</div>
</div>
);

function onChange(event) {
setState({
inputValue: event.target.value,
dropValue: ''
});
updateConnection({
[value]: event.target.value,
label: event.target.value
});
}

function onDragEnter(event) {
event.stopPropagation();
event.preventDefault();
setState({drag: true});
}

function onDragOver(event) {
event.stopPropagation();
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
}

function onDragLeave(event) {
event.stopPropagation();
event.preventDefault();
setState({drag: false});
}

function onDrop(event) {
event.stopPropagation();
event.preventDefault();
setState({drag: false});

const files = event.dataTransfer.files;
if (!files || files.length !== 1) {
return;
}

const file = files[0];
const reader = new FileReader();
reader.onload = () => {
setState({
dropValue: reader.result,
inputValue: file.name
});
updateConnection({
[value]: reader.result,
label: file.name
});
};
reader.readAsDataURL(file);
}
}
}
9 changes: 6 additions & 3 deletions app/constants/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ const hadoopQLOptions = [
export const CONNECTION_CONFIG = {
[DIALECTS.APACHE_IMPALA]: hadoopQLOptions,
[DIALECTS.APACHE_SPARK]: hadoopQLOptions,
[DIALECTS.CSV]: [
{'label': 'URL to CSV File', 'value': 'database', 'type': 'text'}
],
[DIALECTS.CSV]: [{
'inputLabel': 'Type URL to a CSV file',
'dropLabel': '(or drop a CSV file here)',
'value': 'database',
'type': 'filedrop'
}],
[DIALECTS.IBM_DB2]: commonSqlOptions,
[DIALECTS.MYSQL]: commonSqlOptions,
[DIALECTS.MARIADB]: commonSqlOptions,
Expand Down
25 changes: 25 additions & 0 deletions backend/init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Logger from './logger';

import {
deleteAllConnections,
deleteBadConnections
} from './persistent/Connections.js';
import {getSetting} from './settings.js';

const setCSVStorageSize = require('./persistent/datastores/csv.js').setStorageSize;

export default function init() {
try {
deleteBadConnections();
} catch (error) {
Logger.log(`Failed to delete bad connections: ${error.message}`);
deleteAllConnections();
}

try {
setCSVStorageSize(getSetting('CSV_STORAGE_SIZE'));
} catch (error) {
Logger.log(`Failed to get setting CSV_STORAGE_SIZE: ${error.message}`);
setCSVStorageSize(0);
}
}
24 changes: 23 additions & 1 deletion backend/persistent/Connections.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {assoc, dissoc, findIndex} from 'ramda';
import uuid from 'uuid';
import YAML from 'yamljs';
import * as Datastores from './datastores/Datastores.js';
import {DIALECTS} from '../../app/constants/constants.js';

import {getSetting} from '../settings';

Expand Down Expand Up @@ -39,11 +40,32 @@ export function deleteConnectionById(id) {
const connections = getConnections();
const index = findIndex(connection => connection.id === id, connections);
if (index > -1) {
Datastores.disconnect(connections[index]);
connections.splice(index, 1);
fs.writeFileSync(getSetting('CONNECTIONS_PATH'), YAML.stringify(connections, 4));
}
}

export function deleteBadConnections() {
getConnections().forEach(connection => {
const {id, dialect} = connection;

const dialects = Object.getOwnPropertyNames(DIALECTS).map(k => DIALECTS[k]);

const isUnknownDialect = (dialects.indexOf(dialect) === -1);
if (isUnknownDialect) {
deleteConnectionById(id);
}
});
}

export function deleteAllConnections() {
if (!fs.existsSync(getSetting('STORAGE_PATH'))) {
createStoragePath();
}
fs.writeFileSync(getSetting('CONNECTIONS_PATH'), YAML.stringify([], 4));
}

export function getSanitizedConnections() {
const connections = getConnections();
return connections.map(cred => sanitize(cred));
Expand All @@ -60,7 +82,7 @@ export function saveConnection(connectionObject) {
return connectionId;
}

export function validateConnection (connectionObject) {
export function validateConnection(connectionObject) {
return Datastores.connect(connectionObject).then(() => {
return {};
}).catch(err => {
Expand Down
18 changes: 15 additions & 3 deletions backend/persistent/datastores/Datastores.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import * as ApacheDrill from './ApacheDrill';
import * as IbmDb2 from './ibmdb2';
import * as ApacheLivy from './livy';
import * as ApacheImpala from './impala';
import * as CSV from './csv';
import * as DataWorld from './dataworld';
import * as DatastoreMock from './datastoremock';

const CSV = require('./csv');

/*
* Switchboard to all of the different types of connections
* that we support.
Expand Down Expand Up @@ -70,13 +71,24 @@ export function query(queryObject, connection) {
}

/*
* connect functions attempt to ping the connection and
* return a promise that is empty
* connect attempts to ping the connection and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this JSDocs format? Should it be @params {object} connection

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a commit to format it as JSDoc

* returns a promise that resolves to the connection object
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@returns {promise} an empty promise

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This depends on the connector and it's likely to change once Falcon becomes smarter about the connections it creates (at the moment, most of the connectors create a new connection for every request, which is OK if the DB driver implements a pool or the request is stateless).

I'll reword it, so that it only states that the promise resolves once the connection succeeds.

*/
export function connect(connection) {
return getDatastoreClient(connection).connect(connection);
}

/*
* disconnect closes the connection and
* returns a promise that resolves to the connection object
*/
export function disconnect(connection) {
const client = getDatastoreClient(connection);
return (client.disconnect) ?
client.disconnect(connection) :
Promise.resolve(connection);
}

/* SQL-like Connectors */

/*
Expand Down
Loading