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 <[email protected]>
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 <[email protected]>
Co-authored-by: Rohit Yadav <[email protected]>
Signed-off-by: Rohit Yadav <[email protected]>
---
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>