This is an automated email from the ASF dual-hosted git repository.

rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-primate.git


The following commit(s) were added to refs/heads/master by this push:
     new 8ba397a  project: dashboard, custom actions and tabs (#73)
8ba397a is described below

commit 8ba397ac4c8f6fd5485788c8da95fc28394cbc7f
Author: Hoang Nguyen <hoan...@unitech.vn>
AuthorDate: Thu Dec 26 02:43:11 2019 +0700

    project: dashboard, custom actions and tabs (#73)
    
    This fixes #41
    
    Adds project specific dashboard tabs, custom actions and tabs for project 
view. Also adds quickview and other list/details view improvements.
    
    Co-authored-by: hoangnm <hoangci...@gmail.com>
    Co-authored-by: Rohit Yadav <ro...@apache.org>
    Signed-off-by: Rohit Yadav <rohit.ya...@shapeblue.com>
---
 src/assets/logo.svg                                | 315 ++++++++++----------
 src/components/header/ProjectMenu.vue              |   2 +-
 src/components/menu/SideMenu.vue                   |  22 +-
 src/components/page/GlobalFooter.vue               |   8 +-
 src/components/view/ActionButton.vue               | 170 +++++++++++
 src/components/view/DetailSettings.vue             |  45 +--
 src/components/view/InfoCard.vue                   |   6 +-
 src/components/view/ListView.vue                   |   6 +-
 src/components/view/ResourceView.vue               |   3 +-
 src/components/widgets/Breadcrumb.vue              |  32 +-
 src/components/widgets/Status.vue                  |   2 +
 src/config/router.js                               |  22 +-
 src/config/section/project.js                      |  38 +++
 src/locales/en.json                                |  19 ++
 src/store/getters.js                               |   1 +
 src/store/modules/user.js                          |  13 +-
 src/views/AutogenView.vue                          |  87 +++---
 src/views/auth/Login.vue                           |   3 +-
 src/views/compute/InstanceHardware.vue             |  13 +-
 src/views/dashboard/Dashboard.vue                  |   2 +-
 src/views/dashboard/UsageDashboard.vue             |  91 ++++--
 .../dashboard/UsageDashboardChart.vue}             |  69 ++---
 src/views/infra/InfraSummary.vue                   |   6 +-
 src/views/project/AccountsTab.vue                  | 267 +++++++++++++++++
 src/views/project/InvitationTokenTemplate.vue      | 132 +++++++++
 src/views/project/InvitationsTemplate.vue          | 326 +++++++++++++++++++++
 src/views/project/ResourcesTab.vue                 | 178 +++++++++++
 27 files changed, 1568 insertions(+), 310 deletions(-)

diff --git a/src/assets/logo.svg b/src/assets/logo.svg
index 095b866..ed6cb89 100644
--- a/src/assets/logo.svg
+++ b/src/assets/logo.svg
@@ -8,9 +8,9 @@
    xmlns:xlink="http://www.w3.org/1999/xlink";
    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
-   viewBox="0 0 1484.4133 362.9483"
-   height="362.9483"
-   width="1484.4133"
+   viewBox="0 0 256 64"
+   height="64"
+   width="256"
    xml:space="preserve"
    id="svg2"
    version="1.1"
@@ -28,159 +28,171 @@
      inkscape:window-height="704"
      id="namedview93"
      showgrid="false"
-     inkscape:zoom="0.41"
-     inkscape:cx="640.72071"
-     inkscape:cy="181.47415"
+     inkscape:zoom="4.61"
+     inkscape:cx="70.146228"
+     inkscape:cy="46.916542"
      inkscape:window-x="58"
      inkscape:window-y="27"
      inkscape:window-maximized="1"
-     inkscape:current-layer="g116" /><metadata
+     inkscape:current-layer="layer2" /><metadata
      id="metadata8"><rdf:RDF><cc:Work
          rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
            rdf:resource="http://purl.org/dc/dcmitype/StillImage"; /><dc:title 
/></cc:Work></rdf:RDF></metadata><defs
      id="defs6"><marker
-       inkscape:stockid="Arrow1Mend"
-       orient="auto"
-       refY="0.0"
-       refX="0.0"
-       id="Arrow1Mend"
-       style="overflow:visible;"
-       inkscape:isstock="true"><path
-         id="path913"
-         d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
-         
style="fill-rule:evenodd;stroke:#7787ff;stroke-width:1pt;stroke-opacity:1;fill:#5affff;fill-opacity:1"
-         transform="scale(0.4) rotate(180) translate(10,0)" 
/></marker><clipPath
-       id="clipPath18"
-       clipPathUnits="userSpaceOnUse"><path
-         id="path16"
-         d="M 0,2000 H 2000 V 0 H 0 Z" /></clipPath><clipPath
-       id="clipPath90"
-       clipPathUnits="userSpaceOnUse"><path
-         id="path88"
-         d="m 1317.766,1308.723 v -15.945 h -0.107 c -1.215,2.153 -3.975,4.082 
-8.06,4.082 v 0 c -6.512,0 -12.028,-5.46 -11.973,-14.345 v 0 c 0,-8.111 
4.967,-13.573 11.419,-13.573 v 0 c 4.36,0 7.62,2.261 9.107,5.244 v 0 h 0.109 l 
0.218,-4.637 h 4.368 c -0.17,1.822 -0.227,4.523 -0.227,6.898 v 0 32.276 z m 
-15.23,-25.985 c 0,5.904 2.981,10.317 8,10.317 v 0 c 3.645,0 6.291,-2.535 
7.01,-5.628 v 0 c 0.164,-0.607 0.22,-1.435 0.22,-2.042 v 0 -4.632 c 0,-0.776 
-0.056,-1.437 -0.22,-2.099 v 0 c -0 [...]
-       id="clipPath104"
-       clipPathUnits="userSpaceOnUse"><path
-         id="path102"
-         d="m 892.093,1318.587 h 744.665 v 1.564 H 892.093 Z" 
/></clipPath><clipPath
-       id="clipPath122"
-       clipPathUnits="userSpaceOnUse"><path
-         id="path120"
-         d="M 0,2000 H 2000 V 0 H 0 Z" /></clipPath></defs><g
+   inkscape:stockid="Arrow1Mend"
+   orient="auto"
+   refY="0"
+   refX="0"
+   id="Arrow1Mend"
+   style="overflow:visible"
+   inkscape:isstock="true"><path
+     id="path913"
+     d="M 0,0 5,-5 -12.5,0 5,5 Z"
+     
style="fill:#5affff;fill-opacity:1;fill-rule:evenodd;stroke:#7787ff;stroke-width:1.00000003pt;stroke-opacity:1"
+     transform="matrix(-0.4,0,0,-0.4,-4,0)"
+     inkscape:connector-curvature="0" /></marker><clipPath
+   id="clipPath18"
+   clipPathUnits="userSpaceOnUse"><path
+     id="path16"
+     d="M 0,2000 H 2000 V 0 H 0 Z"
+     inkscape:connector-curvature="0" /></clipPath><clipPath
+   id="clipPath90"
+   clipPathUnits="userSpaceOnUse"><path
+     id="path88"
+     d="m 1317.766,1308.723 v -15.945 h -0.107 c -1.215,2.153 -3.975,4.082 
-8.06,4.082 v 0 c -6.512,0 -12.028,-5.46 -11.973,-14.345 v 0 c 0,-8.111 
4.967,-13.573 11.419,-13.573 v 0 c 4.36,0 7.62,2.261 9.107,5.244 v 0 h 0.109 l 
0.218,-4.637 h 4.368 c -0.17,1.822 -0.227,4.523 -0.227,6.898 v 0 32.276 z m 
-15.23,-25.985 c 0,5.904 2.981,10.317 8,10.317 v 0 c 3.645,0 6.291,-2.535 
7.01,-5.628 v 0 c 0.164,-0.607 0.22,-1.435 0.22,-2.042 v 0 -4.632 c 0,-0.776 
-0.056,-1.437 -0.22,-2.099 v 0 c -0.881 [...]
+     inkscape:connector-curvature="0" /></clipPath><clipPath
+   id="clipPath104"
+   clipPathUnits="userSpaceOnUse"><path
+     id="path102"
+     d="m 892.093,1318.587 h 744.665 v 1.564 H 892.093 Z"
+     inkscape:connector-curvature="0" /></clipPath><clipPath
+   id="clipPath122"
+   clipPathUnits="userSpaceOnUse"><path
+     id="path120"
+     d="M 0,2000 H 2000 V 0 H 0 Z"
+     inkscape:connector-curvature="0" /></clipPath>
+
+
+</defs><g
      inkscape:groupmode="layer"
      id="layer2"
      inkscape:label="Layer 2"
-     style="display:inline"><path
-       
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:21;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke
 fill markers"
-       d="M 74.644451,333.81717 C 49.698347,322.45232 37.89043,305.98469 
37.89043,282.55906 c 0,-23.39691 7.977608,-37.44115 26.795419,-47.17222 
14.940601,-7.72609 51.524811,-8.91366 55.912041,-1.81499 2.13409,3.45303 
4.37116,3.47 7.78213,0.059 5.41915,-5.41914 -12.124,-27.19361 
-21.90923,-27.19361 -16.197294,0 -7.273924,-29.50548 15.35848,-50.78343 
18.73596,-17.6147 51.631,-21.18268 73.07972,-7.92664 31.16098,19.25854 
45.20833,67.48547 24.47251,84.01829 -16.44689,13.11323 -25.47165,29.1 [...]
-       id="path97"
+     style="display:inline"
+     transform="translate(0,-298.9483)"
+     sodipodi:insensitive="true"><path
+       
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.89999998;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 24.076585,353.85243 c -3.737293,-0.43539 -7.03116,-2.47076 
-8.345309,-5.15678 -0.479828,-0.98073 -0.607487,-1.58528 -0.678732,-3.21422 
-0.07696,-1.75969 -0.02611,-2.13447 0.413697,-3.04903 1.456842,-3.02942 
4.81563,-4.83882 8.982291,-4.83882 1.159138,0 1.476072,0.0929 2.263912,0.66361 
1.062673,0.7698 1.616301,0.75954 1.708622,-0.0316 0.04784,-0.41001 
-0.171475,-0.80109 -0.829525,-1.47917 -0.831125,-0.85641 -3.188526,-2.18911 
-3.873005,-2.18951 -0.233112,-1.3e-4 -0.26764,-0.23 [...]
+       id="path98"
        inkscape:connector-curvature="0" /></g><g
      inkscape:groupmode="layer"
      id="layer1"
      inkscape:label="Layer 1"
-     style="display:inline"><g
-       transform="matrix(1.3333333,0,0,-1.3333333,-309.27816,2052.7205)"
-       id="g10"><g
-   transform="translate(-317.59883,17.977292)"
-   id="g12"><g
-     clip-path="url(#clipPath18)"
-     id="g14"><g
-       transform="translate(580.6621,1256.1953)"
-       id="g20"><path
-         id="path22"
-         style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,0 9.63,-7.223 31.054,-5.803 0,0 3.756,0.065 6.57,4.261 
0,0 2.303,5.23 -7.628,2.955 0,0 -12.195,-1.746 -23.595,4.163 0,0 -23.961,13.35 
-27.992,33.429 0,0 -7.314,19.798 4.423,40.527 0,0 7.256,12.282 15.439,17.959 
0,0 13.64,12.94 33.83,9.306 0,0 9.942,-0.793 29.76,-16.845 0,0 6.359,-1.79 
4.877,3.766 0,0 -5.246,13.335 -26.608,18.151 0,0 -0.555,17.718 8.149,25.435 0,0 
14.879,28.153 47.352,25.066 0,0 29.265,0.679 45.131,-37.166 0,0 5.062,-16.916 
1.543,-30.251 l 20.744,3.7 [...]
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(769.1968,1270.9512)"
-       id="g24"><path
-         id="path26"
-         style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,0 40.635,35.561 2.852,71.862 0,0 2.075,10.187 
-2.852,13.706 0,0 -1.964,5.001 -15.484,-1.482 0,0 -11.299,-3.704 -21.115,5.001 
l -11.668,-9.446 c 0,0 -15.743,-7.038 -23.151,-24.263 0,0 -5.001,-15.557 
0,-21.484 0,0 3.704,-1.852 5.927,2.593 0,0 -1.853,15.557 10.927,29.819 0,0 
28.418,22.814 56.77,-7.746 0,0 16.982,-24.851 -2.206,-47.818 0,0 -10.484,-9.075 
-9.372,-10.742 0,0 3.001,-2.778 9.372,0"
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(669.2554,1426.3086)"
-       id="g28"><path
-         id="path30"
-         style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,0 -0.249,14.112 4.075,26.088 0,0 2.24,6.106 
10.125,18.028 1.244,1.881 5.186,6.914 6.421,11.483 0,0 6.668,20.99 
-17.225,24.319 0,0 -7.613,1.355 -12.593,-2.648 -3.15,-2.532 -6.113,-4.014 
-8.398,-4.631 0,0 -11.298,-3.889 -10.927,3.334 0,0 -2.038,10.742 23.522,13.705 
0,0 31.271,5.371 34.897,-19.447 0,0 5.108,-15.188 -13.413,-37.969 0,0 
-10.187,-15.774 -7.779,-32.89 2.408,-17.117 0,-0.818 0,-0.818 l -3.148,-2.037 
-6.112,2.855"
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(721.856,1393.8394)"
-       id="g32"><path
-         id="path34"
-         style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,0 16.616,4.073 19.333,17.902 l -23.708,1.604 c 0,0 
-6.419,2.254 -9.877,13.166 0,0 -14.323,7.702 -13.335,18.815 0,0 2.222,13.829 
14.076,15.805 0,0 12.364,3.056 14.879,-16.7 0,0 5.817,-16.732 -5.519,-17.92 0,0 
-2.446,-5.386 6.444,-5.633 0,0 10.838,-3.211 18.507,3.21 0,0 5.2,4.785 
11.375,3.875 0,0 1.326,2.361 -1.266,3.842 0,0 -15.373,13.15 -18.058,31.764 0,0 
-12.688,1.76 -14.262,10.743 0,0 -2.964,13.891 8.52,16.669 0,0 7.316,1.667 
11.02,0 0,0 9.353,25.467 36.024,29.819 [...]
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(788.5322,1499.6885)"
-       id="g36"><path
-         id="path38"
-         style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,0 9.816,6.112 13.521,-0.556 0,0 -1.112,-9.075 
-0.556,-16.484 0,0 1.111,-8.705 8.149,-13.706 0,0 6.668,-7.408 0,-14.075 0,0 
-20.002,-14.077 -23.707,-22.596 0,0 -8.89,-12.595 -23.337,-2.964 0,0 
-18.891,11.483 -15.558,28.152 0,0 2.223,3.334 7.038,4.446 0,0 -11.853,19.262 
-1.111,32.968 0,0 17.41,22.225 35.561,4.815"
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(736.4878,1483.019)"
-       id="g40"><path
-         id="path42"
-         style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,0 -3.982,-5.834 -4.167,-14.539 0,0 -6.112,0.926 
-6.668,6.76 0,0 -1.574,8.427 5.927,9.817 0,0 3.797,0.926 4.908,-2.038"
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(805.7573,1476.4443)"
-       id="g44"><path
-         id="path46"
-         style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,0 4.074,2.778 6.853,0 0,0 4.147,-4.047 1.759,-7.501 0,0 
-1.759,-1.019 -2.871,0 0,0 -2.338,2.892 -4.815,4.537 0,0 -1.759,1.205 
-0.926,2.964"
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(719.4482,1443.7544)"
-       id="g48"><path
-         id="path50"
-         style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,0 -1.852,9.446 -8.026,12.039 0,0 -4.445,1.172 
-7.779,-2.655 0,0 -6.112,-6.236 -4.074,-13.829 0,0 3.519,-8.336 12.841,-10.064 
0,0 5.248,-1.605 8.273,6.36 0,0 0.761,2.479 -1.235,8.149"
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(727.7827,1385.103)"
-       id="g52"><path
-         id="path54"
-         style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,0 12.055,4.899 26.917,22.966 0,0 4.074,5.681 9.755,4.199 
0,0 8.274,-1.975 2.963,-15.187 0,0 -10.371,-32.722 -35.066,-36.055 0,0 0,17.163 
-4.569,24.077"
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(635.6704,1351.6421)"
-       id="g56"><path
-         id="path58"
-         style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,0 -43.462,8.149 -58.773,-28.152 0,0 -13.089,-43.217 
40.252,-57.045 0,0 38.277,-6.174 85.939,1.481 0,0 55.415,7.161 66.108,15.311 
0,0 -4.864,-18.522 -56.477,-26.671 0,0 -75.32,-10.866 -100.756,-5.927 0,0 
3.704,-0.493 6.914,4.939 0,0 1.236,3.951 -9.877,1.729 0,0 -25.188,-2.717 
-42.476,22.225 0,0 -28.398,38.361 11.113,72.892 0,0 23.708,26.136 58.033,-0.782"
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(774.2251,1475.8418)"
-       id="g60"><path
-         id="path62"
-         style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,0 -0.51,5.094 -7.501,5.51 0,0 -6.622,1.297 -8.243,-5.047 
0,0 0.602,-2.5 3.381,-0.787 0,0 -0.009,3.725 5,3.936 0,0 4.769,-0.875 
5.279,-4.535 0,0 1.945,-0.744 2.084,0.923"
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(784.8511,1475.0781)"
-       id="g64"><path
-         id="path66"
-         style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,0 0.371,5.186 4.677,6.159 0,0 6.251,2.013 8.983,-3.589 
0,0 0.972,-2.27 0.092,-2.895 0,0 -1.956,-0.717 -2.546,2.686 0,0 -1.575,3.079 
-4.7,2.431 0,0 -2.778,-0.301 -3.473,-3.217 C 3.033,1.575 2.895,-1.504 0,0"
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(761.3672,1457.7686)"
-       id="g68"><path
-         id="path70"
-         style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 1.418,0.874 3.46,0.125 4.756,-0.666 1.408,-0.859 
2.941,-1.667 4.432,-2.374 2.923,-1.383 5.96,-2.278 9.27,-2.27 1.718,0.005 
3.547,-0.174 5.233,0.063 1.407,0.199 2.852,0.354 4.257,0.497 1.707,0.172 
3.483,0.529 5.035,1.34 0.777,0.407 1.556,0.73 2.306,1.188 0.698,0.429 
1.344,0.996 2.018,1.419 0.311,0.196 1.15,0.817 1.525,0.749 0.5,-0.092 
0.974,-1.441 0.843,-1.912 C 38.728,-2.247 37.82,-3 36.914,-3.439 c 
-1.258,-0.609 -2.431,-1.435 -3.579,-2.226 -1.627,-1.123 -3.529,-2.704 [...]
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(779.457,1464.9844)"
-       id="g72"><path
-         id="path74"
-         style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,-1.036 -0.881,-1.875 -1.968,-1.875 -1.087,0 -1.968,0.839 
-1.968,1.875 0,1.036 0.881,1.875 1.968,1.875 C -0.881,1.875 0,1.036 0,0"
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(787.5132,1465.3081)"
-       id="g76"><path
-         id="path78"
-         style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,-1.036 -0.881,-1.875 -1.967,-1.875 -1.087,0 -1.968,0.839 
-1.968,1.875 0,1.036 0.881,1.875 1.968,1.875 C -0.881,1.875 0,1.036 0,0"
-         inkscape:connector-curvature="0" /></g><g
-       transform="translate(857.1938,1448.3535)"
-       id="g80"><path
-         id="path82"
-         style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         d="m 0,0 c 0,0 -2.887,5.018 0.984,8.986 0,0 5.068,1.737 5.206,5.665 
0,0 -1.398,6.182 1.491,8.342 0,0 4.789,3.286 5.406,-2.412 0,0 1.044,-5.282 
-0.676,-8.613 0,0 7.063,1.813 10.257,0 3.196,-1.814 4.302,-11.503 1.415,-16.522 
0,0 -3.44,-6.009 -13.086,-5.601 0,0 -8.601,-0.35 -8.786,5.895 0,0 -0.829,2.665 
-2.211,4.26"
-         inkscape:connector-curvature="0" /></g></g></g><g
-   transform="matrix(0.8869744,0,0,0.961233,-132.31692,116.07227)"
+     style="display:inline"
+     transform="translate(0,-298.9483)"
+     sodipodi:insensitive="true"><g
+       id="g12"
+       transform="matrix(0.18829928,0,0,-0.18829928,-93.481273,592.20033)"><g
+         id="g14"
+         clip-path="url(#clipPath18)"><g
+           id="g20"
+           transform="translate(580.6621,1256.1953)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,0 9.63,-7.223 31.054,-5.803 0,0 3.756,0.065 
6.57,4.261 0,0 2.303,5.23 -7.628,2.955 0,0 -12.195,-1.746 -23.595,4.163 0,0 
-23.961,13.35 -27.992,33.429 0,0 -7.314,19.798 4.423,40.527 0,0 7.256,12.282 
15.439,17.959 0,0 13.64,12.94 33.83,9.306 0,0 9.942,-0.793 29.76,-16.845 0,0 
6.359,-1.79 4.877,3.766 0,0 -5.246,13.335 -26.608,18.151 0,0 -0.555,17.718 
8.149,25.435 0,0 14.879,28.153 47.352,25.066 0,0 29.265,0.679 45.131,-37.166 
0,0 5.062,-16.916 1.543,-30.251 l 20.744 [...]
+             style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path22" /></g><g
+           id="g24"
+           transform="translate(769.1968,1270.9512)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,0 40.635,35.561 2.852,71.862 0,0 2.075,10.187 
-2.852,13.706 0,0 -1.964,5.001 -15.484,-1.482 0,0 -11.299,-3.704 -21.115,5.001 
l -11.668,-9.446 c 0,0 -15.743,-7.038 -23.151,-24.263 0,0 -5.001,-15.557 
0,-21.484 0,0 3.704,-1.852 5.927,2.593 0,0 -1.853,15.557 10.927,29.819 0,0 
28.418,22.814 56.77,-7.746 0,0 16.982,-24.851 -2.206,-47.818 0,0 -10.484,-9.075 
-9.372,-10.742 0,0 3.001,-2.778 9.372,0"
+             style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path26" /></g><g
+           id="g28"
+           transform="translate(669.2554,1426.3086)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,0 -0.249,14.112 4.075,26.088 0,0 2.24,6.106 
10.125,18.028 1.244,1.881 5.186,6.914 6.421,11.483 0,0 6.668,20.99 
-17.225,24.319 0,0 -7.613,1.355 -12.593,-2.648 -3.15,-2.532 -6.113,-4.014 
-8.398,-4.631 0,0 -11.298,-3.889 -10.927,3.334 0,0 -2.038,10.742 23.522,13.705 
0,0 31.271,5.371 34.897,-19.447 0,0 5.108,-15.188 -13.413,-37.969 0,0 
-10.187,-15.774 -7.779,-32.89 2.408,-17.117 0,-0.818 0,-0.818 l -3.148,-2.037 
-6.112,2.855"
+             style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path30" /></g><g
+           id="g32"
+           transform="translate(721.856,1393.8394)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,0 16.616,4.073 19.333,17.902 l -23.708,1.604 c 0,0 
-6.419,2.254 -9.877,13.166 0,0 -14.323,7.702 -13.335,18.815 0,0 2.222,13.829 
14.076,15.805 0,0 12.364,3.056 14.879,-16.7 0,0 5.817,-16.732 -5.519,-17.92 0,0 
-2.446,-5.386 6.444,-5.633 0,0 10.838,-3.211 18.507,3.21 0,0 5.2,4.785 
11.375,3.875 0,0 1.326,2.361 -1.266,3.842 0,0 -15.373,13.15 -18.058,31.764 0,0 
-12.688,1.76 -14.262,10.743 0,0 -2.964,13.891 8.52,16.669 0,0 7.316,1.667 
11.02,0 0,0 9.353,25.467 36.024,29 [...]
+             style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path34" /></g><g
+           id="g36"
+           transform="translate(788.5322,1499.6885)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,0 9.816,6.112 13.521,-0.556 0,0 -1.112,-9.075 
-0.556,-16.484 0,0 1.111,-8.705 8.149,-13.706 0,0 6.668,-7.408 0,-14.075 0,0 
-20.002,-14.077 -23.707,-22.596 0,0 -8.89,-12.595 -23.337,-2.964 0,0 
-18.891,11.483 -15.558,28.152 0,0 2.223,3.334 7.038,4.446 0,0 -11.853,19.262 
-1.111,32.968 0,0 17.41,22.225 35.561,4.815"
+             style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path38" /></g><g
+           id="g40"
+           transform="translate(736.4878,1483.019)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,0 -3.982,-5.834 -4.167,-14.539 0,0 -6.112,0.926 
-6.668,6.76 0,0 -1.574,8.427 5.927,9.817 0,0 3.797,0.926 4.908,-2.038"
+             style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path42" /></g><g
+           id="g44"
+           transform="translate(805.7573,1476.4443)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,0 4.074,2.778 6.853,0 0,0 4.147,-4.047 1.759,-7.501 
0,0 -1.759,-1.019 -2.871,0 0,0 -2.338,2.892 -4.815,4.537 0,0 -1.759,1.205 
-0.926,2.964"
+             style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path46" /></g><g
+           id="g48"
+           transform="translate(719.4482,1443.7544)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,0 -1.852,9.446 -8.026,12.039 0,0 -4.445,1.172 
-7.779,-2.655 0,0 -6.112,-6.236 -4.074,-13.829 0,0 3.519,-8.336 12.841,-10.064 
0,0 5.248,-1.605 8.273,6.36 0,0 0.761,2.479 -1.235,8.149"
+             style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path50" /></g><g
+           id="g52"
+           transform="translate(727.7827,1385.103)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,0 12.055,4.899 26.917,22.966 0,0 4.074,5.681 
9.755,4.199 0,0 8.274,-1.975 2.963,-15.187 0,0 -10.371,-32.722 -35.066,-36.055 
0,0 0,17.163 -4.569,24.077"
+             style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path54" /></g><g
+           id="g56"
+           transform="translate(635.6704,1351.6421)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,0 -43.462,8.149 -58.773,-28.152 0,0 -13.089,-43.217 
40.252,-57.045 0,0 38.277,-6.174 85.939,1.481 0,0 55.415,7.161 66.108,15.311 
0,0 -4.864,-18.522 -56.477,-26.671 0,0 -75.32,-10.866 -100.756,-5.927 0,0 
3.704,-0.493 6.914,4.939 0,0 1.236,3.951 -9.877,1.729 0,0 -25.188,-2.717 
-42.476,22.225 0,0 -28.398,38.361 11.113,72.892 0,0 23.708,26.136 58.033,-0.782"
+             style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path58" /></g><g
+           id="g60"
+           transform="translate(774.2251,1475.8418)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,0 -0.51,5.094 -7.501,5.51 0,0 -6.622,1.297 
-8.243,-5.047 0,0 0.602,-2.5 3.381,-0.787 0,0 -0.009,3.725 5,3.936 0,0 
4.769,-0.875 5.279,-4.535 0,0 1.945,-0.744 2.084,0.923"
+             style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path62" /></g><g
+           id="g64"
+           transform="translate(784.8511,1475.0781)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,0 0.371,5.186 4.677,6.159 0,0 6.251,2.013 
8.983,-3.589 0,0 0.972,-2.27 0.092,-2.895 0,0 -1.956,-0.717 -2.546,2.686 0,0 
-1.575,3.079 -4.7,2.431 0,0 -2.778,-0.301 -3.473,-3.217 C 3.033,1.575 
2.895,-1.504 0,0"
+             style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path66" /></g><g
+           id="g68"
+           transform="translate(761.3672,1457.7686)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 1.418,0.874 3.46,0.125 4.756,-0.666 1.408,-0.859 
2.941,-1.667 4.432,-2.374 2.923,-1.383 5.96,-2.278 9.27,-2.27 1.718,0.005 
3.547,-0.174 5.233,0.063 1.407,0.199 2.852,0.354 4.257,0.497 1.707,0.172 
3.483,0.529 5.035,1.34 0.777,0.407 1.556,0.73 2.306,1.188 0.698,0.429 
1.344,0.996 2.018,1.419 0.311,0.196 1.15,0.817 1.525,0.749 0.5,-0.092 
0.974,-1.441 0.843,-1.912 C 38.728,-2.247 37.82,-3 36.914,-3.439 c 
-1.258,-0.609 -2.431,-1.435 -3.579,-2.226 -1.627,-1.123 -3.529,-2 [...]
+             style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path70" /></g><g
+           id="g72"
+           transform="translate(779.457,1464.9844)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,-1.036 -0.881,-1.875 -1.968,-1.875 -1.087,0 
-1.968,0.839 -1.968,1.875 0,1.036 0.881,1.875 1.968,1.875 C -0.881,1.875 
0,1.036 0,0"
+             style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path74" /></g><g
+           id="g76"
+           transform="translate(787.5132,1465.3081)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,-1.036 -0.881,-1.875 -1.967,-1.875 -1.087,0 
-1.968,0.839 -1.968,1.875 0,1.036 0.881,1.875 1.968,1.875 C -0.881,1.875 
0,1.036 0,0"
+             style="fill:#2aa5dc;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path78" /></g><g
+           id="g80"
+           transform="translate(857.1938,1448.3535)"><path
+             inkscape:connector-curvature="0"
+             d="m 0,0 c 0,0 -2.887,5.018 0.984,8.986 0,0 5.068,1.737 
5.206,5.665 0,0 -1.398,6.182 1.491,8.342 0,0 4.789,3.286 5.406,-2.412 0,0 
1.044,-5.282 -0.676,-8.613 0,0 7.063,1.813 10.257,0 3.196,-1.814 4.302,-11.503 
1.415,-16.522 0,0 -3.44,-6.009 -13.086,-5.601 0,0 -8.601,-0.35 -8.786,5.895 0,0 
-0.829,2.665 -2.211,4.26"
+             style="fill:#b9e1f6;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path82" /></g></g></g><g
+       id="g545"
+       transform="matrix(1.1498338,0,0,1.1498338,-23.728627,-49.846833)"><g
+   transform="matrix(0.16701663,0,0,-0.18099948,-42.665269,573.72911)"
    id="g84"><g
      clip-path="url(#clipPath90)"
      id="g86"><g
@@ -194,7 +206,7 @@
            style="image-rendering:optimizeSpeed"
            height="1"
            width="1" /></g></g></g></g><g
-   transform="matrix(0.8869744,0,0,0.961233,-132.31692,116.07227)"
+   transform="matrix(0.16701663,0,0,-0.18099948,-42.665269,573.72911)"
    id="g98"><g
      clip-path="url(#clipPath104)"
      id="g100"><g
@@ -208,19 +220,18 @@
            style="image-rendering:optimizeSpeed"
            height="1"
            width="1" /></g></g></g></g><text
-   y="-1396.1345"
-   x="1380.2909"
+   y="309.22644"
+   x="241.4295"
    id="text114"
-   
style="font-variant:normal;font-weight:bold;font-stretch:normal;font-size:11.77262306px;font-family:'Avenir
 LT Std 55 
Roman';-inkscape-font-specification:AvenirLTStd-Heavy;writing-mode:lr-tb;fill:#808181;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.92335749"
-   transform="scale(0.96059698,-1.0410193)"><tspan
+   
style="font-variant:normal;font-weight:bold;font-stretch:normal;font-size:2.21677637px;font-family:'Avenir
 LT Std 55 
Roman';-inkscape-font-specification:AvenirLTStd-Heavy;writing-mode:lr-tb;fill:#808181;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.17386755"
+   transform="scale(0.96059698,1.0410193)"><tspan
      id="tspan112"
-     y="-1396.1345"
-     x="1380.2909 1387.0483"
-     style="stroke-width:0.92335749">TM</tspan></text>
-
+     y="309.22644"
+     x="241.4295 242.70193"
+     style="stroke-width:0.17386755">TM</tspan></text>
 
 <g
-   transform="translate(-317.59883,17.977292)"
+   transform="matrix(0.18829928,0,0,-0.18829928,-77.55372,592.20033)"
    id="g116"><g
      clip-path="url(#clipPath122)"
      id="g118"
diff --git a/src/components/header/ProjectMenu.vue 
b/src/components/header/ProjectMenu.vue
index 489f4bb..2c922d1 100644
--- a/src/components/header/ProjectMenu.vue
+++ b/src/components/header/ProjectMenu.vue
@@ -106,7 +106,7 @@ export default {
 <style lang="less" scoped>
 .project {
   &-select {
-    width: 40%;
+    width: 30vw;
   }
 
   &-icon {
diff --git a/src/components/menu/SideMenu.vue b/src/components/menu/SideMenu.vue
index f12dad2..20c76da 100644
--- a/src/components/menu/SideMenu.vue
+++ b/src/components/menu/SideMenu.vue
@@ -85,7 +85,23 @@ export default {
   height: auto;
 
   /deep/ .ant-layout-sider-children {
-    overflow-y: auto;
+    overflow-y: hidden;
+    &:hover {
+      overflow-y: auto;
+    }
+  }
+
+  /deep/ .ant-menu-vertical .ant-menu-item {
+    margin-top: 0px;
+    margin-bottom: 0px;
+  }
+
+  /deep/ .ant-menu-inline .ant-menu-item:not(:last-child) {
+    margin-bottom: 0px;
+  }
+
+  /deep/ .ant-menu-inline .ant-menu-item {
+    margin-top: 0px;
   }
 
   &.ant-fixed-sidemenu {
@@ -99,14 +115,14 @@ export default {
 
     .ant-menu-light {
       border-right-color: transparent;
-      padding: 10px 0;
+      padding: 14px 0;
     }
   }
 
   &.dark {
     .ant-menu-dark {
       border-right-color: transparent;
-      padding: 10px 0;
+      padding: 14px 0;
     }
   }
 }
diff --git a/src/components/page/GlobalFooter.vue 
b/src/components/page/GlobalFooter.vue
index ccc868f..9438494 100644
--- a/src/components/page/GlobalFooter.vue
+++ b/src/components/page/GlobalFooter.vue
@@ -18,8 +18,11 @@
 <template>
   <div class="footer">
     <div class="links">
-      <a href="https://github.com/apache/cloudstack-primate"; target="_blank">
+      CloudStack Server {{ $store.getters.features.cloudstackversion }}
+      <a-divider type="vertical" />
+      <a href="https://github.com/apache/cloudstack-primate/issues/new/choose"; 
target="_blank">
         <a-icon type="github"/>
+        Report Bug
       </a>
     </div>
   </div>
@@ -51,9 +54,6 @@ export default {
           color: rgba(0, 0, 0, .65);
         }
 
-        &:not(:last-child) {
-          margin-right: 40px;
-        }
       }
     }
     .copyright {
diff --git a/src/components/view/ActionButton.vue 
b/src/components/view/ActionButton.vue
new file mode 100644
index 0000000..fbd842a
--- /dev/null
+++ b/src/components/view/ActionButton.vue
@@ -0,0 +1,170 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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.
+
+<template>
+  <span class="row-action-button">
+    <a-tooltip
+      v-for="(action, actionIndex) in actions"
+      :key="actionIndex"
+      arrowPointAtCenter
+      placement="bottomRight">
+      <template slot="title">
+        {{ $t(action.label) }}
+      </template>
+      <a-badge
+        class="button-action-badge"
+        :overflowCount="9"
+        :count="actionBadge[action.api] ? actionBadge[action.api].badgeNum : 0"
+        v-if="action.api in $store.getters.apis &&
+          action.showBadge &&
+          ((!dataView && (action.listView || action.groupAction && 
selectedRowKeys.length > 0)) || (dataView && action.dataView)) &&
+          ('show' in action ? action.show(resource, $store.getters.userInfo) : 
true)">
+        <a-button
+          :icon="action.icon"
+          :type="action.icon === 'delete' ? 'danger' : (action.icon === 'plus' 
? 'primary' : 'default')"
+          shape="circle"
+          style="margin-right: 5px"
+          @click="execAction(action)" />
+      </a-badge>
+      <a-button
+        v-if="action.api in $store.getters.apis &&
+          !action.showBadge &&
+          ((!dataView && (action.listView || action.groupAction && 
selectedRowKeys.length > 0)) || (dataView && action.dataView)) &&
+          ('show' in action ? action.show(resource, $store.getters.userInfo) : 
true)"
+        :icon="action.icon"
+        :type="action.icon === 'delete' ? 'danger' : (action.icon === 'plus' ? 
'primary' : 'default')"
+        shape="circle"
+        style="margin-left: 5px"
+        @click="execAction(action)" />
+    </a-tooltip>
+  </span>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+  name: 'ActionButton',
+  data () {
+    return {
+      actionBadge: []
+    }
+  },
+  mounted () {
+    this.handleShowBadge()
+  },
+  props: {
+    actions: {
+      type: Array,
+      default () {
+        return []
+      }
+    },
+    resource: {
+      type: Object,
+      default () {
+        return {}
+      }
+    },
+    dataView: {
+      type: Boolean,
+      default: false
+    },
+    selectedRowKeys: {
+      type: Array,
+      default () {
+        return []
+      }
+    },
+    loading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  watch: {
+    resource (newItem, oldItem) {
+      if (!newItem || !newItem.id) {
+        return
+      }
+      this.handleShowBadge()
+    }
+  },
+  methods: {
+    execAction (action) {
+      this.$emit('exec-action', action)
+    },
+    handleShowBadge () {
+      const dataBadge = {}
+      const arrAsync = []
+      const actionBadge = this.actions.filter(action => action.showBadge === 
true)
+
+      if (actionBadge && actionBadge.length > 0) {
+        const dataLength = actionBadge.length
+
+        for (let i = 0; i < dataLength; i++) {
+          const action = actionBadge[i]
+
+          arrAsync.push(new Promise((resolve, reject) => {
+            api(action.api, action.param).then(json => {
+              let responseJsonName
+              const response = {}
+
+              response.api = action.api
+              response.count = 0
+
+              for (const key in json) {
+                if (key.includes('response')) {
+                  responseJsonName = key
+                  break
+                }
+              }
+
+              if (json[responseJsonName].count && json[responseJsonName].count 
> 0) {
+                response.count = json[responseJsonName].count
+              }
+
+              resolve(response)
+            }).catch(error => {
+              reject(error)
+            })
+          }))
+        }
+
+        Promise.all(arrAsync).then(response => {
+          for (let j = 0; j < response.length; j++) {
+            this.$set(dataBadge, response[j].api, {})
+            this.$set(dataBadge[response[j].api], 'badgeNum', 
response[j].count)
+          }
+        })
+
+        this.actionBadge = dataBadge
+      }
+    }
+  }
+}
+</script>
+
+<style scoped >
+.button-action-badge {
+  margin-left: 5px;
+}
+
+/deep/.button-action-badge .ant-badge-count {
+  right: 10px;
+  z-index: 8;
+}
+</style>
diff --git a/src/components/view/DetailSettings.vue 
b/src/components/view/DetailSettings.vue
index 0b80a7a..d73dd6b 100644
--- a/src/components/view/DetailSettings.vue
+++ b/src/components/view/DetailSettings.vue
@@ -43,27 +43,6 @@
         <a-list-item-meta>
           <span slot="title">
             {{ item.name }}
-            <a-button shape="circle" size="small" @click="updateDetail(index)" 
v-if="item.edit">
-              <a-icon type="check-circle" theme="twoTone" 
twoToneColor="#52c41a" />
-            </a-button>
-            <a-button shape="circle" size="small" 
@click="hideEditDetail(index)" v-if="item.edit" style="margin-left: 5px">
-              <a-icon type="close-circle" theme="twoTone" 
twoToneColor="#f5222d" />
-            </a-button>
-            <a-button shape="circle" size="small" 
@click="showEditDetail(index)" v-if="!item.edit">
-              <a-icon type="edit" />
-            </a-button>
-            <a-divider type="vertical" />
-            <a-popconfirm
-              title="Delete setting?"
-              @confirm="deleteDetail(index)"
-              okText="Yes"
-              cancelText="No"
-              placement="right"
-            >
-              <a-button shape="circle" size="small">
-                <a-icon type="delete" theme="twoTone" twoToneColor="#f5222d" />
-              </a-button>
-            </a-popconfirm>
           </span>
           <span slot="description" style="word-break: break-all">
             <span v-if="item.edit" style="display: flex">
@@ -77,6 +56,30 @@
             <span v-else @click="showEditDetail(index)">{{ item.value }}</span>
           </span>
         </a-list-item-meta>
+        <div slot="actions">
+          <a-button shape="circle" size="default" @click="updateDetail(index)" 
v-if="item.edit">
+            <a-icon type="check-circle" theme="twoTone" twoToneColor="#52c41a" 
/>
+          </a-button>
+          <a-button shape="circle" size="default" 
@click="hideEditDetail(index)" v-if="item.edit">
+            <a-icon type="close-circle" theme="twoTone" twoToneColor="#f5222d" 
/>
+          </a-button>
+          <a-button shape="circle" @click="showEditDetail(index)" 
v-if="!item.edit">
+            <a-icon type="edit" />
+          </a-button>
+        </div>
+        <div slot="actions">
+          <a-popconfirm
+            title="Delete setting?"
+            @confirm="deleteDetail(index)"
+            okText="Yes"
+            cancelText="No"
+            placement="left"
+          >
+            <a-button shape="circle">
+              <a-icon type="delete" theme="twoTone" twoToneColor="#f5222d" />
+            </a-button>
+          </a-popconfirm>
+        </div>
       </a-list-item>
     </a-list>
   </a-spin>
diff --git a/src/components/view/InfoCard.vue b/src/components/view/InfoCard.vue
index 7b9e404..935b491 100644
--- a/src/components/view/InfoCard.vue
+++ b/src/components/view/InfoCard.vue
@@ -430,6 +430,7 @@
               :value="annotation"
               placeholder="Add Note" />
             <a-button
+              style="margin-top: 10px"
               @click="saveNote"
               type="primary"
             >
@@ -643,12 +644,15 @@ export default {
 
 <style lang="less" scoped>
 
+/deep/ .ant-card-body {
+  padding: 36px;
+}
+
 .resource-details {
   text-align: center;
   margin-bottom: 24px;
   & > .avatar {
     margin: 0 auto;
-    padding-top: 20px;
     width: 104px;
     //height: 104px;
     margin-bottom: 20px;
diff --git a/src/components/view/ListView.vue b/src/components/view/ListView.vue
index 343c2fe..7d6e836 100644
--- a/src/components/view/ListView.vue
+++ b/src/components/view/ListView.vue
@@ -34,7 +34,7 @@
     </template>
 
     <div slot="expandedRowRender" slot-scope="resource">
-      <info-card :resource="resource" style="margin-right: 50px">
+      <info-card :resource="resource" style="margin-left: 0px; width: 50%">
         <div slot="actions" style="padding-top: 12px">
           <a-tooltip
             v-for="(action, actionIndex) in $route.meta.actions"
@@ -48,12 +48,10 @@
                 ('show' in action ? action.show(resource, 
$store.getters.userInfo) : true)"
               :icon="action.icon"
               :type="action.icon === 'delete' ? 'danger' : (action.icon === 
'plus' ? 'primary' : 'default')"
-              shape="round"
-              size="small"
+              shape="circle"
               style="margin-right: 5px; margin-top: 12px"
               @click="$parent.execAction(action)"
             >
-              {{ $t(action.label) }}
             </a-button>
           </a-tooltip>
         </div>
diff --git a/src/components/view/ResourceView.vue 
b/src/components/view/ResourceView.vue
index 171d558..f197628 100644
--- a/src/components/view/ResourceView.vue
+++ b/src/components/view/ResourceView.vue
@@ -36,7 +36,7 @@
             v-for="tab in tabs"
             :tab="$t(tab.name)"
             :key="tab.name"
-            v-if="'show' in tab ? tab.show(resource, $route) : true">
+            v-if="'show' in tab ? tab.show(resource, $route, 
$store.getters.userInfo) : true">
             <component :is="tab.component" :resource="resource" 
:loading="loading" :tab="activeTab" />
           </a-tab-pane>
         </a-tabs>
@@ -46,7 +46,6 @@
 </template>
 
 <script>
-
 import DetailsTab from '@/components/view/DetailsTab'
 import InfoCard from '@/components/view/InfoCard'
 import ResourceLayout from '@/layouts/ResourceLayout'
diff --git a/src/components/widgets/Breadcrumb.vue 
b/src/components/widgets/Breadcrumb.vue
index 947a03f..445d9e1 100644
--- a/src/components/widgets/Breadcrumb.vue
+++ b/src/components/widgets/Breadcrumb.vue
@@ -31,18 +31,22 @@
       <span v-else>
         {{ $t(item.meta.title) }}
       </span>
-      <a-tooltip v-if="index === (breadList.length - 1)" placement="bottom">
-        <template slot="title">
-          {{ "Open Documentation" }}
-        </template>
-        <a
-          v-if="item.meta.docHelp"
-          style="margin-right: 5px"
-          :href="docBase + '/' + $route.meta.docHelp"
-          target="_blank">
-          <a-icon type="question-circle-o"></a-icon>
-        </a>
-      </a-tooltip>
+      <span v-if="index === (breadList.length - 1)" style="margin-left: 5px">
+        <a-tooltip placement="bottom">
+          <template slot="title">
+            {{ "Open Documentation" }}
+          </template>
+          <a
+            v-if="item.meta.docHelp"
+            style="margin-right: 10px"
+            :href="docBase + '/' + $route.meta.docHelp"
+            target="_blank">
+            <a-icon type="question-circle-o"></a-icon>
+          </a>
+        </a-tooltip>
+        <slot name="end">
+        </slot>
+      </span>
     </a-breadcrumb-item>
   </a-breadcrumb>
 </template>
@@ -72,6 +76,9 @@ export default {
       this.name = this.$route.name
       this.breadList = []
       this.$route.matched.forEach((item) => {
+        if (item && item.parent && item.parent.name !== 'index' && 
!item.path.endsWith(':id')) {
+          this.breadList.pop()
+        }
         this.breadList.push(item)
       })
     },
@@ -90,7 +97,6 @@ export default {
 }
 
 .ant-breadcrumb .anticon {
-  margin-left: 8px;
   vertical-align: text-bottom;
 }
 </style>
diff --git a/src/components/widgets/Status.vue 
b/src/components/widgets/Status.vue
index faa7dc2..264254d 100644
--- a/src/components/widgets/Status.vue
+++ b/src/components/widgets/Status.vue
@@ -66,6 +66,7 @@ export default {
         case 'Down':
         case 'Error':
         case 'Stopped':
+        case 'Declined':
         case 'Disconnected':
           status = 'error'
           break
@@ -78,6 +79,7 @@ export default {
         case 'Alert':
         case 'Allocated':
         case 'Created':
+        case 'Pending':
           status = 'warning'
           break
       }
diff --git a/src/config/router.js b/src/config/router.js
index 62c353b..fb4f40a 100644
--- a/src/config/router.js
+++ b/src/config/router.js
@@ -167,7 +167,27 @@ export const asyncRouterMap = [
       {
         path: '/dashboard',
         name: 'dashboard',
-        meta: { title: 'Dashboard', keepAlive: true, icon: 'dashboard' },
+        meta: {
+          title: 'Dashboard',
+          keepAlive: true,
+          icon: 'dashboard',
+          tabs: [
+            {
+              name: 'Dashboard',
+              component: () => import('@/views/dashboard/UsageDashboardChart')
+            },
+            {
+              name: 'accounts',
+              show: (record, route, user) => { return record.account === 
user.account || ['Admin', 'DomainAdmin'].includes(user.roletype) },
+              component: () => import('@/views/project/AccountsTab')
+            },
+            {
+              name: 'resources',
+              show: (record, route, user) => { return 
['Admin'].includes(user.roletype) },
+              component: () => import('@/views/project/ResourcesTab.vue')
+            }
+          ]
+        },
         component: () => import('@/views/dashboard/Dashboard')
       },
 
diff --git a/src/config/section/project.js b/src/config/section/project.js
index fd48292..d9d4059 100644
--- a/src/config/section/project.js
+++ b/src/config/section/project.js
@@ -23,6 +23,22 @@ export default {
   resourceType: 'Project',
   columns: ['name', 'state', 'displaytext', 'account', 'domain'],
   details: ['name', 'id', 'displaytext', 'projectaccountname', 'vmtotal', 
'cputotal', 'memorytotal', 'volumetotal', 'iptotal', 'vpctotal', 
'templatetotal', 'primarystoragetotal', 'account', 'domain'],
+  tabs: [
+    {
+      name: 'details',
+      component: () => import('@/components/view/DetailsTab.vue')
+    },
+    {
+      name: 'accounts',
+      show: (record, route, user) => { return record.account === user.account 
|| ['Admin', 'DomainAdmin'].includes(user.roletype) },
+      component: () => import('@/views/project/AccountsTab.vue')
+    },
+    {
+      name: 'resources',
+      show: (record, route, user) => { return 
['Admin'].includes(user.roletype) },
+      component: () => import('@/views/project/ResourcesTab.vue')
+    }
+  ],
   actions: [
     {
       api: 'createProject',
@@ -32,6 +48,27 @@ export default {
       args: ['name', 'displaytext']
     },
     {
+      api: 'updateProjectInvitation',
+      icon: 'key',
+      label: 'label.enter.token',
+      listView: true,
+      popup: true,
+      component: () => import('@/views/project/InvitationTokenTemplate.vue')
+    },
+    {
+      api: 'listProjectInvitations',
+      icon: 'team',
+      label: 'label.project.invitation',
+      listView: true,
+      popup: true,
+      showBadge: true,
+      badgeNum: 0,
+      param: {
+        state: 'Pending'
+      },
+      component: () => import('@/views/project/InvitationsTemplate.vue')
+    },
+    {
       api: 'updateProject',
       icon: 'edit',
       label: 'Edit Project',
@@ -58,6 +95,7 @@ export default {
       label: 'Add Account to Project',
       dataView: true,
       args: ['projectid', 'account', 'email'],
+      show: (record, user) => { return record.account === user.account || 
['Admin', 'DomainAdmin'].includes(user.roletype) },
       mapping: {
         projectid: {
           value: (record) => { return record.id }
diff --git a/src/locales/en.json b/src/locales/en.json
index 34ee199..c6d6dcb 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -7,6 +7,8 @@
 "Clusters": "Clusters",
 "Compute": "Compute",
 "Compute Offerings": "Compute Offerings",
+"confirmacceptinvitation": "Please confirm you wish to join this project",
+"confirmdeclineinvitation": "Are you sure you want to decline this project 
invitation?",
 "Configuration": "Configuration",
 "Dashboard": "Dashboard",
 "Disk Offerings": "Disk Offerings",
@@ -263,6 +265,7 @@
 "internaldns2": "Internal DNS 2",
 "interval": "Polling Interval (in sec)",
 "intervaltype": "Interval Type",
+"invitations": "Invitations",
 "ip": "IP Address",
 "ip4Netmask": "IPv4 Netmask",
 "ip4dns1": "IPv4 DNS1",
@@ -380,6 +383,7 @@
 "label.action.manage.cluster": "Manage Cluster",
 "label.action.migrate.router": "Migrate Router",
 "label.action.migrate.systemvm": "Migrate System VM",
+"label.action.project.add.account": "Add Account to Project",
 "label.action.reboot.instance": "Reboot Instance",
 "label.action.reboot.router": "Reboot Router",
 "label.action.reboot.systemvm": "Reboot System VM",
@@ -550,6 +554,7 @@
 "label.outofbandmanagement.configure": "Configure Out-of-band Management",
 "label.outofbandmanagement.disable": "Disable Out-of-band Management",
 "label.outofbandmanagement.enable": "Enable Out-of-band Management",
+"label.project.invitation": "Project Invitations",
 "label.quota.add.credits": "Add Credits",
 "label.quota.dates": "Update Dates",
 "label.recover.vm": "Recover VM",
@@ -613,6 +618,18 @@
 "makeredundant": "Make redundant",
 "managedstate": "Managed State",
 "managementServers": "Number of Management Servers",
+"maxuser_vm": "Max. user VMs",
+"maxpublic_ip": "Max. public IPs",
+"maxvolume": "Max. volumes",
+"maxsnapshot": "Max. snapshots",
+"maxtemplate": "Max. templates",
+"maxproject": "Max. projects",
+"maxnetwork": "Max. networks",
+"maxvpc": "Max. VPCs",
+"maxcpu": "Max. CPU cores",
+"maxmemory": "Max. memory (MiB)",
+"maxprimary_storage": "Max. primary (GiB)",
+"maxsecondary_storage": "Max. secondary (GiB)",
 "maxCPUNumber": "Max CPU Cores",
 "maxInstance": "Max Instances",
 "maxIops": "Max IOPS",
@@ -770,10 +787,12 @@
 "reservedSystemNetmask": "Reserved system netmask",
 "reservedSystemStartIp": "Start Reserved system IP",
 "reservediprange": "Reserved IP Range",
+"resources": "Resources",
 "resourceid": "Resource ID",
 "resourcename": "Resource Name",
 "resourcestate": "Resource state",
 "restartrequired": "Restart required",
+"revokeinvitationconfirm": "Please confirm that you would like to revoke this 
invitation?",
 "role": "Role",
 "rolename": "Role",
 "roletype": "Role Type",
diff --git a/src/store/getters.js b/src/store/getters.js
index 6a5ae39..05c923c 100644
--- a/src/store/getters.js
+++ b/src/store/getters.js
@@ -25,6 +25,7 @@ const getters = {
   nickname: state => state.user.name,
   welcome: state => state.user.welcome,
   apis: state => state.user.apis,
+  features: state => state.user.features,
   userInfo: state => state.user.info,
   addRouters: state => state.permission.addRouters,
   multiTab: state => state.app.multiTab,
diff --git a/src/store/modules/user.js b/src/store/modules/user.js
index d138107..a61e49a 100644
--- a/src/store/modules/user.js
+++ b/src/store/modules/user.js
@@ -29,6 +29,7 @@ const user = {
     avatar: '',
     info: {},
     apis: {},
+    features: {},
     project: {},
     asyncJobIds: []
   },
@@ -54,6 +55,9 @@ const user = {
     SET_APIS: (state, apis) => {
       state.apis = apis
     },
+    SET_FEATURES: (state, features) => {
+      state.features = features
+    },
     SET_ASYNC_JOB_IDS: (state, jobsJsonArray) => {
       Vue.ls.set(ASYNC_JOB_IDS, jobsJsonArray)
       state.asyncJobIds = jobsJsonArray
@@ -86,7 +90,6 @@ const user = {
 
     GetInfo ({ commit }) {
       return new Promise((resolve, reject) => {
-        // Discover allowed APIs
         api('listApis').then(response => {
           const apis = {}
           const apiList = response.listapisresponse.api
@@ -104,7 +107,6 @@ const user = {
           reject(error)
         })
 
-        // Find user info
         api('listUsers').then(response => {
           const result = response.listusersresponse.user[0]
           commit('SET_INFO', result)
@@ -117,6 +119,13 @@ const user = {
         }).catch(error => {
           reject(error)
         })
+
+        api('listCapabilities').then(response => {
+          const result = response.listcapabilitiesresponse.capability
+          commit('SET_FEATURES', result)
+        }).catch(error => {
+          reject(error)
+        })
       })
     },
     Logout ({ commit, state }) {
diff --git a/src/views/AutogenView.vue b/src/views/AutogenView.vue
index 5084aab..d0abf4e 100644
--- a/src/views/AutogenView.vue
+++ b/src/views/AutogenView.vue
@@ -19,46 +19,36 @@
   <div>
     <a-card class="breadcrumb-card">
       <a-row>
-        <a-col :span="24" style="display: flex">
-          <breadcrumb />
-          <a-tooltip placement="bottom">
-            <template slot="title">
-              {{ "Refresh" }}
-            </template>
-            <a-button
-              style="margin-left: 8px"
-              :loading="loading"
-              shape="round"
-              size="small"
-              icon="sync"
-              @click="fetchData()">
-              {{ $t('refresh') }}
-            </a-button>
-          </a-tooltip>
-        </a-col>
-        <a-col :span="24" style="padding-top: 12px">
-          <span>
-            <a-tooltip
-              v-for="(action, actionIndex) in actions"
-              :key="actionIndex"
-              placement="bottom">
+        <a-col :span="14" style="padding-left: 6px">
+          <breadcrumb>
+            <a-tooltip placement="bottom" slot="end">
               <template slot="title">
-                {{ $t(action.label) }}
+                {{ "Refresh" }}
               </template>
               <a-button
-                v-if="action.api in $store.getters.apis &&
-                  ((!dataView && (action.listView || action.groupAction && 
selectedRowKeys.length > 0)) || (dataView && action.dataView)) &&
-                  ('show' in action ? action.show(resource) : true)"
-                :icon="action.icon"
-                :type="action.icon === 'delete' ? 'danger' : (action.icon === 
'plus' ? 'primary' : 'default')"
+                style="margin-top: 4px"
+                :loading="loading"
                 shape="circle"
-                style="margin-right: 5px"
-                @click="execAction(action)"
-              >
+                size="small"
+                type="dashed"
+                icon="reload"
+                @click="fetchData()">
               </a-button>
             </a-tooltip>
+          </breadcrumb>
+        </a-col>
+        <a-col :span="10">
+          <span style="float: right">
+            <action-button
+              style="margin-bottom: 5px"
+              :loading="loading"
+              :actions="actions"
+              :selectedRowKeys="selectedRowKeys"
+              :dataView="dataView"
+              :resource="resource"
+              @exec-action="execAction"/>
             <a-input-search
-              style="width: 50%; padding-left: 6px"
+              style="width: 25vw; margin-left: 10px"
               placeholder="Search"
               v-model="searchQuery"
               v-if="!dataView && !treeView"
@@ -81,7 +71,14 @@
           centered
           width="auto"
         >
-          <component :is="currentAction.component" :resource="resource" 
:loading="loading" v-bind="{currentAction}" />
+          <component
+            :is="currentAction.component"
+            :resource="resource"
+            :loading="loading"
+            v-bind="{currentAction}"
+            @refresh-data="fetchData"
+            @poll-action="pollActionCompletion"
+            @close-action="closeAction"/>
         </a-modal>
       </keep-alive>
       <a-modal
@@ -137,13 +134,18 @@
                   </a-select-option>
                 </a-select>
               </span>
-              <span v-else-if="field.type==='uuid' || field.name==='account' 
|| field.name==='keypair'">
+              <span v-else-if="field.type==='uuid' || (field.name==='account' 
&& !['addAccountToProject'].includes(currentAction.api)) || 
field.name==='keypair'">
                 <a-select
-                  :loading="field.loading"
+                  showSearch
+                  optionFilterProp="children"
                   v-decorator="[field.name, {
                     rules: [{ required: field.required, message: 'Please 
select option' }]
                   }]"
+                  :loading="field.loading"
                   :placeholder="field.description"
+                  :filterOption="(input, option) => {
+                    return 
option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase())
 >= 0
+                  }"
                 >
                   <a-select-option v-for="(opt, optIndex) in field.opts" 
:key="optIndex">
                     {{ opt.name || opt.description }}
@@ -253,6 +255,7 @@ import Status from '@/components/widgets/Status'
 import ListView from '@/components/view/ListView'
 import ResourceView from '@/components/view/ResourceView'
 import TreeView from '@/components/view/TreeView'
+import ActionButton from '@/components/view/ActionButton'
 
 export default {
   name: 'Resource',
@@ -262,7 +265,8 @@ export default {
     ResourceView,
     ListView,
     TreeView,
-    Status
+    Status,
+    ActionButton
   },
   mixins: [mixinDevice],
   provide: function () {
@@ -360,6 +364,7 @@ export default {
         if (this.$route.meta.columns) {
           this.columnKeys = this.$route.meta.columns
         }
+
         if (this.$route.meta.actions) {
           this.actions = this.$route.meta.actions
         }
@@ -625,7 +630,11 @@ export default {
                 } else if (param.type === 'list') {
                   params[key] = input.map(e => { return param.opts[e].id 
}).reduce((str, name) => { return str + ',' + name })
                 } else if (param.name === 'account' || param.name === 
'keypair') {
-                  params[key] = param.opts[input].name
+                  if 
(['addAccountToProject'].includes(this.currentAction.api)) {
+                    params[key] = input
+                  } else {
+                    params[key] = param.opts[input].name
+                  }
                 } else {
                   params[key] = input
                 }
@@ -673,7 +682,7 @@ export default {
                 break
               }
             }
-            if (this.currentAction.icon === 'delete') {
+            if (this.currentAction.icon === 'delete' && this.dataView) {
               this.$router.go(-1)
             } else {
               if (!hasJobId) {
diff --git a/src/views/auth/Login.vue b/src/views/auth/Login.vue
index bc7111e..6ec9857 100644
--- a/src/views/auth/Login.vue
+++ b/src/views/auth/Login.vue
@@ -175,8 +175,7 @@ export default {
     },
     loginSuccess (res) {
       this.$router.push({ name: 'dashboard' })
-      this.$message.success('Login Successful')
-      this.$message.loading('Discoverying Features', 4)
+      this.$message.loading('Login Successful. Discoverying Features...', 5)
     },
     requestFailed (err) {
       if (err && err.response && err.response.data && 
err.response.data.loginresponse) {
diff --git a/src/views/compute/InstanceHardware.vue 
b/src/views/compute/InstanceHardware.vue
index 9720aa2..a42e95a 100644
--- a/src/views/compute/InstanceHardware.vue
+++ b/src/views/compute/InstanceHardware.vue
@@ -665,8 +665,13 @@ export default {
   }
 </style>
 
-<style lang="scss">
-  .wide-modal {
-    min-width: 50vw;
-  }
+<style scoped>
+.wide-modal {
+  min-width: 50vw;
+}
+
+/deep/ .ant-list-item {
+  padding-top: 12px;
+  padding-bottom: 12px;
+}
 </style>
diff --git a/src/views/dashboard/Dashboard.vue 
b/src/views/dashboard/Dashboard.vue
index 5f78714..c46ba82 100644
--- a/src/views/dashboard/Dashboard.vue
+++ b/src/views/dashboard/Dashboard.vue
@@ -21,7 +21,7 @@
       <capacity-dashboard/>
     </div>
     <div v-else>
-      <usage-dashboard/>
+      <usage-dashboard :resource="$store.getters.project" 
:showProject="project" />
     </div>
   </div>
 </template>
diff --git a/src/views/dashboard/UsageDashboard.vue 
b/src/views/dashboard/UsageDashboard.vue
index 5b361ea..79c02ba 100644
--- a/src/views/dashboard/UsageDashboard.vue
+++ b/src/views/dashboard/UsageDashboard.vue
@@ -17,24 +17,43 @@
 
 <template>
   <a-row class="usage-dashboard" :gutter="12">
-    <a-col
-      :xl="16">
-      <a-row :gutter="12">
-        <a-col
-          class="usage-dashboard-chart-tile"
-          :xs="12"
-          :md="8"
-          v-for="stat in stats"
-          :key="stat.type">
-          <chart-card class="usage-dashboard-chart-card" :loading="loading">
-            <router-link :to="{ name: stat.path }">
-              <div class="usage-dashboard-chart-card-inner">
-                <h4>{{ stat.name }}</h4>
-                <h1>{{ stat.count == undefined ? 0 : stat.count }}</h1>
-              </div>
-            </router-link>
-          </chart-card>
-        </a-col>
+    <a-col :xl="16">
+      <a-row>
+        <a-card>
+          <a-tabs
+            v-if="showProject"
+            :animated="false"
+            @change="onTabChange">
+            <a-tab-pane
+              v-for="tab in $route.meta.tabs"
+              :tab="$t(tab.name)"
+              :key="tab.name"
+              v-if="'show' in tab ? tab.show(project, $route, 
$store.getters.userInfo) : true">
+              <component
+                :is="tab.component"
+                :resource="project"
+                :loading="loading"
+                :bordered="false"
+                :stats="stats" />
+            </a-tab-pane>
+          </a-tabs>
+          <a-col
+            v-else
+            class="usage-dashboard-chart-tile"
+            :xs="12"
+            :md="8"
+            v-for="stat in stats"
+            :key="stat.type">
+            <chart-card class="usage-dashboard-chart-card" :loading="loading">
+              <router-link :to="{ name: stat.path }">
+                <div class="usage-dashboard-chart-card-inner">
+                  <h4>{{ stat.name }}</h4>
+                  <h1>{{ stat.count == undefined ? 0 : stat.count }}</h1>
+                </div>
+              </router-link>
+            </chart-card>
+          </a-col>
+        </a-card>
       </a-row>
     </a-col>
     <a-col
@@ -64,22 +83,44 @@
 
 <script>
 import { api } from '@/api'
+import store from '@/store'
 
 import ChartCard from '@/components/widgets/ChartCard'
+import UsageDashboardChart from '@/views/dashboard/UsageDashboardChart'
 
 export default {
   name: 'UsageDashboard',
   components: {
-    ChartCard
+    ChartCard,
+    UsageDashboardChart
+  },
+  props: {
+    resource: {
+      type: Object,
+      default () {
+        return []
+      }
+    },
+    showProject: {
+      type: Boolean,
+      default: false
+    }
   },
   data () {
     return {
       loading: false,
+      showAction: false,
+      showAddAccount: false,
       events: [],
-      stats: []
+      stats: [],
+      project: {}
     }
   },
+  beforeCreate () {
+    this.form = this.$form.createForm(this)
+  },
   mounted () {
+    this.project = store.getters.project
     this.fetchData()
   },
   watch: {
@@ -87,6 +128,9 @@ export default {
       if (to.name === 'dashboard') {
         this.fetchData()
       }
+    },
+    resource (newData, oldData) {
+      this.project = newData
     }
   },
   methods: {
@@ -159,6 +203,13 @@ export default {
         return 'green'
       }
       return 'blue'
+    },
+    onTabChange (key) {
+      this.showAddAccount = false
+
+      if (key !== 'Dashboard') {
+        this.showAddAccount = true
+      }
     }
   }
 }
diff --git a/src/components/page/GlobalFooter.vue 
b/src/views/dashboard/UsageDashboardChart.vue
similarity index 53%
copy from src/components/page/GlobalFooter.vue
copy to src/views/dashboard/UsageDashboardChart.vue
index ccc868f..b2244a9 100644
--- a/src/components/page/GlobalFooter.vue
+++ b/src/views/dashboard/UsageDashboardChart.vue
@@ -16,49 +16,44 @@
 // under the License.
 
 <template>
-  <div class="footer">
-    <div class="links">
-      <a href="https://github.com/apache/cloudstack-primate"; target="_blank">
-        <a-icon type="github"/>
-      </a>
-    </div>
+  <div>
+    <a-col
+      class="usage-dashboard-chart-tile"
+      :xs="12"
+      :md="8"
+      v-for="stat in stats"
+      :key="stat.type">
+      <chart-card class="usage-dashboard-chart-card" :loading="loading">
+        <router-link :to="{ name: stat.path }">
+          <div class="usage-dashboard-chart-card-inner">
+            <h4>{{ stat.name }}</h4>
+            <h1>{{ stat.count == undefined ? 0 : stat.count }}</h1>
+          </div>
+        </router-link>
+      </chart-card>
+    </a-col>
   </div>
 </template>
 
 <script>
+import ChartCard from '@/components/widgets/ChartCard'
+
 export default {
-  name: 'LayoutFooter',
-  data () {
-    return {
+  name: 'UsageDashboardChart',
+  components: {
+    ChartCard
+  },
+  props: {
+    stats: {
+      type: Array,
+      default () {
+        return []
+      }
+    },
+    loading: {
+      type: Boolean,
+      default: false
     }
   }
 }
 </script>
-
-<style lang="less" scoped>
-  .footer {
-    padding: 0 16px;
-    margin: 48px 0 24px;
-    text-align: center;
-
-    .links {
-      margin-bottom: 8px;
-
-      a {
-        color: rgba(0, 0, 0, .45);
-
-        &:hover {
-          color: rgba(0, 0, 0, .65);
-        }
-
-        &:not(:last-child) {
-          margin-right: 40px;
-        }
-      }
-    }
-    .copyright {
-      color: rgba(0, 0, 0, .45);
-      font-size: 14px;
-    }
-  }
-</style>
diff --git a/src/views/infra/InfraSummary.vue b/src/views/infra/InfraSummary.vue
index 1ef76d0..b7baf9d 100644
--- a/src/views/infra/InfraSummary.vue
+++ b/src/views/infra/InfraSummary.vue
@@ -16,11 +16,11 @@
 // under the License.
 
 <template>
-  <a-row :gutter="24">
+  <a-row :gutter="12">
     <a-col :md="24">
       <a-card class="breadcrumb-card">
         <a-col :md="24" style="display: flex">
-          <breadcrumb style="padding-top: 6px" />
+          <breadcrumb style="padding-top: 6px; padding-left: 8px" />
           <a-button
             style="margin-left: 12px; margin-top: 4px"
             :loading="loading"
@@ -141,7 +141,7 @@
     </a-col>
     <a-col
       :md="6"
-      :style="{ marginBottom: '12px', marginTop: '12px' }"
+      style="margin-bottom: 12px"
       v-for="(section, index) in sections"
       v-if="routes[section]"
       :key="index">
diff --git a/src/views/project/AccountsTab.vue 
b/src/views/project/AccountsTab.vue
new file mode 100644
index 0000000..a4a554e
--- /dev/null
+++ b/src/views/project/AccountsTab.vue
@@ -0,0 +1,267 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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.
+
+<template>
+  <div>
+    <a-row :gutter="12">
+      <a-col :md="24" :lg="24">
+        <a-table
+          size="small"
+          :loading="loading"
+          :columns="columns"
+          :dataSource="dataSource"
+          :pagination="false"
+          :rowKey="record => record.accountid || record.account"
+        >
+          <span slot="action" v-if="record.role!==owner" slot-scope="text, 
record" class="account-button-action">
+            <a-tooltip placement="top">
+              <template slot="title">
+                {{ $t('label.make.project.owner') }}
+              </template>
+              <a-button type="default" shape="circle" icon="user" size="small" 
@click="onMakeProjectOwner(record)" />
+            </a-tooltip>
+            <a-tooltip placement="top">
+              <template slot="title">
+                {{ $t('label.remove.project.account') }}
+              </template>
+              <a-button
+                type="danger"
+                shape="circle"
+                icon="delete"
+                size="small"
+                @click="onShowConfirmDelete(record)"/>
+            </a-tooltip>
+          </span>
+        </a-table>
+        <a-pagination
+          class="row-element"
+          size="small"
+          :current="page"
+          :pageSize="pageSize"
+          :total="itemCount"
+          :showTotal="total => `Total ${total} items`"
+          :pageSizeOptions="['10', '20', '40', '80', '100']"
+          @change="changePage"
+          @showSizeChange="changePageSize"
+          showSizeChanger/>
+      </a-col>
+    </a-row>
+  </div>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+  name: 'AccountsTab',
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    }
+  },
+  data () {
+    return {
+      columns: [],
+      dataSource: [],
+      loading: false,
+      page: 1,
+      pageSize: 10,
+      itemCount: 0,
+      owner: 'Admin'
+    }
+  },
+  created () {
+    this.columns = [
+      {
+        title: this.$t('account'),
+        dataIndex: 'account',
+        width: '35%',
+        scopedSlots: { customRender: 'account' }
+      },
+      {
+        title: this.$t('role'),
+        dataIndex: 'role',
+        scopedSlots: { customRender: 'role' }
+      },
+      {
+        title: this.$t('action'),
+        dataIndex: 'action',
+        fixed: 'right',
+        width: 100,
+        scopedSlots: { customRender: 'action' }
+      }
+    ]
+
+    this.page = 1
+    this.pageSize = 10
+    this.itemCount = 0
+  },
+  mounted () {
+    this.fetchData()
+  },
+  watch: {
+    resource (newItem, oldItem) {
+      if (!newItem || !newItem.id) {
+        return
+      }
+      this.resource = newItem
+      this.fetchData()
+    }
+  },
+  methods: {
+    fetchData () {
+      const params = {}
+      params.projectId = this.resource.id
+      params.page = this.page
+      params.pageSize = this.pageSize
+
+      this.loading = true
+
+      api('listProjectAccounts', params).then(json => {
+        const listProjectAccount = 
json.listprojectaccountsresponse.projectaccount
+        const itemCount = json.listprojectaccountsresponse.count
+
+        if (!listProjectAccount || listProjectAccount.length === 0) {
+          this.dataSource = []
+          return
+        }
+
+        this.itemCount = itemCount
+        this.dataSource = listProjectAccount
+      }).catch(error => {
+        this.$notification.error({
+          message: 'Request Failed',
+          description: error.response.headers['x-description']
+        })
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    changePage (page, pageSize) {
+      this.page = page
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    changePageSize (currentPage, pageSize) {
+      this.page = currentPage
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    onMakeProjectOwner (record) {
+      const title = this.$t('label.make.project.owner')
+      const loading = this.$message.loading(title + 'in progress for ' + 
record.account, 0)
+      const params = {}
+
+      params.id = this.resource.id
+      params.account = record.account
+
+      api('updateProject', params).then(json => {
+        const hasJobId = this.checkForAddAsyncJob(json, title, record.account)
+
+        if (hasJobId) {
+          this.fetchData()
+        }
+      }).catch(error => {
+        // show error
+        this.$notification.error({
+          message: 'Request Failed',
+          description: error.response.headers['x-description']
+        })
+      }).finally(() => {
+        setTimeout(loading, 1000)
+      })
+    },
+    onShowConfirmDelete (record) {
+      const self = this
+      let title = this.$t('deleteconfirm')
+      title = title.replace('{name}', this.$t('account'))
+
+      this.$confirm({
+        title: title,
+        okText: 'OK',
+        okType: 'danger',
+        cancelText: 'Cancel',
+        onOk () {
+          self.removeAccount(record)
+        }
+      })
+    },
+    removeAccount (record) {
+      const title = this.$t('label.remove.project.account')
+      const loading = this.$message.loading(title + 'in progress for ' + 
record.account, 0)
+      const params = {}
+
+      params.account = record.account
+      params.projectid = this.resource.id
+
+      api('deleteAccountFromProject', params).then(json => {
+        const hasJobId = this.checkForAddAsyncJob(json, title, record.account)
+
+        if (hasJobId) {
+          this.fetchData()
+        }
+      }).catch(error => {
+        // show error
+        this.$notification.error({
+          message: 'Request Failed',
+          description: error.response.headers['x-description']
+        })
+      }).finally(() => {
+        setTimeout(loading, 1000)
+      })
+    },
+    checkForAddAsyncJob (json, title, description) {
+      let hasJobId = false
+
+      for (const obj in json) {
+        if (obj.includes('response')) {
+          for (const res in json[obj]) {
+            if (res === 'jobid') {
+              hasJobId = true
+              const jobId = json[obj][res]
+              this.$store.dispatch('AddAsyncJob', {
+                title: title,
+                jobid: jobId,
+                description: description,
+                status: 'progress'
+              })
+            }
+          }
+        }
+      }
+
+      return hasJobId
+    }
+  }
+}
+</script>
+
+<style scoped>
+  /deep/.ant-table-fixed-right {
+    z-index: 5;
+  }
+
+  .row-element {
+    margin-top: 10px;
+    margin-bottom: 10px;
+  }
+
+  .account-button-action button {
+    margin-right: 5px;
+  }
+</style>
diff --git a/src/views/project/InvitationTokenTemplate.vue 
b/src/views/project/InvitationTokenTemplate.vue
new file mode 100644
index 0000000..2a5eefe
--- /dev/null
+++ b/src/views/project/InvitationTokenTemplate.vue
@@ -0,0 +1,132 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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.
+
+<template>
+  <div class="row-project-invitation">
+    <a-spin :spinning="loading">
+      <a-form
+        :form="form"
+        @submit="handleSubmit"
+        layout="vertical">
+        <a-form-item :label="$t('projectid')">
+          <a-input
+            v-decorator="['projectid', {
+              rules: [{ required: true, message: 'Please enter input' }]
+            }]"
+            :placeholder="$t('project.projectid.description')"
+          />
+        </a-form-item>
+        <a-form-item :label="$t('token')">
+          <a-input
+            v-decorator="['token', {
+              rules: [{ required: true, message: 'Please enter input' }]
+            }]"
+            :placeholder="$t('project.token.description')"
+          />
+        </a-form-item>
+        <div class="card-footer">
+          <!-- ToDo extract as component -->
+          <a-button @click="() => this.$router.back()">{{ this.$t('cancel') 
}}</a-button>
+          <a-button :loading="loading" type="primary" @click="handleSubmit">{{ 
this.$t('OK') }}</a-button>
+        </div>
+      </a-form>
+    </a-spin>
+  </div>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+  name: 'InvitationTokenTemplate',
+  beforeCreate () {
+    this.form = this.$form.createForm(this)
+  },
+  data () {
+    return {
+      loading: false
+    }
+  },
+  methods: {
+    handleSubmit (e) {
+      e.preventDefault()
+
+      this.form.validateFields((err, values) => {
+        if (err) {
+          return
+        }
+
+        const title = this.$t('label.accept.project.invitation')
+        const description = this.$t('projectid') + ' ' + values.projectid
+        const loading = this.$message.loading(title + 'in progress for ' + 
description, 0)
+
+        this.loading = true
+
+        api('updateProjectInvitation', values).then(json => {
+          this.checkForAddAsyncJob(json, title, description)
+          this.$emit('close-action')
+        }).catch(error => {
+          this.$notification.error({
+            message: 'Request Failed',
+            description: error.response.headers['x-description']
+          })
+        }).finally(() => {
+          this.$emit('refresh-data')
+          this.loading = false
+          setTimeout(loading, 1000)
+        })
+      })
+    },
+    checkForAddAsyncJob (json, title, description) {
+      let hasJobId = false
+
+      for (const obj in json) {
+        if (obj.includes('response')) {
+          for (const res in json[obj]) {
+            if (res === 'jobid') {
+              hasJobId = true
+              const jobId = json[obj][res]
+              this.$store.dispatch('AddAsyncJob', {
+                title: title,
+                jobid: jobId,
+                description: description,
+                status: 'progress'
+              })
+            }
+          }
+        }
+      }
+
+      return hasJobId
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.row-project-invitation {
+  min-width: 450px;
+}
+
+.card-footer {
+  text-align: right;
+
+  button + button {
+    margin-left: 8px;
+  }
+}
+</style>
diff --git a/src/views/project/InvitationsTemplate.vue 
b/src/views/project/InvitationsTemplate.vue
new file mode 100644
index 0000000..ce86af4
--- /dev/null
+++ b/src/views/project/InvitationsTemplate.vue
@@ -0,0 +1,326 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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.
+
+<template>
+  <div class="row-invitation">
+    <a-row :gutter="12">
+      <a-col :md="24" :lg="24">
+        <a-input-search
+          class="input-search-invitation"
+          style="width: unset"
+          placeholder="Search"
+          v-model="searchQuery"
+          @search="onSearch" />
+      </a-col>
+      <a-col :md="24" :lg="24">
+        <a-table
+          size="small"
+          :loading="loading"
+          :columns="columns"
+          :dataSource="dataSource"
+          :pagination="false"
+          :rowKey="record => record.id || record.account"
+          @change="onChangeTable">
+          <template slot="state" slot-scope="text">
+            <status :text="text ? text : ''" displayText />
+          </template>
+          <span slot="action" v-if="record.state===stateAllow" 
slot-scope="text, record" class="account-button-action">
+            <a-tooltip placement="top">
+              <template slot="title">
+                {{ $t('label.accept.project.invitation') }}
+              </template>
+              <a-button
+                type="success"
+                shape="circle"
+                icon="check"
+                size="small"
+                @click="onShowConfirmAcceptInvitation(record)"/>
+            </a-tooltip>
+            <a-tooltip placement="top">
+              <template slot="title">
+                {{ $t('label.decline.invitation') }}
+              </template>
+              <a-button
+                type="danger"
+                shape="circle"
+                icon="close"
+                size="small"
+                @click="onShowConfirmRevokeInvitation(record)"/>
+            </a-tooltip>
+          </span>
+        </a-table>
+        <a-pagination
+          class="row-element"
+          size="small"
+          :current="page"
+          :pageSize="pageSize"
+          :total="itemCount"
+          :showTotal="total => `Total ${total} items`"
+          :pageSizeOptions="['10', '20', '40', '80', '100']"
+          @change="changePage"
+          @showSizeChange="changePageSize"
+          showSizeChanger/>
+      </a-col>
+    </a-row>
+  </div>
+</template>
+
+<script>
+import { api } from '@/api'
+import Status from '@/components/widgets/Status'
+
+export default {
+  name: 'InvitationsTemplate',
+  components: {
+    Status
+  },
+  data () {
+    return {
+      columns: [],
+      dataSource: [],
+      listDomains: [],
+      loading: false,
+      page: 1,
+      pageSize: 10,
+      itemCount: 0,
+      state: undefined,
+      domainid: undefined,
+      projectid: undefined,
+      searchQuery: undefined,
+      stateAllow: 'Pending'
+    }
+  },
+  created () {
+    this.columns = [
+      {
+        title: this.$t('project'),
+        dataIndex: 'project',
+        scopedSlots: { customRender: 'project' }
+      },
+      {
+        title: this.$t('domain'),
+        dataIndex: 'domain',
+        scopedSlots: { customRender: 'domain' }
+      },
+      {
+        title: this.$t('state'),
+        dataIndex: 'state',
+        width: 130,
+        scopedSlots: { customRender: 'state' },
+        filters: [
+          {
+            text: this.$t('Pending'),
+            value: 'Pending'
+          },
+          {
+            text: this.$t('Completed'),
+            value: 'Completed'
+          },
+          {
+            text: this.$t('Declined'),
+            value: 'Declined'
+          }
+        ],
+        filterMultiple: false
+      },
+      {
+        title: this.$t('action'),
+        dataIndex: 'action',
+        width: 80,
+        scopedSlots: { customRender: 'action' }
+      }
+    ]
+
+    this.page = 1
+    this.pageSize = 10
+    this.itemCount = 0
+  },
+  mounted () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      const params = {}
+
+      params.page = this.page
+      params.pageSize = this.pageSize
+      params.state = this.state
+      params.domainid = this.domainid
+      params.projectid = this.projectid
+      params.keyword = this.searchQuery
+      params.listAll = true
+
+      this.loading = true
+      this.dataSource = []
+      this.itemCount = 0
+
+      api('listProjectInvitations', params).then(json => {
+        const listProjectInvitations = 
json.listprojectinvitationsresponse.projectinvitation
+        const itemCount = json.listprojectinvitationsresponse.count
+
+        if (!listProjectInvitations || listProjectInvitations.length === 0) {
+          return
+        }
+
+        this.dataSource = listProjectInvitations
+        this.itemCount = itemCount
+      }).catch(error => {
+        this.$notification.error({
+          message: 'Request Failed',
+          description: error.response.headers['x-description']
+        })
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    changePage (page, pageSize) {
+      this.page = page
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    changePageSize (currentPage, pageSize) {
+      this.page = currentPage
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    onShowConfirmAcceptInvitation (record) {
+      const self = this
+      const title = this.$t('confirmacceptinvitation')
+
+      this.$confirm({
+        title: title,
+        okText: 'OK',
+        okType: 'danger',
+        cancelText: 'Cancel',
+        onOk () {
+          self.updateProjectInvitation(record, true)
+        }
+      })
+    },
+    updateProjectInvitation (record, state) {
+      let title = ''
+
+      if (state) {
+        title = this.$t('label.accept.project.invitation')
+      } else {
+        title = this.$t('label.decline.invitation')
+      }
+
+      const loading = this.$message.loading(title + 'in progress for ' + 
record.project, 0)
+      const params = {}
+
+      params.projectid = record.projectid
+      params.account = record.account
+      params.domainid = record.domainid
+      params.accept = state
+
+      api('updateProjectInvitation', params).then(json => {
+        const hasJobId = this.checkForAddAsyncJob(json, title, record.project)
+
+        if (hasJobId) {
+          this.fetchData()
+          this.$emit('refresh-data')
+        }
+      }).catch(error => {
+        // show error
+        this.$notification.error({
+          message: 'Request Failed',
+          description: error.response.headers['x-description']
+        })
+      }).finally(() => {
+        setTimeout(loading, 1000)
+      })
+    },
+    onShowConfirmRevokeInvitation (record) {
+      const self = this
+      const title = this.$t('confirmdeclineinvitation')
+
+      this.$confirm({
+        title: title,
+        okText: 'OK',
+        okType: 'danger',
+        cancelText: 'Cancel',
+        onOk () {
+          self.updateProjectInvitation(record, false)
+        }
+      })
+    },
+    onChangeTable (pagination, filters, sorter) {
+      if (!filters || Object.keys(filters).length === 0) {
+        return
+      }
+
+      this.state = filters.state && filters.state.length > 0 ? 
filters.state[0] : undefined
+      this.domainid = filters.domain && filters.domain.length > 0 ? 
filters.domain[0] : undefined
+      this.projectid = filters.project && filters.project.length > 0 ? 
filters.project[0] : undefined
+
+      this.fetchData()
+    },
+    onSearch (value) {
+      this.searchQuery = value
+      this.fetchData()
+    },
+    checkForAddAsyncJob (json, title, description) {
+      let hasJobId = false
+
+      for (const obj in json) {
+        if (obj.includes('response')) {
+          for (const res in json[obj]) {
+            if (res === 'jobid') {
+              hasJobId = true
+              const jobId = json[obj][res]
+              this.$store.dispatch('AddAsyncJob', {
+                title: title,
+                jobid: jobId,
+                description: description,
+                status: 'progress'
+              })
+            }
+          }
+        }
+      }
+
+      return hasJobId
+    }
+  }
+}
+</script>
+
+<style scoped>
+  /deep/.ant-table-fixed-right {
+    z-index: 5;
+  }
+
+  .row-invitation {
+    min-width: 500px;
+    max-width: 768px;
+  }
+
+  .row-element {
+    margin-top: 15px;
+    margin-bottom: 15px;
+  }
+
+  .account-button-action button {
+    margin-right: 5px;
+  }
+
+  .input-search-invitation {
+    float: right;
+    margin-bottom: 10px;
+  }
+</style>
diff --git a/src/views/project/ResourcesTab.vue 
b/src/views/project/ResourcesTab.vue
new file mode 100644
index 0000000..254f634
--- /dev/null
+++ b/src/views/project/ResourcesTab.vue
@@ -0,0 +1,178 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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.
+
+<template>
+  <a-spin :spinning="loading || formLoading">
+    <a-form
+      :form="form"
+      @submit="handleSubmit"
+      layout="vertical"
+    >
+      <a-form-item
+        v-for="(item, index) in dataResource"
+        v-if="dataSource.includes(item.resourcetypename)"
+        :key="index"
+        :v-bind="item.resourcetypename"
+        :label="$t('max' + item.resourcetypename)">
+        <a-input-number
+          style="width: 100%;"
+          v-decorator="[item.resourcetype, {
+            initialValue: item.max
+          }]"
+          :placeholder="$t('project.' + item.resourcetypename + 
'.description')"
+        />
+      </a-form-item>
+      <div class="card-footer">
+        <!-- ToDo extract as component -->
+        <a-button :loading="formLoading" type="primary" 
@click="handleSubmit">{{ this.$t('apply') }}</a-button>
+      </div>
+    </a-form>
+  </a-spin>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+  name: 'ResourceTab',
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    },
+    loading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  beforeCreate () {
+    this.form = this.$form.createForm(this)
+  },
+  data () {
+    return {
+      formLoading: false,
+      dataResource: [],
+      dataSource: []
+    }
+  },
+  created () {
+    this.dataSource = [
+      'network',
+      'volume',
+      'public_ip',
+      'template',
+      'user_vm',
+      'snapshot',
+      'vpc', 'cpu',
+      'memory',
+      'primary_storage',
+      'secondary_storage'
+    ]
+  },
+  mounted () {
+    this.fetchData()
+  },
+  watch: {
+    resource (newData, oldData) {
+      if (!newData || !newData.id) {
+        return
+      }
+
+      this.resource = newData
+      this.fetchData()
+    }
+  },
+  methods: {
+    fetchData () {
+      const params = {}
+      params.projectid = this.resource.id
+
+      this.formLoading = true
+
+      api('listResourceLimits', params).then(json => {
+        if (json.listresourcelimitsresponse.resourcelimit) {
+          this.dataResource = json.listresourcelimitsresponse.resourcelimit
+        }
+      }).catch(error => {
+        this.$notification.error({
+          message: 'Request Failed',
+          description: error.response.headers['x-description']
+        })
+      }).finally(() => {
+        this.formLoading = false
+      })
+    },
+    handleSubmit (e) {
+      e.preventDefault()
+
+      this.form.validateFields((err, values) => {
+        if (err) {
+          return
+        }
+
+        const arrAsync = []
+        const params = {}
+        params.projectid = this.resource.id
+
+        // create parameter from form
+        for (const key in values) {
+          const input = values[key]
+
+          if (input === undefined) {
+            continue
+          }
+
+          params.resourcetype = key
+          params.max = input
+
+          arrAsync.push(new Promise((resolve, reject) => {
+            api('updateResourceLimit', params).then(json => {
+              resolve()
+            }).catch(error => {
+              reject(error)
+            })
+          }))
+        }
+
+        this.formLoading = true
+
+        Promise.all(arrAsync).then(() => {
+          this.$message.success('Apply Successful')
+          this.fetchData()
+        }).catch(error => {
+          this.$notification.error({
+            message: 'Request Failed',
+            description: error.response.headers['x-description']
+          })
+        }).finally(() => {
+          this.formLoading = false
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .card-footer {
+    text-align: right;
+
+    button + button {
+      margin-left: 8px;
+    }
+  }
+</style>

Reply via email to