Implemented importing servers from CSV file

This commit is contained in:
Alejandro Celaya 2018-08-21 20:33:12 +02:00
parent ac52f55c5e
commit 9b063a4616
7 changed files with 140 additions and 67 deletions

View file

@ -1,34 +1,33 @@
import { assoc, pick } from 'ramda';
import { assoc, dissoc, pick, pipe } from 'ramda';
import React from 'react';
import { connect } from 'react-redux';
import { createServer } from './reducers/server';
import { resetSelectedServer } from './reducers/selectedServer';
import { v4 as uuid } from 'uuid';
import { UncontrolledTooltip } from 'reactstrap';
import './CreateServer.scss';
import ImportServersBtn from './helpers/ImportServersBtn';
export class CreateServer extends React.Component {
state = {
name: '',
url: '',
apiKey: '',
serversImported: false,
};
submit = e => {
e.preventDefault();
const { createServer, history: { push } } = this.props;
const server = assoc('id', uuid(), this.state);
const server = pipe(
assoc('id', uuid()),
dissoc('serversImported')
)(this.state);
createServer(server);
push(`/server/${server.id}/list-short-urls/1`)
};
constructor(props) {
super(props);
this.fileRef = React.createRef();
}
componentDidMount() {
this.props.resetSelectedServer();
}
@ -60,30 +59,29 @@ export class CreateServer extends React.Component {
{renderInputGroup('apiKey', 'API key')}
<div className="text-right">
<button
type="button"
className="btn btn-outline-secondary mr-2"
onClick={() => this.fileRef.current.click()}
id="importBtn"
>
Import from file
</button>
<UncontrolledTooltip placement="top" target="importBtn">
You can create servers by importing a CSV file with columns "name", "apiKey" and "url"
</UncontrolledTooltip>
<input
type="file"
onChange={file => console.log(file)}
accept="text/csv"
className="create-server__csv-select"
ref={this.fileRef}
/>
<ImportServersBtn onImport={() => {
this.setState({ serversImported: true });
setTimeout(() => this.setState({ serversImported: false }), 4000);
}} />
<button className="btn btn-outline-primary">Create server</button>
</div>
{this.state.serversImported && (
<div className="row">
<div className="col-md-10 offset-md-1">
<div className="p-2 mt-3 bg-main text-white text-center">
Servers properly imported. You can now select one from the list :)
</div>
</div>
</div>
)}
</form>
</div>
);
}
}
export default connect(pick(['selectedServer']), {createServer, resetSelectedServer })(CreateServer);
export default connect(
pick(['selectedServer']),
{createServer, resetSelectedServer }
)(CreateServer);

View file

@ -0,0 +1,63 @@
import React from 'react';
import { connect } from 'react-redux';
import { UncontrolledTooltip } from 'reactstrap';
import serversImporter from '../services/ServersImporter';
import { createServers } from '../reducers/server';
import { assoc } from 'ramda';
import { v4 as uuid } from 'uuid';
import PropTypes from 'prop-types';
const defaultProps = {
serversImporter,
};
const propTypes = {
onChange: PropTypes.func,
};
export class ImportServersBtn extends React.Component {
constructor(props) {
super(props);
this.fileRef = React.createRef();
}
render() {
const { serversImporter, onImport } = this.props;
const onChange = e => serversImporter.importServersFromFile(e.target.files[0]).then(
servers => {
const { createServers } = this.props;
const serversWithIds = servers.map(server => assoc('id', uuid(), server));
createServers(serversWithIds);
onImport(serversWithIds);
}
);
return (
<React.Fragment>
<button
type="button"
className="btn btn-outline-secondary mr-2"
onClick={() => this.fileRef.current.click()}
id="importBtn"
>
Import from file
</button>
<UncontrolledTooltip placement="top" target="importBtn">
You can create servers by importing a CSV file with columns "name", "apiKey" and "url"
</UncontrolledTooltip>
<input
type="file"
onChange={onChange}
accept="text/csv"
className="create-server__csv-select"
ref={this.fileRef}
/>
</React.Fragment>
);
}
}
ImportServersBtn.defaultProps = defaultProps;
ImportServersBtn.propTypes = propTypes;
export default connect(null, { createServers })(ImportServersBtn);

