diff --git a/ui/src/actions/ClientAction.js b/ui/src/actions/ClientAction.js
new file mode 100644
index 0000000..17ba0e9
--- /dev/null
+++ b/ui/src/actions/ClientAction.js
@@ -0,0 +1,29 @@
+import dispatcher from '../stores/dispatcher';
+import config from 'react-global-configuration';
+import axios from 'axios';
+
+/** Fetches all clients. */
+export function fetchClients() {
+ axios.get(config.get('url') + 'client').then((resp) => {
+ dispatcher.dispatch({
+ type: 'UPDATE_CLIENTS',
+ payload: resp.data,
+ });
+ });
+}
+
+/**
+ * Delete a client by id.
+ * @param {int} id the client id
+ */
+export function deleteClient(id) {
+ axios.delete(config.get('url') + 'client/' + id).then(fetchClients);
+}
+
+/**
+ * Create a client.
+ * @param {string} name the client name
+ */
+export function createClient(name) {
+ axios.post(config.get('url') + 'client', {name}).then(fetchClients);
+}
diff --git a/ui/src/pages/Clients.js b/ui/src/pages/Clients.js
new file mode 100644
index 0000000..0082d81
--- /dev/null
+++ b/ui/src/pages/Clients.js
@@ -0,0 +1,146 @@
+import React, {Component} from 'react';
+import Grid from 'material-ui/Grid';
+import Table, {TableBody, TableCell, TableHead, TableRow} from 'material-ui/Table';
+import Paper from 'material-ui/Paper';
+import Button from 'material-ui/Button';
+import IconButton from 'material-ui/IconButton';
+import Dialog, {DialogActions, DialogContent, DialogTitle} from 'material-ui/Dialog';
+import TextField from 'material-ui/TextField';
+import Tooltip from 'material-ui/Tooltip';
+import ClientStore from '../stores/ClientStore';
+import ToggleVisibility from '../component/ToggleVisibility';
+import * as ClientAction from '../actions/ClientAction';
+import DefaultPage from '../component/DefaultPage';
+import ConfirmDialog from '../component/ConfirmDialog';
+import PropTypes from 'prop-types';
+import Delete from 'material-ui-icons/Delete';
+
+class Clients extends Component {
+ constructor() {
+ super();
+ this.state = {clients: [], showDialog: false, deleteId: -1};
+ }
+
+ componentWillMount() {
+ ClientStore.on('change', this.updateClients);
+ this.updateClients();
+ }
+
+ componentWillUnmount() {
+ ClientStore.removeListener('change', this.updateClients);
+ }
+
+ updateClients = () => this.setState({...this.state, clients: ClientStore.get()});
+
+ showCreateDialog = () => this.setState({...this.state, showDialog: true});
+ hideCreateDialog = () => this.setState({...this.state, showDialog: false});
+
+ showDeleteDialog = (deleteId) => this.setState({...this.state, deleteId});
+ hideDeleteDelete = () => this.setState({...this.state, deleteId: -1});
+
+ render() {
+ const {clients, deleteId, showDialog} = this.state;
+ return (
+
+
+
+
+
+
+ Name
+ token
+
+
+
+
+ {clients.map((client) => {
+ return (
+ this.showDeleteDialog(client.id)}/>
+ );
+ })}
+
+
+
+
+ {showDialog && }
+ {deleteId !== -1 && ClientAction.deleteClient(deleteId)}/>}
+
+ );
+ }
+}
+
+class Row extends Component {
+ static propTypes = {
+ name: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired,
+ fDelete: PropTypes.func.isRequired,
+ };
+
+ render() {
+ const {name, value, fDelete} = this.props;
+ return (
+
+ {name}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+class AddDialog extends Component {
+ static propTypes = {
+ fClose: PropTypes.func.isRequired,
+ fOnSubmit: PropTypes.func.isRequired,
+ };
+
+ constructor() {
+ super();
+ this.state = {name: ''};
+ }
+
+ handleChange(propertyName, event) {
+ const state = this.state;
+ state[propertyName] = event.target.value;
+ this.setState(state);
+ }
+
+ render() {
+ const {fClose, fOnSubmit} = this.props;
+ const {name} = this.state;
+ const submitEnabled = this.state.name.length !== 0;
+ const submitAndClose = () => {
+ fOnSubmit(name);
+ fClose();
+ };
+ return (
+
+ );
+ }
+}
+
+export default Clients;
diff --git a/ui/src/stores/ClientStore.js b/ui/src/stores/ClientStore.js
new file mode 100644
index 0000000..0396455
--- /dev/null
+++ b/ui/src/stores/ClientStore.js
@@ -0,0 +1,34 @@
+import {EventEmitter} from 'events';
+import dispatcher from './dispatcher';
+
+class ClientStore extends EventEmitter {
+ constructor() {
+ super();
+ this.clients = [];
+ }
+
+ get() {
+ return this.clients;
+ }
+
+ getById(id) {
+ return this.clients.find((client) => client.id === id);
+ }
+
+ getIdByToken(token) {
+ const client = this.clients.find((client) => client.token === token);
+ return client !== undefined ? client.id : '';
+ }
+
+ handle(data) {
+ if (data.type === 'UPDATE_CLIENTS') {
+ this.clients = data.payload;
+ this.emit('change');
+ }
+ }
+}
+
+
+const store = new ClientStore();
+dispatcher.register(store.handle.bind(store));
+export default store;