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 React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createServer } from './reducers/server';
|
import { createServer } from './reducers/server';
|
||||||
import { resetSelectedServer } from './reducers/selectedServer';
|
import { resetSelectedServer } from './reducers/selectedServer';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
|
||||||
import './CreateServer.scss';
|
import './CreateServer.scss';
|
||||||
|
import ImportServersBtn from './helpers/ImportServersBtn';
|
||||||
|
|
||||||
export class CreateServer extends React.Component {
|
export class CreateServer extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
|
serversImported: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
submit = e => {
|
submit = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const { createServer, history: { push } } = this.props;
|
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);
|
createServer(server);
|
||||||
push(`/server/${server.id}/list-short-urls/1`)
|
push(`/server/${server.id}/list-short-urls/1`)
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.fileRef = React.createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.resetSelectedServer();
|
this.props.resetSelectedServer();
|
||||||
}
|
}
|
||||||
|
@ -60,30 +59,29 @@ export class CreateServer extends React.Component {
|
||||||
{renderInputGroup('apiKey', 'API key')}
|
{renderInputGroup('apiKey', 'API key')}
|
||||||
|
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<button
|
<ImportServersBtn onImport={() => {
|
||||||
type="button"
|
this.setState({ serversImported: true });
|
||||||
className="btn btn-outline-secondary mr-2"
|
setTimeout(() => this.setState({ serversImported: false }), 4000);
|
||||||
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}
|
|
||||||
/>
|
|
||||||
<button className="btn btn-outline-primary">Create server</button>
|
<button className="btn btn-outline-primary">Create server</button>
|
||||||
</div>
|
</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>
|
</form>
|
||||||
</div>
|
</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';
|
import { curry } from 'ramda';
|
||||||
|
|
||||||
export const FETCH_SERVERS = 'shlink/servers/FETCH_SERVERS';
|
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) {
|
export default function reducer(state = {}, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case FETCH_SERVERS:
|
case FETCH_SERVERS:
|
||||||
case DELETE_SERVER:
|
|
||||||
return action.servers;
|
return action.servers;
|
||||||
case CREATE_SERVER:
|
|
||||||
const server = action.server;
|
|
||||||
return { ...state, [server.id]: server };
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -35,3 +29,9 @@ export const _deleteServer = (ServersService, server) => {
|
||||||
return _listServers(ServersService);
|
return _listServers(ServersService);
|
||||||
};
|
};
|
||||||
export const deleteServer = curry(_deleteServer)(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 Storage from '../../utils/Storage';
|
||||||
import { dissoc } from 'ramda';
|
import { assoc, dissoc, reduce } from 'ramda';
|
||||||
|
|
||||||
const SERVERS_STORAGE_KEY = 'servers';
|
const SERVERS_STORAGE_KEY = 'servers';
|
||||||
|
|
||||||
|
@ -8,25 +8,26 @@ export class ServersService {
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
listServers = () => {
|
listServers = () => this.storage.get(SERVERS_STORAGE_KEY) || {};
|
||||||
return 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 => {
|
deleteServer = server =>
|
||||||
const servers = this.listServers();
|
this.storage.set(
|
||||||
return servers[serverId];
|
SERVERS_STORAGE_KEY,
|
||||||
};
|
dissoc(server.id, this.listServers())
|
||||||
|
);
|
||||||
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);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ServersService(Storage);
|
export default new ServersService(Storage);
|
||||||
|
|
|
@ -15,9 +15,6 @@ export class ColorGenerator {
|
||||||
constructor(storage) {
|
constructor(storage) {
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
this.colors = this.storage.get('colors') || {};
|
this.colors = this.storage.get('colors') || {};
|
||||||
|
|
||||||
this.getColorForKey = this.getColorForKey.bind(this);
|
|
||||||
this.setColorForKey = this.setColorForKey.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getColorForKey = key => {
|
getColorForKey = key => {
|
||||||
|
|
|
@ -2,8 +2,6 @@ import reduce, {
|
||||||
_createServer,
|
_createServer,
|
||||||
_deleteServer,
|
_deleteServer,
|
||||||
_listServers,
|
_listServers,
|
||||||
CREATE_SERVER,
|
|
||||||
DELETE_SERVER,
|
|
||||||
FETCH_SERVERS,
|
FETCH_SERVERS,
|
||||||
} from '../../../src/servers/reducers/server';
|
} from '../../../src/servers/reducers/server';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
|
@ -24,17 +22,6 @@ describe('serverReducer', () => {
|
||||||
expect(reduce({}, { type: FETCH_SERVERS, servers })).toEqual(servers)
|
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', () =>
|
it('returns default when action is unknown', () =>
|
||||||
expect(reduce({}, { type: 'unknown' })).toEqual({})
|
expect(reduce({}, { type: 'unknown' })).toEqual({})
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue