diff --git a/frontend/src/app.js b/frontend/src/app.js
index b57b687c6e..5e402e174f 100644
--- a/frontend/src/app.js
+++ b/frontend/src/app.js
@@ -9,6 +9,7 @@ import DraftContent from './pages/drafts/draft-content';
import ReviewContent from './pages/drafts/review-content';
import FilesActivities from './pages/dashboard/files-activities';
import Starred from './pages/starred/starred';
+import LinkedDevices from './pages/linked-devices/linked-devices';
import editUtilties from './utils/editor-utilties';
import 'seafile-ui';
@@ -86,6 +87,7 @@ class App extends Component {
+
diff --git a/frontend/src/components/main-side-nav.js b/frontend/src/components/main-side-nav.js
index df36e03847..674079c687 100644
--- a/frontend/src/components/main-side-nav.js
+++ b/frontend/src/components/main-side-nav.js
@@ -164,11 +164,11 @@ class MainSideNav extends React.Component {
{gettext('Acitivities')}
-
- this.tabItemClick('devices')}>
+
+ this.tabItemClick('linked-devices')}>
{gettext('Linked Devices')}
-
+
this.tabItemClick('drafts')}>
diff --git a/frontend/src/pages/linked-devices/linked-devices.js b/frontend/src/pages/linked-devices/linked-devices.js
new file mode 100644
index 0000000000..baa2cb8a07
--- /dev/null
+++ b/frontend/src/pages/linked-devices/linked-devices.js
@@ -0,0 +1,213 @@
+import moment from 'moment';
+import React, { Component } from 'react';
+import Toast from '../../components/toast';
+
+import { seafileAPI } from '../../utils/seafile-api';
+import { Utils } from '../../utils/utils';
+import { gettext, siteRoot, loginUrl } from '../../utils/constants';
+
+class Content extends Component {
+
+ render() {
+ const {loading, errorMsg, items} = this.props.data;
+
+ if (loading) {
+ return ;
+ } else if (errorMsg) {
+ return {errorMsg}
;
+ } else {
+ const desktopThead = (
+
+
+ {gettext("Platform")} |
+ {gettext("Device Name")} |
+ {gettext("IP")} |
+ {gettext("Last Access")} |
+ |
+
+
+ );
+ const mobileThead = (
+
+
+ {gettext("Platform")} |
+ {gettext('Device Name')} |
+ |
+
+
+ );
+
+ return (
+
+ {window.innerWidth >= 768 ? desktopThead : mobileThead}
+
+
+ );
+ }
+ }
+}
+
+class TableBody extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ items: this.props.items
+ };
+ }
+
+ render() {
+
+ let listLinkedDevices = this.state.items.map(function(item, index) {
+ return ;
+ }, this);
+
+ return (
+ {listLinkedDevices}
+ );
+ }
+}
+
+class Item extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ showOpIcon: false,
+ unlinked: false
+ };
+
+ this.handleMouseOver = this.handleMouseOver.bind(this);
+ this.handleMouseOut = this.handleMouseOut.bind(this);
+ this.handleClick = this.handleClick.bind(this);
+ }
+
+ handleMouseOver() {
+ this.setState({
+ showOpIcon: true
+ });
+ }
+
+ handleMouseOut() {
+ this.setState({
+ showOpIcon: false
+ });
+ }
+
+ handleClick(e) {
+ e.preventDefault();
+
+ const data = this.props.data;
+
+ seafileAPI.unLinkDevice(data.platform, data.device_id).then((res) => {
+ this.setState({
+ unlinked: true
+ });
+ let msg_s = gettext("Successfully unlink %(name)s.");
+ msg_s = msg_s.replace('%(name)s', data.device_name);
+ Toast.success(msg_s);
+ }).catch((error) => {
+ let message = gettext("Failed to unlink %(name)s");
+ message = message.replace('%(name)s', data.device_name);
+ Toast.error(message);
+ });
+ }
+
+ render() {
+ if (this.state.unlinked) {
+ return null;
+ }
+
+ const data = this.props.data;
+
+ let opClasses = 'sf2-icon-delete unlink-device op-icon';
+ opClasses += this.state.showOpIcon ? '' : ' invisible';
+
+ const desktopItem = (
+
+ {data.platform} |
+ {data.device_name} |
+ {data.last_login_ip} |
+ {moment(data.last_accessed).fromNow()} |
+
+
+ |
+
+ );
+
+ const mobileItem = (
+
+ {data.platform} |
+ {data.device_name} |
+
+
+ |
+
+ );
+
+ if (window.innerWidth >= 768) {
+ return desktopItem;
+ } else {
+ return mobileItem;
+ }
+ }
+}
+
+class LinkedDevices extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: '',
+ items: []
+ };
+ }
+
+ componentDidMount() {
+ seafileAPI.listLinkedDevices().then((res) => {
+ //res: {data: Array(2), status: 200, statusText: "OK", headers: {…}, config: {…}, …}
+ this.setState({
+ loading: false,
+ items: res.data
+ });
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ render() {
+ return (
+
+
+
{gettext('Linked Devices')}
+
+
+
+ );
+ }
+}
+
+export default LinkedDevices;
diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html
index 1d94de7e82..cda8144b90 100644
--- a/seahub/templates/js/templates.html
+++ b/seahub/templates/js/templates.html
@@ -1574,7 +1574,7 @@
<% } %>
<% } %>
- {% trans "Linked Devices" %}
+ {% trans "Linked Devices" %}
{% if enable_guest_invitation and user.permissions.can_invite_guest %}
diff --git a/seahub/urls.py b/seahub/urls.py
index 6242cddf1d..f8efddf587 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -192,6 +192,7 @@ urlpatterns = [
### React ###
url(r'^dashboard/$', TemplateView.as_view(template_name="react_app.html"), name="dashboard"),
url(r'^starred/$', TemplateView.as_view(template_name="react_app.html"), name="starred"),
+ url(r'^linked-devices/$', linked_devices, name="linked_devices"),
### Ajax ###
url(r'^ajax/repo/(?P[-0-9a-f]{36})/dirents/$', get_dirents, name="get_dirents"),
diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py
index 22239acae4..40f9e955be 100644
--- a/seahub/views/__init__.py
+++ b/seahub/views/__init__.py
@@ -1209,3 +1209,7 @@ def choose_register(request):
return render(request, 'choose_register.html', {
'login_bg_image_path': login_bg_image_path
})
+
+@login_required
+def linked_devices(request):
+ return render(request, "react_app.html")