View file

@ -2,17 +2,11 @@ import ServersService from '../services/ServersService';
import { curry } from 'ramda';
export const FETCH_SERVERS = 'shlink/servers/FETCH_SERVERS';
export const CREATE_SERVER = 'shlink/servers/CREATE_SERVER';
export const DELETE_SERVER = 'shlink/servers/DELETE_SERVER';
export default function reducer(state = {}, action) {
switch (action.type) {
case FETCH_SERVERS:
case DELETE_SERVER:
return action.servers;
case CREATE_SERVER:
const server = action.server;
return { ...state, [server.id]: server };
default:
return state;
}
@ -35,3 +29,9 @@ export const _deleteServer = (ServersService, server) => {
return _listServers(ServersService);
};
export const deleteServer = curry(_deleteServer)(ServersService);
export const _createServers = (ServersService, servers) => {
ServersService.createServers(servers);
return _listServers(ServersService);
};
export const createServers = curry(_createServers)(ServersService);

View file

@ -0,0 +1,27 @@
import csvjson from 'csvjson';
export class ServersImporter {
constructor(csvjson) {
this.csvjson = csvjson;
}
importServersFromFile = (file) => {
if (!file || file.type !== 'text/csv') {
return Promise.reject('No file provided or file is not a CSV');
}
const reader = new FileReader();
return new Promise(resolve => {
reader.onloadend = e => {
const content = e.target.result;
const servers = this.csvjson.toObject(content);
resolve(servers);
};
reader.readAsText(file);
});
};
}
const serversImporter = new ServersImporter(csvjson);
export default serversImporter;

View file

@ -1,5 +1,5 @@
import Storage from '../../utils/Storage';
import { dissoc } from 'ramda';
import { assoc, dissoc, reduce } from 'ramda';
const SERVERS_STORAGE_KEY = 'servers';
@ -8,25 +8,26 @@ export class ServersService {
this.storage = storage;
}
listServers = () => {
return this.storage.get(SERVERS_STORAGE_KEY) || {};
listServers = () => this.storage.get(SERVERS_STORAGE_KEY) || {};
findServerById = serverId => this.listServers()[serverId];
createServer = server => this.createServers([server]);
createServers = servers => {
const allServers = reduce(
(serversObj, server) => assoc(server.id, server, serversObj),
this.listServers(),
servers
);
this.storage.set(SERVERS_STORAGE_KEY, allServers);
};
findServerById = serverId => {
const servers = this.listServers();
return servers[serverId];
};
createServer = server => {
const servers = this.listServers();
servers[server.id] = server;
this.storage.set(SERVERS_STORAGE_KEY, servers);
};
deleteServer = server => {
const servers = dissoc(server.id, this.listServers());
this.storage.set(SERVERS_STORAGE_KEY, servers);
};
deleteServer = server =>
this.storage.set(
SERVERS_STORAGE_KEY,
dissoc(server.id, this.listServers())
);
}
export default new ServersService(Storage);

View file

@ -15,9 +15,6 @@ export class ColorGenerator {
constructor(storage) {
this.storage = storage;
this.colors = this.storage.get('colors') || {};
this.getColorForKey = this.getColorForKey.bind(this);
this.setColorForKey = this.setColorForKey.bind(this);
}
getColorForKey = key => {

View file

@ -2,8 +2,6 @@ import reduce, {
_createServer,
_deleteServer,
_listServers,
CREATE_SERVER,
DELETE_SERVER,
FETCH_SERVERS,
} from '../../../src/servers/reducers/server';
import * as sinon from 'sinon';
@ -24,17 +22,6 @@ describe('serverReducer', () => {
expect(reduce({}, { type: FETCH_SERVERS, servers })).toEqual(servers)
);
it('returns servers when action is DELETE_SERVER', () =>
expect(reduce({}, { type: DELETE_SERVER, servers })).toEqual(servers)
);
it('adds server to list when action is CREATE_SERVER', () => {
const server = { id: 'abc123' };
expect(reduce({}, { type: CREATE_SERVER, server })).toEqual({
[server.id]: server
})
});
it('returns default when action is unknown', () =>
expect(reduce({}, { type: 'unknown' })).toEqual({})
);