Преглед на файлове

feat: rebuild project router

hi-cactus! преди 3 години
родител
ревизия
fd8b2a6021
променени са 39 файла, в които са добавени 2907 реда и са изтрити 115 реда
  1. 3 0
      app/app.tsx
  2. 36 0
      app/assets/images/logo_white.svg
  3. 2 2
      app/assets/less/variable.less
  4. 66 8
      app/components/Navigator/Navigator.less
  5. 45 40
      app/components/Navigator/index.tsx
  6. 32 6
      app/components/Sidebar/Sidebar.less
  7. 10 3
      app/components/SidebarOption/index.tsx
  8. 14 12
      app/containers/App/index.tsx
  9. 16 0
      app/containers/DataGovernance/Loadable.tsx
  10. 161 0
      app/containers/DataGovernance/Sidebar.tsx
  11. 10 0
      app/containers/DataGovernance/index.tsx
  12. 16 0
      app/containers/DataManager/Loadable.tsx
  13. 161 0
      app/containers/DataManager/Sidebar.tsx
  14. 73 0
      app/containers/DataManager/index.tsx
  15. 16 0
      app/containers/DataShareService/Loadable.tsx
  16. 161 0
      app/containers/DataShareService/Sidebar.tsx
  17. 73 0
      app/containers/DataShareService/index.tsx
  18. 4 0
      app/containers/Main/Main.less
  19. 19 7
      app/containers/Main/Sidebar.tsx
  20. 56 29
      app/containers/Main/index.tsx
  21. 161 0
      app/containers/Viz/DataManagerDisplay/Editor.tsx
  22. 7 0
      app/containers/Viz/DataManagerDisplay/Loadable.tsx
  23. 71 0
      app/containers/Viz/DataManagerDisplay/index.tsx
  24. 201 0
      app/containers/Viz/DataManagerPortal.tsx
  25. 161 0
      app/containers/Viz/DataShareServiceDisplay/Editor.tsx
  26. 7 0
      app/containers/Viz/DataShareServiceDisplay/Loadable.tsx
  27. 71 0
      app/containers/Viz/DataShareServiceDisplay/index.tsx
  28. 202 0
      app/containers/Viz/DataShareServicePortal.tsx
  29. 239 0
      app/containers/Viz/DataVizList.tsx
  30. 32 4
      app/containers/Viz/Loadable.tsx
  31. 286 0
      app/containers/Viz/components/DataDisplayList.tsx
  32. 317 0
      app/containers/Viz/components/DataPortalList.tsx
  33. 42 0
      app/containers/Viz/dataManagerViz.tsx
  34. 42 0
      app/containers/Viz/dataShareServiceViz.tsx
  35. 41 0
      app/containers/Widget/DataManaferWidget.tsx
  36. 41 0
      app/containers/Widget/DataShareServiceWidget.tsx
  37. 11 3
      app/containers/Widget/Loadable.tsx
  38. BIN
      app/favicon.ico
  39. 1 1
      app/index.html

+ 3 - 0
app/app.tsx

@@ -34,6 +34,7 @@ import LanguageProvider from 'containers/LanguageProvider'
 import { translationMessages } from './i18n'
 import moment from 'moment'
 import 'moment/dist/locale/zh-cn'
+
 moment.locale('zh-cn')
 
 import '!file-loader?name=[name].[ext]!./favicon.ico'
@@ -80,6 +81,7 @@ import 'echarts/lib/component/markArea'
 import 'assets/js/china.js'
 
 import { DEFAULT_ECHARTS_THEME } from 'app/globalConstants'
+
 echarts.registerTheme('default', DEFAULT_ECHARTS_THEME)
 
 import configureStore from './configureStore'
@@ -115,6 +117,7 @@ interface IWindow extends Window {
   Intl: any
   __REACT_DEVTOOLS_GLOBAL_HOOK__: any
 }
