mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 01:20:24 +03:00
Implemented importing servers from CSV file
This commit is contained in:
parent
ac52f55c5e
commit
9b063a4616
7 changed files with 140 additions and 67 deletions
|
@ -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);
|
||||
|
|
63
src/servers/helpers/ImportServersBtn.js
Normal file
63
src/servers/helpers/ImportServersBtn.js
Normal 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);
|
|
@ -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);
|
||||
|
|
27
src/servers/services/ServersImporter.js
Normal file
27
src/servers/services/ServersImporter.js
Normal 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;
|
|
@ -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);
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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({})
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue