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>