+
 declare const window: IWindow
 
 if (!window.Intl) {

+ 36 - 0
app/assets/images/logo_white.svg

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="359px" height="50px" viewBox="0 0 359 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>logo_white</title>
+    <defs>
+        <polygon id="path-1" points="0 0 42 0 42 15 0 15"></polygon>
+    </defs>
+    <g id="页面展示" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="数据开放服务" transform="translate(-15.000000, -9.000000)">
+            <g id="logo_white" transform="translate(15.000000, 9.000000)">
+                <rect id="矩形" x="0" y="0" width="60" height="50"></rect>
+                <g id="编组-10" transform="translate(1.000000, 3.000000)">
+                    <path d="M34,31 C34,31 38.2222222,28.3369575 42.8911556,23.7210172 C47.55992,19.1050769 53,7.83153042 53,7.83153042 C53,7.83153042 52.6622222,6.9438496 51.48,4.99095178 C50.5176711,3.40129295 49.3688889,2 49.3688889,2 C49.3688889,2 48.5244444,7.74276234 44.3022222,16.4420344 C39.9084089,25.4949586 34,31 34,31" id="Fill-1" fill="#FFFFFF"></path>
+                    <path d="M43.5709796,24.6642336 C37.3211864,30.620438 30,35 30,35 C30,35 39.6425381,32.5474453 46.428028,26.5912409 C53.1149496,20.7215766 54.999173,17.3065693 54.999173,17.3065693 C54.999173,17.3065693 55.0445286,16.0687299 54.4634764,13.9781022 C53.9277798,12.0510949 53.2135178,11 53.2135178,11 C53.2135178,11 47.8517309,20.5845839 43.5709796,24.6642336" id="Fill-3" fill="#FFFFFF"></path>
+                    <path d="M56.4662577,20 C56.4662577,20 51.7131043,25.7241805 43.4230184,30.7403187 C34.2269939,36.3047474 28,36.9985665 28,36.9985665 C28,36.9985665 37.7852761,37.1720212 45.6134969,33.702926 C53.4417178,30.2338308 56.6441718,26.9381904 56.6441718,26.9381904 C56.6441718,26.9381904 57,26.2497485 57,23.4690952 C57,21.3876381 56.4662577,20 56.4662577,20" id="Fill-5" fill="#FFFFFF"></path>
+                    <path d="M2.87906551,25.0450251 C2.76546038,26.0688574 2.46113911,26.3747871 1.95541304,26.3744743 C1.64162468,26.3743179 1.33715071,26.1314197 1.170255,25.5929146 L0,26.5527769 C0.362803481,27.5240568 0.972820278,27.999374 1.98411971,28 C3.44021988,28.0007817 4.25225225,26.7348332 4.42327073,25.193767 L5,20.0009384 L3.43930371,20 L2.87906551,25.0450251 Z" id="Fill-7" fill="#FFFFFF"></path>
+                    <polygon id="Fill-9" fill="#FFFFFF" points="8.98943754 28 9.78816888 21.5930226 11.8016702 21.5941377 12 20.0030267 6.19850438 20 6 21.5911109 8.0037245 21.5920667 7.20499316 27.9990442"></polygon>
+                    <polygon id="Fill-11" fill="#FFFFFF" points="16.8260976 21.5934053 17 20.0023894 12.8742575 20 12 27.9974513 16.2218786 28 16.3982304 26.3876389 13.7408609 26.386046 13.9265507 24.6876668 16.1029331 24.6889411 16.2792848 23.07658 14.1027494 23.0753056 14.2650175 21.5918124"></polygon>
+                    <path d="M20.7883172,21.5855347 C21.2967539,21.5858418 21.6398563,21.8347668 21.9512023,22.1871931 L23,21.00645 C22.577759,20.4672915 21.8875217,20.0006147 20.956844,20 C19.143543,19.9990791 17.3678792,21.4697484 17.0479639,24.0396192 C16.723008,26.6511054 18.0903768,27.9990782 19.9036777,28 C20.8341874,28.0004603 21.6781654,27.6174751 22.3768038,26.8613326 L21.6195255,25.7002455 C21.2345855,26.0938266 20.7355581,26.4044838 20.1693216,26.4041767 C19.1620255,26.4035624 18.6206564,25.5121313 18.8113622,23.9785012 C18.9985394,22.4757373 19.8673848,21.585074 20.7883172,21.5855347" id="Fill-13" fill="#FFFFFF"></path>
+                    <path d="M28.2449686,22.2703513 C28.1779194,22.8187022 27.80282,23.1733301 27.0604893,23.1730117 L26.3376576,23.1725335 L26.5465023,21.4631188 L27.2691629,21.4635967 C28.0017441,21.4639154 28.3145835,21.7006526 28.2449686,22.2703513 M28.1916029,25.5064502 C28.1076204,26.1945175 27.6370785,26.5276383 26.7972527,26.5271609 L25.9280074,26.5266824 L26.1643901,24.591523 L27.0336354,24.5920009 C27.8833817,24.5924789 28.2718225,24.8507232 28.1916029,25.5064502 M29.9798674,21.9810412 C30.1675026,20.4436831 29.0201398,20.0022304 27.5161504,20.0014338 L24.976832,20 L24,27.9985634 L26.7444001,28 C28.3265566,28.0009531 29.6755802,27.2705088 29.8751885,25.6362892 C30.0051819,24.572087 29.5221538,23.9698887 28.8159133,23.7543399 L28.8212157,23.7113258 C29.437316,23.4643926 29.8931481,22.6906156 29.9798674,21.9810412" id="Fill-15" fill="#FFFFFF"></path>
+                    <g id="编组" transform="translate(7.000000, 29.000000)">
+                        <mask id="mask-2" fill="white">
+                            <use xlink:href="#path-1"></use>
+                        </mask>
+                        <g id="Clip-18"></g>
+                        <path d="M13.3258386,5.62706996 C11.3452361,4.23548663 9.88198493,2.25690165 8.95311588,0 L0,0 C0.491110182,2.21438601 1.28547134,4.11240563 2.37752244,5.44843282 C5.29915881,9.02278338 13.0215449,16.4901732 24.2741548,14.7375642 C35.7438193,12.9511928 42,5.44843282 42,5.44843282 C42,5.44843282 25.403743,14.1123342 13.3258386,5.62706996" id="Fill-17" fill="#FFFFFF" mask="url(#mask-2)"></path>
+                    </g>
+                    <path d="M25.523287,9.24906035 C30.4710706,8.09905661 34.383392,9.77550959 36.0618874,11.6766919 C39.2770536,15.3181392 36.9549891,20 36.9549891,20 C36.9549891,20 43.2067012,18.612782 43.9211826,13.2373122 C44.8880545,5.96326107 36.7763688,0.752349978 29.631555,0.0587409687 C25.3669942,-0.35534361 15.5266208,1.31677431 10.519178,8.38204909 C8.86693985,10.7134424 7.69840555,13.596775 7,16.6186561 L16.2241332,16.6186561 C17.8663687,13.1525185 20.9402462,10.3144438 25.523287,9.24906035" id="Fill-19" fill="#FFFFFF"></path>
+                </g>
+                <text id="苏锡常南部高速公路太湖隧道-数智隧道聚合" font-family="YouSheBiaoTiHei" font-size="24" font-weight="normal" line-spacing="24" fill="#FFFFFF">
+                    <tspan x="70.9484778" y="25">苏锡常南部高速公路太湖隧道</tspan>
+                    <tspan x="70.9484778" y="49">数智隧道聚合管理平台</tspan>
+                </text>
+            </g>
+        </g>
+    </g>
+</svg>

+ 2 - 2
app/assets/less/variable.less

@@ -1,6 +1,6 @@
 @import "~antd/lib/style/themes/default";
 
-@blue                               : #1B98E0;
+@blue                               : #2B55A2;
 @white                              : #fff;
 @black                              : #000;
 @light-green                        : #8BC34A;
@@ -49,7 +49,7 @@
     }
     .ant-form-item-label {
       line-height: @lineHeight;
-  
+
       label {
         color: @disabled-text-color;
         font-size: @fontSize;

+ 66 - 8
app/components/Navigator/Navigator.less

@@ -1,8 +1,8 @@
 @import "~assets/less/variable";
 
 .header {
-  height: 64px;
-  background-color: @body-background;
+  height: 68px;
+  background-color: @blue;
   box-shadow: @header-box-shadow;
   flex-shrink: 0;
   z-index: @header-index;
@@ -17,8 +17,9 @@
 }
 
 .logo {
-  width: 200px;
-  height: 64px;
+  width: 359px;
+  height: 68px;
+  padding-left: 15px;
   display: flex;
   flex-direction: column;
   justify-content: center;
@@ -29,14 +30,14 @@
   }
 
   img {
-    width: 160px;
-    height: 32px;
+    width: 359px;
+    height: 50px;
   }
 }
 
 @media (min-width: 768px) {
   .logoPc {
-    display: block;
+    display: flex;
   }
 
   .logoMobile {
@@ -50,13 +51,69 @@
   }
 
   .logoMobile {
-    display: block;
+    display: flex;
+
     img {
       margin-left: -142px;
     }
   }
 }
 
+.menus {
+  //line-height: 64px;
+  display: flex;
+  margin-left: 40px;
+  align-content: center;
+
+  li {
+    font-size: 16px;
+    font-weight: 400;
+    text-align: left;
+    color: #ffffff;
+    height: 68px;
+    display: flex;
+    align-items: center;
+
+    a {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      padding: 0 20px;
+      align-items: center;
+      color: #ffffff;
+      position: relative;
+
+      &:after {
+        content: '';
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        width: 100%;
+        height: 3px;
+        background: #ffc12f;
+        transform: rotateY(-90deg);
+        transition: transform .5s;
+      }
+    }
+
+    &:hover {
+      background: rgba(0, 0, 0, 0.25);
+    }
+
+    &.active {
+      a {
+        background: rgba(0, 0, 0, 0.25);
+
+        &:after {
+          transform: rotateY(0deg);
+
+        }
+      }
+    }
+  }
+
+}
+
 .tools {
   margin-right: 16px;
   display: flex;
@@ -69,6 +126,7 @@
     flex-direction: column;
     justify-content: center;
     align-items: center;
+    color: #fff;
 
     i {
       font-size: 24px;

+ 45 - 40
app/components/Navigator/index.tsx

@@ -21,14 +21,11 @@
 import React from 'react'
 import { connect } from 'react-redux'
 import { createStructuredSelector } from 'reselect'
-import { Link } from 'react-router-dom'
+import { Link, useLocation } from 'react-router-dom'
 import classnames from 'classnames'
 import DownloadList from '../DownloadList'
 
-import {
-  loadDownloadList,
-  downloadFile
-} from 'containers/App/actions'
+import { loadDownloadList, downloadFile } from 'containers/App/actions'
 import {
   makeSelectLoginUser,
   makeSelectDownloadList,
@@ -52,67 +49,75 @@ interface INavigatorProps {
   onDownloadFile: (id) => void
 }
 
-export function Navigator (props: INavigatorProps) {
-  const {
-    show,
-    downloadList,
-    onLogout,
-    onLoadDownloadList,
-    onDownloadFile
-  } = props
+export function Navigator(props: INavigatorProps) {
+  const { show, downloadList, onLogout, onLoadDownloadList, onDownloadFile } =
+    props
   const headerClass = classnames({
     [styles.header]: true,
     [styles.hide]: !show
   })
   const menu = (
     <Menu>
-      <Menu.Item key="0">
-        <Link to="/account" >
-          用户设置
-        </Link>
-      </Menu.Item>
+      {/*<Menu.Item key='0'>*/}
+      {/*  <Link to='/account'>*/}
+      {/*    用户设置*/}
+      {/*  </Link>*/}
+      {/*</Menu.Item>*/}
       <Menu.Divider />
-      <Menu.Item key="3">
-        <a href="javascript:;" onClick={onLogout}>
+      <Menu.Item key='3'>
+        <a href='javascript:;' onClick={onLogout}>
           退出登录
         </a>
       </Menu.Item>
     </Menu>
   )
 
+  const location = useLocation()
+  const { pathname } = location
+
+  const menus = []
+
   return (
     <nav className={headerClass}>
       <div className={styles.logoPc}>
         <div className={styles.logo}>
-          <Link to="/projects">
-            <img src={require('assets/images/logo.svg')} />
+          <Link to='/project/1/dataManager'>
+            <img src={require('assets/images/logo_white.svg')} />
           </Link>
         </div>
+        <ul className={styles.menus}>
+          {[
+            {
+              link: '/project/1/dataManager',
+              name: '数据管理'
+            },
+            {
+              link: '/project/1/dataShareService',
+              name: '数据开放服务'
+            },
+            {
+              link: '/project/1/dataGovernance',
+              name: '数据治理'
+            }
+          ].map((r) => (
+            <li key={r.name} className={pathname.includes(r.link) ? styles.active : null}>
+              <Link to={r.link}>{r.name}</Link>
+            </li>
+          ))}
+        </ul>
       </div>
       <div className={styles.logoMobile}>
         <div className={styles.logo}>
-          <Link to="/projects">
-            <img src={require('assets/images/logo_mobile.svg')} />
+          <Link to='/projects'>
+            <img src={require('assets/images/logo_white.svg')} />
           </Link>
         </div>
       </div>
+
       <ul className={styles.tools}>
         <li>
-          <DownloadList
-            downloadList={downloadList}
-            onLoadDownloadList={onLoadDownloadList}
-            onDownloadFile={onDownloadFile}
-          />
-        </li>
-        <li>
-          <Icon type="file-text" onClick={goDoc} />
-        </li>
-        <li>
-          <Icon type="github" onClick={goGithub}/>
-        </li>
-        <li>
-          <Dropdown overlay={menu} trigger={['click']} placement="bottomCenter">
-            <Icon type="user" />
+          <Dropdown overlay={menu} trigger={['click']} placement='bottomCenter'>
+            <Icon type='user' />
           </Dropdown>
         </li>
       </ul>
@@ -125,7 +130,7 @@ const mapStateToProps = createStructuredSelector({
   downloadList: makeSelectDownloadList()
 })
 
-function mapDispatchToProps (dispatch) {
+function mapDispatchToProps(dispatch) {
   return {
     onLoadDownloadList: () => dispatch(loadDownloadList()),
     onDownloadFile: (id) => dispatch(downloadFile(id))

+ 32 - 6
app/components/Sidebar/Sidebar.less

@@ -1,36 +1,62 @@
 @import "~assets/less/variable";
 
 .sidebar {
-  width: 64px;
+  width: 88px;
   background-color: @body-background;
+  background-color: #001529;
+  box-shadow: 2px 0px 6px 0px rgba(0, 21, 41, 0.12);
   flex-shrink: 0;
 
   .option {
-    height: 64px;
+    //height: 64px;
     text-align: center;
     cursor: pointer;
+    color: #fff;
 
     &.active {
       background-color: @body-bg;
-      color: @primary-color;
+      background-color: lighten(#001529, 3%);
+
+      color: #fff;
     }
 
     &:hover:not(.active) {
-      background-color: lighten(@body-bg, 3%);
+      background-color: lighten(#001529, 3%);
     }
 
     a {
       color: inherit;
       display: block;
 
+      > div {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        padding: 24px 0;
+
+        > span {
+          display: flex;
+          align-items: center;
+
+          &:first-child,
+          &:only-child {
+            height: 30px;
+          }
+
+          &:nth-child(2) {
+            padding: 8px 0;
+          }
+        }
+      }
+
       &:focus {
         text-decoration: none;
       }
     }
 
     i {
-      font-size: 24px;
-      line-height: 64px;
+      font-size: 30px;
     }
   }
 }

+ 10 - 3
app/components/SidebarOption/index.tsx

@@ -29,19 +29,26 @@ interface ISidebarOptionProps {
   active: boolean
   projectId: number
   icon: React.ReactNode
+  name: string
+  link: string
 }
 
 const SidebarOption: React.FC<ISidebarOptionProps> = (props) => {
-  const { indexRoute, active, projectId, icon } = props
+  const { indexRoute, active, projectId, icon, name , link } = props
   const optionClass = classnames(
     { [styles.option]: true },
     { [styles.active]: active }
   )
-  const linkRoute = `/project/${projectId}/${indexRoute}`
+  const linkRoute = link ?? `/project/${projectId}/${indexRoute}`
 
   return (
     <div className={optionClass}>
-      <Link to={linkRoute}>{icon}</Link>
+      <Link to={linkRoute}>
+        <div>
+          <span>{icon}</span>
+          {name && <span>{name}</span>}
+        </div>
+      </Link>
     </div>
   )
 }

+ 14 - 12
app/containers/App/index.tsx

@@ -49,7 +49,7 @@ type AppProps = MappedStates & MappedDispatches & RouteComponentWithParams
 
 export class App extends React.PureComponent<AppProps> {
 
-  constructor (props: AppProps) {
+  constructor(props: AppProps) {
     super(props)
     props.onGetServerConfigurations()
     this.checkTokenLink()
@@ -115,22 +115,24 @@ export class App extends React.PureComponent<AppProps> {
 
     return (
       logged ? (
-        <Redirect to="/projects" />
+        <Redirect to='/project/1/dataManager' />
       ) : (
-        <Redirect to="/login" />
+        <Redirect to='/login' />
       )
     )
   }
 
-  public render () {
+  public render() {
     const { logged } = this.props
-    if (typeof logged !== 'boolean') { return null }
+    if (typeof logged !== 'boolean') {
+      return null
+    }
 
     return (
       <div>
         <Helmet
-          titleTemplate="%s - Davinci"
-          defaultTitle="Davinci Web Application"
+          titleTemplate='%s - Davinci'
+          defaultTitle='Davinci Web Application'
           meta={[
             {
               name: 'description',
@@ -140,11 +142,11 @@ export class App extends React.PureComponent<AppProps> {
         />
         <Router>
           <Switch>
-            <Route path="/activate" component={Activate} />
-            <Route path="/joinOrganization" exact component={Background} />
-            <Route path="/findPassword" component={FindPassword} />
-            <Route path="/" exact render={this.renderRoute} />
-            <Route path="/" component={logged ? Main : Background} />
+            <Route path='/activate' component={Activate} />
+            <Route path='/joinOrganization' exact component={Background} />
+            <Route path='/findPassword' component={FindPassword} />
+            <Route path='/' exact render={this.renderRoute} />
+            <Route path='/' component={logged ? Main : Background} />
           </Switch>
         </Router>
       </div>

+ 16 - 0
app/containers/DataGovernance/Loadable.tsx

@@ -0,0 +1,16 @@
+import React from 'react'
+import loadable from 'utils/loadable'
+import { Skeleton } from 'antd'
+
+export const DataGovernance = loadable(() => import('./'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+
+export const Sidebar = loadable(() => import('./Sidebar'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+//
+
+// export const Grid = loadable(() => import('./Grid'), {
+//   fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+// })

+ 161 - 0
app/containers/DataGovernance/Sidebar.tsx

@@ -0,0 +1,161 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React, { useEffect, useMemo, PropsWithChildren } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { useLocation, matchPath, useHistory } from 'react-router-dom'
+
+import { showNavigator } from 'containers/App/actions'
+import { makeSelectCurrentProject } from 'containers/Projects/selectors'
+
+import { Icon } from 'antd'
+import SidebarOption from 'components/SidebarOption'
+import Sidebar from 'components/Sidebar'
+
+import { SidebarPermissions } from '../Main/constants'
+import { IRouteParams } from 'utils/types'
+import useProjectPermission from '../Projects/hooks/projectPermission'
+
+import styles from '../Main/Main.less'
+
+const sidebarSource: Array<{
+  icon: React.ReactNode
+  name: string
+  routes: string[]
+  permissionName: typeof SidebarPermissions[number]
+}> = [
+  {
+    icon: <i className='iconfont icon-dashboard' />,
+    name: '数据看板',
+    routes: ['vizs'],
+    permissionName: 'vizPermission'
+  },
+  {
+    icon: <i className='iconfont icon-widget-gallery' />,
+    name: '可视化组件',
+    routes: ['widgets'],
+    permissionName: 'widgetPermission'
+  },
+  // {
+  //   icon: <i className='iconfont icon-custom-business' />,
+  //   name: '数据资产',
+  //   routes: ['views'],
+  //   permissionName: 'viewPermission'
+  // },
+  // {
+  //   icon: <i className='iconfont icon-datasource24' />,
+  //   name: '数据源',
+  //   routes: ['sources'],
+  //   permissionName: 'sourcePermission'
+  // },
+  {
+    icon: <Icon type='clock-circle' />,
+    name: '定时任务',
+    routes: ['schedules'],
+    permissionName: 'schedulePermission'
+  }
+]
+
+const MainSidebar: React.FC<PropsWithChildren<{}>> = (props) => {
+  const dispatch = useDispatch()
+
+  useEffect(() => {
+    dispatch(showNavigator())
+  }, [])
+
+  const location = useLocation()
+  const { pathname } = location
+  const history = useHistory()
+
+  const currentProject = useSelector(makeSelectCurrentProject())
+
+  useEffect(() => {
+    if (!currentProject) {
+      return
+    }
+    const { id: projectId, permission } = currentProject
+    const match = matchPath<IRouteParams>(pathname, {
+      path: `/project/:projectId/dataShareService`,
+      exact: true,
+      strict: false
+    })
+    if (match) {
+      const hasPermission = SidebarPermissions.some((sidebarPermission) => {
+        if (permission[sidebarPermission] > 0) {
+          const path = sidebarPermission.slice(0, -10)
+          history.replace(`/project/${projectId}/dataShareService/${path}s`)
+          return true
+        }
+      })
+      !hasPermission && history.replace('/noAuthorization')
+    }
+  }, [pathname, currentProject])
+
+  const AuthorizedSidebarOptions = useProjectPermission(
+    SidebarOption,
+    sidebarSource.map(({ permissionName }) => permissionName)
+  )
+
+  const sidebar = useMemo(() => {
+    if (!currentProject) {
+      return null
+    }
+    const { id: projectId, permission } = currentProject
+    const vizOnly = SidebarPermissions.every((permissionName) =>
+      permissionName === 'vizPermission'
+        ? permission[permissionName]
+        : !permission[permissionName]
+    )
+    if (vizOnly) {
+      return null
+    }
+    const sidebarOptions = sidebarSource.map(
+      ({ permissionName, routes, icon, name }, idx) => {
+        if (!permission[permissionName]) {
+          return null
+        }
+
+        const active = routes.some((route) => pathname.includes(route))
+        const AuthorizedSidebarOption = AuthorizedSidebarOptions[idx]
+        return (
+          <AuthorizedSidebarOption
+            key={permissionName}
+            active={active}
+            indexRoute={routes[0]}
+            projectId={projectId}
+            icon={icon}
+            name={name}
+            link={`/project/${projectId}/dataShareService/${routes[0]}`}
+          />
+        )
+      }
+    )
+    return <Sidebar>{sidebarOptions}</Sidebar>
+  }, [currentProject, pathname])
+
+  return (
+    <div className={styles.sidebar}>
+      {sidebar}
+      <div className={styles.content}>{props.children}</div>
+    </div>
+  )
+}
+
+export default MainSidebar

+ 10 - 0
app/containers/DataGovernance/index.tsx

@@ -0,0 +1,10 @@
+import { Project } from 'containers/Projects/Loadable'
+import React from 'react'
+
+export default function DataGovernance() {
+  return (
+    <Project>
+
+    </Project>
+  )
+}

+ 16 - 0
app/containers/DataManager/Loadable.tsx

@@ -0,0 +1,16 @@
+import React from 'react'
+import loadable from 'utils/loadable'
+import { Skeleton } from 'antd'
+
+export const DataManager = loadable(() => import('./'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+
+export const Sidebar = loadable(() => import('./Sidebar'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+//
+
+// export const Grid = loadable(() => import('./Grid'), {
+//   fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+// })

+ 161 - 0
app/containers/DataManager/Sidebar.tsx

@@ -0,0 +1,161 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React, { useEffect, useMemo, PropsWithChildren } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { useLocation, matchPath, useHistory } from 'react-router-dom'
+
+import { showNavigator } from 'containers/App/actions'
+import { makeSelectCurrentProject } from 'containers/Projects/selectors'
+
+import { Icon } from 'antd'
+import SidebarOption from 'components/SidebarOption'
+import Sidebar from 'components/Sidebar'
+
+import { SidebarPermissions } from '../Main/constants'
+import { IRouteParams } from 'utils/types'
+import useProjectPermission from '../Projects/hooks/projectPermission'
+
+import styles from '../Main/Main.less'
+
+const sidebarSource: Array<{
+  icon: React.ReactNode
+  name: string
+  routes: string[]
+  permissionName: typeof SidebarPermissions[number]
+}> = [
+  {
+    icon: <i className="iconfont icon-dashboard" />,
+    name: '数据看板',
+    routes: ['vizs'],
+    permissionName: 'vizPermission'
+  },
+  {
+    icon: <i className="iconfont icon-widget-gallery" />,
+    name: '可视化组件',
+    routes: ['widgets'],
+    permissionName: 'widgetPermission'
+  },
+  {
+    icon: <i className="iconfont icon-custom-business" />,
+    name: '数据资产',
+    routes: ['views'],
+    permissionName: 'viewPermission'
+  },
+  {
+    icon: <i className="iconfont icon-datasource24" />,
+    name: '数据源',
+    routes: ['sources'],
+    permissionName: 'sourcePermission'
+  },
+  {
+    icon: <Icon type="clock-circle" />,
+    name: '定时任务',
+    routes: ['schedules'],
+    permissionName: 'schedulePermission'
+  }
+]
+
+const MainSidebar: React.FC<PropsWithChildren<{}>> = (props) => {
+  const dispatch = useDispatch()
+
+  useEffect(() => {
+    dispatch(showNavigator())
+  }, [])
+
+  const location = useLocation()
+  const { pathname } = location
+  const history = useHistory()
+
+  const currentProject = useSelector(makeSelectCurrentProject())
+
+  useEffect(() => {
+    if (!currentProject) {
+      return
+    }
+    const { id: projectId, permission } = currentProject
+    const match = matchPath<IRouteParams>(pathname, {
+      path: `/project/:projectId/dataManager`,
+      exact: true,
+      strict: false
+    })
+    if (match) {
+      const hasPermission = SidebarPermissions.some((sidebarPermission) => {
+        if (permission[sidebarPermission] > 0) {
+          const path = sidebarPermission.slice(0, -10)
+          history.replace(`/project/${projectId}/dataManager/${path}s`)
+          return true
+        }
+      })
+      !hasPermission && history.replace('/noAuthorization')
+    }
+  }, [pathname, currentProject])
+
+  const AuthorizedSidebarOptions = useProjectPermission(
+    SidebarOption,
+    sidebarSource.map(({ permissionName }) => permissionName)
+  )
+
+  const sidebar = useMemo(() => {
+    if (!currentProject) {
+      return null
+    }
+    const { id: projectId, permission } = currentProject
+    const vizOnly = SidebarPermissions.every((permissionName) =>
+      permissionName === 'vizPermission'
+        ? permission[permissionName]
+        : !permission[permissionName]
+    )
+    if (vizOnly) {
+      return null
+    }
+    const sidebarOptions = sidebarSource.map(
+      ({ permissionName, routes, icon ,name}, idx) => {
+        if (!permission[permissionName]) {
+          return null
+        }
+
+        const active = routes.some((route) => pathname.includes(route))
+        const AuthorizedSidebarOption = AuthorizedSidebarOptions[idx]
+        return (
+          <AuthorizedSidebarOption
+            key={permissionName}
+            active={active}
+            indexRoute={routes[0]}
+            projectId={projectId}
+            icon={icon}
+            name={name}
+            link={`/project/${projectId}/dataManager/${routes[0]}`}
+          />
+        )
+      }
+    )
+    return <Sidebar>{sidebarOptions}</Sidebar>
+  }, [currentProject, pathname])
+
+  return (
+    <div className={styles.sidebar}>
+      {sidebar}
+      <div className={styles.content}>{props.children}</div>
+    </div>
+  )
+}
+
+export default MainSidebar

+ 73 - 0
app/containers/DataManager/index.tsx

@@ -0,0 +1,73 @@
+import { Route, Switch } from 'react-router-dom'
+import { DataManagerViz as Viz } from 'containers/Viz/Loadable'
+import { DataManagerWidget as Widget, Workbench } from 'containers/Widget/Loadable'
+import { Sidebar } from './Loadable'
+import AuthorizedRoute from 'containers/Main/AuthorizedRoute'
+import { View, ViewEditor } from 'containers/View/Loadable'
+import { Source } from 'containers/Source/Loadable'
+import { Schedule, ScheduleEditor } from 'containers/Schedule/Loadable'
+import { Project } from 'containers/Projects/Loadable'
+import React from 'react'
+import { Dashboard } from '../Dashboard/Loadable'
+
+export default function DataManager() {
+  return (
+    <Project>
+      <Switch>
+        <Route
+          path='/project/:projectId/dataManager/portal/:portalId'
+          component={Dashboard}
+        />
+        <Route
+          path='/project/:projectId/dataManager/display/:displayId'
+          component={Viz}
+        />
+        <Route
+          exact
+          path='/project/:projectId/dataManager/widget/:widgetId?'
+          component={Workbench}
+        />
+        <Route
+          exact
+          path='/project/:projectId/dataManager/view/:viewId?'
+          component={ViewEditor}
+        />
+        <Route
+          exact
+          path='/project/:projectId/dataManager/schedule/:scheduleId?'
+          component={ScheduleEditor}
+        />
+        <Sidebar>
+          <Switch>
+            <AuthorizedRoute
+              permission='vizPermission'
+              path='/project/:projectId/dataManager/vizs'
+              component={Viz}
+            />
+            <AuthorizedRoute
+              permission='widgetPermission'
+              path='/project/:projectId/dataManager/widgets'
+              component={Widget}
+            />
+            <AuthorizedRoute
+              exact
+              permission='viewPermission'
+              path='/project/:projectId/dataManager/views'
+              component={View}
+            />
+            <AuthorizedRoute
+              permission='sourcePermission'
+              path='/project/:projectId/dataManager/sources'
+              component={Source}
+            />
+            <AuthorizedRoute
+              permission='schedulePermission'
+              path='/project/:projectId/dataManager/schedules'
+              component={Schedule}
+            />
+          </Switch>
+        </Sidebar>
+      </Switch>
+    </Project>
+  )
+}

+ 16 - 0
app/containers/DataShareService/Loadable.tsx

@@ -0,0 +1,16 @@
+import React from 'react'
+import loadable from 'utils/loadable'
+import { Skeleton } from 'antd'
+
+export const DataShareService = loadable(() => import('./'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+
+export const Sidebar = loadable(() => import('./Sidebar'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+//
+
+// export const Grid = loadable(() => import('./Grid'), {
+//   fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+// })

+ 161 - 0
app/containers/DataShareService/Sidebar.tsx

@@ -0,0 +1,161 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React, { useEffect, useMemo, PropsWithChildren } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { useLocation, matchPath, useHistory } from 'react-router-dom'
+
+import { showNavigator } from 'containers/App/actions'
+import { makeSelectCurrentProject } from 'containers/Projects/selectors'
+
+import { Icon } from 'antd'
+import SidebarOption from 'components/SidebarOption'
+import Sidebar from 'components/Sidebar'
+
+import { SidebarPermissions } from '../Main/constants'
+import { IRouteParams } from 'utils/types'
+import useProjectPermission from '../Projects/hooks/projectPermission'
+
+import styles from '../Main/Main.less'
+
+const sidebarSource: Array<{
+  icon: React.ReactNode
+  name: string
+  routes: string[]
+  permissionName: typeof SidebarPermissions[number]
+}> = [
+  {
+    icon: <i className='iconfont icon-dashboard' />,
+    name: '数据看板',
+    routes: ['vizs'],
+    permissionName: 'vizPermission'
+  },
+  {
+    icon: <i className='iconfont icon-widget-gallery' />,
+    name: '可视化组件',
+    routes: ['widgets'],
+    permissionName: 'widgetPermission'
+  },
+  // {
+  //   icon: <i className='iconfont icon-custom-business' />,
+  //   name: '数据资产',
+  //   routes: ['views'],
+  //   permissionName: 'viewPermission'
+  // },
+  // {
+  //   icon: <i className='iconfont icon-datasource24' />,
+  //   name: '数据源',
+  //   routes: ['sources'],
+  //   permissionName: 'sourcePermission'
+  // },
+  {
+    icon: <Icon type='clock-circle' />,
+    name: '定时任务',
+    routes: ['schedules'],
+    permissionName: 'schedulePermission'
+  }
+]
+
+const MainSidebar: React.FC<PropsWithChildren<{}>> = (props) => {
+  const dispatch = useDispatch()
+
+  useEffect(() => {
+    dispatch(showNavigator())
+  }, [])
+
+  const location = useLocation()
+  const { pathname } = location
+  const history = useHistory()
+
+  const currentProject = useSelector(makeSelectCurrentProject())
+
+  useEffect(() => {
+    if (!currentProject) {
+      return
+    }
+    const { id: projectId, permission } = currentProject
+    const match = matchPath<IRouteParams>(pathname, {
+      path: `/project/:projectId/dataShareService`,
+      exact: true,
+      strict: false
+    })
+    if (match) {
+      const hasPermission = SidebarPermissions.some((sidebarPermission) => {
+        if (permission[sidebarPermission] > 0) {
+          const path = sidebarPermission.slice(0, -10)
+          history.replace(`/project/${projectId}/dataShareService/${path}s`)
+          return true
+        }
+      })
+      !hasPermission && history.replace('/noAuthorization')
+    }
+  }, [pathname, currentProject])
+
+  const AuthorizedSidebarOptions = useProjectPermission(
+    SidebarOption,
+    sidebarSource.map(({ permissionName }) => permissionName)
+  )
+
+  const sidebar = useMemo(() => {
+    if (!currentProject) {
+      return null
+    }
+    const { id: projectId, permission } = currentProject
+    const vizOnly = SidebarPermissions.every((permissionName) =>
+      permissionName === 'vizPermission'
+        ? permission[permissionName]
+        : !permission[permissionName]
+    )
+    if (vizOnly) {
+      return null
+    }
+    const sidebarOptions = sidebarSource.map(
+      ({ permissionName, routes, icon, name }, idx) => {
+        if (!permission[permissionName]) {
+          return null
+        }
+
+        const active = routes.some((route) => pathname.includes(route))
+        const AuthorizedSidebarOption = AuthorizedSidebarOptions[idx]
+        return (
+          <AuthorizedSidebarOption
+            key={permissionName}
+            active={active}
+            indexRoute={routes[0]}
+            projectId={projectId}
+            icon={icon}
+            name={name}
+            link={`/project/${projectId}/dataShareService/${routes[0]}`}
+          />
+        )
+      }
+    )
+    return <Sidebar>{sidebarOptions}</Sidebar>
+  }, [currentProject, pathname])
+
+  return (
+    <div className={styles.sidebar}>
+      {sidebar}
+      <div className={styles.content}>{props.children}</div>
+    </div>
+  )
+}
+
+export default MainSidebar

+ 73 - 0
app/containers/DataShareService/index.tsx

@@ -0,0 +1,73 @@
+import { Route, Switch } from 'react-router-dom'
+import { DataShareServiceViz as Viz } from 'containers/Viz/Loadable'
+import { DataShareServiceWidget as Widget, Workbench } from 'containers/Widget/Loadable'
+import { Sidebar } from './Loadable'
+import AuthorizedRoute from 'containers/Main/AuthorizedRoute'
+import { View, ViewEditor } from 'containers/View/Loadable'
+import { Source } from 'containers/Source/Loadable'
+import { Schedule, ScheduleEditor } from 'containers/Schedule/Loadable'
+import { Project } from 'containers/Projects/Loadable'
+import React from 'react'
+import { Dashboard } from '../Dashboard/Loadable'
+
+export default function DataShareService() {
+  return (
+    <Project>
+      <Switch>
+        <Route
+          path='/project/:projectId/dataShareService/portal/:portalId'
+          component={Dashboard}
+        />
+        <Route
+          path='/project/:projectId/dataShareService/display/:displayId'
+          component={Viz}
+        />
+        <Route
+          exact
+          path='/project/:projectId/dataShareService/widget/:widgetId?'
+          component={Workbench}
+        />
+        <Route
+          exact
+          path='/project/:projectId/dataShareService/view/:viewId?'
+          component={ViewEditor}
+        />
+        <Route
+          exact
+          path='/project/:projectId/dataShareService/schedule/:scheduleId?'
+          component={ScheduleEditor}
+        />
+        <Sidebar>
+          <Switch>
+            <AuthorizedRoute
+              permission='vizPermission'
+              path='/project/:projectId/dataShareService/vizs'
+              component={Viz}
+            />
+            <AuthorizedRoute
+              permission='widgetPermission'
+              path='/project/:projectId/dataShareService/widgets'
+              component={Widget}
+            />
+            <AuthorizedRoute
+              exact
+              permission='viewPermission'
+              path='/project/:projectId/dataShareService/views'
+              component={View}
+            />
+            <AuthorizedRoute
+              permission='sourcePermission'
+              path='/project/:projectId/dataShareService/sources'
+              component={Source}
+            />
+            <AuthorizedRoute
+              permission='schedulePermission'
+              path='/project/:projectId/dataShareService/schedules'
+              component={Schedule}
+            />
+          </Switch>
+        </Sidebar>
+      </Switch>
+    </Project>
+  )
+}

+ 4 - 0
app/containers/Main/Main.less

@@ -28,6 +28,10 @@
     flex: 1;
     overflow-y: auto;
   }
+
+  &.hide {
+    display: none;
+  }
 }
 
 .container {

+ 19 - 7
app/containers/Main/Sidebar.tsx

@@ -34,34 +34,41 @@ import { IRouteParams } from 'utils/types'
 import useProjectPermission from '../Projects/hooks/projectPermission'
 
 import styles from './Main.less'
+import classnames from 'classnames'
 
 const sidebarSource: Array<{
   icon: React.ReactNode
+  name: string
   routes: string[]
   permissionName: typeof SidebarPermissions[number]
 }> = [
   {
-    icon: <i className="iconfont icon-dashboard" />,
+    icon: <i className='iconfont icon-dashboard' />,
+    name: 'vizs',
     routes: ['vizs'],
     permissionName: 'vizPermission'
   },
   {
-    icon: <i className="iconfont icon-widget-gallery" />,
+    icon: <i className='iconfont icon-widget-gallery' />,
+    name: 'widgets',
     routes: ['widgets'],
     permissionName: 'widgetPermission'
   },
   {
-    icon: <i className="iconfont icon-custom-business" />,
+    icon: <i className='iconfont icon-custom-business' />,
+    name: 'views',
     routes: ['views'],
     permissionName: 'viewPermission'
   },
   {
-    icon: <i className="iconfont icon-datasource24" />,
+    icon: <i className='iconfont icon-datasource24' />,
+    name: 'sources',
     routes: ['sources'],
     permissionName: 'sourcePermission'
   },
   {
-    icon: <Icon type="clock-circle" />,
+    icon: <Icon type='clock-circle' />,
+    name: 'schedules',
     routes: ['schedules'],
     permissionName: 'schedulePermission'
   }
@@ -121,7 +128,7 @@ const MainSidebar: React.FC<PropsWithChildren<{}>> = (props) => {
       return null
     }
     const sidebarOptions = sidebarSource.map(
-      ({ permissionName, routes, icon }, idx) => {
+      ({ permissionName, routes, icon, name }, idx) => {
         if (!permission[permissionName]) {
           return null
         }
@@ -135,6 +142,7 @@ const MainSidebar: React.FC<PropsWithChildren<{}>> = (props) => {
             indexRoute={routes[0]}
             projectId={projectId}
             icon={icon}
+            name={name}
           />
         )
       }
@@ -142,8 +150,12 @@ const MainSidebar: React.FC<PropsWithChildren<{}>> = (props) => {
     return <Sidebar>{sidebarOptions}</Sidebar>
   }, [currentProject, pathname])
 
+  const sidebarClass = classnames(styles.sidebar, {
+    [styles.hide]: pathname.includes('dataManager') || pathname.includes('dataShareService') || pathname.includes('dataGovernance')
+  })
+
   return (
-    <div className={styles.sidebar}>
+    <div className={sidebarClass}>
       {sidebar}
       <div className={styles.content}>{props.children}</div>
     </div>

+ 56 - 29
app/containers/Main/index.tsx

@@ -28,8 +28,15 @@ import { createStructuredSelector } from 'reselect'
 import Navigator from 'components/Navigator'
 
 import { logged, logout, loadDownloadList } from '../App/actions'
-import { makeSelectLogged, makeSelectNavigator, makeSelectOauth2Enabled } from '../App/selectors'
-import { DOWNLOAD_LIST_POLLING_FREQUENCY, EXTERNAL_LOG_OUT_URL } from 'app/globalConstants'
+import {
+  makeSelectLogged,
+  makeSelectNavigator,
+  makeSelectOauth2Enabled
+} from '../App/selectors'
+import {
+  DOWNLOAD_LIST_POLLING_FREQUENCY,
+  EXTERNAL_LOG_OUT_URL
+} from 'app/globalConstants'
 
 import { Project, ProjectList } from 'containers/Projects/Loadable'
 
@@ -50,6 +57,9 @@ import {
   Organization
 } from 'containers/Organizations/Loadable'
 import { NoAuthorization } from 'containers/NoAuthorization/Loadable'
+import { DataManager } from 'containers/DataManager/Loadable'
+import { DataShareService } from 'containers/DataShareService/Loadable'
+import { DataGovernance } from 'containers/DataGovernance/Loadable'
 
 const styles = require('./Main.less')
 
@@ -91,13 +101,13 @@ export class Main extends React.Component<IMainProps, {}> {
   private renderAccount = () => (
     <Account>
       <Switch>
-        <Redirect from="/account" exact to="/account/profile" />
-        <Route path="/account/profile" component={Profile} />
-        <Route path="/account/profile/:userId" component={UserProfile} />
-        <Route path="/account/resetPassword" component={ResetPassword} />
-        <Route path="/account/organizations" component={OrganizationList} />
+        <Redirect from='/account' exact to='/account/profile' />
+        <Route path='/account/profile' component={Profile} />
+        <Route path='/account/profile/:userId' component={UserProfile} />
+        <Route path='/account/resetPassword' component={ResetPassword} />
+        <Route path='/account/organizations' component={OrganizationList} />
         <Route
-          path="/account/organization/:organizationId"
+          path='/account/organization/:organizationId'
           component={Organization}
         />
       </Switch>
@@ -111,73 +121,90 @@ export class Main extends React.Component<IMainProps, {}> {
       <div className={styles.container}>
         <Navigator show={navigator} onLogout={this.logout} />
         <Switch>
-          <Route path="/project(s?)">
+          <Route path='/project(s?)'>
             <Switch>
-              <Route path="/projects" exact component={ProjectList} />
-              <Route path="/project/:projectId">
+              <Route path='/projects' exact component={ProjectList} />
+              <Route path='/project/:projectId'>
                 <Project>
                   <Switch>
                     <Route
-                      path="/project/:projectId/portal/:portalId"
+                      path='/project/:projectId/portal/:portalId'
                       component={Dashboard}
                     />
                     <Route
-                      path="/project/:projectId/display/:displayId"
+                      path='/project/:projectId/display/:displayId'
                       component={Viz}
                     />
                     <Route
                       exact
-                      path="/project/:projectId/widget/:widgetId?"
+                      path='/project/:projectId/widget/:widgetId?'
                       component={Workbench}
                     />
                     <Route
                       exact
-                      path="/project/:projectId/view/:viewId?"
+                      path='/project/:projectId/view/:viewId?'
                       component={ViewEditor}
                     />
                     <Route
                       exact
-                      path="/project/:projectId/schedule/:scheduleId?"
+                      path='/project/:projectId/schedule/:scheduleId?'
                       component={ScheduleEditor}
                     />
                     <Sidebar>
                       <Switch>
                         <AuthorizedRoute
-                          permission="vizPermission"
-                          path="/project/:projectId/vizs"
+                          permission='vizPermission'
+                          path='/project/:projectId/vizs'
                           component={Viz}
                         />
                         <AuthorizedRoute
-                          permission="widgetPermission"
-                          path="/project/:projectId/widgets"
+                          permission='widgetPermission'
+                          path='/project/:projectId/widgets'
                           component={Widget}
                         />
                         <AuthorizedRoute
                           exact
-                          permission="viewPermission"
-                          path="/project/:projectId/views"
+                          permission='viewPermission'
+                          path='/project/:projectId/views'
                           component={View}
                         />
                         <AuthorizedRoute
-                          permission="sourcePermission"
-                          path="/project/:projectId/sources"
+                          permission='sourcePermission'
+                          path='/project/:projectId/sources'
                           component={Source}
                         />
                         <AuthorizedRoute
-                          permission="schedulePermission"
-                          path="/project/:projectId/schedules"
+                          permission='schedulePermission'
+                          path='/project/:projectId/schedules'
                           component={Schedule}
                         />
                       </Switch>
                     </Sidebar>
                   </Switch>
                 </Project>
+                {/*<Switch> */}
+                <Route
+                  path='/project/:projectId/dataManager'
+                  component={DataManager}
+                />
+                <Route
+                  path='/project/:projectId/dataShareService'
+                  component={DataShareService}
+                />
+                <Route
+                  exact
+                  path='/project/:projectId/dataGovernance'
+                  component={DataGovernance}
+                />
+
+                {/* </Switch> */}
               </Route>
             </Switch>
           </Route>
-          <Route path="/account" render={this.renderAccount} />
-          <Route path="/noAuthorization" component={NoAuthorization} />
-          <Redirect to="/projects" />
+          <Route path='/account' render={this.renderAccount} />
+          <Route path='/noAuthorization' component={NoAuthorization} />
+          {/*<Redirect to='/projects' />*/}
+          <Redirect to='/project/1/dataManager' />
         </Switch>
       </div>
     ) : (

+ 161 - 0
app/containers/Viz/DataManagerDisplay/Editor.tsx

@@ -0,0 +1,161 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React, { useEffect, useCallback, useState } from 'react'
+import Helmet from 'react-helmet'
+import { useDispatch, useSelector } from 'react-redux'
+
+import { makeSelectCurrentProject } from 'containers/Projects/selectors'
+import {
+  makeSelectCurrentDisplay,
+  makeSelectCurrentSlides,
+  makeSelectCurrentSlide
+} from '../selectors'
+
+import { VizActions } from '../actions'
+
+import { Route } from 'react-router-dom'
+import { RouteComponentWithParams } from 'utils/types'
+
+import { Layout, PageHeader } from 'antd'
+import SplitPane from 'components/SplitPane'
+import SlideThumbnailList from '../components/SlideThumbnail'
+import DisplayHeader from 'containers/Display/Editor/Header'
+import { Display } from 'containers/Display/Loadable'
+import { ISlideFormed } from 'containers/Viz/components/types'
+
+import styles from '../Viz.less'
+
+const VizDisplayEditor: React.FC<RouteComponentWithParams> = (props) => {
+  const dispatch = useDispatch()
+  const { id: projectId } = useSelector(makeSelectCurrentProject())
+  const currentDisplay = useSelector(makeSelectCurrentDisplay())
+  const displayId = currentDisplay.id
+  const { id: slideId } = useSelector(makeSelectCurrentSlide())
+  const currentSlides = useSelector(makeSelectCurrentSlides())
+  const { history } = props
+
+  const [selectedSlideIds, setSelectedSlideIds] = useState([])
+
+  const clearSelectedSlide = useCallback(() => {
+    setSelectedSlideIds([])
+  }, [])
+
+  useEffect(() => {
+    window.addEventListener('click', clearSelectedSlide, false)
+    return () => {
+      window.removeEventListener('click', clearSelectedSlide, false)
+    }
+  }, [])
+
+  const goToViz = useCallback(() => {
+    history.replace(`/project/${projectId}/vizs`)
+  }, [projectId])
+
+  const selectSlide = useCallback(
+    (slideId: number, append: boolean) => {
+      if (append) {
+        setSelectedSlideIds(
+          selectedSlideIds.includes(slideId)
+            ? selectedSlideIds.filter((id) => id !== slideId)
+            : selectedSlideIds.concat(slideId)
+        )
+      } else {
+        setSelectedSlideIds([slideId])
+        history.replace(
+          `/project/${projectId}/display/${displayId}/slide/${slideId}`
+        )
+      }
+    },
+    [projectId, displayId, selectedSlideIds]
+  )
+
+  const changeDisplayAvatar = useCallback(
+    (avatar: string) => {
+      dispatch(
+        VizActions.editDisplay({
+          ...currentDisplay,
+          avatar
+        })
+      )
+    },
+    [currentDisplay]
+  )
+
+  const editSlides = useCallback((newSlides: ISlideFormed[]) => {
+    dispatch(VizActions.editSlides(newSlides))
+  }, [])
+
+  const deleteSlides = useCallback(
+    (targetSlideId?: number) => {
+      if (!targetSlideId || selectedSlideIds.includes(targetSlideId)) {
+        dispatch(VizActions.deleteSlides(displayId, selectedSlideIds))
+        return
+      }
+      if (targetSlideId) {
+        dispatch(VizActions.deleteSlides(displayId, [targetSlideId]))
+      }
+    },
+    [displayId, selectedSlideIds]
+  )
+
+  return (
+    <>
+      <Helmet title={`${currentDisplay.name} - Display`} />
+      <Layout>
+        <PageHeader
+          ghost={false}
+          title={currentDisplay.name}
+          subTitle={currentDisplay.description}
+          avatar={{
+            src: currentDisplay.avatar,
+            shape: 'square'
+          }}
+          extra={<DisplayHeader />}
+          onBack={goToViz}
+        />
+        <SplitPane
+          className="ant-layout-content"
+          type="horizontal"
+          initialSize={120}
+          minSize={120}
+          maxSize={200}
+        >
+          <SlideThumbnailList
+            className={styles.slides}
+            currentSlideId={slideId}
+            selectedSlideIds={selectedSlideIds}
+            slides={currentSlides}
+            onChange={editSlides}
+            onSelect={selectSlide}
+            onDelete={deleteSlides}
+            onChangeDisplayAvatar={changeDisplayAvatar}
+          />
+          <Route
+            path="/project/:projectId/display/:displayId/slide/:slideId"
+            component={Display}
+          />
+        </SplitPane>
+      </Layout>
+    </>
+  )
+}
+
+export default VizDisplayEditor

+ 7 - 0
app/containers/Viz/DataManagerDisplay/Loadable.tsx

@@ -0,0 +1,7 @@
+import React from 'react'
+import loadable from 'utils/loadable'
+import { Skeleton } from 'antd'
+
+export const VizDisplayEditor = loadable(() => import('./Editor'), {
+  fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+})

+ 71 - 0
app/containers/Viz/DataManagerDisplay/index.tsx

@@ -0,0 +1,71 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React, { useEffect } from 'react'
+import Helmet from 'react-helmet'
+import { useDispatch, useSelector } from 'react-redux'
+
+import {
+  makeSelectCurrentDisplay,
+  makeSelectCurrentSlide
+} from '../selectors'
+
+import { hideNavigator } from 'containers/App/actions'
+import { VizActions } from '../actions'
+
+import { Route, matchPath } from 'react-router-dom'
+import { RouteComponentWithParams, IRouteParams } from 'utils/types'
+
+import { Display } from 'containers/Display/Loadable'
+import { VizDisplayEditor } from './Loadable'
+
+const VizDisplay: React.FC<RouteComponentWithParams> = (props) => {
+  const dispatch = useDispatch()
+  const currentDisplay = useSelector(makeSelectCurrentDisplay())
+  const currentSlide = useSelector(makeSelectCurrentSlide())
+  const {
+    history,
+    match: { params }
+  } = props
+  const displayId = +params.displayId
+  const { pathname } = history.location
+
+  useEffect(() => {
+    dispatch(hideNavigator())
+  }, [])
+
+  useEffect(() => {
+    dispatch(VizActions.loadDisplaySlides(displayId))
+  }, [displayId])
+
+  if (!currentDisplay || !currentSlide) {
+    return null
+  }
+
+  return (
+    <>
+      <Helmet title={`${currentDisplay.name} - Display`} />
+      <Route exact path="/project/:projectId/display/:displayId/preview/slide/:slideId" component={Display} />
+      <Route exact path="/project/:projectId/display/:displayId/slide/:slideId" component={VizDisplayEditor} />
+    </>
+  )
+}
+
+export default VizDisplay

+ 201 - 0
app/containers/Viz/DataManagerPortal.tsx

@@ -0,0 +1,201 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React, { useEffect, useCallback, useState } from 'react'
+import { createStructuredSelector } from 'reselect'
+import { useDispatch, useSelector } from 'react-redux'
+import {
+  makeSelectDownloadList
+} from 'containers/App/selectors'
+import {
+  makeSelectPortals,
+  makeSelectCurrentPortal,
+  makeSelectCurrentDashboards
+} from './selectors'
+
+import {
+  hideNavigator,
+  loadDownloadList,
+  downloadFile
+} from 'containers/App/actions'
+import { VizActions } from './actions'
+
+import { Route } from 'react-router-dom'
+import { RouteComponentWithParams } from 'utils/types'
+
+import {
+  Layout,
+  Result,
+  PageHeader,
+  Tree,
+  Icon,
+  Button,
+  Menu,
+  Dropdown
+} from 'antd'
+const { Header, Sider, Content } = Layout
+const { DirectoryTree } = Tree
+import SplitPane from 'components/SplitPane'
+import DownloadList from 'components/DownloadList'
+import useDashboardConfigMenu from './hooks/dashboardConfigMenu'
+import { Grid } from 'containers/Dashboard/Loadable'
+import useDashboardTreeNodes from './hooks/dashboardTreeNodes'
+import { AntTreeNodeMouseEvent } from 'antd/lib/tree'
+
+const mapStateToProps = createStructuredSelector({
+  downloadList: makeSelectDownloadList(),
+  portals: makeSelectPortals(),
+  currentPortal: makeSelectCurrentPortal(),
+  currentDashboards: makeSelectCurrentDashboards()
+})
+
+// tslint:disable-next-line:no-empty-interface
+interface IVizPortalProps extends RouteComponentWithParams {}
+
+const DataManagerPortal: React.FC<IVizPortalProps> = (props) => {
+  const dispatch = useDispatch()
+  const {
+    portals,
+    currentPortal,
+    currentDashboards,
+    downloadList
+  } = useSelector(mapStateToProps)
+  const {
+    history,
+    match: { params }
+  } = props
+  const portalId = +params.portalId
+  const projectId = +params.projectId
+
+  useEffect(() => {
+    dispatch(hideNavigator())
+    if (!portals.length) {
+      dispatch(VizActions.loadPortals(projectId))
+    }
+  }, [])
+
+  useEffect(() => {
+    dispatch(VizActions.loadPortalDashboards(portalId))
+  }, [portalId])
+
+  const goToViz = useCallback(() => {
+    history.replace(`/project/${projectId}/dataManager/vizs`)
+  }, [])
+
+  const onLoadDownloadList = useCallback(() => dispatch(loadDownloadList()), [])
+  const onDownloadFile = useCallback((id) => dispatch(downloadFile(id)), [])
+
+  const [dashboardTreeNodes, firstDashboardKey] = useDashboardTreeNodes(currentDashboards)
+  const [dashboardMenuVisible, setDashboardMenuVisible] = useState(false)
+  const [dashboardMenuStyle, setDashboardMenuStyle] = useState({})
+  const dashboardConfigMenu = useDashboardConfigMenu(dashboardMenuStyle)
+
+  const closeDashboardMenu = useCallback(() => {
+    setDashboardMenuVisible(false)
+  }, [])
+
+  useEffect(() => {
+    document.addEventListener('click', closeDashboardMenu, false)
+    return () => {
+      document.removeEventListener('click', closeDashboardMenu, false)
+    }
+  }, [])
+
+  const showDashboardContextMenu = useCallback((options: AntTreeNodeMouseEvent) => {
+    const { node, event } = options
+    const { pageX, pageY } = event
+    const menuStyle: React.CSSProperties = {
+      position: 'absolute',
+      left: pageX,
+      top: pageY
+    }
+    setDashboardMenuStyle(menuStyle)
+    setDashboardMenuVisible(true)
+  }, [])
+
+  return (
+    <Layout>
+      <PageHeader
+        ghost={false}
+        title={currentPortal.name}
+        subTitle={currentPortal.description}
+        onBack={goToViz}
+        extra={
+          <DownloadList
+            downloadList={downloadList}
+            onLoadDownloadList={onLoadDownloadList}
+            onDownloadFile={onDownloadFile}
+          />
+        }
+      />
+      {dashboardMenuVisible && dashboardConfigMenu}
+      {Array.isArray(currentDashboards) &&
+        (currentDashboards.length ? (
+          <SplitPane
+            spliter
+            className="ant-layout-content"
+            type="horizontal"
+            initialSize={150}
+            minSize={150}
+          >
+            <DirectoryTree
+              defaultExpandAll
+              blockNode
+              defaultSelectedKeys={firstDashboardKey}
+              onRightClick={showDashboardContextMenu}
+            >
+              {dashboardTreeNodes}
+            </DirectoryTree>
+            <Route
+              path="/project/:projectId/dataManager/portal/:portalId/dashboard/:dashboardId"
+              component={Grid}
+            />
+          </SplitPane>
+        ) : (
+          <Content
+            style={{
+              display: 'flex',
+              flexDirection: 'column',
+              alignItems: 'center',
+              justifyContent: 'center'
+            }}
+          >
+            <Result
+              icon={<img src={require('assets/images/noDashboard.png')} />}
+              extra={
+                <p>
+                  请
+                  <Button size="small" type="link">
+                    创建文件夹
+                  </Button>
+                  或
+                  <Button size="small" type="link">
+                    创建 Dashboard
+                  </Button>
+                </p>
+              }
+            />
+          </Content>
+        ))}
+    </Layout>
+  )
+}
+
+export default DataManagerPortal

+ 161 - 0
app/containers/Viz/DataShareServiceDisplay/Editor.tsx

@@ -0,0 +1,161 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React, { useEffect, useCallback, useState } from 'react'
+import Helmet from 'react-helmet'
+import { useDispatch, useSelector } from 'react-redux'
+
+import { makeSelectCurrentProject } from 'containers/Projects/selectors'
+import {
+  makeSelectCurrentDisplay,
+  makeSelectCurrentSlides,
+  makeSelectCurrentSlide
+} from '../selectors'
+
+import { VizActions } from '../actions'
+
+import { Route } from 'react-router-dom'
+import { RouteComponentWithParams } from 'utils/types'
+
+import { Layout, PageHeader } from 'antd'
+import SplitPane from 'components/SplitPane'
+import SlideThumbnailList from '../components/SlideThumbnail'
+import DisplayHeader from 'containers/Display/Editor/Header'
+import { Display } from 'containers/Display/Loadable'
+import { ISlideFormed } from 'containers/Viz/components/types'
+
+import styles from '../Viz.less'
+
+const VizDisplayEditor: React.FC<RouteComponentWithParams> = (props) => {
+  const dispatch = useDispatch()
+  const { id: projectId } = useSelector(makeSelectCurrentProject())
+  const currentDisplay = useSelector(makeSelectCurrentDisplay())
+  const displayId = currentDisplay.id
+  const { id: slideId } = useSelector(makeSelectCurrentSlide())
+  const currentSlides = useSelector(makeSelectCurrentSlides())
+  const { history } = props
+
+  const [selectedSlideIds, setSelectedSlideIds] = useState([])
+
+  const clearSelectedSlide = useCallback(() => {
+    setSelectedSlideIds([])
+  }, [])
+
+  useEffect(() => {
+    window.addEventListener('click', clearSelectedSlide, false)
+    return () => {
+      window.removeEventListener('click', clearSelectedSlide, false)
+    }
+  }, [])
+
+  const goToViz = useCallback(() => {
+    history.replace(`/project/${projectId}/vizs`)
+  }, [projectId])
+
+  const selectSlide = useCallback(
+    (slideId: number, append: boolean) => {
+      if (append) {
+        setSelectedSlideIds(
+          selectedSlideIds.includes(slideId)
+            ? selectedSlideIds.filter((id) => id !== slideId)
+            : selectedSlideIds.concat(slideId)
+        )
+      } else {
+        setSelectedSlideIds([slideId])
+        history.replace(
+          `/project/${projectId}/display/${displayId}/slide/${slideId}`
+        )
+      }
+    },
+    [projectId, displayId, selectedSlideIds]
+  )
+
+  const changeDisplayAvatar = useCallback(
+    (avatar: string) => {
+      dispatch(
+        VizActions.editDisplay({
+          ...currentDisplay,
+          avatar
+        })
+      )
+    },
+    [currentDisplay]
+  )
+
+  const editSlides = useCallback((newSlides: ISlideFormed[]) => {
+    dispatch(VizActions.editSlides(newSlides))
+  }, [])
+
+  const deleteSlides = useCallback(
+    (targetSlideId?: number) => {
+      if (!targetSlideId || selectedSlideIds.includes(targetSlideId)) {
+        dispatch(VizActions.deleteSlides(displayId, selectedSlideIds))
+        return
+      }
+      if (targetSlideId) {
+        dispatch(VizActions.deleteSlides(displayId, [targetSlideId]))
+      }
+    },
+    [displayId, selectedSlideIds]
+  )
+
+  return (
+    <>
+      <Helmet title={`${currentDisplay.name} - Display`} />
+      <Layout>
+        <PageHeader
+          ghost={false}
+          title={currentDisplay.name}
+          subTitle={currentDisplay.description}
+          avatar={{
+            src: currentDisplay.avatar,
+            shape: 'square'
+          }}
+          extra={<DisplayHeader />}
+          onBack={goToViz}
+        />
+        <SplitPane
+          className="ant-layout-content"
+          type="horizontal"
+          initialSize={120}
+          minSize={120}
+          maxSize={200}
+        >
+          <SlideThumbnailList
+            className={styles.slides}
+            currentSlideId={slideId}
+            selectedSlideIds={selectedSlideIds}
+            slides={currentSlides}
+            onChange={editSlides}
+            onSelect={selectSlide}
+            onDelete={deleteSlides}
+            onChangeDisplayAvatar={changeDisplayAvatar}
+          />
+          <Route
+            path="/project/:projectId/display/:displayId/slide/:slideId"
+            component={Display}
+          />
+        </SplitPane>
+      </Layout>
+    </>
+  )
+}
+
+export default VizDisplayEditor

+ 7 - 0
app/containers/Viz/DataShareServiceDisplay/Loadable.tsx

@@ -0,0 +1,7 @@
+import React from 'react'
+import loadable from 'utils/loadable'
+import { Skeleton } from 'antd'
+
+export const VizDisplayEditor = loadable(() => import('./Editor'), {
+  fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+})

+ 71 - 0
app/containers/Viz/DataShareServiceDisplay/index.tsx

@@ -0,0 +1,71 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React, { useEffect } from 'react'
+import Helmet from 'react-helmet'
+import { useDispatch, useSelector } from 'react-redux'
+
+import {
+  makeSelectCurrentDisplay,
+  makeSelectCurrentSlide
+} from '../selectors'
+
+import { hideNavigator } from 'containers/App/actions'
+import { VizActions } from '../actions'
+
+import { Route, matchPath } from 'react-router-dom'
+import { RouteComponentWithParams, IRouteParams } from 'utils/types'
+
+import { Display } from 'containers/Display/Loadable'
+import { VizDisplayEditor } from './Loadable'
+
+const VizDisplay: React.FC<RouteComponentWithParams> = (props) => {
+  const dispatch = useDispatch()
+  const currentDisplay = useSelector(makeSelectCurrentDisplay())
+  const currentSlide = useSelector(makeSelectCurrentSlide())
+  const {
+    history,
+    match: { params }
+  } = props
+  const displayId = +params.displayId
+  const { pathname } = history.location
+
+  useEffect(() => {
+    dispatch(hideNavigator())
+  }, [])
+
+  useEffect(() => {
+    dispatch(VizActions.loadDisplaySlides(displayId))
+  }, [displayId])
+
+  if (!currentDisplay || !currentSlide) {
+    return null
+  }
+
+  return (
+    <>
+      <Helmet title={`${currentDisplay.name} - Display`} />
+      <Route exact path="/project/:projectId/display/:displayId/preview/slide/:slideId" component={Display} />
+      <Route exact path="/project/:projectId/display/:displayId/slide/:slideId" component={VizDisplayEditor} />
+    </>
+  )
+}
+
+export default VizDisplay

+ 202 - 0
app/containers/Viz/DataShareServicePortal.tsx

@@ -0,0 +1,202 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React, { useEffect, useCallback, useState } from 'react'
+import { createStructuredSelector } from 'reselect'
+import { useDispatch, useSelector } from 'react-redux'
+import {
+  makeSelectDownloadList
+} from 'containers/App/selectors'
+import {
+  makeSelectPortals,
+  makeSelectCurrentPortal,
+  makeSelectCurrentDashboards
+} from './selectors'
+
+import {
+  hideNavigator,
+  loadDownloadList,
+  downloadFile
+} from 'containers/App/actions'
+import { VizActions } from './actions'
+
+import { Route } from 'react-router-dom'
+import { RouteComponentWithParams } from 'utils/types'
+
+import {
+  Layout,
+  Result,
+  PageHeader,
+  Tree,
+  Icon,
+  Button,
+  Menu,
+  Dropdown
+} from 'antd'
+const { Header, Sider, Content } = Layout
+const { DirectoryTree } = Tree
+import SplitPane from 'components/SplitPane'
+import DownloadList from 'components/DownloadList'
+import useDashboardConfigMenu from './hooks/dashboardConfigMenu'
+import { Grid } from 'containers/Dashboard/Loadable'
+import useDashboardTreeNodes from './hooks/dashboardTreeNodes'
+import { AntTreeNodeMouseEvent } from 'antd/lib/tree'
+
+const mapStateToProps = createStructuredSelector({
+  downloadList: makeSelectDownloadList(),
+  portals: makeSelectPortals(),
+  currentPortal: makeSelectCurrentPortal(),
+  currentDashboards: makeSelectCurrentDashboards()
+})
+
+interface IVizPortalProps extends RouteComponentWithParams {}
+
+const VizPortal: React.FC<IVizPortalProps> = (props) => {
+  const dispatch = useDispatch()
+  const {
+    portals,
+    currentPortal,
+    currentDashboards,
+    downloadList
+  } = useSelector(mapStateToProps)
+  const {
+    history,
+    match: { params }
+  } = props
+  const portalId = +params.portalId
+  const projectId = +params.projectId
+
+  useEffect(() => {
+    dispatch(hideNavigator())
+    if (!portals.length) {
+      dispatch(VizActions.loadPortals(projectId))
+    }
+  }, [])
+
+  useEffect(() => {
+    dispatch(VizActions.loadPortalDashboards(portalId))
+  }, [portalId])
+
+  const goToViz = useCallback(() => {
+    // eslint-disable-next-line react/prop-types
+    history.replace(`/project/${projectId}/dataShareService/vizs`)
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [])
+
+  const onLoadDownloadList = useCallback(() => dispatch(loadDownloadList()), [])
+  const onDownloadFile = useCallback((id) => dispatch(downloadFile(id)), [])
+
+  const [dashboardTreeNodes, firstDashboardKey] = useDashboardTreeNodes(currentDashboards)
+  const [dashboardMenuVisible, setDashboardMenuVisible] = useState(false)
+  const [dashboardMenuStyle, setDashboardMenuStyle] = useState({})
+  const dashboardConfigMenu = useDashboardConfigMenu(dashboardMenuStyle)
+
+  const closeDashboardMenu = useCallback(() => {
+    setDashboardMenuVisible(false)
+  }, [])
+
+  useEffect(() => {
+    document.addEventListener('click', closeDashboardMenu, false)
+    return () => {
+      document.removeEventListener('click', closeDashboardMenu, false)
+    }
+  }, [])
+
+  const showDashboardContextMenu = useCallback((options: AntTreeNodeMouseEvent) => {
+    const { node, event } = options
+    const { pageX, pageY } = event
+    const menuStyle: React.CSSProperties = {
+      position: 'absolute',
+      left: pageX,
+      top: pageY
+    }
+    setDashboardMenuStyle(menuStyle)
+    setDashboardMenuVisible(true)
+  }, [])
+
+  return (
+    <Layout>
+      <PageHeader
+        ghost={false}
+        title={currentPortal.name}
+        subTitle={currentPortal.description}
+        onBack={goToViz}
+        extra={
+          <DownloadList
+            downloadList={downloadList}
+            onLoadDownloadList={onLoadDownloadList}
+            onDownloadFile={onDownloadFile}
+          />
+        }
+      />
+      {dashboardMenuVisible && dashboardConfigMenu}
+      {Array.isArray(currentDashboards) &&
+        (currentDashboards.length ? (
+          <SplitPane
+            spliter
+            className="ant-layout-content"
+            type="horizontal"
+            initialSize={150}
+            minSize={150}
+          >
+            <DirectoryTree
+              defaultExpandAll
+              blockNode
+              defaultSelectedKeys={firstDashboardKey}
+              onRightClick={showDashboardContextMenu}
+            >
+              {dashboardTreeNodes}
+            </DirectoryTree>
+            <Route
+              path="/project/:projectId/dataShareService/portal/:portalId/dashboard/:dashboardId"
+              component={Grid}
+            />
+          </SplitPane>
+        ) : (
+          <Content
+            style={{
+              display: 'flex',
+              flexDirection: 'column',
+              alignItems: 'center',
+              justifyContent: 'center'
+            }}
+          >
+            <Result
+              icon={<img src={require('assets/images/noDashboard.png')} />}
+              extra={
+                <p>
+                  请
+                  <Button size="small" type="link">
+                    创建文件夹
+                  </Button>
+                  或
+                  <Button size="small" type="link">
+                    创建 Dashboard
+                  </Button>
+                </p>
+              }
+            />
+          </Content>
+        ))}
+    </Layout>
+  )
+}
+
+export default VizPortal

+ 239 - 0
app/containers/Viz/DataVizList.tsx

@@ -0,0 +1,239 @@
+import React from 'react'
+import classnames from 'classnames'
+import Helmet from 'react-helmet'
+import { Link } from 'react-router-dom'
+
+import { compose } from 'redux'
+import { connect } from 'react-redux'
+import { createStructuredSelector } from 'reselect'
+
+import { checkNameUniqueAction } from '../App/actions'
+import { ProjectActions } from '../Projects/actions'
+import { VizActions } from '../Viz/actions'
+
+import { makeSelectCurrentProject } from '../Projects/selectors'
+import { makeSelectPortals, makeSelectDisplays } from '../Viz/selectors'
+
+import { Icon, Row, Col, Breadcrumb } from 'antd'
+import Box from 'components/Box'
+import Container, { ContainerTitle, ContainerBody } from 'components/Container'
+import PortalList from './components/DataPortalList'
+import DisplayList from './components/DataDisplayList'
+
+import { IProject } from '../Projects/types'
+import { IPortal, Display, IDisplayFormed } from './types'
+
+import styles from './Viz.less'
+import utilStyles from 'assets/less/util.less'
+
+import { RouteComponentWithParams } from 'utils/types'
+import OrganizationActions from '../Organizations/actions'
+
+interface IVizProps {
+  currentProject: IProject
+
+  displays: Display[]
+  portals: IPortal[]
+
+  onLoadDisplays: (projectId: number) => void
+  onAddDisplay: (display: IDisplayFormed, resolve: () => void) => void
+  onEditDisplay: (display: IDisplayFormed, resolve: () => void) => void
+  onDeleteDisplay: (displayId: number) => void
+  onCopyDisplay: (display: IDisplayFormed, resolve: () => void) => void
+
+  onLoadPortals: (projectId: number) => void
+  onAddPortal: (portal: IPortal, resolve) => void
+  onEditPortal: (portal: IPortal, resolve) => void
+  onDeletePortal: (portalId: number) => void
+
+  onCheckUniqueName: (
+    pathname: string,
+    data: any,
+    resolve: () => any,
+    reject: (error: string) => any
+  ) => any
+  onLoadProjectRoles: (projectId: number) => void
+  onExcludeRoles: (type: string, id: number, resolve?: any) => any
+}
+
+interface IVizStates {
+  collapse: { dashboard: boolean; display: boolean }
+}
+
+export class VizList extends React.Component<IVizProps & RouteComponentWithParams,
+  IVizStates> {
+  public state: Readonly<IVizStates> = {
+    collapse: {
+      dashboard: true,
+      display: true
+    }
+  }
+
+  public componentWillMount() {
+    const { match, onLoadDisplays, onLoadPortals, onLoadProjectRoles } = this.props
+    const projectId = +match.params.projectId
+    onLoadDisplays(projectId)
+    onLoadPortals(projectId)
+    onLoadProjectRoles(projectId)
+  }
+
+  private goToPortal = (portalId: number) => () => {
+    const { history, match } = this.props
+    const next = this.props.location.pathname.replace('/vizs', '')
+    // history.push(`/project/${match.params.projectId}/portal/${portalId}`)
+    history.push(`${next}/portal/${portalId}`)
+  }
+
+  private goToDisplay = (displayId: number) => () => {
+    const {
+      match,
+      currentProject: {
+        permission: { vizPermission }
+      }
+    } = this.props
+    const projectId = match.params.projectId
+    const isToPreview = vizPermission === 1
+    // const path = `/project/${projectId}/display/${displayId}${isToPreview ? '/preview' : ''}`
+    const next = this.props.location.pathname.replace('/vizs', '')
+    const path = `${next}/display/${displayId}${isToPreview ? '/preview' : ''}`
+    this.props.history.push(path)
+  }
+
+  private onCollapseChange = (key: string) => () => {
+    const { collapse } = this.state
+    this.setState({
+      collapse: {
+        ...collapse,
+        [key]: !collapse[key]
+      }
+    })
+  }
+
+  public render() {
+    const {
+      displays,
+      match,
+      onAddDisplay,
+      onEditDisplay,
+      onDeleteDisplay,
+      onCopyDisplay,
+      portals,
+      onAddPortal,
+      onEditPortal,
+      onDeletePortal,
+      currentProject,
+      onCheckUniqueName
+    } = this.props
+    const projectId = +match.params.projectId
+    const isHideDashboardStyle = classnames({
+      [styles.listPadding]: true,
+      [utilStyles.hide]: !this.state.collapse.dashboard
+    })
+    const isHideDisplayStyle = classnames({
+      [styles.listPadding]: true,
+      [utilStyles.hide]: !this.state.collapse.display
+    })
+    return (
+      <Container>
+        <Helmet title='Viz' />
+        <ContainerBody>
+          <Box>
+            <Box.Header>
+              <Box.Title>
+                <Row onClick={this.onCollapseChange('dashboard')}>
+                  <Col span={20}>
+                    <Icon
+                      type={`${this.state.collapse.dashboard ? 'down' : 'right'
+                      }`}
+                    />
+                    仪表板
+                  </Col>
+                </Row>
+              </Box.Title>
+            </Box.Header>
+            <div className={isHideDashboardStyle}>
+              <PortalList
+                currentProject={currentProject}
+                projectId={projectId}
+                portals={portals}
+                onPortalClick={this.goToPortal}
+                onAdd={onAddPortal}
+                onEdit={onEditPortal}
+                onDelete={onDeletePortal}
+                onCheckUniqueName={onCheckUniqueName}
+                onExcludeRoles={this.props.onExcludeRoles}
+              />
+            </div>
+          </Box>
+          <div className={styles.spliter16} />
+          <Box>
+            <Box.Header>
+              <Box.Title>
+                <Row onClick={this.onCollapseChange('display')}>
+                  <Col span={20}>
+                    <Icon
+                      type={`${this.state.collapse.display ? 'down' : 'right'}`}
+                    />
+                    大屏
+                  </Col>
+                </Row>
+              </Box.Title>
+            </Box.Header>
+            <div className={isHideDisplayStyle}>
+              <DisplayList
+                currentProject={currentProject}
+                projectId={projectId}
+                displays={displays}
+                onDisplayClick={this.goToDisplay}
+                onAdd={onAddDisplay}
+                onEdit={onEditDisplay}
+                onCopy={onCopyDisplay}
+                onDelete={onDeleteDisplay}
+                onCheckName={onCheckUniqueName}
+                onExcludeRoles={this.props.onExcludeRoles}
+              />
+            </div>
+          </Box>
+        </ContainerBody>
+      </Container>
+    )
+  }
+}
+
+const mapStateToProps = createStructuredSelector({
+  displays: makeSelectDisplays(),
+  portals: makeSelectPortals(),
+  currentProject: makeSelectCurrentProject()
+})
+
+export function mapDispatchToProps(dispatch) {
+  return {
+    onLoadDisplays: (projectId) => dispatch(VizActions.loadDisplays(projectId)),
+    onAddDisplay: (display: IDisplayFormed, resolve) =>
+      dispatch(VizActions.addDisplay(display, resolve)),
+    onEditDisplay: (display: IDisplayFormed, resolve) =>
+      dispatch(VizActions.editDisplay(display, resolve)),
+    onDeleteDisplay: (id) => dispatch(VizActions.deleteDisplay(id)),
+    onCopyDisplay: (display: IDisplayFormed, resolve) =>
+      dispatch(VizActions.copyDisplay(display, resolve)),
+    onLoadPortals: (projectId) => dispatch(VizActions.loadPortals(projectId)),
+    onAddPortal: (portal, resolve) =>
+      dispatch(VizActions.addPortal(portal, resolve)),
+    onEditPortal: (portal, resolve) =>
+      dispatch(VizActions.editPortal(portal, resolve)),
+    onDeletePortal: (id) => dispatch(VizActions.deletePortal(id)),
+    onCheckUniqueName: (pathname, data, resolve, reject) =>
+      dispatch(checkNameUniqueAction(pathname, data, resolve, reject)),
+    onLoadProjectRoles: (projectId) =>
+      dispatch(OrganizationActions.loadProjectRoles(projectId)),
+    onExcludeRoles: (type, id, resolve) =>
+      dispatch(ProjectActions.excludeRoles(type, id, resolve))
+  }
+}
+
+const withConnect = connect(
+  mapStateToProps,
+  mapDispatchToProps
+)
+
+export default compose(withConnect)(VizList)

+ 32 - 4
app/containers/Viz/Loadable.tsx

@@ -3,19 +3,47 @@ import loadable from 'utils/loadable'
 import { Skeleton } from 'antd'
 
 export const Viz = loadable(() => import('./'), {
-  fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+
+export const DataManagerViz = loadable(() => import('./dataManagerViz'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+export const DataShareServiceViz = loadable(() => import('./dataShareServiceViz'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
 })
 
 export const VizList = loadable(() => import('./VizList'), {
-  fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+
+
+export const DataVizList = loadable(() => import('./DataVizList'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
 })
 
 export const PortalIndex = loadable(() => import('./Portal'), {
-  fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+
+export const DataManagerPortalIndex = loadable(() => import('./DataManagerPortal'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+
+export const DataShareServicePortalIndex = loadable(() => import('./DataShareServicePortal'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
 })
 
 export const VizDisplay = loadable(() => import('./Display'), {
-  fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+
+export const DataManagerVizDisplay = loadable(() => import('./DataManagerDisplay'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+
+export const DataShareServiceVizDisplay = loadable(() => import('./DataShareServiceDisplay'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
 })
 
 export default Viz

+ 286 - 0
app/containers/Viz/components/DataDisplayList.tsx

@@ -0,0 +1,286 @@
+import React from 'react'
+import classnames from 'classnames'
+import { createStructuredSelector } from 'reselect'
+import { makeSelectProjectRoles } from 'containers/Projects/selectors'
+import { connect } from 'react-redux'
+import {compose} from 'redux'
+import { Col, Tooltip, Icon, Popconfirm, Row } from 'antd'
+import { IconProps } from 'antd/lib/icon'
+const styles = require('../Viz.less')
+
+import DisplayFormModal from './DisplayFormModal'
+import ModulePermission from 'containers/Account/components/checkModulePermission'
+import { IProject } from 'containers/Projects/types'
+import { IExludeRoles } from 'containers/Viz/components/PortalList'
+import { IProjectRoles } from 'containers/Organizations/component/ProjectRole'
+import { Display, DisplayFormType } from './types'
+
+export interface IDisplayEvent {
+  onDisplayClick: (displayId: number) => () => void
+  onAdd: (display: Display, resolve: () => void) => void
+  onEdit: (display: Display, resolve: () => void) => void
+  onCopy: (display: Display, resolve: () => void) => void
+  onDelete: (displayId: number) => void
+}
+
+interface IDisplayListProps extends IDisplayEvent {
+  projectId: number
+  displays: Display[],
+  currentProject?: IProject
+  projectRoles: IProjectRoles[]
+  onCheckName: (type, data, resolve, reject) => void
+  onExcludeRoles: (type: string, id: number, resolve?: any) => any
+}
+
+interface IDisplayListStates {
+  editingDisplay: Display
+  modalLoading: boolean
+  formType: DisplayFormType
+  formVisible: boolean
+  exludeRoles: IExludeRoles[]
+}
+
+export class DisplayList extends React.PureComponent<IDisplayListProps, IDisplayListStates> {
+
+  constructor (props: IDisplayListProps) {
+    super(props)
+    this.state = {
+      editingDisplay: null,
+      modalLoading: false,
+      formType: 'add',
+      formVisible: false,
+      exludeRoles: []
+    }
+  }
+
+  private stopPPG = (e) => {
+    e.stopPropagation()
+  }
+
+  public componentWillReceiveProps (nextProps) {
+    if (nextProps && nextProps.projectRoles) {
+      this.setState({
+        exludeRoles: nextProps.projectRoles.map((role) => {
+          return {
+            ...role,
+            permission: false
+          }
+        })
+      })
+    }
+  }
+
+
+  private saveDisplay = (display: Display, type: DisplayFormType) => {
+    this.setState({ modalLoading: true })
+    const { onAdd, onEdit, onCopy } = this.props
+    const val = {
+      ...display,
+      roleIds: this.state.exludeRoles.filter((role) => !role.permission).map((p) => p.id)
+    }
+    if (typeof display.config === 'string' && display.config) {
+      val.config = JSON.parse(display.config)
+    }
+    switch (type) {
+      case 'add':
+        onAdd({
+          ...val
+        }, () => { this.hideDisplayFormModal() })
+        break
+      case 'edit':
+        onEdit({
+          ...val
+        }, () => { this.hideDisplayFormModal() })
+        break
+      case 'copy':
+        onCopy({
+          ...val
+        }, () => { this.hideDisplayFormModal() })
+        break
+    }
+  }
+
+  private cancel = () => {
+    this.setState({
+      formVisible: false,
+      modalLoading: false
+    })
+  }
+
+  private showDisplayFormModal = (formType: DisplayFormType, display?: Display) => (e: React.MouseEvent<HTMLDivElement>) => {
+    e.stopPropagation()
+    this.setState({
+      editingDisplay: formType === 'copy'
+        ? {
+          ...display,
+          name: `${display.name}_copy`
+        }
+        : display,
+      formType,
+      formVisible: true
+    })
+    const { onExcludeRoles, projectRoles } = this.props
+    if (onExcludeRoles && display) {
+      onExcludeRoles('display', display.id, (result: number[]) => {
+        this.setState({
+          exludeRoles:  projectRoles.map((role) => {
+            return result.some((re) => re === role.id) ? role : {...role, permission: true}
+          })
+        })
+      })
+    } else {
+      this.setState({
+        exludeRoles: this.state.exludeRoles.map((role) => {
+          return {
+            ...role,
+            permission: true
+          }
+        })
+      })
+    }
+  }
+
+  private hideDisplayFormModal = () => {
+    this.setState({
+      formVisible: false,
+      modalLoading: false
+    })
+  }
+
+  private delegate = (func: (...args) => void, ...args) => (e: React.MouseEvent<any>) => {
+    func.apply(this, args)
+    e.stopPropagation()
+  }
+
+  private changePermission = (scope: IExludeRoles, event) => {
+    scope.permission = event.target.checked
+    this.setState({
+      exludeRoles: this.state.exludeRoles.map((role) => role && role.id === scope.id ? scope : role)
+    })
+  }
+
+  private renderCreate () {
+    return (
+      <Col
+        xxl={4}
+        xl={6}
+        lg={8}
+        md={12}
+        sm={24}
+        key="createDisplay"
+      >
+        <div className={styles.display}>
+          <div className={styles.container} onClick={this.showDisplayFormModal('add')}>
+            <div className={styles.central}>
+              <div className={`${styles.item} ${styles.icon}`}><Icon type="plus-circle-o" /></div>
+              <div className={`${styles.item} ${styles.text}`}>创建新 大屏</div>
+            </div>
+          </div>
+        </div>
+      </Col>
+    )
+  }
+
+  private renderDisplay (display: Display) {
+    const coverStyle: React.CSSProperties = {
+      backgroundImage: `url(${display.avatar})`
+    }
+    const { onDisplayClick, onDelete, currentProject } = this.props
+
+    const editHint = !display.publish && '(编辑中…)'
+    const displayClass = classnames({
+      [styles.display]: true,
+      [styles.editing]: !display.publish
+    })
+
+    const EditIcon = ModulePermission<IconProps>(currentProject, 'viz', false)(Icon)
+    const AdminIcon = ModulePermission<IconProps>(currentProject, 'viz', true)(Icon)
+
+    return (
+      <Col
+        xxl={4}
+        xl={6}
+        lg={8}
+        md={12}
+        sm={24}
+        key={display.id}
+        onClick={onDisplayClick(display.id)}
+      >
+        <div className={displayClass} style={coverStyle}>
+          <div className={styles.container}>
+            <header>
+              <h3 className={styles.title}>{display.name} {editHint}</h3>
+              <p className={styles.content}>{display.description}</p>
+            </header>
+            <div className={styles.displayActions}>
+              <Tooltip title="编辑">
+                <EditIcon className={styles.edit} type="setting" onClick={this.showDisplayFormModal('edit', display)} />
+              </Tooltip>
+              <Tooltip title="复制">
+                <AdminIcon className={styles.copy} type="copy" onClick={this.showDisplayFormModal('copy', display)} />
+              </Tooltip>
+              <Popconfirm
+                title="确定删除?"
+                placement="bottom"
+                onConfirm={this.delegate(onDelete, display.id)}
+              >
+                <Tooltip title="删除">
+                  <AdminIcon className={styles.delete} type="delete" onClick={this.stopPPG} />
+                </Tooltip>
+              </Popconfirm>
+            </div>
+          </div>
+        </div>
+      </Col>
+    )
+  }
+
+  public render () {
+    const { displays, projectId, currentProject, onCheckName } = this.props
+    if (!Array.isArray(displays)) { return null }
+
+    const { editingDisplay, formType, formVisible, modalLoading } = this.state
+
+    let addAction
+    if (currentProject && currentProject.permission) {
+      const vizPermission = currentProject.permission.vizPermission
+      addAction = vizPermission === 3
+        ? [this.renderCreate(), ...displays.map((d) => this.renderDisplay(d))]
+        : [...displays.map((d) => this.renderDisplay(d))]
+    }
+
+    return (
+      <div>
+        <Row
+          gutter={20}
+        >
+          {addAction}
+        </Row>
+        <DisplayFormModal
+          projectId={projectId}
+          display={editingDisplay}
+          visible={formVisible}
+          loading={modalLoading}
+          exludeRoles={this.state.exludeRoles}
+          onChangePermission={this.changePermission}
+          type={formType}
+          onCheckName={onCheckName}
+          onSave={this.saveDisplay}
+          onCancel={this.cancel}
+        />
+      </div>
+    )
+  }
+}
+
+
+const mapStateToProps = createStructuredSelector({
+  projectRoles: makeSelectProjectRoles()
+})
+
+const withConnect = connect(mapStateToProps, null)
+
+
+export default compose(
+  withConnect
+)(DisplayList)

+ 317 - 0
app/containers/Viz/components/DataPortalList.tsx

@@ -0,0 +1,317 @@
+import React from 'react'
+import classnames from 'classnames'
+import { createStructuredSelector } from 'reselect'
+import { compose } from 'redux'
+import { connect } from 'react-redux'
+import { Icon, Col, Button, Tooltip, Popconfirm, Modal, Row } from 'antd'
+import { IconProps } from 'antd/lib/icon'
+import AntdFormType from 'antd/lib/form/Form'
+const styles = require('../Viz.less')
+
+import PortalForm from './PortalForm'
+import ModulePermission from 'containers/Account/components/checkModulePermission'
+import { IProject } from 'containers/Projects/types'
+import { IPortal } from 'containers/Viz/types'
+import { makeSelectProjectRoles } from 'containers/Projects/selectors'
+import {IProjectRoles} from 'containers/Organizations/component/ProjectRole'
+
+interface IPortalListProps {
+  projectId: number
+  portals: IPortal[]
+  projectRoles: IProjectRoles[]
+  currentProject: IProject
+  onPortalClick: (portalId: number) => () => void
+  onAdd: (portal, resolve) => void
+  onEdit: (portal, resolve) => void
+  onDelete: (portalId: number) => void
+  onExcludeRoles: (type: string, id: number, resolve?: any) => any
+  onCheckUniqueName: (pathname: string, data: any, resolve: () => any, reject: (error: string) => any) => any
+}
+
+export interface IExludeRoles extends IProjectRoles {
+  permission?: boolean
+}
+
+interface IPortalListStates {
+  modalLoading: boolean
+  formType: 'edit' | 'add'
+  formVisible: boolean
+  exludeRoles: IExludeRoles[]
+}
+
+export class PortalList extends React.Component<IPortalListProps, IPortalListStates> {
+
+  private portalForm: AntdFormType
+  private refHandlers = {
+    portalForm: (ref) => this.portalForm = ref
+  }
+
+  constructor (props: IPortalListProps) {
+    super(props)
+    this.state = {
+      modalLoading: false,
+      formType: 'add',
+      formVisible: false,
+      exludeRoles: []
+    }
+  }
+
+  private stopPPG = (e) => {
+    e.stopPropagation()
+  }
+
+  private delegate = (func: (...args) => void, ...args) => (e: React.MouseEvent) => {
+    func.apply(this, args)
+    e.stopPropagation()
+  }
+
+  public componentWillReceiveProps (nextProps) {
+    if (nextProps && nextProps.projectRoles) {
+      this.setState({
+        exludeRoles: nextProps.projectRoles.map((role) => {
+          return {
+            ...role,
+            permission: false
+          }
+        })
+      })
+    }
+  }
+
+  private hidePortalForm = () => {
+    this.setState({
+      formVisible: false,
+      modalLoading: false
+    }, () => {
+      this.portalForm.props.form.resetFields()
+    })
+  }
+
+  private onModalOk = () => {
+    this.portalForm.props.form.validateFieldsAndScroll((err, values) => {
+      if (!err) {
+        const {  projectId, onAdd, onEdit } = this.props
+        const { formType } = this.state
+        const { id, name, description, publish, avatar } = values
+        const val = {
+          description,
+          name,
+          publish,
+          roleIds: this.state.exludeRoles.filter((role) => !role.permission).map((p) => p.id),
+          avatar: formType === 'add' ? `${Math.ceil(Math.random() * 19)}` : avatar
+        }
+
+        if (formType === 'add') {
+          onAdd({
+            ...val,
+            projectId: Number(projectId)
+          }, () => {
+            this.hidePortalForm()
+          })
+        } else {
+          onEdit({
+            ...val,
+            id
+          }, () => {
+            this.hidePortalForm()
+          })
+        }
+      }
+    })
+  }
+
+  private showPortalForm = (formType: 'edit' | 'add', portal?: any) => (e: React.MouseEvent<HTMLDivElement>) => {
+    e.stopPropagation()
+    this.setState({
+      formType,
+      formVisible: true
+    }, () => {
+      setTimeout(() => {
+        if (portal) {
+          this.portalForm.props.form.setFieldsValue(portal)
+        }
+      }, 0)
+      const { onExcludeRoles, projectRoles } = this.props
+      if (onExcludeRoles && portal) {
+        onExcludeRoles('portal', portal.id, (result: number[]) => {
+          this.setState({
+            exludeRoles:  projectRoles.map((role) => {
+              return result.some((re) => re === role.id) ? role : {...role, permission: true}
+            })
+          })
+        })
+      } else {
+        this.setState({
+          exludeRoles: this.state.exludeRoles.map((role) => {
+            return {
+              ...role,
+              permission: true
+            }
+          })
+        })
+      }
+    })
+  }
+
+  private changePermission = (scope: IExludeRoles, event) => {
+    scope.permission = event.target.checked
+    this.setState({
+      exludeRoles: this.state.exludeRoles.map((role) => role && role.id === scope.id ? scope : role)
+    })
+  }
+
+  private renderCreate () {
+    return (
+      <Col
+        key="createPortal"
+        xxl={4}
+        xl={6}
+        lg={8}
+        md={12}
+        sm={24}
+      >
+        <div className={styles.unit} onClick={this.showPortalForm('add')}>
+            <div className={styles.central}>
+              <div className={`${styles.item} ${styles.add}`}><Icon type="plus-circle-o" /></div>
+              <div className={`${styles.item} ${styles.text}`}>创建新 仪表板</div>
+            </div>
+        </div>
+      </Col>
+    )
+  }
+
+  private renderPortal = (portal: any) => {
+    const { onPortalClick, onDelete, currentProject } = this.props
+
+    const editHint = !portal.publish && '(编辑中…)'
+    const itemClass = classnames({
+      [styles.unit]: true,
+      [styles.editing]: !portal.publish
+    })
+
+    const EditIcon = ModulePermission<IconProps>(currentProject, 'viz', false)(Icon)
+    const AdminIcon = ModulePermission<IconProps>(currentProject, 'viz', true)(Icon)
+    return (
+      <Col
+        key={portal.id}
+        xxl={4}
+        xl={6}
+        lg={8}
+        md={12}
+        sm={24}
+        onClick={onPortalClick(portal.id)}
+      >
+        <div
+          className={itemClass}
+          style={{ backgroundImage: `url(${require(`assets/images/bg${portal.avatar}.png`)}` }}
+        >
+          <header>
+            <h3 className={styles.title}>
+              {portal.name} {editHint}
+            </h3>
+            <p className={styles.content}>
+              {portal.description}
+            </p>
+          </header>
+          <div className={styles.portalActions}>
+            <Tooltip title="编辑">
+              <EditIcon className={styles.edit} type="setting" onClick={this.showPortalForm('edit', portal)} />
+            </Tooltip>
+            <Popconfirm
+              title="确定删除?"
+              placement="bottom"
+              onConfirm={this.delegate(onDelete, portal.id)}
+            >
+              <Tooltip title="删除">
+                <AdminIcon className={styles.delete} type="delete" onClick={this.stopPPG} />
+              </Tooltip>
+            </Popconfirm>
+          </div>
+        </div>
+      </Col>
+    )
+  }
+
+  public render () {
+    const {
+      projectId,
+      portals,
+      currentProject,
+      onCheckUniqueName
+    } = this.props
+    if (!Array.isArray(portals)) { return null }
+
+    const {
+      formType,
+      formVisible,
+      modalLoading
+    } = this.state
+
+    const modalButtons = [(
+      <Button
+        key="back"
+        size="large"
+        onClick={this.hidePortalForm}
+      >
+        取 消
+      </Button>
+    ), (
+      <Button
+        key="submit"
+        size="large"
+        type="primary"
+        loading={modalLoading}
+        disabled={modalLoading}
+        onClick={this.onModalOk}
+      >
+        保 存
+      </Button>
+    )]
+
+    let addAction
+    if (currentProject && currentProject.permission) {
+      const vizPermission = currentProject.permission.vizPermission
+      addAction = vizPermission === 3
+        ? [this.renderCreate(), ...portals.map((p) => this.renderPortal(p))]
+        : [...portals.map((p) => this.renderPortal(p))]
+    }
+
+    return (
+      <div>
+        <Row
+          gutter={20}
+        >
+          {addAction}
+        </Row>
+        <Modal
+          title={`${formType === 'add' ? '新增' : '修改'} Portal`}
+          wrapClassName="ant-modal-small"
+          visible={formVisible}
+          footer={modalButtons}
+          onCancel={this.hidePortalForm}
+        >
+          <PortalForm
+            type={formType}
+            onCheckUniqueName={onCheckUniqueName}
+            projectId={projectId}
+            exludeRoles={this.state.exludeRoles}
+            onChangePermission={this.changePermission}
+            wrappedComponentRef={this.refHandlers.portalForm}
+          />
+        </Modal>
+      </div>
+    )
+  }
+}
+
+const mapStateToProps = createStructuredSelector({
+  projectRoles: makeSelectProjectRoles()
+})
+
+const withConnect = connect(mapStateToProps, null)
+
+
+export default compose(
+  withConnect
+)(PortalList)
+

+ 42 - 0
app/containers/Viz/dataManagerViz.tsx

@@ -0,0 +1,42 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React from 'react'
+import { Switch, Route } from 'react-router-dom'
+import { useInjectReducer } from 'utils/injectReducer'
+import { useInjectSaga } from 'utils/injectSaga'
+
+import reducer from './reducer'
+import saga from './sagas'
+
+import { DataManagerPortalIndex, DataManagerVizDisplay, DataVizList } from './Loadable'
+
+export default () => {
+  useInjectReducer({ key: 'viz', reducer })
+  useInjectSaga({ key: 'viz', saga })
+
+  return (
+    <Switch>
+      <Route path="/project/:projectId/dataManager/vizs" component={DataVizList} />
+      <Route path="/project/:projectId/dataManager/portal/:portalId" component={DataManagerPortalIndex} />
+      <Route path="/project/:projectId/dataManager/display/:displayId" component={DataManagerVizDisplay} />
+    </Switch>
+  )
+}

+ 42 - 0
app/containers/Viz/dataShareServiceViz.tsx

@@ -0,0 +1,42 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React from 'react'
+import { Switch, Route } from 'react-router-dom'
+import { useInjectReducer } from 'utils/injectReducer'
+import { useInjectSaga } from 'utils/injectSaga'
+
+import reducer from './reducer'
+import saga from './sagas'
+
+import { DataShareServicePortalIndex, DataShareServiceVizDisplay, DataVizList } from './Loadable'
+
+export default () => {
+  useInjectReducer({ key: 'viz', reducer })
+  useInjectSaga({ key: 'viz', saga })
+
+  return (
+    <Switch>
+      <Route path='/project/:projectId/dataShareService/vizs' component={DataVizList} />
+      <Route path='/project/:projectId/dataShareService/portal/:portalId' component={DataShareServicePortalIndex} />
+      <Route path='/project/:projectId/dataShareService/display/:displayId' component={DataShareServiceVizDisplay} />
+    </Switch>
+  )
+}

+ 41 - 0
app/containers/Widget/DataManaferWidget.tsx

@@ -0,0 +1,41 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React from 'react'
+import { Switch, Route } from 'react-router-dom'
+import { useInjectReducer } from 'utils/injectReducer'
+import { useInjectSaga } from 'utils/injectSaga'
+
+import reducer from './reducer'
+import saga from './sagas'
+
+import { WidgetList, Workbench } from './Loadable'
+
+export default () => {
+  useInjectReducer({ key: 'widget', reducer })
+  useInjectSaga({ key: 'widget', saga })
+
+  return (
+    <Switch>
+      <Route exact path="/project/:projectId/dataManager/widget/:widgetId?" component={Workbench} />
+      <Route exact path="/project/:projectId/dataManager/widgets" component={WidgetList} />
+    </Switch>
+  )
+}

+ 41 - 0
app/containers/Widget/DataShareServiceWidget.tsx

@@ -0,0 +1,41 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React from 'react'
+import { Switch, Route } from 'react-router-dom'
+import { useInjectReducer } from 'utils/injectReducer'
+import { useInjectSaga } from 'utils/injectSaga'
+
+import reducer from './reducer'
+import saga from './sagas'
+
+import { WidgetList, Workbench } from './Loadable'
+
+export default () => {
+  useInjectReducer({ key: 'widget', reducer })
+  useInjectSaga({ key: 'widget', saga })
+
+  return (
+    <Switch>
+      <Route exact path="/project/:projectId/dataShareService/widget/:widgetId?" component={Workbench} />
+      <Route exact path="/project/:projectId/dataShareService/widgets" component={WidgetList} />
+    </Switch>
+  )
+}

+ 11 - 3
app/containers/Widget/Loadable.tsx

@@ -3,13 +3,21 @@ import loadable from 'utils/loadable'
 import { Skeleton } from 'antd'
 
 export const Widget = loadable(() => import('./'), {
-  fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+
+export const DataManagerWidget = loadable(() => import('./DataManaferWidget'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
+})
+
+export const DataShareServiceWidget = loadable(() => import('./DataShareServiceWidget'), {
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
 })
 
 export const WidgetList = loadable(() => import('./List'), {
-  fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
 })
 
 export const Workbench = loadable(() => import('./components/Workbench'), {
-  fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+  fallback: <Skeleton active paragraph={{ rows: 15 }} />
 })

BIN
app/favicon.ico


+ 1 - 1
app/index.html

@@ -19,7 +19,7 @@
   -->
 
 <!doctype html>
-<html lang="en">
+<html lang="zh-CN">
   <head>
     <!-- The first thing in any HTML file should be the charset -->
     <meta charset="utf-8">