1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-03 16:10:26 +00:00

React activity (#2315)

This commit is contained in:
shanshuirenjia
2018-08-30 15:10:52 +08:00
committed by Daniel Pan
parent 99080e95c3
commit 740d6a86cf
17 changed files with 762 additions and 18 deletions

View File

@@ -0,0 +1,53 @@
import React from 'react';
const gettext = window.gettext;
class Notification extends React.Component {
constructor(props) {
super(props);
this.state = {
showNotice: false,
notice_html: ''
}
}
onClick = () => {
this.setState({
showNotice: !this.state.showNotice
})
if (!this.state.showNotice) {
this.loadNotices()
}
}
loadNotices = () => {
this.props.seafileAPI.getPopupNotices().then(res => {
this.setState({
notice_html: res.data.notice_html
})
})
}
render() {
return (
<div id="notifications">
<a href="#" onClick={this.onClick} className="no-deco" id="notice-icon" title="Notifications" aria-label="Notifications">
<span className="sf2-icon-bell"></span>
<span className="num hide">0</span>
</a>
<div id="notice-popover" className={`sf-popover ${this.state.showNotice ? '': 'hide'}`}>
<div className="outer-caret up-outer-caret"><div className="inner-caret"></div></div>
<div className="sf-popover-hd ovhd">
<h3 className="sf-popover-title">{gettext('Notifications')}</h3>
<a href="#" onClick={this.onClick} title={gettext('Close')} aria-label={gettext('Close')} className="sf-popover-close js-close sf2-icon-x1 op-icon float-right"></a>
</div>
<div className="sf-popover-con">
<ul className="notice-list" dangerouslySetInnerHTML={{__html: this.state.notice_html}}></ul>
<a href="/notification/list/" className="view-all">{gettext('See All Notifications')}</a>
</div>
</div>
</div>
)
}
}
export default Notification;

View File

@@ -2,8 +2,9 @@ import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import cookie from 'react-cookies'; import cookie from 'react-cookies';
import { keyCodes, bytesToSize } from './utils'; import { keyCodes, bytesToSize } from './utils';
import { siteRoot, avatarInfo, gettext } from './constance';
const siteRoot = window.app.config.siteRoot;
const gettext = window.gettext;
class Account extends Component { class Account extends Component {
constructor(props) { constructor(props) {

View File

@@ -0,0 +1,243 @@
import React, { Component } from 'react';
const gettext = window.gettext;
const siteRoot = window.app.config.siteRoot;
const per_page = 25; // default
class FileActivitiesContent extends Component {
render() {
const {loading, error_msg, events} = this.props.data;
if (loading) {
return <span className="loading-icon loading-tip"></span>;
} else if (error_msg) {
return <p className="error text-center">{error_msg}</p>;
} else {
return (
<React.Fragment>
<table className="table table-hover table-vcenter activity-table">
<thead>
<tr>
<th width="8%">{/* avatar */}</th>
<th width="10%">{gettext("User")}</th>
<th width="25%">{gettext("Operation")}</th>
<th width="37%">{gettext("File")} / {gettext("Library")}</th>
<th width="20%">{gettext("Time")}</th>
</tr>
</thead>
<TableBody items={events.items} />
</table>
{events.has_more ? <span className="loading-icon loading-tip"></span> : ''}
{events.error_msg ? <p className="error text-center">{events.error_msg}</p> : ''}
</React.Fragment>
);
}
}
}
class TableBody extends Component {
encodePath(path) {
let path_arr = path.split('/'),
path_arr_ = [];
for (let i = 0, len = path_arr.length; i < len; i++) {
path_arr_.push(encodeURIComponent(path_arr[i]));
}
return path_arr_.join('/');
}
render() {
let listFilesActivities = this.props.items.map(function(item, index) {
let op, details;
let userProfileURL = `${siteRoot}profile/${encodeURIComponent(item.author_email)}/`;
let libURL = `${siteRoot}#common/lib/${item.repo_id}`;
let libLink = <a href={libURL}>{item.repo_name}</a>;
let smallLibLink = <a className="small text-secondary" href={libURL}>{item.repo_name}</a>;
if (item.obj_type == 'repo') {
switch(item.op_type) {
case 'create':
op = gettext("Created library");
details = <td>{libLink}</td>;
break;
case 'rename':
op = gettext("Renamed library");
details = <td>{item.old_repo_name} => {libLink}</td>;
break;
case 'delete':
op = gettext("Deleted library");
details = <td>{item.repo_name}</td>;
break;
case 'recover':
op = gettext("Restored library");
details = <td>{libLink}</td>;
break;
case 'clean-up-trash':
if (item.days == 0) {
op = gettext("Removed all items from trash.");
} else {
op = gettext("Removed items older than {n} days from trash.").replace('{n}', item.days);
}
details = <td>{libLink}</td>;
break;
}
} else if (item.obj_type == 'file') {
let fileURL = `${siteRoot}lib/${item.repo_id}/file${this.encodePath(item.path)}`;
let fileLink = <a href={fileURL}>{item.name}</a>;
switch(item.op_type) {
case 'create':
op = gettext("Created file");
details = <td>{fileLink}<br />{smallLibLink}</td>;
break;
case 'delete':
op = gettext("Deleted file");
details = <td>{item.name}<br />{smallLibLink}</td>;
break;
case 'recover':
op = gettext("Restored file");
details = <td>{fileLink}<br />{smallLibLink}</td>;
break;
case 'rename':
op = gettext("Renamed file");
details = <td>{item.old_name} => {fileLink}<br />{smallLibLink}</td>;
break;
case 'move':
let filePathLink = <a href={fileURL}>{item.path}</a>;
op = gettext("Moved file");
details = <td>{item.old_path} => {filePathLink}<br />{smallLibLink}</td>;
break;
case 'edit': // update
op = gettext("Updated file");
details = <td>{fileLink}<br />{smallLibLink}</td>;
break;
}
} else { // dir
let dirURL = `${siteRoot}#common/lib/${item.repo_id}${this.encodePath(item.path)}`;
let dirLink = <a href={dirURL}>{item.name}</a>;
switch(item.op_type) {
case 'create':
op = gettext("Created folder");
details = <td>{dirLink}<br />{smallLibLink}</td>;
break;
case 'delete':
op = gettext("Deleted folder");
details = <td>{item.name}<br />{smallLibLink}</td>;
break;
case 'recover':
op = gettext("Restored folder");
details = <td>{dirLink}<br />{smallLibLink}</td>;
break;
case 'rename':
op = gettext("Renamed folder");
details = <td>{item.old_name} => {dirLink}<br />{smallLibLink}</td>;
break;
case 'move':
let dirPathLink = <a href={dirURL}>{item.path}</a>;
op = gettext("Moved folder");
details = <td>{item.old_path} => {dirPathLink}<br />{smallLibLink}</td>;
break;
}
}
return (
<tr key={index}>
<td className="text-center">
<img src={item.avatar_url} alt="" width="24" className="avatar" />
</td>
<td>
<a href={userProfileURL}>{item.author_name}</a>
</td>
<td><span className="activity-op">{op}</span></td>
{details}
<td className="text-secondary" dangerouslySetInnerHTML={{__html:item.time_relative}}></td>
</tr>
);
}, this);
return (
<tbody>{listFilesActivities}</tbody>
);
}
}
class FilesActivities extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
error_msg: '',
events: {}
};
this.handleScroll = this.handleScroll.bind(this);
}
componentDidMount() {
const pageNum = 1
this.props.seafileAPI.getActivities(pageNum)
.then(res => {
// not logged in
if (res.status == 403) {
this.setState({
loading: false,
error_msg: gettext("Permission denied")
});
} else {
// {"events":[...]}
this.setState({
loading: false,
events: {
page: 1,
items: res.data.events,
has_more: res.data.events.length == per_page ? true : false
}
});
}
});
}
getMore() {
const pageNum = this.state.events.page + 1;
this.props.seafileAPI.getActivities(pageNum)
.then(res => {
this.setState(function(prevState, props) {
let events = prevState.events;
if (res.status == 403) { // log out
events.error_msg = gettext("Permission denied");
events.has_more = false;
}
if (res.ok) {
events.page += 1;
events.items = events.items.concat(res.data.events);
events.has_more = res.data.events.length == per_page ? true : false;
}
return {events: events};
});
});
}
handleScroll(e) {
let $el = e.target;
if (this.state.events.has_more &&
$el.scrollTop > 0 &&
$el.clientHeight + $el.scrollTop == $el.scrollHeight) { // scroll to the bottom
this.getMore();
}
}
render() {
return (
<div className="main-panel-main" id="activities">
<div className="cur-view-path">
<h3 className="sf-heading">{gettext("Activities")}</h3>
</div>
<div className="cur-view-main-con" onScroll={this.handleScroll}>
<FileActivitiesContent data={this.state} />
</div>
</div>
);
}
}
export default FilesActivities;

View File

@@ -0,0 +1,124 @@
import React from 'react';
const siteRoot = window.app.config.siteRoot;
const serverRoot = window.app.config.serverRoot;
class MainSideNav extends React.Component {
constructor(props) {
super(props);
this.state = {
groupsExtended: false,
sharedExtended: false,
closeSideBar:false,
groupItems: []
};
this.listHeight = 24; //for caculate tabheight
this.groupsHeight = 0;
this.adminHeight = 0;
}
grpsExtend = () => {
this.setState({
groupsExtended: !this.state.groupsExtended,
})
this.loadGroups();
}
shExtend = () => {
this.setState({
sharedExtended: !this.state.sharedExtended,
})
}
loadGroups = () => {
let _this = this;
this.props.seafileAPI.getGroups().then(res =>{
let data = res.data.groups;
this.groupsHeight = (data.length + 1) * _this.listHeight;
_this.setState({
groupItems: data
})
})
}
renderSharedGroups() {
let style = {height: 0};
if (this.state.groupsExtended) {
style = {height: this.groupsHeight};
}
return (
<ul className={`grp-list ${this.state.groupsExtended ? 'side-panel-slide' : 'side-panel-slide-up'}`} style={style}>
<li>
<a href={siteRoot + '#groups/'}>
<span className="sharp" aria-hidden="true">#</span>All Groups</a>
</li>
{this.state.groupItems.map(item => {
return (
<li key={item.id}>
<a href={siteRoot + '#group/' + item.id + '/'}>
<span className="sharp" aria-hidden="true">#</span>{item.name}</a>
</li>
)
})}
</ul>
)
}
renderSharedAdmin() {
let height = 0;
if (this.state.sharedExtended) {
if (!this.adminHeight) {
this.adminHeight = 3 * this.listHeight;
}
height = this.adminHeight;
}
let style = {height: height};
return (
<ul className={`${this.state.sharedExtended ? 'side-panel-slide' : 'side-panel-slide-up'}`} style={style} >
<li>
<a href={siteRoot + '#share-admin-libs/'}><span aria-hidden="true" className="sharp">#</span>Libraries</a>
</li>
<li>
<a href={siteRoot + '#share-admin-folders/'}><span aria-hidden="true" className="sharp">#</span>Folders</a>
</li>
<li>
<a href={siteRoot + '#share-admin-share-links/'}><span aria-hidden="true" className="sharp">#</span>Links</a>
</li>
</ul>
)
}
render() {
return (
<div id="side-nav" className="home-side-nav">
<div className="side-nav-con">
<h3 className="sf-heading">Files</h3>
<ul className="side-tabnav-tabs">
<li className="tab"><a href={siteRoot + '#my-libs'} className="ellipsis" title="My Libraries"><span className="sf2-icon-user" aria-hidden="true"></span>My Libraries</a></li>
<li className="tab"><a href={serverRoot + siteRoot + '#shared-libs/'} className="ellipsis" title="Shared with me"><span className="sf2-icon-share" aria-hidden="true"></span>Shared with me</a></li>
<li className="tab"><a href={serverRoot + siteRoot + '#org/'} className="ellipsis" title="Shared with all"><span className="sf2-icon-organization" aria-hidden="true"></span>Shared with all</a></li>
<li className="tab" id="group-nav">
<a className="ellipsis user-select-no" title="Shared with groups" onClick={this.grpsExtend}><span className={`toggle-icon float-right ${this.state.groupsExtended ?'icon-caret-down':'icon-caret-left'}`} aria-hidden="true"></span><span className="sf2-icon-group" aria-hidden="true"></span>Shared with groups</a>
{this.renderSharedGroups()}
</li>
</ul>
<div className="hd w-100 o-hidden">
<h3 className="float-left sf-heading">Tools</h3>
</div>
<ul className="side-tabnav-tabs">
<li className="tab"><a href={siteRoot + '#starred/'}><span className="sf2-icon-star" aria-hidden="true"></span>Favorites</a></li>
<li className="tab"><a href={siteRoot + 'dashboard'}><span className="sf2-icon-clock" aria-hidden="true"></span>Acitivities</a></li>
<li className="tab"><a href={siteRoot + '#devices/'} className="ellipsis" title="Linked Devices"><span className="sf2-icon-monitor" aria-hidden="true"></span>Linked Devices</a></li>
<li className="tab" id="share-admin-nav">
<a className="ellipsis user-select-no" title="Share Admin" onClick={this.shExtend}><span className={`toggle-icon float-right ${this.state.sharedExtended ? 'icon-caret-down':'icon-caret-left'}`} aria-hidden="true"></span><span aria-hidden="true" className="sf2-icon-wrench"></span>Share Admin</a>
{this.renderSharedAdmin()}
</li>
</ul>
</div>
</div>
)
}
}
export default MainSideNav;

View File

@@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { gettext, repoID } from './constance'; import { gettext, repoID } from './constance';
import SearchResultItem from './SearchResultItem'; import SearchResultItem from './search-result-item';
class Search extends Component { class Search extends Component {

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
class About extends React.Component {
constructor(props) {
super(props);
this.state = {
modal: false
};
this.toggle = this.toggle.bind(this);
}
toggle() {
this.setState({
modal: !this.state.modal
});
}
render() {
return (
<div>
<a href="#" className="item" onClick={this.toggle}>About</a>
<Modal isOpen={this.state.modal} toggle={this.toggle} className={this.props.className}>
<ModalBody>
<div className="about-content">
<p><img src="/media/img/seafile-logo.png" title="Private Seafile" alt="logo" width="128" height="32" /></p>
<p>Server Version: 6.3.3<br /> © 2018 Seafile</p>
<p><a href="http://seafile.com/about/" target="_blank">About Us</a></p>
</div>
</ModalBody>
</Modal>
</div>
);
}
}
class SideNavFooter extends React.Component {
render() {
return (
<div className="side-nav-footer">
<a href="/help/" target="_blank" className="item">Help</a>
<About />
<a href="/download_client_program/" className="item last-item">
<span aria-hidden="true" className="sf2-icon-monitor vam"></span>{' '}
<span className="vam">Clients</span>
</a>
</div>
);
}
}
export default SideNavFooter;

View File

@@ -0,0 +1,10 @@
.activity-table a {
color: #333;
}
.activity-op {
color: #707070;
background: #f0f0f0;
padding: 4px;
border-radius: 4px;
}

52
frontend/src/dashboard.js Normal file
View File

@@ -0,0 +1,52 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import SidePanel from './pages/dashboard/side-panel';
import MainPanel from './pages/dashboard/main-panel';
import Account from './components/account';
import Notification from './components/notification';
import { SeafileAPI } from './seafile-api';
import cookie from 'react-cookies';
import 'seafile-ui';
import './css/dashboard.css';
const siteRoot = window.app.config.siteRoot;
let seafileAPI = new SeafileAPI();
let xcsrfHeaders = cookie.load('csrftoken');
seafileAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
class DashBoard extends Component {
constructor(props) {
super(props);
this.state = {
isOpen: false
}
}
isOpen = () => {
this.setState({
isOpen: !this.state.isOpen,
})
}
render() {
return (
<div id="main">
<SidePanel isOpen={this.state.isOpen} toggleClose={this.isOpen} seafileAPI={seafileAPI}/>
<MainPanel isOpen={this.isOpen} seafileAPI={seafileAPI} >
<Notification seafileAPI={seafileAPI} />
<Account seafileAPI={seafileAPI}/>
</MainPanel>
</div>
)
}
}
ReactDOM.render(
<DashBoard />,
document.getElementById('wrapper')
);

View File

@@ -0,0 +1,30 @@
import React, { Component } from 'react';
import FilesActivities from '../../components/files-activities';
class MainPanel extends Component {
constructor(props) {
super(props);
}
onMenuClick = () => {
this.props.isOpen();
}
render() {
const { children } = this.props
return (
<div className="main-panel o-hidden">
<div className="main-panel-top panel-top">
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu" onClick={this.onMenuClick}></span>
<div className="common-toolbar">
{children}
</div>
</div>
<FilesActivities seafileAPI={this.props.seafileAPI} />
</div>
)
}
}
export default MainPanel;

View File

@@ -0,0 +1,30 @@
import React, { Component } from 'react';
import SideNavFooter from '../../components/side-nav-footer';
import MainSideNav from '../../components/main-side-nav';
const siteRoot = window.app.config.siteRoot;
const serverRoot = window.app.config.serverRoot;
const logoPath = window.app.config.logoPath;
const mediaUrl = window.app.config.mediaUrl;
const siteTitle = window.app.config.siteTitle;
const logoWidth = window.app.config.logoWidth;
const logoHeight = window.app.config.logoHeight;
class SidePanel extends Component {
render() {
return (
<div className={`side-panel ${this.props.isOpen ? "left-zero": ""}`}>
<div className="side-panel-top panel-top">
<a href={siteRoot} id="logo">
<img src={mediaUrl + logoPath} title={siteTitle} alt="logo" width={logoWidth} height={logoHeight} />
</a>
<a href="#" title="Close" aria-label="Close" onClick={this.props.toggleClose} className="sf2-icon-x1 sf-popover-close side-panel-close op-icon d-md-none "></a>
</div>
<MainSideNav seafileAPI={this.props.seafileAPI}/>
<SideNavFooter />
</div>
)
}
}
export default SidePanel;

View File

@@ -1,8 +1,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import Search from './search'; import Search from '../../components/search';
import MarkdownViewer from './markdown-viewer'; import MarkdownViewer from '../../components/markdown-viewer';
import Account from './account'; import Account from '../../components/account';
import { gettext, repoID, serviceUrl, slug, siteRoot } from './constance'; import { gettext, repoID, serviceUrl, slug, siteRoot } from '../../components/constance';
class MainPanel extends Component { class MainPanel extends Component {

View File

@@ -1,10 +1,10 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import TreeView from './tree-view/tree-view'; import TreeView from '../../components/tree-view/tree-view';
import { siteRoot, logoPath, mediaUrl, siteTitle, logoWidth, logoHeight } from './constance'; import { siteRoot, logoPath, mediaUrl, siteTitle, logoWidth, logoHeight } from '../../components/constance';
import Tree from './tree-view/tree'; import Tree from '../../components/tree-view/tree';
import Node from './tree-view/node' import Node from '../../components/tree-view/node'
import NodeMenu from './menu-component/node-menu'; import NodeMenu from '../../components/menu-component/node-menu';
import MenuControl from './menu-component/node-menu-control'; import MenuControl from '../../components/menu-component/node-menu-control';
const gettext = window.gettext; const gettext = window.gettext;
class SidePanel extends Component { class SidePanel extends Component {

View File

@@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import SidePanel from './components/SidePanel'; import SidePanel from './pages/wiki/side-panel';
import MainPanel from './components/MainPanel'; import MainPanel from './pages/wiki/main-panel';
import moment from 'moment'; import moment from 'moment';
import cookie from 'react-cookies'; import cookie from 'react-cookies';
import { SeafileAPI } from 'seafile-js'; import { SeafileAPI } from 'seafile-js';

View File

@@ -79,7 +79,10 @@ a:hover { color:#eb8205; }
.left-zero { .left-zero {
left: 0px !important; left: 0px !important;
} }
ul,ol,li {
padding:0;
margin:0;
}
/* common elements */ /* common elements */
@@ -261,6 +264,7 @@ a:hover { color:#eb8205; }
padding:12px 20px 16px; padding:12px 20px 16px;
background:#f8f8f8; background:#f8f8f8;
border-top:1px solid #eee; border-top:1px solid #eee;
border-right: 1px solid #eee;
} }
.side-nav-footer .item { .side-nav-footer .item {
color:#666; color:#666;
@@ -572,6 +576,14 @@ a.op-icon:focus {
color:#999; color:#999;
margin-right:15px; margin-right:15px;
} }
.side-panel-slide {
transition: all .3s ease-in-out;
}
.side-panel-slide-up {
transition: all .3s ease-in-out;
height: 0;
}
#group-nav .sharp, #group-nav .sharp,
#share-admin-nav .sharp { #share-admin-nav .sharp {
display:inline-block; display:inline-block;
@@ -614,13 +626,33 @@ a.op-icon:focus {
text-decoration:none; text-decoration:none;
} }
.user-select-none {
-moz-user-select:none;
-webkit-user-select:none;
-ms-user-select:none;
-khtml-user-select:none;
user-select: none;
}
.common-toolbar { .common-toolbar {
margin-left:auto; margin-left:auto;
display:flex; display:flex;
} }
#notifications { #notifications {
position:relative; position:relative;
margin:5px 0 0 25px; width: 32px;
}
#notifications .no-deco {
position: relative;
display: block;
width: 100%;
height: 100%;
}
#notifications .sf2-icon-bell {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
} }
@media (max-width: 390px) { @media (max-width: 390px) {
#notifications { #notifications {
@@ -628,6 +660,13 @@ a.op-icon:focus {
} }
} }
.sf-heading {
font-size: 1rem;
color: #322;
font-weight: normal;
line-height: 1.5;
}
/* about dialog */ /* about dialog */
.about-content { .about-content {
@@ -679,8 +718,6 @@ a.op-icon:focus {
padding:1px 30px 1px 5px; padding:1px 30px 1px 5px;
} }
.wiki-page-ops { .wiki-page-ops {
position:fixed; position:fixed;
top:10px; top:10px;
@@ -726,3 +763,105 @@ select {
background:#fff; background:#fff;
line-height:17px; line-height:17px;
} }
#notifications .sf2-icon-bell {
font-size:24px;
line-height:1;
color:#999;
}
#notifications .num {
position:absolute;
color:#fff;
font-size:12px;
line-height:1;
padding:1px 2px;
background:#feac74;
border:1px solid #cb8a5d;
top:0;
left:15px;
}
#notice-popover {
top:38px;
right:-12px;
}
#notice-popover .outer-caret {
right:18px;
}
#notice-popover a {
font-weight:normal;
}
#notice-popover li {
padding:9px 0 3px;
border-bottom:1px solid #dfdfe1;
}
#notice-popover li.unread {
background:#f5f5f7;
padding-right:10px;
padding-left:8px;
border-left:2px solid #feac74;
margin:0 -10px;
}
#notice-popover .avatar {
border-radius:1000px;
float:left;
}
#notice-popover .brief {
margin-left:40px;
}
#notice-popover .time {
color:#999;
text-align:right;
margin:0;
clear:both;
}
#notice-popover .view-all {
display:block;
padding:7px 0;
text-align:center;
color:#a4a4a4;
}
#notice-popover .sf-popover-close {
position: absolute;
right: 10px;
top: 5px;
}
/**** sf-popover ****/ /* e.g. top notice popup, group members popup */
.sf-popover-container {
position:relative;
}
.sf-popover {
width:240px;
background:#fff;
border:1px solid #c9c9c9;
border-radius:3px;
box-shadow:0 0 4px #ccc;
position:absolute;
z-index: 20;
}
.sf-popover-hd {
padding:5px 0 3px;
border-bottom:1px solid #dfdfe1;
margin:0 10px;
}
.sf-popover-title {
text-align:center;
}
.sf-popover-close {
font-size:16px;
color:#b9b9b9;
margin:4px 0 0;
}
.sf-popover-con {
padding:0 10px;
overflow:auto;
}
.main-panel-main {
flex:auto;
display:flex;
flex-direction:column;
overflow:hidden; /* for ff */
}

View File

@@ -0,0 +1,7 @@
{% extends "base_for_react.html" %}
{% load render_bundle from webpack_loader %}
{% block extra_script %}
{% render_bundle 'dashboard' %}
{% endblock %}

View File

@@ -178,6 +178,7 @@ urlpatterns = [
url(r'^modules/toggle/$', toggle_modules, name="toggle_modules"), url(r'^modules/toggle/$', toggle_modules, name="toggle_modules"),
url(r'^download_client_program/$', TemplateView.as_view(template_name="download.html"), name="download_client"), url(r'^download_client_program/$', TemplateView.as_view(template_name="download.html"), name="download_client"),
url(r'^choose_register/$', choose_register, name="choose_register"), url(r'^choose_register/$', choose_register, name="choose_register"),
url(r'^dashboard/$', TemplateView.as_view(template_name="dashboard.html"), name="dashboard"),
### Ajax ### ### Ajax ###
url(r'^ajax/repo/(?P<repo_id>[-0-9a-f]{36})/dirents/$', get_dirents, name="get_dirents"), url(r'^ajax/repo/(?P<repo_id>[-0-9a-f]{36})/dirents/$', get_dirents, name="get_dirents"),