merge: Merge apache/cloudstack-primate repo under 'ui' (#4598)

This merges the apache/cloudstack-primate UI under the ui directory as discussed and voted under:
https://markmail.org/message/bgnn4xkjnlzseeuv

ASF infra requested to archive the apache/cloudstack-primate repo here:
https://issues.apache.org/jira/browse/INFRA-21321

The purpose of the PR is to do a merge commit of the import as well as perform basic Travis and packaging checks. I'll merge once these basic checks (Travis and pkging) work.
This commit is contained in:
Rohit Yadav 2021-01-20 13:47:25 +05:30 committed by GitHub
commit abfe0b0269
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
371 changed files with 155496 additions and 29 deletions

View File

@ -27,10 +27,14 @@ jdk:
python:
- "2.7"
node_js:
- 12
cache:
directories:
- $HOME/.m2
timeout: 500
npm: false
notifications:
email: false

View File

@ -606,7 +606,9 @@
</fileset>
</copy>
<copy todir="${project.build.directory}/classes/META-INF/webapp">
<fileset dir="${basedir}/../ui" />
<fileset dir="${basedir}/../ui">
<exclude name=".*"/>
</fileset>
</copy>
<copy overwrite="true" todir="${basedir}/target/utilities/bin">
<fileset dir="${basedir}/../setup/bindir">

32
pom.xml
View File

@ -1005,33 +1005,11 @@
<exclude>tools/ngui/static/bootstrap/*</exclude>
<exclude>tools/ngui/static/js/lib/*</exclude>
<exclude>tools/transifex/.tx/config</exclude>
<exclude>ui/legacy/css/src/scss/components/token-input-facebook.scss</exclude>
<exclude>ui/l10n/*</exclude>
<exclude>ui/legacy/lib/flot/jquery.colorhelpers.js</exclude>
<exclude>ui/legacy/lib/flot/jquery.flot.crosshair.js</exclude>
<exclude>ui/legacy/lib/flot/jquery.flot.fillbetween.js</exclude>
<exclude>ui/legacy/lib/flot/jquery.flot.image.js</exclude>
<exclude>ui/legacy/lib/flot/jquery.flot.js</exclude>
<exclude>ui/legacy/lib/flot/jquery.flot.navigate.js</exclude>
<exclude>ui/legacy/lib/flot/jquery.flot.pie.js</exclude>
<exclude>ui/legacy/lib/flot/jquery.flot.resize.js</exclude>
<exclude>ui/legacy/lib/flot/jquery.flot.selection.js</exclude>
<exclude>ui/legacy/lib/flot/jquery.flot.stack.js</exclude>
<exclude>ui/legacy/lib/flot/jquery.flot.symbol.js</exclude>
<exclude>ui/legacy/lib/flot/jquery.flot.threshold.js</exclude>
<exclude>ui/legacy/lib/jquery-ui/css/jquery-ui.css</exclude>
<exclude>ui/legacy/lib/jquery-ui/index.html</exclude>
<exclude>ui/legacy/lib/jquery-ui/js/jquery-ui.js</exclude>
<exclude>ui/legacy/lib/jquery.cookies.js</exclude>
<exclude>ui/legacy/lib/jquery.easing.js</exclude>
<exclude>ui/legacy/lib/jquery.js</exclude>
<exclude>ui/legacy/lib/jquery.md5.js</exclude>
<exclude>ui/legacy/lib/jquery.validate.js</exclude>
<exclude>ui/legacy/lib/jquery.tokeninput.js</exclude>
<exclude>ui/legacy/lib/qunit/qunit.css</exclude>
<exclude>ui/legacy/lib/qunit/qunit.js</exclude>
<exclude>ui/legacy/lib/reset.css</exclude>
<exclude>ui/legacy/lib/require.js</exclude>
<exclude>ui/.*</exclude>
<exclude>ui/.*/**</exclude>
<exclude>ui/src/assets/**</exclude>
<exclude>ui/public/**</exclude>
<exclude>ui/legacy/**</exclude>
<exclude>utils/testsmallfileinactive</exclude>
</excludes>
</configuration>

View File

@ -37,6 +37,9 @@ export JAVA_HOME=$(readlink -f /usr/lib/jvm/java-11-openjdk-amd64/bin/java | sed
mvn -v
if [ $TEST_SEQUENCE_NUMBER -eq 1 ]; then
# npm lint, test and build
cd ui && npm install && npm run lint && npm run test:unit && npm run build
cd $DIR
# Pylint/pep8 systemvm python codebase
cd systemvm/test && bash -x runtests.sh
# Build noredist

7
ui/.babelrc Normal file
View File

@ -0,0 +1,7 @@
{
"env": {
"test": {
"plugins": ["require-context-hook"]
}
}
}

38
ui/.editorconfig Normal file
View File

@ -0,0 +1,38 @@
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=false
indent_style=space
indent_size=2
[{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}]
indent_style=space
indent_size=2
[{*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}]
indent_style=space
indent_size=2
[{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}]
indent_style=space
indent_size=2
[*.svg]
indent_style=space
indent_size=2
[*.js.map]
indent_style=space
indent_size=2
[*.less]
indent_style=space
indent_size=2
[*.vue]
indent_style=space
indent_size=2
[{.analysis_options,*.yml,*.yaml}]
indent_style=space
indent_size=2

1
ui/.env.local.example Normal file
View File

@ -0,0 +1 @@
CS_URL=http://localhost:8080

View File

@ -0,0 +1,7 @@
CS_URL=http://localhost:8080
PUBLIC_HOST=primate.example.com
HTTPS_CERT=/etc/ssl/certs/primate.example.com.pem
HTTPS_KEY=/etc/ssl/private/primate.example.com.key
HTTPS_CA=/etc/ssl/certs/ca.pem
HTTPS_DHPARAM=/etc/ssl/keys/dh2048.pem
ALLOWED_HOSTS=["primate.example.com","cloud.example.com"]

1
ui/.env.primate-qa Normal file
View File

@ -0,0 +1 @@
CS_URL=http://primate-qa.cloudstack.cloud:8080

1
ui/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
public/* linguist-vendored

39
ui/.gitignore vendored Normal file
View File

@ -0,0 +1,39 @@
# 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.
.DS_Store
node_modules
coverage
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

5
ui/.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"printWidth": 120,
"semi": false,
"singleQuote": true
}

125
ui/CONTRIBUTING.md Normal file
View File

@ -0,0 +1,125 @@
# Contributing to CloudStack UI
## Summary
This document covers how to contribute to the UI project. It uses Github PRs to manage code contributions.
These instructions assume you have a GitHub.com account, so if you don't have one you will have to create one.
Your proposed code changes will be published to your own fork of the project and you will submit a Pull Request for your changes to be added.
Please refer to project [docs](docs) for reference on standard way of component
configuration, development, usage, extension and testing.
*Lets get started!!!*
### Bug fixes
It's very important that we can easily track bug fix commits, so their hashes should remain the same in all branches.
Therefore, a pull request (PR) that fixes a bug, should be sent against a release branch.
This can be either the "current release" or the "previous release", depending on which ones are maintained.
Since the goal is a stable master, bug fixes should be "merged forward" to the next branch in order: "previous release" -> "current release" -> master (in other words: old to new)
### New features
Development should be done in a feature branch, branched off of master.
Send a PR (steps below) to get it into master (at least 2x LGTM applies).
PR will only be merged when master is open, will be held otherwise until master is open again.
No back porting / cherry-picking features to existing branches!
## Forking
In your browser, navigate to: [https://github.com/apache/cloudstack](https://github.com/apache/cloudstack)
Fork the repository by clicking on the 'Fork' button on the top right hand side.
The fork will happen and you will be taken to your own fork of the repository.
Copy the Git repository URL by clicking on the clipboard next to the URL on the right hand side of the page under '**HTTPS** clone URL'. You will paste this URL when doing the following `git clone` command.
On your workstation, follow these steps to setup a local repository for working on UI:
``` bash
$ git clone https://github.com/YOUR_ACCOUNT/cloudstack.git
$ cd cloudstack/ui
$ git remote add upstream https://github.com/apache/cloudstack.git
$ git checkout master
$ git fetch upstream
$ git rebase upstream/master
```
## Making changes
It is important that you create a new branch to make changes on and that you do not change the `master` branch (other than to rebase in changes from `upstream/master`). In this example I will assume you will be making your changes to a branch called `feature_x`.
This `feature_x` branch will be created on your local repository and will be pushed to your forked repository on GitHub. Once this branch is on your fork you will create a Pull Request for the changes to be added to the UI project.
It is best practice to create a new branch each time you want to contribute to the project and only track the changes for that pull request in this branch.
``` bash
$ git checkout -b feature_x
(make your changes)
$ git status
$ git add .
$ git commit -a -m "descriptive commit message for your changes"
```
> The `-b` specifies that you want to create a new branch called `feature_x`. You only specify `-b` the first time you checkout because you are creating a new branch. Once the `feature_x` branch exists, you can later switch to it with only `git checkout feature_x`.
### Updating your branch
It is important that you maintain an up-to-date `master` branch in your local repository. You may do this by either rebasing against the upstream repository or merging the upstream branch.
For example:
1. Checkout your local `master` branch
2. Synchronize your local `master` branch with the `upstream/master` so you have all the latest changes from the project
3. Merge or Rebase the latest project code into your `feature_x` branch so it is up-to-date with the upstream code
``` bash
$ git checkout master
$ git fetch upstream
$ git rebase upstream/master
$ git checkout feature_x
$ git merge master
```
> Now your `feature_x` branch is up-to-date with all the code in `upstream/master`.
## Sending a Pull Request
When you are happy with your changes and you are ready to contribute them, you will create a Pull Request on GitHub to do so.
This is done by pushing your local changes to your forked repository (default remote name is `origin`) and then initiating a pull request on GitHub.
Please include relevant issue ids, links, detailed information about the bug/feature, what all tests are executed, how the reviewer can test this feature etc. A screenshot is preferred.
> **IMPORTANT:** Make sure you have rebased your `feature_x` branch to include the latest code from `upstream/master` _before_ you do this.
``` bash
$ git push origin master
$ git push origin feature_x
```
Now that the `feature_x` branch has been pushed to your GitHub repository, you can initiate the pull request.
To initiate the pull request, do the following:
1. In your browser, navigate to your forked repository: [https://github.com/YOUR_ACCOUNT/cloudstack](https://github.com/YOUR_ACCOUNT/cloudstack)
2. Click the new button called '**Compare & pull request**' that showed up just above the main area in your forked repository
3. Validate the pull request will be into the upstream `master` and will be from your `feature_x` branch
4. Enter a detailed description of the work you have done and then click '**Send pull request**'
If you are requested to make modifications to your proposed changes, make the changes locally on your `feature_x` branch, re-push the `feature_x` branch to your fork. The existing pull request should automatically pick up the change and update accordingly.
Cleaning up after a successful pull request
-------------------------------------------
Once the `feature_x` branch has been committed into the `upstream/master` branch, your local `feature_x` branch and the `origin/feature_x` branch are no longer needed. If you want to make additional changes, restart the process with a new branch.
> **IMPORTANT:** Make sure that your changes are in `upstream/master` before you delete your `feature_x` and `origin/feature_x` branches!
You can delete these deprecated branches with the following:
``` bash
$ git checkout master
$ git branch -D feature_x
$ git push origin :feature_x
``

48
ui/Dockerfile Normal file
View File

@ -0,0 +1,48 @@
# 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.
# Build example: docker build -t <name> .
FROM node:lts-stretch AS build
MAINTAINER "Apache CloudStack" <dev@cloudstack.apache.org>
LABEL Description="Apache CloudStack UI; Modern role-base progressive UI for Apache CloudStack"
LABEL Vendor="Apache.org"
LABEL License=ApacheV2
LABEL Version=0.5.0
WORKDIR /build
RUN apt-get -y update && apt-get -y upgrade
COPY . /build/
RUN npm install
RUN npm run build
FROM nginx:alpine AS runtime
LABEL org.opencontainers.image.title="Apache CloudStack UI" \
org.opencontainers.image.description="A modern role-based progressive CloudStack UI" \
org.opencontainers.image.authors="Apache CloudStack Contributors" \
org.opencontainers.image.url="https://github.com/apache/cloudstack" \
org.opencontainers.image.documentation="https://github.com/apache/cloudstack/blob/master/ui/README.md" \
org.opencontainers.image.source="https://github.com/apache/cloudstack" \
org.opencontainers.image.vendor="The Apache Software Foundation" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.ref.name="latest"
COPY --from=build /build/dist/. /usr/share/nginx/html/

201
ui/LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

186
ui/README.md Normal file
View File

@ -0,0 +1,186 @@
# CloudStack UI
A modern role-based progressive CloudStack UI based on VueJS and Ant Design.
![Screenshot](ui/docs/screenshot-dashboard.png)
## Getting Started
Install node: (Debian/Ubuntu)
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs
# Or use distro provided: sudo apt-get install npm nodejs
Install node: (CentOS/Fedora/RHEL)
curl -sL https://rpm.nodesource.com/setup_12.x | sudo bash -
sudo yum install nodejs
Optionally, you may also install system-wide dev tools:
sudo npm install -g @vue/cli npm-check-updates
## Development
Clone the repository:
git clone https://github.com/apache/cloudstack.git
cd cloudstack/ui
npm install
Override the default `CS_URL` to a running CloudStack management server:
cp .env.local.example .env.local
Change the `CS_URL` in the `.env.local` file
To configure https, you may use `.env.local.https.example`.
Build and run:
npm run serve
# Or run: npm start
Upgrade dependencies to the latest versions:
ncu -u
Run Tests:
npm run test
npm run lint
npm run test:unit
Fix issues and vulnerabilities:
npm audit
A basic development guide and explaination of the basic components can be found
[here](docs/development.md)
## Production
Fetch dependencies and build:
npm install
npm run build
This creates a static webpack application in `dist/`, which can then be served
from any web server or CloudStack management server (jetty).
To use CloudStack management server (jetty), you may copy the built UI to the
webapp directory on the management server host. For example:
npm install
npm run build
cd dist
mkdir -p /usr/share/cloudstack-management/webapp/
cp -vr . /usr/share/cloudstack-management/webapp/
# Access UI at {management-server}:8080/client in browser
If the webapp directory is changed, please change the `webapp.dir` in the
`/etc/cloudstack/management/server.properties` and restart the management server host.
To use a separate webserver, note that the API server is accessed through the path
`/client`, which needs be forwarded to an actual CloudStack instance.
For example, a simple way to serve UI with nginx can be implemented with the
following nginx configuration (to be put into /etc/nginx/conf.d/default.conf or similar):
```nginx
server {
listen 80;
server_name localhost;
location / {
# /src/ui/dist contains the built UI webpack
root /src/ui/dist;
index index.html;
}
location /client/ {
# http://127.0.0.1:800 should be replaced your CloudStack management
# server's actual URI
proxy_pass http://127.0.0.1:8000;
}
}
```
### Docker
A production-ready Docker container can also be built with the provided
Dockerfile and build script.
Make sure Docker is installed, then run:
bash docker.sh
Change the example configuration in `nginx/default.conf` according to your needs.
Run UI:
docker run -ti --rm -p 8080:80 -v $(pwd)/nginx:/etc/nginx/conf.d:ro cloudstack-ui:latest
### Packaging
The following is tested to work on any Ubuntu 18.04/20.04 base installation or
docker container:
# Install nodejs (lts)
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs debhelper rpm
# Install build tools
npm install -g @vue/cli webpack eslint
# Clone this repository and run package.sh
cd <cloned-repository>/packaging
bash -x package.sh
## Documentation
- VueJS Guide: https://vuejs.org/v2/guide/
- Vue Ant Design: https://www.antdv.com/docs/vue/introduce/
- UI Developer [Docs](docs)
- JavaScript ES6 Reference: https://www.tutorialspoint.com/es6/
- Introduction to ES6: https://scrimba.com/g/gintrotoes6
## Attributions
The UI uses the following:
- [VueJS](https://vuejs.org/)
- [Ant Design Spec](https://ant.design/docs/spec/introduce)
- [Ant Design Vue](https://vue.ant.design/)
- [Ant Design Pro Vue](https://github.com/sendya/ant-design-pro-vue)
- [Fontawesome](https://github.com/FortAwesome/vue-fontawesome)
- [ViserJS](https://viserjs.github.io/docs.html#/viser/guide/installation)
- [Icons](https://www.iconfinder.com/iconsets/cat-force) by [Iconka](https://iconka.com/en/downloads/cat-power/)
## History
The modern UI, originally called Primate, was created by [Rohit
Yadav](https://rohityadav.cloud) over several weekends during late 2018 and
early 2019. During ApacheCon CCCUS19, on 9th September 2019, Primate was
introduced and demoed as part of the talk [Modern UI
for CloudStack](https://rohityadav.cloud/files/talks/cccna19-primate.pdf)
([video](https://www.youtube.com/watch?v=F2KwZhechzs)).
[Primate](https://markmail.org/message/vxnskmwhfaagnm4r) was accepted by the
Apache CloudStack project on 21 Oct 2019. The original repo was [merged with the
main apache/cloudstack](https://markmail.org/message/bgnn4xkjnlzseeuv) repo on
20 Jan 2021.
## License
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.

38
ui/babel.config.js Normal file
View File

@ -0,0 +1,38 @@
// 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.
const babelConfig = {
presets: [
'@vue/app'
],
plugins: []
// if your use import on Demand, Use this code
// ,
// plugins: [
// [ 'import', {
// 'libraryName': 'ant-design-vue',
// 'libraryDirectory': 'es',
// 'style': true
// } ]
// ]
}
if (process.env.NODE_ENV === 'test') {
babelConfig.plugins.push('require-context-hook')
}
module.exports = babelConfig

33
ui/docker.sh Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash
# 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.
set -e
set -x
cd $(dirname $0)
GIT_TAG="$(git tag --points-at | head -n 1)"
if [ -n "${GIT_REV}" ]; then
LABEL_GIT_TAG="--label \"org.opencontainers.image.version=${GIT_TAG}\""
fi
DATE="$(date --iso-8601=seconds)"
LABEL_DATE="--label \"org.opencontainers.image.created=${DATE}\""
GIT_REV="$(git rev-parse HEAD)"
LABEL_GIT_REV="--label \"org.opencontainers.image.revision=${GIT_REV}\""
docker build -t cloudstack-ui ${LABEL_DATE} ${LABEL_GIT_REV} ${LABEL_GIT_TAG} .

85
ui/docs/customize.md Normal file
View File

@ -0,0 +1,85 @@
# UI customization
Use a `public/config.json` (or `dist/config.json` after build) file for customizing theme, logos,...
## Images
Change the image of the logo, login banner, error page, etc.
```json
{
"logo": "assets/logo.svg",
"banner": "assets/banner.svg",
"error": {
"404": "assets/404.png",
"403": "assets/403.png",
"500": "assets/500.png"
}
}
```
- `logo` changes the logo top-left side image.
- `banner` changes the login banner image.
- `error.404` change the image of error Page not found.
- `error.403` change the image of error Forbidden.
- `error.500` change the image of error Internal Server Error.
## Theme
Customize themes like colors, border color, etc.
```json
{
"theme": {
"@primary-color": "#1890ff",
"@success-color": "#52c41a",
"@processing-color": "#1890ff",
"@warning-color": "#faad14",
"@error-color": "#f5222d",
"@font-size-base": "14px",
"@heading-color": "rgba(0, 0, 0, 0.85)",
"@text-color": "rgba(0, 0, 0, 0.65)",
"@text-color-secondary": "rgba(0, 0, 0, 0.45)",
"@disabled-color": "rgba(0, 0, 0, 0.25)",
"@border-color-base": "#d9d9d9",
"@logo-width": "256px",
"@logo-height": "64px",
"@banner-width": "700px",
"@banner-height": "110px",
"@error-width": "256px",
"@error-height": "256px"
}
}
```
- `@logo-background-color` changes the logo background color.
- `@project-nav-background-color` changes the navigation menu background color of the project .
- `@project-nav-text-color` changes the navigation menu background color of the project view.
- `@navigation-background-color` changes the navigation menu background color.
- `@navigation-text-color` changes the navigation text color.
- `@primary-color` change the major background color of the page (background button, icon hover, etc).
- `@link-color` changes the link color.
- `@link-hover-color` changes the link hover color.
- `@loading-color` changes the message loading color and page loading bar at the top page.
- `@success-color` change success state color.
- `@processing-color` change processing state color. Exp: progress status.
- `@warning-color` change warning state color.
- `@error-color` change error state color.
- `@heading-color` change table header color.
- `@text-color` change in major text color.
- `@text-color-secondary` change of secondary text color (breadcrumb icon).
- `@disabled-color` change disable state color (disabled button, switch, etc).
- `@border-color-base` change in major border color.
- `@logo-width` change the width of the logo top-left side.
- `@logo-height` change the height of the logo top-left side.
- `@banner-width` changes the width of the login banner.
- `@banner-height` changes the height of the login banner.
- `@error-width` changes the width of the error image.
- `@error-height` changes the height of the error image.
Assorted primary theme colours:
- Blue: #1890FF
- Red: #F5222D
- Yellow: #FAAD14
- Cyan: #13C2C2
- Green: #52C41A
- Purple: #722ED1
Also, to add other properties, we can add new properties into `theme.config.js` based on the Ant Design Vue Less variable.
Refer: https://www.antdv.com/docs/vue/customize-theme/#Ant-Design-Vue-Less-variables

232
ui/docs/development.md Normal file
View File

@ -0,0 +1,232 @@
# UI Development
The modern CloudStack UI is role-based progressive app that uses VueJS and Ant Design.
Javascript, VueJS references:
- https://www.w3schools.com/js/
- https://www.geeksforgeeks.org/javascript-tutorial/
- https://vuejs.org/v2/guide/
- https://www.youtube.com/watch?v=Wy9q22isx3U
All the source is in the `src` directory with its entry point at `main.js`.
The following tree shows the basic UI codebase filesystem:
```bash
src
├── assests # sprites, icons, images
├── components # Shared vue files used to render various generic / widely used components
├── config # Contains the layout details of the various routes / sections available in the UI
├── locales # Custom translation keys for the various supported languages
├── store # A key-value storage for all the application level state information such as user info, etc
├── utils # Collection of custom libraries
├── views # Custom vue files used to render specific components
├── ...
└── main.js # Main entry-point
```
## Development
Clone the repository:
```
git clone https://github.com/apache/cloudstack.git
cd cloudstack/ui
npm install
```
Override the default `CS_URL` to a running CloudStack management server:
```
cp .env.local.example .env.local
```
Change the `CS_URL` in the `.env.local` file
To configure https, you may use `.env.local.https.example`.
Build and run:
```
npm run serve
```
## Implementation
## Defining a new Section
### Section Config Definition
A new section may be added in `src/config/section` and in `src/config/router.js`,
import the new section's (newconfig.js as example) configuration file and rules to
`asyncRouterMap` as:
import newconfig from '@/config/section/newconfig'
[ ... snipped ... ]
generateRouterMap(newSection),
### Section
An existing or new section's config/js file must export the following parameters:
- `name`: Unique path in URL
- `title`: The name to be displayed in navigation and breadcrumb
- `icon`: The icon to be displayed, from AntD's icon set
https://vue.ant.design/components/icon/
- `docHelp`: Allows to provide a link to a document to provide details on the
section
- `searchFilters`: List of parameters by which the resources can be filtered
via the list API
- `children`: (optional) Array of resources sub-navigation under the parent
group
- `permission`: When children are not defined, the array of APIs to check against
allowed auto-discovered APIs
- `columns`: When children is not defined, list of column keys
- `component`: When children is not defined, the custom component for rendering
the route view
See `src/config/section/compute.js` and `src/config/section/project.js` for example.
The children should have:
- `name`: Unique path in the URL
- `title`: The name to be displayed in navigation and breadcrumb
- `icon`: The icon to be displayed, from AntD's icon set
https://vue.ant.design/components/icon/
- `permission`: The array of APIs to check against auto-discovered APIs
- `columns`: List of column keys for list view rendering
- `details`: List of keys for detail list rendering for a resource
- `tabs`: Array of custom components that will get rendered as tabs in the
resource view
- `component`: The custom component for rendering the route view
- `related`: A list of associated entitiy types that can be listed via passing
the current resource's id as a parameter in their respective list APIs
- `actions`: Array of actions that can be performed on the resource
## Custom Actions
The actions defined for children show up as group of buttons on the default
autogen view (that shows tables, actions etc.). Each action item should define:
- `api`: The CloudStack API for the action. The action button will be hidden if
the user does not have permission to execute the API
- `icon`: The icon to be displayed, from AntD's icon set
https://vue.ant.design/components/icon/
- `label`: The action button name label and modal header
- `message`: The action button confirmation message
- `docHelp`: Allows to provide a link to a document to provide details on the
action
- `listView`: (boolean) Whether to show the action button in list view (table).
Defaults to false
- `dataView`: (boolean) Whether to show the action button in resource/data view.
Defaults to false
- `args`: List of API arguments to render/show on auto-generated action form.
Can be a function which returns a list of arguments
- `show`: Function that takes in a records and returns a boolean to control if
the action button needs to be shown or hidden. Defaults to true
- `groupShow`: Same as show but for group actions. Defaults to true
- `popup`: (boolean) When true, displays any custom component in a popup modal
than in its separate route view. Defaults to false
- `groupAction`: Whether the button supports groupable actions when multiple
items are selected in the table. Defaults to false
- `mapping`: The relation of an arg to an api and the associated parameters to
be passed and filtered on the result (from which its id is used as a
select-option) or a given hardcoded list of select-options
- `groupMap`: Function that maps the args and returns the list of parameters to
be passed to the api
- `component`: The custom component to render the action (in a separate route
view under src/views/<component>). Uses an autogenerated form by default.
Examples of such views can be seen in the src/views/ directory
For Example:
```
{
api: 'startVirtualMachine',
icon: 'caret-right',
label: 'label.action.start.instance',
message: 'message.action.start.instance',
docHelp: 'adminguide/virtual_machines.html#stopping-and-starting-vms',
dataView: true,
groupAction: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) },
show: (record) => { return ['Stopped'].includes(record.state) },
args: (record, store) => {
var fields = []
if (store.userInfo.roletype === 'Admin') {
fields = ['podid', 'clusterid', 'hostid']
}
if (record.hypervisor === 'VMware') {
if (store.apis.startVirtualMachine.params.filter(x => x.name === 'bootintosetup').length > 0) {
fields.push('bootintosetup')
}
}
return fields
},
response: (result) => { return result.virtualmachine && result.virtualmachine.password ? `Password of the VM is ${result.virtualmachine.password}` : null }
}
```
## Resource List View
After having, defined a section and the actions that can be performed in the
particular section; on navigating to the section, we can have a list of
resources available, for example, on navigating to **Compute > Instances**
section, we see a list of all the VM instances (each instance referred to as a
resource).
The columns that should be made available while displaying the list of
resources can be defined in the section's configuration file under the
columns attribute (as mentioned above). **columns** maybe defined as an array
or a function in case we need to selectively (i.e., based on certain
conditions) restrict the view of certain columns.
It also contains router-links to the resouce and other related data such as the
account, domain, etc of the resource if present
For example:
```
...
// columns defined as an array
columns: ['name', 'state', 'displaytext', 'account', 'domain'],
// columns can also be defined as a function, so as to conditionally restrict view of certain columns
columns: () => {
var fields = ['name', 'hypervisor', 'ostypename']
if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
fields.push('account')
}
...
}
```
## Resource Detail View Customization
From the List View of the resources, on can navigate to the individual
resource's detail view, which in CloudStack UI we refer to as the
*Resource View* by click on the specific resource.
The Resource View has 2 sections:
- InfoCard to the left that has basic / minimal details of that resource along
with the related entities
- DetailsTab to the right which provide the basic details about the resource.
Custom tabs to render custom details, addtional information of the resource
The list of fields to be displayed maybe defined as an array
or a function in case we need to selectively (i.e., based on certain
conditions) restrict the view of certain columns. The names specified in the
details array should correspond to the api parameters
For example,
```
...
details: ['name', 'id', 'displaytext', 'projectaccountname', 'account', 'domain'],
...
// To render the above mentioned details in the right section of the Resource View, we must import the DetailsTab
tabs: [
{
name: 'details',
component: () => import('@/components/view/DetailsTab.vue')
},
...
]
```
Additional tabs can be defined by adding on to the tabs section.

View File

@ -0,0 +1,661 @@
---
name: Full Test Plan
about: Create a high level full-test plan
title: "[TESTPLAN] Full Test Plan for $Version for $Role, $Hypervisor, ACS $Version"
labels: testing
---
Note: for User role test exclude after Account/User feature, for DomainAdmin role exclude after Infrastructure (except for Offerings)
**Common**
- [ ] Project selector
- [ ] Language selector
- [ ] Notifications / clear notifications
- [ ] Paginations
- [ ] Profile
- [ ] Help
- [ ] Logout
- [ ] Context-sensitive help
**Dashboard**
- [ ] Fetch latest (only on Admin dashboard)
- [ ] View hosts in alert state
- [ ] View alerts
- [ ] View events
**Compute > Instances**
- [ ] Basic search
- [ ] Extended search
- [ ] Sort
- [ ] Links
- [ ] Filter by
- [ ] Create new instance
**Compute > Kubernetes**
This requires configuring and setting up CKS: http://docs.cloudstack.apache.org/en/latest/plugins/cloudstack-kubernetes-service.html
- [ ] Basic search
- [ ] Extended search
- [ ] Sort
- [ ] Links
- [ ] Filter by
- [ ] Add Kubernetes cluster
- [ ] Start/stop a Kubernetes cluster
- [ ] Scale Kubernetes cluster
- [ ] Upgrade Kubernetes cluster
- [ ] Delete Kubernetes cluster
**Compute > Instances > selected instance**
- [ ] View console
- [ ] Reboot instance
- [ ] Update instance
- [ ] Start/Stop instance
- [ ] Reinstall instance
- [ ] Take snapshot
- [ ] Assign VM to backup offering
- [ ] Attach ISO
- [ ] Scale VM
- [ ] Migrate instance to another host
- [ ] Change affinity
- [ ] Change service offering
- [ ] Reset Instance Password
- [ ] Assign Instance to Another Account (VM must be stopped)
- [ ] Network adapters
- [ ] - Add network to VM
- [ ] - Set default NIC
- [ ] - Add/delete secondary IP address
- [ ] - Delete VM network
- [ ] Settings
- [ ] - Add setting
- [ ] - Update setting
- [ ] - Delete setting
- [ ] Add / delete comment
- [ ] Add / delete tags
- [ ] Links
**Compute > Instance groups**
- [ ] Search
- [ ] Sort
- [ ] Links
- [ ] New instance group
**Compute > Instance groups > selected instance group**
- [ ] Links
- [ ] Update instance group
- [ ] Delete instance group
**Compute > SSH Key Pairs**
- [ ] Search
- [ ] Sorting
- [ ] Links
- [ ] New SSH key pair
**Compute > SSH Key Pairs > selected SSH key pair**
- [ ] Links
- [ ] Delete SSH key pair
**Compute > Affinity Groups**
- [ ] Search
- [ ] Sort
- [ ] Links
- [ ] New affinity group
**Compute > Affinity Groups > selected affinity group**
- [ ] Links
- [ ] Delete affinity group
**Storage > Volumes**
- [ ] Basic earch
- [ ] Extended search
- [ ] Sort
- [ ] Links
- [ ] Create volume
- [ ] Upload local volume
- [ ] Upload volume from URL
**Storage > Volumes > selected volume**
- [ ] Detach volume
- [ ] Take snapshot
- [ ] Recurring snapshot
- [ ] Resize volume
- [ ] Migrate volume
- [ ] Download volume
- [ ] Delete volume
- [ ] Links
- [ ] Add/delete tags
**Storage > Snapshots**
- [ ] Basic search
- [ ] Extended search
- [ ] Sort
- [ ] Links
**Storage > Snapshots > selected snapshot**
- [ ] Links
- [ ] Add/delete tags
- [ ] Create template
- [ ] Create volume
- [ ] Revert snapshot
- [ ] Delete snapshot
**Storage > VM Snapshots**
- [ ] Basic search
- [ ] Extended search
- [ ] Sort
- [ ] Links
**Storage > VM Snapshots > selected snapshot**
- [ ] Links
- [ ] Add/delete tags
- [ ] Revert VM snapshot
- [ ] Delete VM snapshot
**Storage > Backups**
- [ ] Import offering
- [ ] Configure backup provider (Veeam)
- [ ] Create backup offering
- [ ] Assign VM to backup offering
- [ ] Revert to backup
- [ ] Delete backup
**Network > Guest networks**
- [ ] Basic search
- [ ] Extended search
- [ ] Sort
- [ ] Links
- [ ] Add network
**Network > Guest networks > selected network**
- [ ] Links
- [ ] Add/delete tags
- [ ] Update network
- [ ] Restart network
- [ ] Delete network
- [ ] Acquire new IP (only for isolated networks)
- [ ] Replace ACL list(only for VPC isolated networks)
- [ ] Delete public IP address (only for isolated networks)
- [ ] Add/delete egress rule (only for isolated networks)
**Network > VPC **
- [ ] Basic search
- [ ] Extended search
- [ ] Sort
- [ ] Links
- [ ] Add VPC
**Network > VPC > selected VPC**
- [ ] Links
- [ ] Update VPC
- [ ] Restart VPC
- [ ] Delete VPC
- [ ] Networks
- [ ] - Links
- [ ] - Paginations
- [ ] - Add network
- [ ] - Add internal LB
- [ ] Public IP addresses
- [ ] - Links
- [ ] - Pagination
- [ ] - Select tier
- [ ] - Acquire new IP
- [ ] - Delete IP address
- [ ] Network ACL Lists
- [ ] - Links
- [ ] - Pagination
- [ ] - Add network ACL list
- [ ] Private Gateways
- [ ] - Links
- [ ] - Pagination
- [ ] - Add private gateway
- [ ] VPN Gateway
- [ ] - Links
- [ ] VPN Connections
- [ ] - Links
- [ ] - Pagination
- [ ] - Create Site-to-site VPN connection
- [ ] Virtual routers
- [ ] - Links
- [ ] Add/delete tags
**Network > Security groups**
- [ ] Search
- [ ] Sort
- [ ] Links
- [ ] Add security group
**Network > Security groups > selected security group**
- [ ] Links
- [ ] Add/delete tags
- [ ] Add ingress rule by CIDR
- [ ] Add ingress rule by Account
- [ ] Ingress rule - add/delete tags
- [ ] Ingress rule - delete
- [ ] Add egress rule by CIDR
- [ ] Add egress rule by Account
- [ ] Egress rule - add/delete tags
- [ ] Egress rule - delete
- [ ] Ingress/egress rules pagination
**Network > Public IP Addresses**
- [ ] Search
- [ ] Sort
- [ ] Links
- [ ] Acquire new IP
**Network > Public IP Addresses > selected IP address**
- [ ] Links
- [ ] Add/delete tags
- [ ] Enable/Disable static NAT
- [ ] Release IP
- [ ] Firewall - add rule
- [ ] Firewall rule - add/delete tags
- [ ] Firewall rule - delete
- [ ] VPN - Enable/Disable VPN
- [ ] VPN - Manage VPN Users
**Network > VPN Users**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Add VPN user
**Network > VPN Users > selected VPN user**
- [ ] Links
- [ ] Delete VPN User
**Network > VPN Customer Gateway**
- [ ] Links
- [ ] Basic search
- [ ] Extended search
- [ ] Sort
- [ ] Add VPN Customer Gateway
**Network > VPN Customer Gateway > selected gateway**
- [ ] Links
- [ ] Edit VPN Customer Gateway
- [ ] Delete VPN Customer Gateway
- [ ] Add/delete tags
**Images > Templates**
- [ ] Links
- [ ] Basic search
- [ ] Extended search
- [ ] Sort
- [ ] Change order (move to the top/bottom, move one row up/down)
- [ ] Register template
- [ ] Upload local template
**Images > Templates > selected template**
- [ ] Links
- [ ] Add/delete tags
- [ ] Edit template
- [ ] Copy template
- [ ] Update template permissions
- [ ] Delete template
- [ ] Download template
- [ ] Zones pagination
- [ ] Settings - add/edit/remove setting
**Images > ISOs**
- [ ] Links
- [ ] Basic search
- [ ] Extended search
- [ ] Sort
- [ ] Change order (move to the top/bottom, move one row up/down)
- [ ] Register ISO
- [ ] Upload local ISO
**Images > ISOs > selected ISO**
- [ ] Links
- [ ] Add/delete tags
- [ ] Edit ISO
- [ ] Download ISO
- [ ] Update ISO permissions
- [ ] Copy ISO
- [ ] Delete ISO
- [ ] Zones - pagination
**Images > Kubernetes ISOs**
- [ ] Links
- [ ] Basic search
- [ ] Sort
- [ ] Refresh
- [ ] Pagination
- [ ] Enable/Disable
- [ ] Add Kubernetes Version
**Projects**
- [ ] Links
- [ ] Basic search
- [ ] Extended search
- [ ] Sort
- [ ] Switch to project
- [ ] New project
- [ ] Enter token
- [ ] Project invitations
**Projects > selected project**
- [ ] Links
- [ ] Add/delete tags
- [ ] Edit project
- [ ] Suspend/Activate project
- [ ] Add account to project
- [ ] Accounts - Make account project owner
- [ ] Accounts - Remove account from project
- [ ] Delete project
- [ ] Accounts - pagination
- [ ] Resources - edit
**Events**
- [ ] Links
- [ ] Basic search
- [ ] Extended search
- [ ] Sort
- [ ] Archive event
- [ ] Delete event
**Events > selected event**
- [ ] Links
- [ ] Archive event
- [ ] View event timeline
- [ ] Delete event
**Users**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Add user
**Accounts**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Add account
- [ ] Add LDAP account
**Accounts > selected account**
- [ ] Links
- [ ] Update account
- [ ] Update resource count
- [ ] Disable/enable account
- [ ] Lock/unlock account
- [ ] Add certificate
- [ ] Delete account
- [ ] Settings
**Users > selected user**
- [ ] Links
- [ ] Edit user
- [ ] Change password
- [ ] Generate keys
- [ ] Disable/enable user
- [ ] Delete user
- [ ] Copy API Key
- [ ] Copy Secret Key
**Domains**
- [ ] Search
- [ ] Expand/collapse
- [ ] Add/delete note
- [ ] Add domain
- [ ] Edit domain
- [ ] Delete domain
- [ ] Update resource count
- [ ] Link domain to LDAP Group/OU
- [ ] Settings
**Roles**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Create role
**Roles > selected role**
- [ ] Edit role
- [ ] Delete role
- [ ] Rules - add new rule
- [ ] Rules - modify rule
- [ ] Rules - delete rule
- [ ] Rules - change rules order
**Infrastructure > Summary**
- [ ] Links
- [ ] Setup SSL certificate
**Infrastructure > Zones**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Pagination
- [ ] Add zone
**Infrastructure > Zones > selected zone**
- [ ] Links
- [ ] Edit zone
- [ ] Enable/disable zone
- [ ] Enable/disable out-of-band management
- [ ] Enable HA (disable?)
- [ ] Add VMWare datacenter
- [ ] Delete zone
- [ ] Settings - edit
**Infrastructure > Pods**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Add Pod
**Infrastructure > Pods > selected Pod**
- [ ] Links
- [ ] Dedicate/Release Pod
- [ ] Edit Pod
- [ ] Disable/enable Pod
- [ ] Delete Pod
**Infrastructure > Clusters**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Add Cluster
**Infrastructure > Clusters > selected cluster**
- [ ] Links
- [ ] Dedicate/Release cluster
- [ ] Enable/disable cluster
- [ ] Manage/unmanage cluster
- [ ] Enable/disable out-of-band management
- [ ] Enable/disable HA
- [ ] Configure HA
- [ ] Delete cluster
- [ ] Settings - edit
**Infrastructure > Hosts**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Add host
**Infrastructure > Hosts > selected host**
- [ ] Links
- [ ] Add/delete notes
- [ ] Dedicate/release host
- [ ] Edit host
- [ ] Force reconnect
- [ ] Disable/enable host
- [ ] Enable/cancel maintenance mode
- [ ] Enable/disable out-of-band management
- [ ] Enable/disale HA
- [ ] Delete host (only if disabled)
**Infrastructure > Primary Storage**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Add Primary storage
**Infrastructure > Primary Storage > selected primary storage**
- [ ] Links
- [ ] Edit primary storage
- [ ] Enable/cancel maintenance mode
- [ ] Delete primary storage
- [ ] Settings - edit
**Infrastructure > Secondary Storage**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Add Secondary storage
**Infrastructure > Secondary Storage > selected secondary storage**
- [ ] Links
- [ ] Delete secondary storage
- [ ] Settings - edit
**Infrastructure > System VMs**
- [ ] Links
- [ ] Search
- [ ] Sort
**Infrastructure > System VMs > selected system VM**
- [ ] Links
- [ ] View console
- [ ] Start/Stop system VM
- [ ] Reboot system VM
- [ ] Change service offering
- [ ] Migrate system VM
- [ ] Run diagnostics
- [ ] Get diagnostics data
- [ ] Destroy system VM
**Infrastructure > Virtual routers**
- [ ] Links
- [ ] Search
- [ ] Sort
**Infrastructure > Virtual routers > selected virtual router**
- [ ] Links
- [ ] View console (running)
- [ ] Start/Stop router
- [ ] Reboot router
- [ ] Change service offering
- [ ] Migrate router (running)
- [ ] Run diagnostics (running)
- [ ] Get diagnostics data
- [ ] Destroy router
**Infrastructure > Internal LB VMs**
- [ ] Links
- [ ] Search
- [ ] Sort
**Infrastructure > Internal LB VMs > selected internal LB VM**
- [ ] Links
- [ ] View console
- [ ] Stop router
- [ ] Migrate router
**Infrastructure > CPU Sockets**
- [ ] Search
- [ ] Sort
**Infrastructure > Management servers**
- [ ] Links
- [ ] Search
- [ ] Sort
**Infrastructure > Management servers > selected management server**
**Infrastructure > Alerts**
- [ ] Links
- [ ] Search
- [ ] Sort
**Infrastructure > Alerts > selected alert**
- [ ] Archive alert
- [ ] Delete alert
**Offerings > Compute offerings**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Add offering
**Offerings > Compute offerings > selected offering**
- [ ] Links
- [ ] Edit offering
- [ ] Update offering access
- [ ] Delete offering
**Offerings > System offerings**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Change order (move to the top/bottom, move one row up/down)
- [ ] Add offering
**Offerings > System offerings > selected offering**
- [ ] Edit offering
- [ ] Delete offering
**Offerings > Disk offerings**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Change order (move to the top/bottom, move one row up/down)
- [ ] Add offering
**Offerings > Disk offerings > selected offering**
- [ ] Links
- [ ] Edit offering
- [ ] Update offering access
- [ ] Delete offering
**Offerings > Backup offerings**
**Offerings > Network offerings**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Change order (move to the top/bottom, move one row up/down)
- [ ] Add offering
**Offerings > Network offerings > selected offering**
- [ ] Edit offering
- [ ] Enable/Disable offering
- [ ] Update offering access
- [ ] Delete offering
**Offerings > VPC offerings**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Change order
- [ ] Add offering
**Offerings > VPC offerings > selected offering**
- [ ] Links
- [ ] Add / delete tags
- [ ] Edit offering
- [ ] Enable/Disable offering
- [ ] Update offering access
- [ ] Delete offering
**Configuration > Global settings**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Edit value
**Configuration > LDAP Configuration**
- [ ] Links
- [ ] Search
- [ ] Sort
- [ ] Configure LDAP
**Configuration > LDAP Configuration > selected LDAP configuration**
- [ ] TBD
**Configuration > Hypervisor capabilities**
- [ ] Data
- [ ] Search
- [ ] Sort

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -0,0 +1,181 @@
---
name: Smoke Test Plan
about: Create a smoke test plan for a release
title: "[TESTPLAN] Smoketest for $VERSION with $Role, $Hypervisor and ACS $Version"
labels: testing
---
Note: for User role test exclude after Account/User feature, for DomainAdmin role exclude after Infrastructure (except for Offerings)
**Instances**
- [ ] Create instance using template
- [ ] Create instance using ISO image and different parameters than the previous one
- [ ] Test all VM actions - Start/Stop/Reboot/Reinstall/Update, etc
- [ ] Add/modify/delete VM setting
- [ ] Add network to VM, change IP address, make it default, delete
- [ ] Add/delete secondary IP address
**Compute > Kubernetes**
This requires configuring and setting up CKS: http://docs.cloudstack.apache.org/en/latest/plugins/cloudstack-kubernetes-service.html
- [ ] Add Kubernetes cluster
- [ ] Start/stop a Kubernetes cluster
- [ ] Scale Kubernetes cluster
- [ ] Upgrade Kubernetes cluster
- [ ] Delete Kubernetes cluster
**Add Instance groups**
- [ ] Add/modify/delete instance group
**SSH Key Pairs**
- [ ] Add/delete SSH key pair
**Affinity Groups**
- [ ] Add/delete host affinity group
- [ ] Add/delete host anti-affinity group
**Volumes**
- [ ] Create volume
- [ ] Upload local volume
- [ ] Upload volume from URL
- [ ] Volume actions - snapshots, resize, migrate, download, create template
**Snapshots**
- [ ] Snapshot actions - create template/volume, revert, delete
**VM Snapshots**
- [ ] VM Snapshot actions - revert, delete
**Backups**
**Guest networks**
- [ ] Add isolated network
- [ ] Add L2 network
- [ ] Add shared network
- [ ] Network actions - update, restart, replace ACL list, delete
- [ ] Add/delete egress rules
- [ ] Acquire IP address
**VPC**
- [ ] Add VPC
- [ ] VPC actions - updat, restart, delete
- [ ] Add security group
- [ ] Add/delete ingress/egress rule
**Public IP Addresses**
- [ ] Acquire new IP
- [ ] Actions - enable static NAT, release IP, enable VPN
**Templates**
- [ ] Register template
- [ ] Upload local template
- [ ] Template actions - edit, download, update permissions, copy, delete
**ISOs**
- [ ] Register ISO
- [ ] Upload local ISO
- [ ] ISO actions - edit, download update permissions, copy, delete
**Events**
- [ ] Search, archive, delete
**Projects**
- [ ] Add project
- [ ] Project actions - edit, suspend, add account, delete
- [ ] Different projects with different permission
**Accounts, users, roles**
- [ ] Create/modify/check role/delete regular user account
- [ ] Create/modify/check role/delete resource admin account
- [ ] Create/modify/check role/delete domain admin account
- [ ] Create/modify/check role/delete admin user
- [ ] Account actions - edit, disable, lock, delete
**Domains**
- [ ] Create new domain
- [ ] Create subdomain in the new domain
- [ ] Delete the first domain (2nd, not 3rd level)
- [ ] Edit/delete domain
- [ ] Modify domain limits/settings
**Roles**
- [ ] Add new role
- [ ] Role actions - edit, delete
**Infrastructure summary**
**Zones**
- [ ] Add zone
- [ ] Zone actions - edit, enable/disable, enable/disable HA, delete, etc.
- [ ] Modify settings
**Pods**
- [ ] Add pod
- [ ] Pod actions - edit, enable/disable, delete
**Clusters**
- [ ] Add cluster
- [ ] Cluster actions - enable/disable, unmanage, enable/disable HA, delete, etc
**Hosts**
- [ ] Add host
- [ ] Host actions - edit, enable/disable, maintenance mode, enable/disable/configure HA, etc.
**Primary storage**
- [ ] Add primary storage
- [ ] Primary storage actions - edit, enable/disable maintenance mode
- [ ] Settings - modify
**Secondary storage**
- [ ] Add secondary storage
- [ ] Delete secondary storage
- [ ] Settings - modify
**Compute offering**
- [ ] Add shared thin compute offering
- [ ] Add local fat compute offering
- [ ] Offering actions - edit, access, delete
**System offering**
- [ ] Add shared thin system offering for VR
- [ ] Add local sparse system offering for console proxy
- [ ] Offering actions - edit, delete
**Disk offering**
- [ ] Add shared thin disk offering
- [ ] Add local fat disk offering
- [ ] Offering actions - edit, access, delete
**Backup offering**
- [ ] Import offering
- [ ] Configure backup provider (Veeam)
- [ ] Create backup offering
- [ ] Assign VM to backup offering
- [ ] Revert to backup
- [ ] Delete backup
**Network offering**
- [ ] Add isolated network with some supported services
- [ ] Add L2 network
- [ ] Add shared network with some supported services
- [ ] Network actions - edit, enable/disable, access, delete
**VPC offering**
- [ ] Change VPC offerings order
- [ ] Add new VPC offering with some supported services
- [ ] VPC offering actions - edit, enable/disable, access, delete
**Global settings**
- [ ] Search setting
- [ ] Modify setting
**LDAP configuration**
- [ ] Add LDAP configuration
- [ ] Login with LDAP account
**Common functionality**
- [ ] Sorting
- [ ] Pagination
- [ ] Searching
- [ ] Add/remove tags
- [ ] Refresh
- [ ] Links

View File

@ -24,6 +24,6 @@
<title>Apache CloudStack</title>
</head>
<body>
<p>The legacy UI has been deprecated in this version as notified in the <a href="http://docs.cloudstack.apache.org/en/4.14.0.0/releasenotes/about.html#new-user-interface-depreciation-notice-of-existing-ui">previous release</a>. The legacy UI will be <a href="http://docs.cloudstack.apache.org/en/4.15.0.0/releasenotes/about.html#primate-ga-and-legacy-ui-deprecation-and-removal-notice">removed in the next release</a>.<br/>To access the legacy UI <a href="legacy">click here</a>.</p>
<p>You're in developer mode, please build and run UI using npm.</p>
</body>
</html>

53
ui/jest.config.js Normal file
View File

@ -0,0 +1,53 @@
// 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.
module.exports = {
testURL: 'http://localhost/',
setupFiles: ['<rootDir>/tests/setup.js'],
moduleFileExtensions: [
'js',
'jsx',
'json',
'vue'
],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|png|svg|jpg|ttf|woff|woff2)?$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapper: {
'.+\\.svg?.+$': 'jest-transform-stub',
'^@/(.*)$': '<rootDir>/src/$1',
'^@public/(.*)$': '<rootDir>/public/$1'
},
snapshotSerializers: [
'jest-serializer-vue'
],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
transformIgnorePatterns: [
'<rootDir>/node_modules/(?!ant-design-vue|vue)'
],
collectCoverage: true,
collectCoverageFrom: [
'<rootDir>/src/**/*.{js,vue}',
'!**/node_modules/**',
'!<rootDir>/src/locales/*.{js, json}'
],
coverageReporters: ['html', 'text-summary']
}

11
ui/jsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es6",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"],
"include": ["src/**/*"]
}

30
ui/nginx.conf Normal file
View File

@ -0,0 +1,30 @@
# 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.
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
}
location /client/ {
# http://127.0.0.1:8080 should be replaced your CloudStack management
# Server's actual URI
proxy_pass http://127.0.0.1:8080;
}
}

30
ui/nginx/default.conf Normal file
View File

@ -0,0 +1,30 @@
# 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.
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
}
location /client/ {
# http://127.0.0.1:8080 should be replaced your CloudStack management
# Server's actual URI
proxy_pass http://127.0.0.1:8080;
}
}

26493
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

165
ui/package.json Normal file
View File

@ -0,0 +1,165 @@
{
"name": "cloudstack-ui",
"description": "Modern role-based Apache CloudStack UI",
"version": "1.0.0",
"homepage": "https://cloudstack.apache.org/",
"repository": {
"type": "git",
"url": "https://github.com/apache/cloudstack.git"
},
"author": {
"name": "Apache CloudStack Developers",
"email": "dev@cloudstack.apache.org",
"url": "https://cloudstack.apache.org"
},
"license": "Apache-2.0",
"licenses": [
{
"type": "Apache-2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0"
}
],
"bugs": {
"mail": "dev@cloudstack.apache.org",
"url": "https://github.com/apache/cloudstack/issues"
},
"scripts": {
"start": "vue-cli-service lint --no-fix && vue-cli-service serve",
"serve": "vue-cli-service lint --no-fix && vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.34",
"@fortawesome/free-brands-svg-icons": "^5.15.2",
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/vue-fontawesome": "^2.0.2",
"ant-design-vue": "~1.7.2",
"antd-theme-webpack-plugin": "^1.3.7",
"axios": "^0.21.1",
"babel-plugin-require-context-hook": "^1.0.0",
"core-js": "^3.6.5",
"enquire.js": "^2.1.6",
"js-cookie": "^2.2.1",
"lodash": "^4.17.15",
"md5": "^2.2.1",
"moment": "^2.26.0",
"npm-check-updates": "^6.0.1",
"nprogress": "^0.2.0",
"viser-vue": "^2.4.8",
"vue": "^2.6.12",
"vue-clipboard2": "^0.3.1",
"vue-cropper": "0.5.6",
"vue-i18n": "^8.22.4",
"vue-ls": "^3.2.2",
"vue-router": "^3.4.9",
"vue-svg-component-runtime": "^1.0.1",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.0"
},
"devDependencies": {
"@vue/cli": "^4.4.1",
"@vue/cli-plugin-babel": "^4.4.1",
"@vue/cli-plugin-eslint": "^4.4.1",
"@vue/cli-plugin-unit-jest": "^4.4.1",
"@vue/cli-service": "^4.4.1",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/test-utils": "^1.0.3",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.3",
"babel-jest": "^25.1.0",
"babel-plugin-import": "^1.13.0",
"eslint": "^6.8.0",
"eslint-plugin-html": "^6.0.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.2.2",
"less": "^3.11.1",
"less-loader": "^5.0.0",
"node-sass": "^4.13.1",
"sass-loader": "^8.0.2",
"uglifyjs-webpack-plugin": "^2.2.0",
"vue-cli-plugin-i18n": "^1.0.1",
"vue-svg-icon-loader": "^2.1.1",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.43.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/strongly-recommended",
"@vue/standard"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {
"generator-star-spacing": "off",
"no-mixed-operators": 0,
"vue/max-attributes-per-line": [
2,
{
"singleline": 5,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}
],
"vue/attribute-hyphenation": 0,
"vue/html-self-closing": 0,
"vue/component-name-in-template-casing": 0,
"vue/html-closing-bracket-spacing": 0,
"vue/singleline-html-element-content-newline": 0,
"vue/no-unused-components": 0,
"vue/multiline-html-element-content-newline": 0,
"vue/no-use-v-if-with-v-for": 0,
"vue/html-closing-bracket-newline": 0,
"vue/no-parsing-error": 0,
"no-console": 0,
"no-tabs": 0,
"quotes": [
2,
"single",
{
"avoidEscape": true,
"allowTemplateLiterals": true
}
],
"semi": [
2,
"never",
{
"beforeStatementContinuationChars": "never"
}
],
"no-delete-var": 2,
"prefer-const": [
2,
{
"ignoreReadBeforeAssign": false
}
]
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"jest": {
"testEnvironment": "node"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
]
}

BIN
ui/public/assets/403.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
ui/public/assets/404.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
ui/public/assets/500.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

319
ui/public/assets/banner.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
ui/public/assets/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

332
ui/public/assets/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
ui/public/cloud.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

7700
ui/public/color.less vendored Normal file

File diff suppressed because it is too large Load Diff

50
ui/public/config.json vendored Normal file
View File

@ -0,0 +1,50 @@
{
"apiBase": "/client/api",
"docBase": "http://docs.cloudstack.apache.org/en/latest",
"appTitle": "CloudStack",
"footer": "Licensed under the <a href='http://www.apache.org/licenses/' target='_blank'>Apache License</a>, Version 2.0.",
"logo": "assets/logo.svg",
"banner": "assets/banner.svg",
"error": {
"404": "assets/404.png",
"403": "assets/403.png",
"500": "assets/500.png"
},
"theme": {
"@logo-background-color": "#ffffff",
"@navigation-background-color": "#ffffff",
"@project-nav-background-color": "#001529",
"@project-nav-text-color": "rgba(255, 255, 255, 0.65)",
"@navigation-text-color": "rgba(0, 0, 0, 0.65)",
"@primary-color": "#1890ff",
"@link-color": "#1890ff",
"@link-hover-color": "#40a9ff",
"@loading-color": "#1890ff",
"@processing-color": "#1890ff",
"@success-color": "#52c41a",
"@warning-color": "#faad14",
"@error-color": "#f5222d",
"@font-size-base": "14px",
"@heading-color": "rgba(0, 0, 0, 0.85)",
"@text-color": "rgba(0, 0, 0, 0.65)",
"@text-color-secondary": "rgba(0, 0, 0, 0.45)",
"@disabled-color": "rgba(0, 0, 0, 0.25)",
"@border-color-base": "#d9d9d9",
"@border-radius-base": "4px",
"@box-shadow-base": "0 2px 8px rgba(0, 0, 0, 0.15)",
"@logo-width": "256px",
"@logo-height": "64px",
"@banner-width": "700px",
"@banner-height": "110px",
"@error-width": "256px",
"@error-height": "256px"
},
"keyboardOptions": {
"us": "label.standard.us.keyboard",
"uk": "label.uk.keyboard",
"fr": "label.french.azerty.keyboard",
"jp": "label.japanese.keyboard",
"sc": "label.simplified.chinese.keyboard"
},
"plugins": []
}

30
ui/public/example.html vendored Normal file
View File

@ -0,0 +1,30 @@
<!--
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.
-->
<!DOCTYPE html>
<html lang="en-gb">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Example Plugin</title>
</head>
<body>
This is an example iframe plugin, please configure the config.json to remove this in production environment.
</body>
</html>

57
ui/public/index.html vendored Normal file
View File

@ -0,0 +1,57 @@
<!--
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.
-->
<!DOCTYPE html>
<html lang="en-gb">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>cloud.ico">
<title>Apache CloudStack</title>
<style>
.loader {
border: 16px solid #F3F3F3;
border-top: 16px solid #39A7DE;
border-radius: 50%;
width: 80px;
height: 80px;
animation: spin 1s linear infinite;
position: fixed;
left: 0; right: 0;
top: 0; bottom: 0;
margin: auto;
max-width: 100%;
max-height: 100%;
overflow: hidden;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<noscript>
<strong>We're sorry but CloudStack UI needs JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
<div class="loader"></div>
</div>
</body>
</html>

2364
ui/public/locales/ar.json Normal file

File diff suppressed because it is too large Load Diff

2364
ui/public/locales/ca.json Normal file

File diff suppressed because it is too large Load Diff

3176
ui/public/locales/de_DE.json Normal file

File diff suppressed because it is too large Load Diff

3298
ui/public/locales/en.json Normal file

File diff suppressed because it is too large Load Diff

2395
ui/public/locales/es.json Normal file

File diff suppressed because it is too large Load Diff

2366
ui/public/locales/fr_FR.json Normal file

File diff suppressed because it is too large Load Diff

528
ui/public/locales/hi.json Normal file
View File

@ -0,0 +1,528 @@
{
"label.about": "अबाउट",
"label.access": "एक्सेस",
"label.accesskey": "एक्सेस की",
"label.account": "खाता",
"label.accounts": "लेखा",
"label.accounttype": "खाता प्रकार",
"label.aclid": "ACL",
"label.aclname": "ACL नाम",
"label.acltotal": "नेटवर्क ACL कुल",
"label.actions": "क्रियाएँ",
"label.add": "जोड़ें",
"label.addanothernetwork": "कोई अन्य नेटवर्क जोड़ें",
"label.adding": "जोड़ना",
"label.addnewnetworks": "नए नेटवर्क जोड़ें",
"label.address": "पता",
"label.admin": "डोमेन व्यवस्थापक",
"label.agentstate": "Agent State",
"label.agree": "सहमत",
"label.alert": "अलर्ट",
"label.alerts": "अलर्ट",
"label.all": "ऑल",
"label.allocated": "आवंटित",
"label.allocatediops": "IOPS आवंटित",
"label.allocationstate": "आवंटन राज्य",
"label.allow": "अनुमति दें",
"label.apikey": "API कुंजी",
"label.apply": "लागू करें",
"label.assign": "असाइन करें",
"label.associatednetwork": "एसोसिएटेड नेटवर्क",
"label.associatednetworkid": "एसोसिएटेड नेटवर्क आईडी",
"label.associatednetworkname": "नेटवर्क नाम",
"label.backupofferingid": "बैकअप ऑफ़रिंग",
"label.basicsetup": "मूल सेटअप",
"label.broadcastdomainrange": "ब्रॉडकास्ट डोमेन रेंज",
"label.broadcastdomaintype": "ब्रॉडकास्ट डोमेन टाइप",
"label.bypassvlanoverlapcheck": "बाईपास वीएलएएन आईडी / रेंज ओवरलैप",
"label.cachemode": "लिखें-कैश प्रकार",
"label.cancel": "रद्द करें",
"label.capacitybytes": "क्षमता बाइट्स",
"label.capacityiops": "IOPS कुल",
"label.checksum": "चेकसम",
"label.cidr": "गेस्ट नेटवर्क के लिए सुपर CIDR",
"label.cidrlist": "CIDR सूची",
"label.cleanup": "क्लीन अप",
"label.close": "बंद करें",
"label.cluster": "क्लस्टर",
"label.clusterid": "क्लस्टर",
"label.clustername": "क्लस्टर",
"label.clusters": "label.क्लस्टर्स",
"label.clustertype": "क्लस्टर प्रकार",
"label.clvm": "CLVM",
"label.compute": "कंप्यूट",
"label.configuration": "कॉन्फ़िगरेशन",
"label.configure": "कॉन्फ़िगर करें",
"label.confirmpassword": "पासवर्ड की पुष्टि करें",
"label.congratulations": "बधाई!",
"label.connectiontimeout": "कनेक्शन टाइमआउट",
"label.conservemode": "संरक्षण मोड",
"label.continue": "जारी रखें",
"label.cpu": "CPU",
"label.cpuallocated": "VM के लिए आवंटित CPU",
"label.cpuallocatedghz": "आवंटित",
"label.cpulimit": "CPU सीमाएँ",
"label.cpunumber": "CPU कोर",
"label.cpusockets": "CPU सॉकेट्स की संख्या",
"label.cputotal": "कुल CPU",
"label.cputotalghz": "कुल",
"label.cpuused": "CPU उपयोग किया गया",
"label.cpuusedghz": "प्रयुक्त CPU",
"label.created": "बनाया गया",
"label.createnfscache": "NFS सेकेंडरी स्टेजिंग स्टोर बनाएँ",
"label.crosszones": "क्रॉस ज़ोन",
"label.current": "Current",
"label.currentpassword": "वर्तमान पासवर्ड",
"label.customconstrained": "कस्टम संकुचित",
"label.customdisksize": "कस्टम डिस्क आकार",
"label.customunconstrained": "कस्टम असंबंधित",
"label.daily": "डेली",
"label.dashboard": "डैशबोर्ड",
"label.date": "दिनांक",
"label.dedicate": "समर्पित करें",
"label.dedicated": "समर्पित",
"label.defaultnetwork": "डिफ़ॉल्ट नेटवर्क",
"label.delete": "हटाएँ",
"label.deleteconfirm": "कृपया पुष्टि करें कि आप इस {नाम} को हटाना चाहेंगे",
"label.deleteprofile": "प्रोफ़ाइल हटाएं",
"label.deploymentplanner": "परिनियोजन योजनाकार",
"label.description": "विवरण",
"label.destcidr": "गंतव्य CIDR",
"label.destination": "गंतव्य",
"label.destinationphysicalnetworkid": "गंतव्य भौतिक नेटवर्क ID",
"label.destinationzoneid": "गंतव्य क्षेत्र",
"label.destroy": "नष्ट",
"label.destroyvmgraceperiod": "वीएम ग्रेस अवधि को नष्ट करें",
"label.details": "विवरण",
"label.deviceid": "डिवाइस आईडी",
"label.devices": "डिवाइसेस",
"label.dhcp": "DHCP",
"label.directdownload": "डायरेक्ट डाउनलोड",
"label.disconnected": "लास्ट डिस्कनेक्टेड",
"label.disk": "डिस्क",
"label.diskiopsmax": "मैक्स IOPS",
"label.diskiopsmin": "न्यूनतम IOPS",
"label.diskiopstotal": "डिस्क IOPS",
"label.diskoffering": "डिस्क ऑफ़र",
"label.diskofferingdisplaytext": "डिस्क की पेशकश",
"label.diskofferingid": "डिस्क की पेशकश",
"label.disksizeallocated": "डिस्क आवंटित",
"label.disksizeallocatedgb": "आवंटित",
"label.disksizetotal": "डिस्क कुल",
"label.disksizetotalgb": "कुल",
"label.disksizeunallocatedgb": "Unallocated",
"label.disksizeusedgb": "प्रयुक्त",
"label.displayname": "प्रदर्शन नाम",
"label.displaytext": "विवरण",
"label.distributedvpcrouter": "वितरित VPC रूटर",
"label.dns": "DNS",
"label.domain": "डोमेन",
"label.domains": "डोमेन",
"label.domainid": "डोमेन आईडी",
"label.domainname": "डोमेन",
"label.done": "संपन्न",
"label.dpd": "डेड पीयर डिटेक्शन",
"label.driver": "ड्राइवर",
"label.edit": "संपादित करें",
"label.egressdefaultpolicy": "डिफॉल्ट ईगरिंग पॉलिसी",
"label.email": "ईमेल",
"label.endip": "एंड आईपी",
"label.endpoint": "समापन बिंदु",
"label.ports": "एंड पोर्ट",
"label.error": "त्रुटि",
"label.esppolicy": "ESP नीति",
"label.event": "इवेंट",
"label.events": "इवेंट्स",
"label.example": "उदाहरण",
"label.existingnetworks": "मौजूदा नेटवर्क",
"label.expunge": "Expunge",
"label.externalid": "बाहरी आईडी",
"label.externalloadbalanceripaddress": "एक्सटर्नल लोड बैलेंसर आईपी एड्रेस",
"label.extra": "अतिरिक्त तर्क",
"label.featured": "फीचर्ड",
"label.filterby": "फ़िल्टर द्वारा",
"label.firstname": "पहला नाम",
"label.fixed": "फिक्स्ड ऑफ़र",
"label.forceencap": "ESP पैकेट्स का बल UDP इनकैप्सुलेशन",
"label.forgedtransmits": "जाली प्रसारण",
"label.format": "प्रारूप",
"label.fwdeviceid": "ID",
"label.fwdevicename": "टाइप",
"label.fwdevicestate": "स्थिति",
"label.gateway": "गेटवे",
"label.glustervolume": "वॉल्यूम",
"label.gpu": "GPU",
"label.group": "समूह",
"label.gslb": "GSLB",
"label.gslbdomainname": "GSLB डोमेन नाम",
"label.gslbprovider": "GSLB सेवा",
"label.gslbproviderpStreetip": "GSLB सेवा निजी आईपी",
"label.gslbproviderpublicip": "GSLB सेवा सार्वजनिक IP",
"label.gslbservicetype": "सेवा प्रकार",
"label.guest": "अतिथि",
"label.guestcidraddress": "अतिथि CIDR",
"label.guestendip": "अतिथि अंत आईपी",
"label.guestgateway": "गेस्ट गेटवे",
"label.guestipaddress": "अतिथि IP पता",
"label.guestiptype": "अतिथि प्रकार",
"label.guestnetmask": "अतिथि नेटमास्क",
"label.guestnetwork": "अतिथि नेटवर्क",
"label.guestnetworkid": "नेटवर्क आईडी",
"label.guestnetworkname": "नेटवर्क नाम",
"label.guestosid": "OS टाइप",
"label.gueststartip": "अतिथि प्रारंभ IP",
"label.haprovider": "हा प्रोवाइडर",
"label.hastate": "हा स्टेट",
"label.help": "सहायता",
"label.hideipaddressusage": "IP पता उपयोग छिपाएँ",
"label.hostid": "होस्ट",
"label.hostname": "होस्ट",
"label.hostnamelabel": "होस्ट नाम",
"label.hosttags": "होस्ट टैग",
"label.hourly": "प्रति घंटा",
"label.hypervisers": "Hypervisers",
"label.hypervisersnapshotreserve": "हाइपरवाइज़र स्नैपशॉट रिज़र्व",
"label.hypervisortype": "हाइपरवाइज़र",
"label.hypervisorversion": "हाइपरवाइज़र संस्करण",
"label.hypervnetworklabel": "हाइपर ट्रैफ़िक label",
"label.icmp": "ICMP",
"label.icmpcode": "ICMP कोड",
"label.icmptype": "ICMP प्रकार",
"label.id": "ID",
"label.ikedh": "IKE DH",
"label.ikepolicy": "IKE नीति",
"label.info": "जानकारी",
"label.infrastructure": "Infrastructure",
"label.insideportprofile": "इनसाइड पोर्ट प्रोफाइल",
"label.instance": "Instance",
"label.instancename": "आंतरिक नाम",
"label.instances": "Instances",
"label.internallbvm": "InternalLbVm",
"label.intervaltype": "अंतराल प्रकार",
"label.invitations": "निमंत्रण",
"label.invite": "आमंत्रित करें",
"label.ip": "IP एड्रेस",
"label.ipaddress": "IP एड्रेस",
"label.iplimit": "सार्वजनिक आईपी सीमाएँ",
"label.ips": "आईपी",
"label.ipsecpsk": "IPsec प्रेस्डेड-की",
"label.iptotal": "IP पते का कुल",
"label.isadvanced": "उन्नत सेटिंग दिखाएं",
"label.iscsi": "iSCSI",
"label.iscustomized": "कस्टम डिस्क आकार",
"label.iscustomizeddiskiops": "कस्टम IOPS",
"label.iscustomizediops": "कस्टम IOPS",
"label.isdedicated": "समर्पित",
"label.isextractable": "Extractable",
"label.isolatedpvlanid": "माध्यमिक पृथक वीएलएएन आईडी",
"label.isolatedpvlantype": "माध्यमिक पृथक वीएलएएन प्रकार",
"label.isolationmethod": "अलगाव विधि",
"label.isolationmethods": "अलगाव विधि",
"label.isolationuri": "अलगाव URI",
"label.isoname": "संलग्न आईएसओ",
"label.isostate": "ISO State",
"label.ispasswordenabled": "पासवर्ड सक्षम",
"label.isself": "स्व",
"label.issourcenat": "स्रोत NAT",
"label.issystem": "सिस्टम है",
"label.keyboardtype": "कीबोर्ड प्रकार",
"label.kubernetesversionid": "Kubernetes संस्करण",
"label.kubernetesversionname": "कुबेरनेट्स संस्करण",
"label.kvmnetworklabel": "KVM ट्रैफ़िक label",
"label.label": "label",
"label.lastannotated": "अंतिम एनोटेशन तिथि",
"label.lastname": "अंतिम नाम",
"label.launch": "लॉन्च",
"label.lbdevicededicated": "समर्पित",
"label.lbdeviceid": "ID",
"label.lbdevicename": "टाइप",
"label.lbdevicestate": "स्थिति",
"label.lbtype": "लोड बैलेंसर टाइप",
"label.limitcpuuse": "CPU कैप",
"label.linklocalip": "लिंक स्थानीय आईपी पता",
"label.loadbalancerinstance": "असाइन किया गया VMs",
"label.loadbalancerrule": "लोड संतुलन नियम",
"label.localstorageenabled": "उपयोगकर्ता VM के लिए स्थानीय संग्रहण सक्षम करें",
"label.localstorageenabledforsystemvm": "सिस्टम VM के लिए स्थानीय संग्रहण सक्षम करें",
"label.lxcnetworklabel": "LXC ट्रैफ़िक label",
"label.macaddress": "मैक एड्रेस",
"label.macaddresschanges": "मैक एड्रेस चेंजेस",
"label.makeredundant": "अनावश्यक बनाएँ",
"label.manage": "प्रबंधित करें",
"label.managedstate": "प्रबंधित राज्य",
"label.managementservers": "प्रबंधन सर्वर की संख्या",
"label.maxcpunumber": "मैक्स सीपीयू कोर",
"label.maxerrorretry": "मैक्स एरर रिट्री",
"label.maxguestslimit": "अधिकतम अतिथि सीमा",
"label.maximum": "अधिकतम",
"label.maxinstance": "मैक्स इंस्टेंस",
"label.maxiops": "अधिकतम IOPS",
"label.maxnetwork": "अधिकतम नेटवर्क",
"label.maxproject": "मैक्स। प्रोजेक्ट्स",
"label.maxpublicip": "मैक्स। सार्वजनिक आईपी",
"label.memoryallocated": "मेमोरी आवंटित",
"label.memoryallocatedgb": "आवंटित",
"label.memorytotal": "मेमोरी आवंटित",
"label.memorytotalgb": "कुल",
"label.memoryused": "यूज्ड मेमोरी",
"label.memoryusedgb": "प्रयुक्त",
"label.memused": "मेमोरी उपयोग",
"label.mincpunumber": "मिन सीपीयू कोरेस",
"label.mininstance": "न्यूनतम उदाहरण",
"label.miniops": "न्यूनतम IOPS",
"label.minmaxiops": "न्यूनतम IOPS / अधिकतम IOPS",
"label.monitor": "मॉनिटर",
"label.netmask": "नेटमास्क",
"label.नेटवर्क": "नेटवर्क",
"label.networkcidr": "नेटवर्क CIDR",
"label.networkdevicetype": "Type",
"label.networkdomain": "नेटवर्क डोमेन",
"label.networkdomaintext": "नेटवर्क डोमेन",
"label.networkkbsread": "नेटवर्क रीड",
"label.networkkbswrite": "नेटवर्क लिखें",
"label.networkname": "नेटवर्क नाम",
"label.networkofferingdisplaytext": "नेटवर्क की पेशकश",
"label.networkofferingid": "नेटवर्क ऑफ़रिंग",
"label.networkofferingidtext": "नेटवर्क ऑफ़रिंग आईडी",
"label.networkofferingname": "नेटवर्क ऑफ़रिंग",
"label.networktype": "नेटवर्क प्रकार",
"label.networkwrite": "नेटवर्क लिखें",
"label.newdiskoffering": "नई पेशकश",
"label.newinstance": "नया उदाहरण",
"label.next": "अगला",
"label.nfs": "एनएफएस",
"label.nfscachenfsserver": "NFS सर्वर",
"label.nfscachepath": "पाथ",
"label.nfscachezoneid": "ज़ोन",
"label.nfsserver": "NFS सर्वर",
"label.nicadcapetype": "NIC एडेप्टर प्रकार",
"label.no": "नहीं",
"label.nodiskcache": "नो डिस्क कैश",
"label.noselect": "नो थैंक्स",
"label.notifications": "सूचनाएं",
"label.numberofrouterrequiresupgrad": "वर्चुअल राउटर्स की कुल जिन्हें अपग्रेड की आवश्यकता है",
"label.numretries": "रिट्रीट की संख्या",
"label.nvpdeviceid": "ID",
"label.offerha": "ऑफ़र हा",
"label.offeringtype": "कंप्यूट ऑफ़र प्रकार",
"label.ok": "ओके",
"label.oscategoryid": "OS वरीयता",
"label.ostypeid": "OS टाइप",
"label.ostypename": "OS टाइप",
"label.other": "अन्य",
"label.outofbandmanagement": "आउट-ऑफ-बैंड प्रबंधन",
"label.overrideguesttraffic": "ओवरराइड गेस्ट-ट्रैफ़िक",
"label.overridepuburtraffic": "ओवरराइड पब्लिक-ट्रैफ़िक",
"label.ovmnetworklabel": "OVM ट्रैफ़िक label",
"label.ovs": "OVS",
"label.parentname": "जनक",
"label.passwordenabled": "पासवर्ड सक्षम",
"label.path": "पथ",
"label.pcidevice": "GPU",
"label.perfectforwardsecrecy": "परफेक्ट फॉरवर्ड सेक्रेसी",
"label.performfreshchecks": "ताज़ा जांचें करें",
"label.permission": "अनुमति",
"label.physicalnetworkid": "भौतिक नेटवर्क",
"label.physicalsize": "भौतिक आकार",
"label.podname": "पोड नाम",
"label.pods": "पॉड्स",
"label.port": "पोर्ट",
"label.portableipaddress": "पोर्टेबल आईपी",
"label.powerstate": "पावर स्टेट",
"label.presetup": "PreSetup",
"label.prev": "Prev",
"label.primarystoragetotal": "प्राथमिक संग्रहण",
"label.privatenetwork": "निजी नेटवर्क",
"label.profiledn": "एसोसिएटेड प्रोफाइल",
"label.profilename": "प्रोफ़ाइल",
"label.project": "प्रोजेक्ट",
"label.projectid": "प्रोजेक्ट आईडी",
"label.projects": "प्रोजेक्ट्स",
"label.promiscuousmode": "प्रमुख विधा",
"label.providername": "प्रदाता",
"label.provisioning": "प्रावधान",
"label.provisioningtype": "प्रावधान प्रकार",
"label.publicinterface": "पब्लिक इंटरफ़ेस",
"label.publicip": "IP एड्रेस",
"label.publickey": "सार्वजनिक कुंजी",
"label.publicnetwork": "सार्वजनिक नेटवर्क",
"label.purpose": "उद्देश्य",
"label.qostype": "QoS प्रकार",
"label.quickview": "त्वरित दृश्य",
"label.quiescevm": "Quiesce VM",
"label.quota": "कोटा मान",
"label.quota.value": "कोटा मान",
"label.ram": "RAM",
"label.rbd": "RBD",
"label.rbdid": "सेफ यूजर",
"label.rbdmonitor": "सिफ मॉनिटर",
"label.rbdsecret": "सेफक्स सीक्रेट",
"label.reason": "कारण",
"label.reenterpassword": "पासवर्ड पुनः दर्ज करें",
"label.refresh": "ताज़ा करें",
"label.remove": "निकालें",
"label.removing": "हटाना",
"label.required": "आवश्यक",
"label.requireshvm": "HVM",
"label.requiresupgrad": "अपग्रेड की आवश्यकता है",
"label.reservediprange": "आरक्षित आईपी श्रेणी",
"label.reservedsystemnetmask": "आरक्षित सिस्टम नेटमास्क",
"label.reservedsystemstartip": "आरक्षित सिस्टम IP प्रारंभ करें",
"label.resetvm": "VM रीसेट करें",
"label.resource": "संसाधन",
"label.resourceid": "संसाधन ID",
"label.resourcename": "संसाधन का नाम",
"label.resourcestate": "संसाधन स्थिति",
"label.restartrequired": "पुनरारंभ आवश्यक",
"label.revokeinvitationconfirm": "कृपया पुष्टि करें कि आप इस निमंत्रण को रद्द करना चाहेंगे?",
"label.role": "भूमिका",
"label.roletype": "भूमिका प्रकार",
"label.rootdiskcontrollertype": "रूट डिस्क नियंत्रक",
"label.rootdiskcontrollertypekvm": "रूट डिस्क नियंत्रक",
"label.routercount": "वर्चुअल राउटर्स का कुल",
"label.routerrequiresupgrad": "अपग्रेड आवश्यक है",
"label.routertype": "टाइप",
"label.rule": "नियम",
"label.rules": "नियम",
"label.samlenable": "SAML SSO को अधिकृत करें",
"label.samlentity": "आइडेंटिटी प्रोवाइडर",
"label.save": "सहेजें",
"label.schedule": "अनुसूची",
"label.search": "खोज",
"label.secondaryips": "सेकेंडरी आईपी",
"label.secretkey": "सीक्रेट की",
"label.securitygroup": "सुरक्षा समूह",
"label.select": "Select",
"label.server": "Server",
"label.servicecapabilities": "सेवा क्षमताएं",
"label.servicelist": "सेवाएँ",
"label.serviceofferingid": "कंप्यूट ऑफ़र",
"label.serviceofferingname": "कंप्यूट ऑफ़र",
"label.sharewith": "शेयर विथ",
"label.sizegb": "आकार",
"label.smbdomain": "SMB डोमेन",
"label.smbpassword": "SMB पासवर्ड",
"label.smbusername": "SMB उपयोगकर्ता नाम",
"label.snapshotlimit": "स्नैपशॉट सीमाएँ",
"label.snapshotmemory": "स्नैपशॉट मेमोरी",
"label.snapshots": "स्नैपशॉट",
"label.snmpcommunity": "SNMP समुदाय",
"label.sockettimeout": "सॉकेट टाइमआउट",
"label.sourcecidr": "स्रोत CIDR",
"label.sourceipaddress": "स्रोत IP पता",
"label.sourcenattype": "समर्थित स्रोत NAT प्रकार",
"label.specifyipranges": "IP पर्वतमाला निर्दिष्ट करें",
"label.specifyvlan": "वीएलएएन निर्दिष्ट करें",
"label.sshkeypair": "न्यू SSH कुंजी जोड़ी",
"label.sshkeypairs": "SSH keypairs",
"label.sslcert प्रमाणपत्र": "SSL प्रमाणपत्र",
"label.startquota": "कोटा मान",
"label.storagepolicy": "संग्रहण नीति",
"label.storagetags": "संग्रहण टैग",
"label.storagetype": "संग्रहण प्रकार",
"label.subdomainaccess": "उपडोमेन एक्सेस",
"label.submit": "सबमिट करें",
"label.supportedservices": "समर्थित सेवाएं",
"label.supportspublicaccess": "सार्वजनिक पहुँच का समर्थन करता है",
"label.supportsregionlevelvpc": "क्षेत्र स्तर VPC का समर्थन करता है",
"label.systemvmtype": "सिस्टम VM प्रकार",
"label.tagged": "टैग किया हुआ",
"label.tags": "टैग",
"label.tariffvalue": "टैरिफ वैल्यू",
"label.tcp": "TCP",
"label.template": "एक टेम्पलेट का चयन करें",
"label.templatebody": "बॉडी",
"label.templatedn": "टेम्पलेट चुनें",
"label.templatefileupload": "स्थानीय फ़ाइल",
"label.templateiso": "टेम्प्लेट / आईएसओ",
"label.templatelimit": "टेम्पलेट सीमाएँ",
"label.templatename": "टेम्प्लेट",
"label.templatenames": "टेम्प्लेट",
"label.templatesubject": "विषय",
"label.templatetotal": "टेम्प्लेट",
"label.templatetype": "ईमेल टेम्प्लेट",
"label.tftpdir": "Tftp root directory",
"label.threshold": "थ्रेसहोल्ड",
"label.thursday": "गुरुवार",
"label.tiername": "टियर",
"label.token": "टोकन",
"label.totalcpu": "कुल CPU",
"label.traffictype": "ट्रैफ़िक प्रकार",
"label.transportzoneuuid": "ट्रांसपोर्ट ज़ोन यूआईडी",
"label.tuesday": "मंगलवार",
"label.unavailable": "अनुपलब्ध",
"label.unit": "यूज़ यूनिट",
"label.unlimited": "अनलिमिटेड",
"label.untagged": "अनटैग्ड",
"label.upload": "अपलोड",
"label.url": "URL",
"label.usageinterface": "उपयोग इंटरफ़ेस",
"label.usagename": "उपयोग प्रकार",
"label.usageunit": "Unit",
"label.usehttps": "HTTPS का उपयोग करें",
"label.user": "उपयोगकर्ता",
"label.userdata": "Userdata",
"label.username": "उपयोगकर्ता नाम",
"label.users": "उपयोगकर्ता",
"label.uuid": "ID",
"label.vcdcname": "vCenter DC नाम",
"label.vcenter": "VMware डाटासेंटर vCenter",
"label.esx.host": "ESX / ESXi होस्ट",
"label.vcenterpassword": "vCenter पासवर्ड",
"label.vcipaddress": "vCenter IP पता",
"label.version": "संस्करण",
"label.versions": "संस्करण",
"label.vgpu": "VGPU",
"label.vgputype": "vGPU प्रकार",
"label.view": "देखें",
"label.viewing": "देखना",
"label.virtualmachinedisplayname": "VM नाम",
"label.virtualsize": "वर्चुअल साइज़",
"label.vlanid": "VLAN / VNI ID",
"label.vlanrange": "वीएलएएन / वीएनआई रेंज",
"label.vm": "VM",
"label.vmfs": "VMFS",
"label.vmipaddress": "VM IP पता",
"label.vmlimit": "इंस्टेंस लिमिट्स",
"label.vmname": "VM नाम",
"label.vms": "VMs",
"label.vmstate": "VM राज्य",
"label.vmwarenetworklabel": "VMware ट्रैफिक label",
"label.vnmc": "VNMC",
"label.volumechecksum": "MD5 चेकसम",
"label.volumefileupload": "स्थानीय फ़ाइल",
"label.volumeids": "हटाए जाने वाले वॉल्यूम",
"label.volumelimit": "वॉल्यूम सीमाएं",
"label.volumename": "वॉल्यूम नाम",
"label.volumetotal": "वॉल्यूम",
"label.vpc": "VPC",
"label.vpcid": "VPC",
"label.vpclimit": "VPC सीमाएँ",
"label.vpcname": "VPC",
"label.vpcoffering": "VPC ऑफ़रिंग",
"label.vpn": "वीपीएन",
"label.vpncustomergateway": "दूरस्थ गेटवे का IP पता",
"label.vpncustomergatewayid": "वीपीएन ग्राहक गेटवे",
"label.vpncustomergatewayname": "वीपीएन ग्राहक गेटवे के लिए अद्वितीय नाम",
"label.vsmctrlvlanid": "कंट्रोल वीएलएएन आईडी",
"label.vsmdeviceid": "नाम",
"label.vsmdevicestate": "State",
"label.vsmipaddress": "Nexus 1000v IP पता",
"label.vsmpassword": "Nexus 1000v पासवर्ड",
"label.vsmpktvlanid": "पैकेट वीएलएएन आईडी",
"label.vsmstoragevlanid": "संग्रहण VLAN ID",
"label.vsmusername": "Nexus 1000v उपयोगकर्ता नाम",
"label.vswitchguestname": "अतिथि ट्रैफ़िक vSwitch नाम",
"label.vswitchguesttype": "अतिथि ट्रैफ़िक vSwitch प्रकार",
"label.vswitchpublicname": "पब्लिक ट्रैफ़िक vSwitch नाम",
"label.vswitchpuburtype": "सार्वजनिक ट्रैफ़िक vSwitch प्रकार",
"label.writeback": "राइट-बैक डिस्क कैशिंग",
"label.writecachetype": "लिखें-कैश प्रकार",
"label.writethrough": "राइट-थ्रू",
"label.xennetworklabel": "XenServer ट्रैफ़िक label",
"label.yes": "हाँ",
"label.yourinstance": "आपका उदाहरण",
"label.zone": "ज़ोन",
"label.zoneid": "ज़ोन",
"label.zonename": "ज़ोन नाम"
}

2363
ui/public/locales/hu.json Normal file

File diff suppressed because it is too large Load Diff

2364
ui/public/locales/it_IT.json Normal file

File diff suppressed because it is too large Load Diff

2367
ui/public/locales/ja_JP.json Normal file

File diff suppressed because it is too large Load Diff

2363
ui/public/locales/ko_KR.json Normal file

File diff suppressed because it is too large Load Diff

2364
ui/public/locales/nb_NO.json Normal file

File diff suppressed because it is too large Load Diff

2364
ui/public/locales/nl_NL.json Normal file

File diff suppressed because it is too large Load Diff

2364
ui/public/locales/pl.json Normal file

File diff suppressed because it is too large Load Diff

2363
ui/public/locales/pt_BR.json Normal file

File diff suppressed because it is too large Load Diff

2363
ui/public/locales/ru_RU.json Normal file

File diff suppressed because it is too large Load Diff

2367
ui/public/locales/zh_CN.json Normal file

File diff suppressed because it is too large Load Diff

48
ui/src/App.vue Normal file
View File

@ -0,0 +1,48 @@
// 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-config-provider :locale="locale">
<div id="app">
<router-view/>
</div>
</a-config-provider>
</template>
<script>
import enUS from 'ant-design-vue/lib/locale-provider/en_US'
import { AppDeviceEnquire } from '@/utils/mixin'
export default {
mixins: [AppDeviceEnquire],
data () {
return {
locale: enUS,
configs: {}
}
},
created () {
window.less.modifyVars(this.$config.theme)
console.log('config and theme applied')
}
}
</script>
<style>
#app {
height: 100%;
}
</style>

61
ui/src/api/index.js Normal file
View File

@ -0,0 +1,61 @@
// 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.
import { axios } from '@/utils/request'
export function api (command, args = {}, method = 'GET', data = {}) {
let params = {}
args.command = command
args.response = 'json'
if (data) {
params = new URLSearchParams()
Object.entries(data).forEach(([key, value]) => {
params.append(key, value)
})
}
return axios({
params: {
...args
},
url: '/',
method,
data: params || {}
})
}
export function login (arg) {
const params = new URLSearchParams()
params.append('command', 'login')
params.append('username', arg.username)
params.append('password', arg.password)
params.append('domain', arg.domain)
params.append('response', 'json')
return axios({
url: '/',
method: 'post',
data: params,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}
export function logout () {
return api('logout')
}

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xml:space="preserve"
width="160"
height="160"
version="1.1"
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
viewBox="0 0 1666.6667 1666.8369"
id="svg38"
sodipodi:docname="cloudian.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata42"><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></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1673"
inkscape:window-height="931"
id="namedview40"
showgrid="false"
units="px"
inkscape:zoom="1.11"
inkscape:cx="159.90991"
inkscape:cy="95.225335"
inkscape:window-x="54"
inkscape:window-y="26"
inkscape:window-maximized="0"
inkscape:current-layer="svg38" />
<defs
id="defs4">
<style
type="text/css"
id="style2">
<![CDATA[
.fil0 {fill:#424B53;fill-rule:nonzero}
.fil2 {fill:#969C98;fill-rule:nonzero}
.fil1 {fill:#BFD43F;fill-rule:nonzero}
]]>
</style>
</defs>
<g
id="g4573"
transform="translate(-686.25631,82.403259)"><polygon
transform="translate(0,-69.453818)"
class="fil0"
points="1524,293 1524,-5 815,404 1073,553 "
id="polygon7"
style="fill:#424b53;fill-rule:nonzero" /><polygon
transform="translate(0,-69.453818)"
class="fil0"
points="1524,814 1726,697 1524,581 1323,697 "
id="polygon9"
style="fill:#424b53;fill-rule:nonzero" /><polygon
transform="translate(0,-69.453818)"
class="fil1"
points="1524,814 1524,1047 1726,930 1726,697 "
id="polygon11"
style="fill:#bfd43f;fill-rule:nonzero" /><polygon
transform="translate(0,-69.453818)"
class="fil1"
points="2234,404 1976,553 1976,1074 1524,1335 1524,1632 2234,1223 "
id="polygon13"
style="fill:#bfd43f;fill-rule:nonzero" /><polygon
transform="translate(0,-69.453818)"
class="fil2"
points="1524,1047 1524,814 1323,697 1323,930 "
id="polygon15"
style="fill:#969c98;fill-rule:nonzero" /><polygon
transform="translate(0,-69.453818)"
class="fil2"
points="1073,1074 1073,553 815,404 815,1223 1524,1632 1524,1335 "
id="polygon17"
style="fill:#969c98;fill-rule:nonzero" /></g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:ns4="http://ns.adobe.com/SaveForWeb/1.0/"
xmlns:ns3="http://ns.adobe.com/Variables/1.0/"
xmlns:ns2="http://ns.adobe.com/AdobeIllustrator/10.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
ns2:viewOrigin="262 450"
ns2:rulerOrigin="0 0"
ns2:pageBounds="0 792 612 0"
width="56"
height="56.000069"
viewBox="0 0 55.999999 56.000069"
overflow="visible"
enable-background="new 0 0 87.041 108.445"
xml:space="preserve"
version="1.1"
id="svg31"
sodipodi:docname="debian.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
style="overflow:visible"><defs
id="defs35" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1866"
inkscape:window-height="1017"
id="namedview33"
showgrid="false"
inkscape:zoom="2.1762184"
inkscape:cx="-62.475298"
inkscape:cy="28.002047"
inkscape:window-x="54"
inkscape:window-y="26"
inkscape:window-maximized="1"
inkscape:current-layer="g28"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata2">
<ns3:variableSets>
<ns3:variableSet
varSetName="binding1"
locked="none">
<ns3:variables />
<ns3:sampleDataSets />
</ns3:variableSet>
</ns3:variableSets>
<ns4:sfw>
<ns4:slices />
<ns4:sliceSourceBounds
y="341.555"
x="262"
width="87.041"
height="108.445"
bottomLeftOrigin="true" />
</ns4:sfw>
<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></dc:title></cc:Work></rdf:RDF></metadata>
<g
id="Layer_1"
ns2:layer="yes"
ns2:dimmedPercent="50"
ns2:rgbTrio="#4F008000FFFF"
transform="translate(-20.985947,-26.22447)">
<g
id="g28">
<path
ns2:knockout="Off"
d="m 53.872479,55.811055 c -0.927921,0.01291 0.175567,0.478161 1.386977,0.664571 0.334609,-0.261284 0.638236,-0.525667 0.908815,-0.78282 -0.75442,0.184861 -1.522266,0.188992 -2.295792,0.118249"
id="path4"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 58.852891,54.569696 c 0.552518,-0.762682 0.955289,-1.597656 1.097291,-2.461031 -0.123929,0.615516 -0.458022,1.146863 -0.772493,1.707644 -1.734495,1.092127 -0.163174,-0.648564 -10e-4,-1.310037 -1.865137,2.347429 -0.256121,1.407631 -0.323766,2.063424"
id="path6"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 60.691176,49.786022 c 0.112053,-1.670981 -0.328929,-1.142732 -0.477128,-0.505012 0.172985,0.08985 0.309824,1.177846 0.477128,0.505012"
id="path8"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 50.353918,26.946873 c 0.495201,0.08882 1.069924,0.156977 0.98937,0.275226 0.541674,-0.118765 0.664571,-0.228236 -0.98937,-0.275226"
id="path10"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 51.343288,27.222099 -0.350101,0.07229 0.325831,-0.02892 0.02427,-0.04338"
id="path12"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 66.784887,50.419611 c 0.05525,1.500578 -0.438917,2.228663 -0.884546,3.517529 l -0.801926,0.400705 c -0.65631,1.274407 0.06351,0.809155 -0.406386,1.822794 -1.024482,0.910881 -3.109077,2.850376 -3.776231,3.027491 -0.486939,-0.01085 0.329962,-0.574722 0.436851,-0.79573 -1.371485,0.941864 -1.100389,1.413828 -3.197894,1.985969 l -0.06145,-0.136323 c -5.173018,2.433663 -12.358856,-2.389255 -12.26436,-8.969904 -0.05525,0.417745 -0.156977,0.313438 -0.271612,0.482292 -0.266964,-3.385854 1.563576,-6.786682 4.650966,-8.175207 3.019746,-1.494898 6.559995,-0.881448 8.723078,1.13447 -1.188172,-1.556347 -3.553158,-3.206156 -6.356027,-3.051761 -2.745552,0.04337 -5.313988,1.788197 -6.171166,3.682251 -1.406598,0.885579 -1.569772,3.413738 -2.182706,3.876408 -0.824647,6.060662 1.551183,8.679186 5.570109,11.759347 0.632556,0.426524 0.178148,0.49107 0.263866,0.815869 -1.335339,-0.625327 -2.558109,-1.569256 -3.563486,-2.724897 0.533413,0.780755 1.109168,1.539822 1.853261,2.136232 -1.258916,-0.426523 -2.940741,-3.050728 -3.431811,-3.157617 2.170313,3.885702 8.805182,6.814566 12.279335,5.361494 -1.607467,0.05938 -3.64972,0.03305 -5.455991,-0.634621 -0.758551,-0.390378 -1.790263,-1.199017 -1.605918,-1.350314 4.741331,1.771157 9.639123,1.341535 13.741702,-1.94724 1.043588,-0.81277 2.183738,-2.195615 2.513184,-2.214721 -0.496234,0.746158 0.08469,0.358879 -0.296398,1.01777 1.039974,-1.677178 -0.451826,-0.682645 1.075087,-2.896333 l 0.563879,0.776624 c -0.209647,-1.39214 1.728815,-3.082743 1.532077,-5.284555 0.444596,-0.673349 0.496234,0.724471 0.02427,2.273588 0.654761,-1.718487 0.172469,-1.994746 0.340806,-3.412705 0.181763,0.476612 0.420327,0.983173 0.542707,1.48612 -0.426523,-1.660654 0.437884,-2.796673 0.651662,-3.761773 -0.21068,-0.09346 -0.658374,0.734282 -0.760616,-1.227417 0.01497,-0.852014 0.237015,-0.446662 0.322733,-0.656309 -0.167305,-0.09605 -0.606222,-0.749257 -0.873186,-2.001976 0.19364,-0.294332 0.517405,0.763198 0.780755,0.806574 -0.16937,-0.996083 -0.461121,-1.755666 -0.472997,-2.519897 -0.769395,-1.607984 -0.272128,0.214294 -0.896423,-0.69039 -0.818966,-2.554494 0.679547,-0.592796 0.780755,-1.753601 1.24136,1.798525 1.949306,4.585903 2.274104,5.740512 -0.247858,-1.407631 -0.648563,-2.771371 -1.137568,-4.090702 0.376952,0.158526 -0.607254,-2.896333 0.490037,-0.873186 -1.172165,-4.312742 -5.016557,-8.342512 -8.553191,-10.233467 0.43272,0.396057 0.979042,0.893324 0.78282,0.971296 -1.758764,-1.047203 -1.449457,-1.12879 -1.701447,-1.571321 -1.432933,-0.582984 -1.526913,0.04699 -2.476005,0.001 -2.70063,-1.432419 -3.221133,-1.280089 -5.706433,-2.177544 l 0.113085,0.528249 c -1.78923,-0.595894 -2.084595,0.226171 -4.018409,0.0021 -0.117733,-0.09191 0.619646,-0.332544 1.226384,-0.420843 -1.729847,0.228236 -1.648777,-0.340806 -3.341446,0.063 0.417229,-0.292783 0.858211,-0.486423 1.303324,-0.735314 -1.410729,0.08572 -3.36778,0.821032 -2.763625,0.15233 -2.300955,1.026548 -6.387526,2.467743 -8.680735,4.617918 l -0.07229,-0.481776 c -1.050818,1.261498 -4.582289,3.767453 -4.863712,5.401255 l -0.280906,0.06558 c -0.546839,0.925856 -0.900553,1.975125 -1.334306,2.927832 -0.715176,1.218638 -1.048236,0.468866 -0.946511,0.659924 -1.406598,2.851924 -2.10525,5.248408 -2.708889,7.213721 0.430138,0.642884 0.01033,3.870211 0.172985,6.453106 -0.706398,12.756463 8.952863,25.142168 19.511129,28.001841 1.547568,0.553548 3.849039,0.532381 5.806607,0.589182 -2.309733,-0.660445 -2.608197,-0.350104 -4.858031,-1.134472 -1.622958,-0.764231 -1.978739,-1.636901 -3.128184,-2.634532 l 0.454924,0.803992 c -2.25448,-0.7978 -1.311068,-0.987308 -3.145223,-1.568227 l 0.485907,-0.634622 c -0.730667,-0.05525 -1.935364,-1.231548 -2.26481,-1.882693 l -0.799344,0.0315 c -0.960453,-1.185074 -1.472178,-2.039154 -1.434999,-2.700627 l -0.258186,0.460088 C 37.555113,72.462514 34.31436,68.520528 35.995668,69.438122 35.683263,69.152568 35.2681,68.973386 34.817823,68.155453 l 0.342355,-0.391411 c -0.809156,-1.041006 -1.489218,-2.375312 -1.437581,-2.819909 0.431688,0.582984 0.731184,0.691939 1.027581,0.791599 -2.043285,-5.069744 -2.15792,-0.279358 -3.705488,-5.160626 l 0.32738,-0.02634 c -0.250956,-0.377984 -0.403286,-0.7885 -0.605188,-1.191271 l 0.142519,-1.420024 c -1.471145,-1.70093 -0.411549,-7.232311 -0.19932,-10.265999 0.147166,-1.233613 1.227933,-2.546748 2.049998,-4.606041 l -0.500881,-0.08623 c 0.957354,-1.669948 5.466318,-6.706644 7.554528,-6.447425 1.011573,-1.270793 -0.200869,-0.0046 -0.39864,-0.324799 2.22195,-2.299406 2.920602,-1.624507 4.420148,-2.038121 1.617278,-0.959937 -1.388009,0.37437 -0.621196,-0.366108 2.79564,-0.714143 1.98132,-1.623475 5.628458,-1.985968 0.384698,0.218941 -0.892807,0.338223 -1.213475,0.622228 2.329356,-1.139634 7.371216,-0.880415 10.646049,0.632556 3.799984,1.775805 8.069351,7.025246 8.237688,11.964348 l 0.191575,0.05164 c -0.09708,1.963248 0.300528,4.233737 -0.388312,6.319365 l 0.468866,-0.987304"
id="path14"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 43.744352,57.084946 -0.130126,0.650629 c 0.609836,0.828261 1.093677,1.725716 1.872366,2.373247 -0.560264,-1.093677 -0.97646,-1.545502 -1.74224,-3.023876"
id="path16"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 45.186064,57.028145 c -0.322733,-0.356814 -0.513791,-0.786435 -0.727569,-1.214508 0.204483,0.752354 0.623261,1.398853 1.013123,2.056195 l -0.285554,-0.841687"
id="path18"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 70.696924,51.483338 -0.136323,0.341839 c -0.249924,1.775288 -0.789533,3.531987 -1.616762,5.160625 0.91398,-1.718487 1.505226,-3.598082 1.753085,-5.502464"
id="path20"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 50.53723,26.50176 c 0.627393,-0.229786 1.542405,-0.125995 2.208009,-0.277292 -0.867506,0.07281 -1.730881,0.116184 -2.583411,0.226171 l 0.375402,0.05112"
id="path22"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 28.511368,38.214118 c 0.144584,1.338437 -1.006926,1.857908 0.255088,0.975427 0.676447,-1.523815 -0.264383,-0.420843 -0.255088,-0.975427"
id="path24"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 27.028346,44.408521 c 0.290718,-0.892292 0.343388,-1.428286 0.454408,-1.944659 -0.803476,1.027065 -0.369723,1.246007 -0.454408,1.944659"
id="path26"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -0,0 +1,116 @@
// 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-checkbox v-decorator="[checkBoxDecorator, {}]" class="pair-checkbox" @change="handleCheckChange">
{{ checkBoxLabel }}
</a-checkbox>
<a-form-item class="pair-select-container" :label="selectLabel" v-if="this.checked">
<a-select
v-decorator="[selectDecorator, {
initialValue: this.getSelectInitialValue()
}]"
showSearch
optionFilterProp="children"
:filterOption="(input, option) => {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@change="val => { this.handleSelectChange(val) }">
<a-select-option v-for="(opt) in selectOptions" :key="opt.name" :disabled="opt.enabled === false">
{{ opt.name || opt.description }}
</a-select-option>
</a-select>
</a-form-item>
</div>
</template>
<script>
export default {
name: 'CheckBoxSelectPair',
props: {
resourceKey: {
type: String,
required: true
},
checkBoxLabel: {
type: String,
required: true
},
checkBoxDecorator: {
type: String,
default: ''
},
selectOptions: {
type: Array,
required: true
},
selectLabel: {
type: String,
default: ''
},
selectDecorator: {
type: String,
default: ''
}
},
data () {
return {
checked: false,
selectedOption: ''
}
},
methods: {
arrayHasItems (array) {
return array !== null && array !== undefined && Array.isArray(array) && array.length > 0
},
getSelectInitialValue () {
if (this.arrayHasItems(this.selectOptions)) {
for (var i = 0; i < this.selectOptions.length; i++) {
if (this.selectOptions[i].enabled !== false) {
return this.selectOptions[i].name
}
}
}
return ''
},
handleCheckChange (e) {
this.checked = e.target.checked
if (this.checked && this.arrayHasItems(this.selectOptions)) {
this.selectedOption = this.selectOptions[0].name
}
this.$emit('handle-checkpair-change', this.resourceKey, this.checked, '')
},
handleSelectChange (val) {
this.$emit('handle-checkpair-change', this.resourceKey, this.checked, val)
}
}
}
</script>
<style scoped lang="scss">
.pair-checkbox {
width: 180px;
}
.pair-select-container {
position: relative;
float: right;
margin-bottom: -5px;
width: 20vw;
}
</style>

View File

@ -0,0 +1,164 @@
// 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-popover
v-model="visible"
trigger="click"
placement="bottom"
:autoAdjustOverflow="true"
:arrowPointAtCenter="true"
overlayClassName="header-notice-popover">
<template slot="content">
<a-spin :spinning="loading">
<a-list style="min-width: 200px; max-width: 300px">
<a-list-item>
<a-list-item-meta :title="$t('label.notifications')">
<a-avatar :style="{backgroundColor: '#6887d0', verticalAlign: 'middle'}" icon="notification" slot="avatar"/>
<a-button size="small" slot="description" @click="clearJobs">{{ $t('label.clear.list') }}</a-button>
</a-list-item-meta>
</a-list-item>
<a-list-item v-for="(job, index) in jobs" :key="index">
<a-list-item-meta :title="job.title" :description="job.description">
<a-avatar :style="notificationAvatar[job.status].style" :icon="notificationAvatar[job.status].icon" slot="avatar"/>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-spin>
</template>
<span @click="showNotifications" class="header-notice-opener">
<a-badge :count="jobs.length">
<a-icon class="header-notice-icon" type="bell" />
</a-badge>
</span>
</a-popover>
</template>
<script>
import { api } from '@/api'
import store from '@/store'
export default {
name: 'HeaderNotice',
data () {
return {
loading: false,
visible: false,
jobs: [],
poller: null,
notificationAvatar: {
done: { icon: 'check-circle', style: 'backgroundColor:#87d068' },
progress: { icon: 'loading', style: 'backgroundColor:#ffbf00' },
failed: { icon: 'close-circle', style: 'backgroundColor:#f56a00' }
}
}
},
methods: {
showNotifications () {
this.visible = !this.visible
},
clearJobs () {
this.jobs = this.jobs.filter(x => x.status === 'progress')
this.$store.commit('SET_ASYNC_JOB_IDS', this.jobs)
},
startPolling () {
this.poller = setInterval(() => {
this.pollJobs()
}, 4000)
},
async pollJobs () {
var hasUpdated = false
for (var i in this.jobs) {
if (this.jobs[i].status === 'progress') {
await api('queryAsyncJobResult', { jobid: this.jobs[i].jobid }).then(json => {
var result = json.queryasyncjobresultresponse
if (result.jobstatus === 1 && this.jobs[i].status !== 'done') {
hasUpdated = true
const title = this.jobs[i].title
const description = this.jobs[i].description
this.$message.success({
content: title + (description ? ' - ' + description : ''),
key: this.jobs[i].jobid,
duration: 2
})
this.jobs[i].status = 'done'
} else if (result.jobstatus === 2 && this.jobs[i].status !== 'failed') {
hasUpdated = true
this.jobs[i].status = 'failed'
if (result.jobresult.errortext !== null) {
this.jobs[i].description = '(' + this.jobs[i].description + ') ' + result.jobresult.errortext
}
this.$notification.error({
message: this.jobs[i].title,
description: this.jobs[i].description,
key: this.jobs[i].jobid,
duration: 0
})
}
}).catch(function (e) {
console.log(this.$t('error.fetching.async.job.result') + e)
})
}
}
if (hasUpdated) {
this.$store.commit('SET_ASYNC_JOB_IDS', this.jobs.reverse())
}
}
},
beforeDestroy () {
clearInterval(this.poller)
},
created () {
this.startPolling()
},
mounted () {
this.jobs = (store.getters.asyncJobIds || []).reverse()
this.$store.watch(
(state, getters) => getters.asyncJobIds,
(newValue, oldValue) => {
if (oldValue !== newValue && newValue !== undefined) {
this.jobs = newValue.reverse()
}
}
)
}
}
</script>
<style lang="less" scoped>
.header-notice {
display: inline-block;
transition: all 0.3s;
&-popover {
top: 50px !important;
width: 300px;
top: 50px;
}
&-opener {
display: inline-block;
transition: all 0.3s;
vertical-align: initial;
}
&-icon {
font-size: 18px;
padding: 4px;
}
}
</style>

View File

@ -0,0 +1,71 @@
// 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="logo">
<img
v-if="$config.logo"
:style="{
width: $config.theme['@logo-width'],
height: $config.theme['@logo-height']
}"
:src="$config.logo"
class="logo-image" />
</div>
</template>
<script>
export default {
name: 'Logo',
components: {
},
props: {
title: {
type: String,
default: 'CloudStack',
required: false
},
showTitle: {
type: Boolean,
default: true,
required: false
}
}
}
</script>
<style type="less" scoped>
.logo {
height: 64px;
position: relative;
line-height: 64px;
-webkit-transition: all .3s;
transition: all .3s;
overflow: hidden;
}
.sider.light .logo {
box-shadow: 1px 1px 0px 0px #e8e8e8;
}
.logo-image {
display: inline-block;
vertical-align: middle;
}
</style>

View File

@ -0,0 +1,122 @@
// 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="header-notice-opener">
<a-select
class="project-select"
:defaultValue="$t('label.default.view')"
:loading="loading"
:value="($store.getters.project && 'id' in $store.getters.project) ? ($store.getters.project.displaytext || $store.getters.project.name) : $t('label.default.view')"
:disabled="isDisabled()"
:filterOption="filterProject"
@change="changeProject"
@focus="fetchData"
showSearch>
<a-tooltip placement="bottom" slot="suffixIcon">
<template slot="title">
<span>{{ $t('label.projects') }}</span>
</template>
<span style="font-size: 20px; color: #999; margin-top: -5px">
<a-icon v-if="!loading" type="project" />
<a-icon v-else type="loading" />
</span>
</a-tooltip>
<a-select-option v-for="(project, index) in projects" :key="index">
{{ project.displaytext || project.name }}
</a-select-option>
</a-select>
</span>
</template>
<script>
import store from '@/store'
import { api } from '@/api'
import _ from 'lodash'
export default {
name: 'ProjectMenu',
data () {
return {
projects: [],
loading: false
}
},
mounted () {
this.fetchData()
},
methods: {
fetchData () {
if (this.isDisabled()) {
return
}
var page = 1
const projects = []
const getNextPage = () => {
this.loading = true
api('listProjects', { listAll: true, details: 'min', page: page, pageSize: 500 }).then(json => {
if (json && json.listprojectsresponse && json.listprojectsresponse.project) {
projects.push(...json.listprojectsresponse.project)
}
if (projects.length < json.listprojectsresponse.count) {
page++
getNextPage()
}
}).finally(() => {
this.projects = _.orderBy(projects, ['displaytext'], ['asc'])
this.projects.unshift({ name: this.$t('label.default.view') })
this.loading = false
})
}
getNextPage()
},
isDisabled () {
return !Object.prototype.hasOwnProperty.call(store.getters.apis, 'listProjects')
},
changeProject (index) {
const project = this.projects[index]
this.$store.dispatch('ProjectView', project.id)
this.$store.dispatch('SetProject', project)
this.$store.dispatch('ToggleTheme', project.id === undefined ? 'light' : 'dark')
this.$message.success(`${this.$t('message.switch.to')} "${project.displaytext || project.name}"`)
if (this.$route.name !== 'dashboard') {
this.$router.push({ name: 'dashboard' })
}
},
filterProject (input, option) {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
}
}
</script>
<style lang="less" scoped>
.project {
&-select {
width: 30vw;
}
&-icon {
font-size: 20px;
line-height: 1;
padding-top: 5px;
padding-right: 5px;
}
}
</style>

View File

@ -0,0 +1,124 @@
// 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="header-notice-opener" v-if="showSwitcher">
<a-select
class="domain-select"
:loading="loading"
:defaultValue="currentAccount"
:value="currentAccount"
@change="changeAccount"
@focus="fetchData" >
<a-tooltip placement="bottom" slot="suffixIcon">
<template slot="title">
<span>{{ $t('label.domain') }}</span>
</template>
<span style="font-size: 20px; color: #999; margin-top: -5px">
<a-icon v-if="!loading" type="block" />
<a-icon v-else type="loading" />
</span>
</a-tooltip>
<a-select-option v-for="(account, index) in samlAccounts" :key="index">
{{ `${account.accountName} (${account.domainName})` }}
</a-select-option>
</a-select>
</span>
</template>
<script>
import store from '@/store'
import { api } from '@/api'
import _ from 'lodash'
export default {
name: 'SamlDomainSwitcher',
data () {
return {
showSwitcher: false,
samlAccounts: [],
currentAccount: '',
loading: false
}
},
mounted () {
this.fetchData()
},
methods: {
fetchData () {
var page = 1
const samlAccounts = []
const getNextPage = () => {
this.loading = true
api('listAndSwitchSamlAccount', { listAll: true, details: 'min', page: page, pageSize: 500 }).then(json => {
if (json && json.listandswitchsamlaccountresponse && json.listandswitchsamlaccountresponse.samluseraccount) {
samlAccounts.push(...json.listandswitchsamlaccountresponse.samluseraccount)
}
if (samlAccounts.length < json.listandswitchsamlaccountresponse.count) {
page++
getNextPage()
}
}).catch(error => {
console.log(error)
}).finally(() => {
if (samlAccounts.length < 2) {
this.showSwitcher = false
return
}
this.samlAccounts = _.orderBy(samlAccounts, ['domainPath'], ['asc'])
const currentAccount = this.samlAccounts.filter(x => {
return x.userId === store.getters.userInfo.id
})[0]
this.currentAccount = `${currentAccount.accountName} (${currentAccount.domainName})`
this.loading = false
this.showSwitcher = true
})
}
getNextPage()
},
changeAccount (index) {
const account = this.samlAccounts[index]
api('listAndSwitchSamlAccount', {}, 'POST', {
userid: account.userId,
domainid: account.domainId
}).then(response => {
store.dispatch('GetInfo').then(() => {
this.$message.success(`Switched to "${account.accountName} (${account.domainPath})"`)
this.$router.go()
})
})
}
}
}
</script>
<style lang="less" scoped>
.domain {
&-select {
width: 20vw;
}
&-icon {
font-size: 20px;
line-height: 1;
padding-top: 5px;
padding-right: 5px;
}
}
</style>

View File

@ -0,0 +1,94 @@
// 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-dropdown>
<span class="action ant-dropdown-link translation-menu">
<font-awesome-icon :icon="['fas', 'language']" size="lg" />
</span>
<a-menu
slot="overlay"
:selectedKeys="[language]"
@click="onClick">
<a-menu-item key="en" value="enUS">English</a-menu-item>
<a-menu-item key="hi" value="hi">ि</a-menu-item>
<a-menu-item key="ja_JP" value="jpJP">日本語</a-menu-item>
<a-menu-item key="ko_KR" value="koKR">한국어</a-menu-item>
<a-menu-item key="zh_CN" value="zhCN">简体中文</a-menu-item>
<a-menu-item key="ar" value="arEG">Arabic</a-menu-item>
<a-menu-item key="ca" value="caES">Catalan</a-menu-item>
<a-menu-item key="de_DE" value="deDE">Deutsch</a-menu-item>
<a-menu-item key="es" value="esES">Español</a-menu-item>
<a-menu-item key="fr_FR" value="frFR">Français</a-menu-item>
<a-menu-item key="it_IT" value="itIT">Italiano</a-menu-item>
<a-menu-item key="hu" value="huHU">Magyar</a-menu-item>
<a-menu-item key="nl_NL" value="nlNL">Nederlands</a-menu-item>
<a-menu-item key="nb_NO" value="nbNO">Norsk</a-menu-item>
<a-menu-item key="pl" value="plPL">Polish</a-menu-item>
<a-menu-item key="pt_BR" value="ptBR">Português brasileiro</a-menu-item>
<a-menu-item key="ru_RU" value="ruRU">Русский</a-menu-item>
</a-menu>
</a-dropdown>
</template>
<script>
import Vue from 'vue'
import moment from 'moment'
import 'moment/locale/zh-cn'
import { loadLanguageAsync } from '@/locales'
moment.locale('en')
export default {
name: 'TranslationMenu',
data () {
return {
language: 'en'
}
},
mounted () {
this.language = Vue.ls.get('LOCALE') || 'en'
this.setLocale(this.language)
},
methods: {
moment,
onClick (e) {
let localeValue = e.key
if (!localeValue) {
localeValue = 'en'
}
this.setLocale(localeValue)
},
setLocale (localeValue) {
this.$locale = localeValue
this.$i18n.locale = localeValue
this.language = localeValue
moment.locale(localeValue)
Vue.ls.set('LOCALE', localeValue)
loadLanguageAsync(localeValue)
}
}
}
</script>
<style lang="less" scoped>
.translation-menu {
font-size: 18px;
line-height: 1;
}
</style>

View File

@ -0,0 +1,112 @@
// 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="user-menu">
<translation-menu class="action"/>
<header-notice class="action"/>
<a-dropdown>
<span class="user-menu-dropdown action">
<a-avatar class="user-menu-avatar avatar" size="small" :src="avatar()"/>
<span>{{ nickname() }}</span>
</span>
<a-menu slot="overlay" class="user-menu-wrapper">
<a-menu-item class="user-menu-item" key="0">
<router-link :to="{ path: '/accountuser/' + $store.getters.userInfo.id }">
<a-icon class="user-menu-item-icon" type="user"/>
<span class="user-menu-item-name">{{ $t('label.profilename') }}</span>
</router-link>
</a-menu-item>
<a-menu-item class="user-menu-item" key="1">
<a @click="toggleUseBrowserTimezone">
<a-icon class="user-menu-item-icon" type="clock-circle"/>
<span class="user-menu-item-name" style="margin-right: 5px">{{ $t('label.use.local.timezone') }}</span>
<a-switch
:checked="$store.getters.usebrowsertimezone" />
</a>
</a-menu-item>
<a-menu-item class="user-menu-item" key="2" disabled>
<a :href="$config.docBase" target="_blank">
<a-icon class="user-menu-item-icon" type="question-circle-o"></a-icon>
<span class="user-menu-item-name">{{ $t('label.help') }}</span>
</a>
</a-menu-item>
<a-menu-divider/>
<a-menu-item class="user-menu-item" key="3">
<a href="javascript:;" @click="handleLogout">
<a-icon class="user-menu-item-icon" type="logout"/>
<span class="user-menu-item-name">{{ $t('label.logout') }}</span>
</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</div>
</template>
<script>
import HeaderNotice from './HeaderNotice'
import TranslationMenu from './TranslationMenu'
import { mapActions, mapGetters } from 'vuex'
export default {
name: 'UserMenu',
components: {
TranslationMenu,
HeaderNotice
},
methods: {
...mapActions(['Logout']),
...mapGetters(['nickname', 'avatar']),
toggleUseBrowserTimezone () {
this.$store.dispatch('SetUseBrowserTimezone', !this.$store.getters.usebrowsertimezone)
},
handleLogout () {
return this.Logout({}).then(() => {
this.$router.push('/user/login')
}).catch(err => {
this.$message.error({
title: 'Failed to Logout',
description: err.message
})
})
}
}
}
</script>
<style lang="less" scoped>
.user-menu {
&-wrapper {
padding: 4px 0;
}
&-item {
width: auto;
}
&-item-name {
user-select: none;
margin-left: 8px;
}
&-item-icon i {
min-width: 12px;
margin-right: 8px;
}
}
</style>

View File

@ -0,0 +1,128 @@
// 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-layout-sider
:class="['sider', isDesktop() ? null : 'shadow', theme, fixSiderbar ? 'ant-fixed-sidemenu' : null ]"
width="256px"
:collapsible="collapsible"
v-model="collapsed"
:trigger="null">
<logo />
<s-menu
:collapsed="collapsed"
:menu="menus"
:theme="theme"
:mode="mode"
@select="onSelect"></s-menu>
</a-layout-sider>
</template>
<script>
import ALayoutSider from 'ant-design-vue/es/layout/Sider'
import Logo from '../header/Logo'
import SMenu from './index'
import { mixin, mixinDevice } from '@/utils/mixin.js'
export default {
name: 'SideMenu',
components: { ALayoutSider, Logo, SMenu },
mixins: [mixin, mixinDevice],
props: {
mode: {
type: String,
required: false,
default: 'inline'
},
theme: {
type: String,
required: false,
default: 'dark'
},
collapsible: {
type: Boolean,
required: false,
default: false
},
collapsed: {
type: Boolean,
required: false,
default: false
},
menus: {
type: Array,
required: true
}
},
methods: {
onSelect (obj) {
this.$emit('menuSelect', obj)
}
}
}
</script>
<style lang="less" scoped>
.sider {
box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
position: relative;
z-index: 10;
height: auto;
/deep/ .ant-layout-sider-children {
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 {
position: fixed;
height: 100%;
}
&.light {
box-shadow: 2px 0px 8px 0px rgba(29, 35, 41, 0.05);
.ant-menu-light {
border-right-color: transparent;
padding: 14px 0;
}
}
&.dark {
.ant-menu-dark {
border-right-color: transparent;
padding: 14px 0;
}
}
}
</style>

View File

@ -0,0 +1,19 @@
// 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.
import SMenu from './menu'
export default SMenu

View File

@ -0,0 +1,207 @@
// 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.
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
const { Item, SubMenu } = Menu
export default {
name: 'SMenu',
props: {
menu: {
type: Array,
required: true
},
theme: {
type: String,
required: false,
default: 'dark'
},
mode: {
type: String,
required: false,
default: 'inline'
},
collapsed: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
openKeys: [],
selectedKeys: [],
cachedOpenKeys: [],
cachedPath: null
}
},
computed: {
rootSubmenuKeys: vm => {
const keys = []
vm.menu.forEach(item => keys.push(item.path))
return keys
}
},
created () {
this.updateMenu()
},
watch: {
collapsed (val) {
if (val) {
this.cachedOpenKeys = this.openKeys.concat()
this.openKeys = []
} else {
this.openKeys = this.cachedOpenKeys
}
},
$route: function () {
this.updateMenu()
}
},
methods: {
// select menu item
onOpenChange (openKeys) {
if (this.mode === 'horizontal') {
this.openKeys = openKeys
return
}
const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
this.openKeys = openKeys
} else {
this.openKeys = latestOpenKey ? [latestOpenKey] : []
}
},
updateMenu () {
const routes = this.$route.matched.concat()
if (routes.length >= 4 && this.$route.meta.hidden) {
routes.pop()
this.selectedKeys = [routes[2].path]
} else {
this.selectedKeys = [routes.pop().path]
}
const openKeys = []
if (this.mode === 'inline') {
routes.forEach(item => {
openKeys.push(item.path)
})
}
this.cachedPath = this.selectedKeys[0]
this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
},
// render
renderItem (menu) {
if (!menu.hidden) {
return menu.children && !menu.hideChildrenInMenu ? this.renderSubMenu(menu) : this.renderMenuItem(menu)
}
return null
},
renderMenuItem (menu) {
const target = menu.meta.target || null
const props = {
to: { name: menu.name },
target: target
}
return (
<Item {...{ key: menu.path }}>
<router-link {...{ props }}>
{this.renderIcon(menu.meta.icon, menu)}
<span>{this.$t(menu.meta.title)}</span>
</router-link>
</Item>
)
},
renderSubMenu (menu) {
const itemArr = []
const on = {
click: () => {
this.handleClickParentMenu(menu)
}
}
if (!menu.hideChildrenInMenu) {
menu.children.forEach(item => itemArr.push(this.renderItem(item)))
}
return (
<SubMenu {...{ key: menu.path }}>
<span slot="title">
{this.renderIcon(menu.meta.icon, menu)}
<span {...{ on: on }}>{this.$t(menu.meta.title)}</span>
</span>
{itemArr}
</SubMenu>
)
},
renderIcon (icon, menuItem) {
if (icon === 'none' || icon === undefined) {
return null
}
const props = {}
const on = {
click: () => {
this.handleClickParentMenu(menuItem)
}
}
typeof (icon) === 'object' ? props.component = icon : props.type = icon
return (
<Icon {... { props, on } } />
)
},
handleClickParentMenu (menuItem) {
if (this.cachedPath === menuItem.redirect) {
return
}
if (menuItem.redirect) {
this.cachedPath = menuItem.redirect
setTimeout(() => this.$router.push({ path: menuItem.path }))
}
}
},
render () {
const { mode, theme, menu } = this
const props = {
mode: mode,
theme: theme,
openKeys: this.openKeys
}
const on = {
select: obj => {
this.selectedKeys = obj.selectedKeys
this.$emit('select', obj)
},
openChange: this.onOpenChange
}
const menuTree = menu.map(item => {
if (item.hidden) {
return null
}
return this.renderItem(item)
})
// {...{ props, on: on }}
return (
<Menu vModel={this.selectedKeys} {...{ props, on: on }}>
{menuTree}
</Menu>
)
}
}

View File

@ -0,0 +1,173 @@
// 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.
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
const { Item, SubMenu } = Menu
export default {
name: 'SMenu',
props: {
menu: {
type: Array,
required: true
},
theme: {
type: String,
required: false,
default: 'dark'
},
mode: {
type: String,
required: false,
default: 'inline'
},
collapsed: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
openKeys: [],
selectedKeys: [],
cachedOpenKeys: []
}
},
computed: {
rootSubmenuKeys: vm => {
const keys = []
vm.menu.forEach(item => keys.push(item.path))
return keys
}
},
created () {
this.updateMenu()
},
watch: {
collapsed (val) {
if (val) {
this.cachedOpenKeys = this.openKeys.concat()
this.openKeys = []
} else {
this.openKeys = this.cachedOpenKeys
}
},
$route: function () {
this.updateMenu()
}
},
methods: {
renderIcon: function (h, icon) {
if (icon === 'none' || icon === undefined) {
return null
}
const props = {}
typeof (icon) === 'object' ? props.component = icon : props.type = icon
return h(Icon, { props: { ...props } })
},
renderMenuItem: function (h, menu, pIndex, index) {
const target = menu.meta.target || null
return h(Item, { key: menu.path ? menu.path : 'item_' + pIndex + '_' + index }, [
h('router-link', { attrs: { to: { name: menu.name }, target: target } }, [
this.renderIcon(h, menu.meta.icon),
h('span', [menu.meta.title])
])
])
},
renderSubMenu: function (h, menu, pIndex, index) {
const this2_ = this
const subItem = [h('span', { slot: 'title' }, [this.renderIcon(h, menu.meta.icon), h('span', [menu.meta.title])])]
const itemArr = []
const pIndex_ = pIndex + '_' + index
console.log('menu', menu)
if (!menu.hideChildrenInMenu) {
menu.children.forEach(function (item, i) {
itemArr.push(this2_.renderItem(h, item, pIndex_, i))
})
}
return h(SubMenu, { key: menu.path ? menu.path : 'submenu_' + pIndex + '_' + index }, subItem.concat(itemArr))
},
renderItem: function (h, menu, pIndex, index) {
if (!menu.hidden) {
return menu.children && !menu.hideChildrenInMenu
? this.renderSubMenu(h, menu, pIndex, index)
: this.renderMenuItem(h, menu, pIndex, index)
}
},
renderMenu: function (h, menuTree) {
const this2_ = this
const menuArr = []
menuTree.forEach(function (menu, i) {
if (!menu.hidden) {
menuArr.push(this2_.renderItem(h, menu, '0', i))
}
})
return menuArr
},
onOpenChange (openKeys) {
const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
this.openKeys = openKeys
} else {
this.openKeys = latestOpenKey ? [latestOpenKey] : []
}
},
updateMenu () {
const routes = this.$route.matched.concat()
if (routes.length >= 4 && this.$route.meta.hidden) {
routes.pop()
this.selectedKeys = [routes[2].path]
} else {
this.selectedKeys = [routes.pop().path]
}
const openKeys = []
if (this.mode === 'inline') {
routes.forEach(item => {
openKeys.push(item.path)
})
}
this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
}
},
render (h) {
return h(
Menu,
{
props: {
theme: this.$props.theme,
mode: this.$props.mode,
openKeys: this.openKeys,
selectedKeys: this.selectedKeys
},
on: {
openChange: this.onOpenChange,
select: obj => {
this.selectedKeys = obj.selectedKeys
this.$emit('select', obj)
}
}
},
this.renderMenu(h, this.menu)
)
}
}

View File

@ -0,0 +1,204 @@
// 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 style="margin: -23px -24px 24px -24px">
&lt;!&ndash;<a-dropdown :trigger="['contextmenu']" overlayClassName="multi-tab-menu-wrapper">
<a-menu slot="overlay">
<a-menu-item key="1">1st menu item</a-menu-item>
<a-menu-item key="2">2nd menu item</a-menu-item>
<a-menu-item key="3">3rd menu item</a-menu-item>
</a-menu>
</a-dropdown>&ndash;&gt;
<a-tabs
hideAdd
v-model="activeKey"
type="editable-card"
:tabBarStyle="{ background: '#FFF', margin: 0, paddingLeft: '16px', paddingTop: '1px' }"
@edit="onEdit"
>
<a-tab-pane v-for="page in pages" :style="{ height: 0 }" :tab="page.meta.title" :key="page.fullPath" :closable="pages.length > 1">
</a-tab-pane>
<template slot="renderTabBar" slot-scope="props, DefaultTabBar">
<component :is="DefaultTabBar" {...props} />
</template>
</a-tabs>
</div>
</template>
-->
<script>
export default {
name: 'MultiTab',
data () {
return {
fullPathList: [],
pages: [],
activeKey: '',
newTabIndex: 0
}
},
created () {
this.pages.push(this.$route)
this.fullPathList.push(this.$route.fullPath)
this.selectedLastPath()
},
methods: {
onEdit (targetKey, action) {
this[action](targetKey)
},
remove (targetKey) {
this.pages = this.pages.filter(page => page.fullPath !== targetKey)
this.fullPathList = this.fullPathList.filter(path => path !== targetKey)
//
if (!this.fullPathList.includes(this.activeKey)) {
this.selectedLastPath()
}
},
selectedLastPath () {
this.activeKey = this.fullPathList[this.fullPathList.length - 1]
},
// content menu
closeThat (e) {
this.remove(e)
},
closeLeft (e) {
const currentIndex = this.fullPathList.indexOf(e)
if (currentIndex > 0) {
this.fullPathList.forEach((item, index) => {
if (index < currentIndex) {
this.remove(item)
}
})
} else {
this.$message.info('左侧没有标签')
}
},
closeRight (e) {
const currentIndex = this.fullPathList.indexOf(e)
if (currentIndex < (this.fullPathList.length - 1)) {
this.fullPathList.forEach((item, index) => {
if (index > currentIndex) {
this.remove(item)
}
})
} else {
this.$message.info('右侧没有标签')
}
},
closeAll (e) {
const currentIndex = this.fullPathList.indexOf(e)
this.fullPathList.forEach((item, index) => {
if (index !== currentIndex) {
this.remove(item)
}
})
},
closeMenuClick ({ key, item, domEvent }) {
const vkey = domEvent.target.getAttribute('data-vkey')
switch (key) {
case 'close-right':
this.closeRight(vkey)
break
case 'close-left':
this.closeLeft(vkey)
break
case 'close-all':
this.closeAll(vkey)
break
default:
case 'close-that':
this.closeThat(vkey)
break
}
},
renderTabPaneMenu (e) {
return (
<a-menu {...{ on: { click: this.closeMenuClick } }}>
<a-menu-item key="close-that" data-vkey={e}>关闭当前标签</a-menu-item>
<a-menu-item key="close-right" data-vkey={e}>关闭右侧</a-menu-item>
<a-menu-item key="close-left" data-vkey={e}>关闭左侧</a-menu-item>
<a-menu-item key="close-all" data-vkey={e}>关闭全部</a-menu-item>
</a-menu>
)
},
// render
renderTabPane (title, keyPath) {
const menu = this.renderTabPaneMenu(keyPath)
return (
<a-dropdown overlay={menu} trigger={['contextmenu']}>
<span style={{ userSelect: 'none' }}>{ title }</span>
</a-dropdown>
)
}
},
watch: {
$route: function (newVal) {
this.activeKey = newVal.fullPath
if (this.fullPathList.indexOf(newVal.fullPath) < 0) {
this.fullPathList.push(newVal.fullPath)
this.pages.push(newVal)
}
},
activeKey: function (newPathKey) {
this.$router.push({ path: newPathKey })
}
},
render () {
const { onEdit, $data: { pages } } = this
const panes = pages.map(page => {
return (
<a-tab-pane
style={{ height: 0 }}
tab={this.renderTabPane(page.meta.title, page.fullPath)}
key={page.fullPath} closable={pages.length > 1}
>
</a-tab-pane>)
})
return (
<div class="multi-tab">
<div class="multi-tab-wrapper">
<a-tabs
hideAdd
type={'editable-card'}
v-model={this.activeKey}
tabBarStyle={{ background: '#FFF', margin: 0, paddingLeft: '16px', paddingTop: '1px' }}
{...{ on: { edit: onEdit } }}>
{panes}
</a-tabs>
</div>
</div>
)
}
}
</script>
<style lang="less" scoped>
.multi-tab {
margin: -23px -24px 24px -24px;
background: #fff;
}
.topmenu .multi-tab-wrapper {
max-width: 1200px;
margin: 0 auto;
}
</style>

View File

@ -0,0 +1,21 @@
// 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.
import MultiTab from './MultiTab'
import './index.less'
export default MultiTab

View File

@ -0,0 +1,31 @@
// 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.
@import '../../style/variables/prefixes';
@multi-tab-prefix-cls: ~"@{ant-pro-prefix}-multi-tab";
@multi-tab-wrapper-prefix-cls: ~"@{ant-pro-prefix}-multi-tab-wrapper";
.@{multi-tab-prefix-cls} {
margin: -23px -24px 24px -24px;
background: #fff;
}
.topmenu .@{multi-tab-wrapper-prefix-cls} {
max-width: 1200px;
margin: 0 auto;
}

View File

@ -0,0 +1,67 @@
// 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="footer">
<div class="line">
<span v-html="$config.footer" />
</div>
<div class="line" v-if="$store.getters.userInfo.roletype === 'Admin'">
CloudStack {{ $store.getters.features.cloudstackversion }}
<a-divider type="vertical" />
<a href="https://github.com/apache/cloudstack/issues/new" target="_blank">
<a-icon type="github"/>
{{ $t('label.report.bug') }}
</a>
</div>
</div>
</template>
<script>
export default {
name: 'LayoutFooter',
data () {
return {
}
}
}
</script>
<style lang="less" scoped>
.footer {
padding: 0 16px;
margin: 48px 0 24px;
text-align: center;
.line {
margin-bottom: 8px;
a {
color: rgba(0, 0, 0, .45);
&:hover {
color: rgba(0, 0, 0, .65);
}
}
}
.copyright {
color: rgba(0, 0, 0, .45);
font-size: 14px;
}
}
</style>

View File

@ -0,0 +1,147 @@
// 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-layout-header v-if="!headerBarFixed" :class="[fixedHeader && 'ant-header-fixedHeader', sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed', theme ]" :style="{ padding: '0' }">
<div v-if="mode === 'sidemenu'" class="header">
<a-icon
v-if="device==='mobile'"
class="trigger"
:type="collapsed ? 'menu-fold' : 'menu-unfold'"
@click="toggle"></a-icon>
<a-icon
v-else
class="trigger"
:type="collapsed ? 'menu-unfold' : 'menu-fold'"
@click="toggle"/>
<project-menu v-if="device !== 'mobile'" />
<saml-domain-switcher style="margin-left: 20px" />
<user-menu></user-menu>
</div>
<div v-else :class="['top-nav-header-index', theme]">
<div class="header-index-wide">
<div class="header-index-left">
<logo class="top-nav-header" :show-title="device !== 'mobile'" />
<s-menu
v-if="device !== 'mobile'"
mode="horizontal"
:menu="menus"
:theme="theme"
></s-menu>
<a-icon
v-else
class="trigger"
:type="collapsed ? 'menu-fold' : 'menu-unfold'"
@click="toggle"></a-icon>
</div>
<project-menu v-if="device !== 'mobile'" />
<saml-domain-switcher style="margin-left: 20px" />
<user-menu></user-menu>
</div>
</div>
</a-layout-header>
</template>
<script>
import Breadcrumb from '@/components/widgets/Breadcrumb'
import Logo from '../header/Logo'
import SMenu from '../menu/'
import ProjectMenu from '../header/ProjectMenu'
import SamlDomainSwitcher from '../header/SamlDomainSwitcher'
import UserMenu from '../header/UserMenu'
import { mixin } from '@/utils/mixin.js'
export default {
name: 'GlobalHeader',
components: {
Breadcrumb,
Logo,
SMenu,
ProjectMenu,
SamlDomainSwitcher,
UserMenu
},
mixins: [mixin],
props: {
mode: {
type: String,
// sidemenu, topmenu
default: 'sidemenu'
},
menus: {
type: Array,
required: true
},
theme: {
type: String,
required: false,
default: 'dark'
},
collapsed: {
type: Boolean,
required: false,
default: false
},
device: {
type: String,
required: false,
default: 'desktop'
}
},
data () {
return {
headerBarFixed: false
}
},
mounted () {
window.addEventListener('scroll', this.handleScroll)
},
methods: {
handleScroll () {
if (this.autoHideHeader) {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
if (scrollTop > 100) {
this.headerBarFixed = true
} else {
this.headerBarFixed = false
}
} else {
this.headerBarFixed = false
}
},
toggle () {
this.$emit('toggle')
}
}
}
</script>
<style lang="less" scoped>
.ant-layout-header {
.anticon-menu-fold {
font-size: 18px;
line-height: 1;
}
.ant-breadcrumb {
display: inline;
vertical-align: middle;
}
}
</style>

View File

@ -0,0 +1,205 @@
// 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-layout class="layout" :class="[device]">
<template v-if="isSideMenu()">
<a-drawer
v-if="isMobile()"
:wrapClassName="'drawer-sider ' + navTheme"
:closable="false"
:visible="collapsed"
placement="left"
@close="() => this.collapsed = false"
>
<side-menu
:menus="menus"
:theme="navTheme"
:collapsed="false"
:collapsible="true"
mode="inline"
@menuSelect="menuSelect"></side-menu>
</a-drawer>
<side-menu
v-else
mode="inline"
:menus="menus"
:theme="navTheme"
:collapsed="collapsed"
:collapsible="true"></side-menu>
</template>
<template v-else>
<a-drawer
v-if="isMobile()"
:wrapClassName="'drawer-sider ' + navTheme"
placement="left"
@close="() => this.collapsed = false"
:closable="false"
:visible="collapsed"
>
<side-menu
:menus="menus"
:theme="navTheme"
:collapsed="false"
:collapsible="true"
mode="inline"
@menuSelect="menuSelect"></side-menu>
</a-drawer>
</template>
<a-layout :class="[layoutMode, `content-width-${contentWidth}`]" :style="{ paddingLeft: contentPaddingLeft, minHeight: '100vh' }">
<!-- layout header -->
<global-header
:mode="layoutMode"
:menus="menus"
:theme="navTheme"
:collapsed="collapsed"
:device="device"
@toggle="toggle"
/>
<!-- layout content -->
<a-layout-content class="layout-content" :class="{'is-header-fixed': fixedHeader}">
<slot></slot>
</a-layout-content>
<!-- layout footer -->
<a-layout-footer style="padding: 0">
<global-footer />
</a-layout-footer>
</a-layout>
</a-layout>
</template>
<script>
import SideMenu from '@/components/menu/SideMenu'
import GlobalHeader from '@/components/page/GlobalHeader'
import GlobalFooter from '@/components/page/GlobalFooter'
import { triggerWindowResizeEvent } from '@/utils/util'
import { mapState, mapActions } from 'vuex'
import { mixin, mixinDevice } from '@/utils/mixin.js'
export default {
name: 'GlobalLayout',
components: {
SideMenu,
GlobalHeader,
GlobalFooter
},
mixins: [mixin, mixinDevice],
data () {
return {
collapsed: false,
menus: []
}
},
computed: {
...mapState({
mainMenu: state => state.permission.addRouters
}),
contentPaddingLeft () {
if (!this.fixSidebar || this.isMobile()) {
return '0'
}
if (this.sidebarOpened) {
return '256px'
}
return '80px'
}
},
watch: {
sidebarOpened (val) {
this.collapsed = !val
},
mainMenu (newMenu) {
this.menus = newMenu.find((item) => item.path === '/').children
}
},
created () {
this.menus = this.mainMenu.find((item) => item.path === '/').children
this.collapsed = !this.sidebarOpened
},
mounted () {
const userAgent = navigator.userAgent
if (userAgent.indexOf('Edge') > -1) {
this.$nextTick(() => {
this.collapsed = !this.collapsed
setTimeout(() => {
this.collapsed = !this.collapsed
}, 16)
})
}
},
methods: {
...mapActions(['setSidebar']),
toggle () {
this.collapsed = !this.collapsed
this.setSidebar(!this.collapsed)
triggerWindowResizeEvent()
},
paddingCalc () {
let left = ''
if (this.sidebarOpened) {
left = this.isDesktop() ? '256px' : '80px'
} else {
left = this.isMobile() && '0' || (this.fixSidebar && '80px' || '0')
}
return left
},
menuSelect () {
if (!this.isDesktop()) {
this.collapsed = false
}
}
}
}
</script>
<style lang="less">
.layout-content {
&.is-header-fixed {
margin: 78px 12px 0;
}
}
// Todo try to get this rules scoped
.ant-drawer.drawer-sider {
.sider {
box-shadow: none;
}
&.dark {
.ant-drawer-content {
background-color: rgb(0, 21, 41);
}
}
&.light {
box-shadow: none;
.ant-drawer-content {
background-color: #fff;
}
}
.ant-drawer-body {
padding: 0
}
}
</style>

View File

@ -0,0 +1,250 @@
// 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="page-header">
<div class="page-header-index-wide">
<a-breadcrumb class="breadcrumb">
<a-breadcrumb-item v-for="(item, index) in breadList" :key="index">
<router-link
v-if="item.name"
:to="{ path: item.path === '' ? '/' : item.path }"
>
<a-icon v-if="index == 0" :type="item.meta.icon" />
{{ item.meta.title }}
</router-link>
<span v-else-if="$route.params.id">{{ $route.params.id }}</span>
<span v-else>{{ item.meta.title }}</span>
</a-breadcrumb-item>
</a-breadcrumb>
<div class="detail">
<div class="main" v-if="!$route.meta.hiddenHeaderContent">
<div class="row">
<img v-if="logo" :src="logo" class="logo"/>
<h1 v-if="title" class="title">{{ title }}</h1>
<div class="action">
<slot name="action"></slot>
</div>
</div>
<div>
<slot name="pageMenu"></slot>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Breadcrumb from '@/components/widgets/Breadcrumb'
export default {
name: 'PageHeader',
components: {
's-breadcrumb': Breadcrumb
},
props: {
title: {
type: String,
default: '',
required: false
},
breadcrumb: {
type: Array,
default: null,
required: false
},
logo: {
type: String,
default: '',
required: false
},
avatar: {
type: String,
default: '',
required: false
}
},
data () {
return {
name: '',
breadList: []
}
},
created () {
this.getBreadcrumb()
},
methods: {
getBreadcrumb () {
this.breadList = []
this.name = this.$route.name
this.$route.matched.forEach((item) => {
// item.name !== 'index' && this.breadList.push(item)
this.breadList.push(item)
})
}
},
watch: {
$route () {
this.getBreadcrumb()
}
}
}
</script>
<style lang="less" scoped>
.page-header {
background: #fff;
padding: 16px 32px 0;
border-bottom: 1px solid #e8e8e8;
.breadcrumb {
margin-bottom: 16px;
}
.detail {
display: flex;
/*margin-bottom: 16px;*/
.avatar {
flex: 0 1 72px;
margin: 0 24px 8px 0;
& > span {
border-radius: 72px;
display: block;
width: 72px;
height: 72px;
}
}
.main {
width: 100%;
flex: 0 1 auto;
.row {
display: flex;
width: 100%;
.avatar {
margin-bottom: 16px;
}
}
.title {
font-size: 20px;
font-weight: 500;
font-size: 20px;
line-height: 28px;
font-weight: 500;
color: rgba(0,0,0,.85);
margin-bottom: 16px;
flex: auto;
}
.logo {
width: 28px;
height: 28px;
border-radius: 4px;
margin-right: 16px;
}
.content, .headerContent {
flex: auto;
color: rgba(0,0,0,.45);
line-height: 22px;
.link {
margin-top: 16px;
line-height: 24px;
a {
font-size: 14px;
margin-right: 32px;
}
}
}
.extra {
flex: 0 1 auto;
margin-left: 88px;
min-width: 242px;
text-align: right;
}
.action {
margin-left: 56px;
min-width: 266px;
flex: 0 1 auto;
text-align: right;
&:empty {
display: none;
}
}
}
}
}
.mobile .page-header {
.main {
.row {
flex-wrap: wrap;
.avatar {
flex: 0 1 25%;
margin: 0 2% 8px 0;
}
.content, .headerContent {
flex: 0 1 70%;
.link {
margin-top: 16px;
line-height: 24px;
a {
font-size: 14px;
margin-right: 10px;
}
}
}
.extra {
flex: 1 1 auto;
margin-left: 0;
min-width: 0;
text-align: right;
}
.action {
margin-left: unset;
min-width: 266px;
flex: 0 1 auto;
text-align: left;
margin-bottom: 12px;
&:empty {
display: none;
}
}
}
}
}
</style>

View File

@ -0,0 +1,141 @@
// 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 :style="!$route.meta.pageHeader ? 'margin: -24px -24px 0px;' : null">
<!-- pageHeader , route meta hideHeader:true on hide -->
<page-header v-if="!$route.meta.pageHeader" :title="title" :logo="logo" :avatar="avatar">
<slot slot="action" name="action"></slot>
<slot slot="content" name="headerContent"></slot>
<div slot="content" v-if="!this.$slots.headerContent && desc">
<p style="font-size: 14px;color: rgba(0,0,0,.65)">{{ desc }}</p>
<div class="link">
<template v-for="(link, index) in linkList">
<a :key="index" :href="link.href">
<a-icon :type="link.icon"/>
<span>{{ link.title }}</span>
</a>
</template>
</div>
</div>
<slot slot="extra" name="extra"></slot>
<div slot="pageMenu">
<div class="page-menu-search" v-if="search">
<a-input-search style="width: 80%; max-width: 522px;" placeholder="请输入..." size="large" enterButton="搜索" />
</div>
<div class="page-menu-tabs" v-if="tabs && tabs.items">
<!-- @change="callback" :activeKey="activeKey" -->
<a-tabs :tabBarStyle="{margin: 0}" @change="tabs.callback" :activeKey="tabs.active()">
<a-tab-pane v-for="item in tabs.items" :tab="item.title" :key="item.key"></a-tab-pane>
</a-tabs>
</div>
</div>
</page-header>
<div class="content">
<div :class="['page-header-index-wide']">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
import PageHeader from './PageHeader'
export default {
name: 'LayoutContent',
components: {
PageHeader
},
// ['desc', 'logo', 'title', 'avatar', 'linkList', 'extraImage']
props: {
desc: {
type: String,
default: null
},
logo: {
type: String,
default: null
},
title: {
type: String,
default: null
},
avatar: {
type: String,
default: null
},
linkList: {
type: Array,
default: null
},
extraImage: {
type: String,
default: null
},
search: {
type: Boolean,
default: false
},
tabs: {
type: Object,
default: () => ({})
}
},
methods: {
}
}
</script>
<style lang="less" scoped>
.content {
margin: 24px 24px 0;
.link {
margin-top: 16px;
&:not(:empty) {
margin-bottom: 16px;
}
a {
margin-right: 32px;
height: 24px;
line-height: 24px;
display: inline-block;
i {
font-size: 24px;
margin-right: 8px;
vertical-align: middle;
}
span {
height: 24px;
line-height: 24px;
display: inline-block;
vertical-align: middle;
}
}
}
}
.page-menu-search {
text-align: center;
margin-bottom: 16px;
}
.page-menu-tabs {
margin-top: 48px;
}
</style>

View File

@ -0,0 +1,76 @@
// 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-popover trigger="click" placement="bottomRight" :overlayStyle="{ width: '300px' }">
<template slot="content">
<a-spin :spinning="loadding">
<a-tabs>
<a-tab-pane v-for="(tab, k) in tabs" :tab="tab.title" :key="k">
</a-tab-pane>
</a-tabs>
</a-spin>
</template>
<span @click="fetchNotice" class="header-notice">
<a-badge count="12">
<a-icon style="font-size: 16px; padding: 4px" type="bell" />
</a-badge>
</span>
</a-popover>
</template>
<script>
export default {
name: 'HeaderNotice',
props: {
tabs: {
type: Array,
default: null,
required: true
}
},
data () {
return {
loadding: false
}
},
methods: {
fetchNotice () {
if (this.loadding) {
this.loadding = false
return
}
this.loadding = true
setTimeout(() => {
this.loadding = false
}, 2000)
}
}
}
</script>
<style lang="less" scoped>
.header-notice {
display: inline-block;
transition: all 0.3s;
span {
vertical-align: initial;
}
}
</style>

View File

@ -0,0 +1,192 @@
// 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">
<console :resource="resource" :size="size" v-if="resource && resource.id && dataView" />
<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 && ('show' in action ? action.show(resource, $store.getters) : true)) || (action.groupAction && selectedRowKeys.length > 0 && ('groupShow' in action ? action.show(resource, $store.getters) : true)))) ||
(dataView && action.dataView && ('show' in action ? action.show(resource, $store.getters) : true))
)" >
<a-button
:type="action.icon === 'delete' ? 'danger' : (action.icon === 'plus' ? 'primary' : 'default')"
:shape="!dataView && action.icon === 'plus' ? 'round' : 'circle'"
style="margin-left: 5px"
:size="size"
@click="execAction(action)">
<span v-if="!dataView && action.icon === 'plus'">
{{ $t(action.label) }}
</span>
<a-icon v-if="(typeof action.icon === 'string')" :type="action.icon" />
<font-awesome-icon v-else :icon="action.icon" />
</a-button>
</a-badge>
<a-button
v-if="action.api in $store.getters.apis &&
!action.showBadge && (
(!dataView && ((action.listView && ('show' in action ? action.show(resource, $store.getters) : true)) || (action.groupAction && selectedRowKeys.length > 0 && ('groupShow' in action ? action.show(resource, $store.getters) : true)))) ||
(dataView && action.dataView && ('show' in action ? action.show(resource, $store.getters) : true))
)"
:type="action.icon === 'delete' ? 'danger' : (action.icon === 'plus' ? 'primary' : 'default')"
:shape="!dataView && ['plus', 'user-add'].includes(action.icon) ? 'round' : 'circle'"
style="margin-left: 5px"
:size="size"
@click="execAction(action)">
<span v-if="!dataView && ['plus', 'user-add'].includes(action.icon)">
{{ $t(action.label) }}
</span>
<a-icon v-if="(typeof action.icon === 'string')" :type="action.icon" />
<font-awesome-icon v-else :icon="action.icon" />
</a-button>
</a-tooltip>
</span>
</template>
<script>
import { api } from '@/api'
import Console from '@/components/widgets/Console'
export default {
name: 'ActionButton',
components: {
Console
},
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
},
size: {
type: String,
default: 'default'
}
},
watch: {
resource (newItem, oldItem) {
if (!newItem || !newItem.id) {
return
}
this.handleShowBadge()
}
},
methods: {
execAction (action) {
action.resource = this.resource
this.$emit('exec-action', action)
},
handleShowBadge () {
this.actionBadge = {}
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] && 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(this.actionBadge, response[j].api, {})
this.$set(this.actionBadge[response[j].api], 'badgeNum', response[j].count)
}
}).catch(() => {})
}
}
}
}
</script>
<style scoped >
.button-action-badge {
margin-left: 5px;
}
/deep/.button-action-badge .ant-badge-count {
right: 10px;
z-index: 8;
}
</style>

View File

@ -0,0 +1,309 @@
// 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-list-item v-if="dedicatedDomainId">
<div>
<div style="margin-bottom: 10px;">
<strong>{{ $t('label.dedicated') }}</strong>
<div>{{ $t('label.yes') }}</div>
</div>
<p>
<strong>{{ $t('label.domainid') }}</strong><br/>
<router-link :to="{ path: '/domain/' + dedicatedDomainId }">{{ dedicatedDomainId }}</router-link>
</p>
<p v-if="dedicatedAccountId">
<strong>{{ $t('label.account') }}</strong><br/>
<router-link :to="{ path: '/account/' + dedicatedAccountId }">{{ dedicatedAccountId }}</router-link>
</p>
<a-button style="margin-top: 10px; margin-bottom: 10px;" type="danger" @click="handleRelease">
{{ releaseButtonLabel }}
</a-button>
</div>
</a-list-item>
<a-list-item v-else>
<div>
<strong>{{ $t('label.dedicated') }}</strong>
<div>{{ $t('label.no') }}</div>
<a-button type="primary" style="margin-top: 10px; margin-bottom: 10px;" @click="modalActive = true" :disabled="!dedicateButtonAvailable">
{{ dedicatedButtonLabel }}
</a-button>
</div>
<DedicateModal
:resource="resource"
:active="modalActive"
:label="dedicatedModalLabel"
@close="modalActive = false"
:fetchData="fetchData" />
</a-list-item>
</template>
<script>
import { api } from '@/api'
import DedicateModal from './DedicateModal'
export default {
props: {
resource: {
type: Object,
required: true
}
},
components: {
DedicateModal
},
inject: ['parentFetchData'],
data () {
return {
modalActive: false,
dedicateButtonAvailable: true,
dedicatedButtonLabel: this.$t('label.dedicate'),
releaseButtonLabel: this.$t('label.release'),
dedicatedModalLabel: this.$t('label.dedicate'),
dedicatedDomainId: null,
dedicatedAccountId: null
}
},
watch: {
resource (newItem, oldItem) {
if (this.resource && this.resource.id && newItem && newItem.id !== oldItem.id) {
this.fetchData()
}
}
},
methods: {
fetchData () {
this.dedicateButtonAvailable = true
if (this.$route.meta.name === 'zone') {
this.fetchDedicatedZones()
this.releaseButtonLabel = this.$t('label.release.dedicated.zone')
this.dedicateButtonAvailable = ('dedicateZone' in this.$store.getters.apis)
this.dedicatedButtonLabel = this.$t('label.dedicate.zone')
this.dedicatedModalLabel = this.$t('label.dedicate.zone')
}
if (this.$route.meta.name === 'pod') {
this.fetchDedicatedPods()
this.releaseButtonLabel = this.$t('label.release.dedicated.pod')
this.dedicateButtonAvailable = ('dedicatePod' in this.$store.getters.apis)
this.dedicatedButtonLabel = this.$t('label.dedicate.pod')
this.dedicatedModalLabel = this.$t('label.dedicate.pod')
}
if (this.$route.meta.name === 'cluster') {
this.fetchDedicatedClusters()
this.releaseButtonLabel = this.$t('label.release.dedicated.cluster')
this.dedicateButtonAvailable = ('dedicateCluster' in this.$store.getters.apis)
this.dedicatedButtonLabel = this.$t('label.dedicate.cluster')
this.dedicatedModalLabel = this.$t('label.dedicate.cluster')
}
if (this.$route.meta.name === 'host') {
this.fetchDedicatedHosts()
this.releaseButtonLabel = this.$t('label.release.dedicated.host')
this.dedicateButtonAvailable = ('dedicateHost' in this.$store.getters.apis)
this.dedicatedButtonLabel = this.$t('label.dedicate.host')
this.dedicatedModalLabel = this.$t('label.dedicate.host')
}
},
fetchDedicatedZones () {
api('listDedicatedZones', {
zoneid: this.resource.id
}).then(response => {
if (response.listdedicatedzonesresponse.dedicatedzone &&
response.listdedicatedzonesresponse.dedicatedzone.length > 0) {
this.dedicatedDomainId = response.listdedicatedzonesresponse.dedicatedzone[0].domainid
this.dedicatedAccountId = response.listdedicatedzonesresponse.dedicatedzone[0].accountid
}
}).catch(error => {
this.$notifyError(error)
})
},
fetchDedicatedPods () {
api('listDedicatedPods', {
podid: this.resource.id
}).then(response => {
if (response.listdedicatedpodsresponse.dedicatedpod &&
response.listdedicatedpodsresponse.dedicatedpod.length > 0) {
this.dedicatedDomainId = response.listdedicatedpodsresponse.dedicatedpod[0].domainid
this.dedicatedAccountId = response.listdedicatedpodsresponse.dedicatedpod[0].accountid
}
}).catch(error => {
this.$notifyError(error)
})
},
fetchDedicatedClusters () {
api('listDedicatedClusters', {
clusterid: this.resource.id
}).then(response => {
if (response.listdedicatedclustersresponse.dedicatedcluster &&
response.listdedicatedclustersresponse.dedicatedcluster.length > 0) {
this.dedicatedDomainId = response.listdedicatedclustersresponse.dedicatedcluster[0].domainid
this.dedicatedAccountId = response.listdedicatedclustersresponse.dedicatedcluster[0].accountid
}
}).catch(error => {
this.$notifyError(error)
})
},
fetchDedicatedHosts () {
api('listDedicatedHosts', {
hostid: this.resource.id
}).then(response => {
if (response.listdedicatedhostsresponse.dedicatedhost &&
response.listdedicatedhostsresponse.dedicatedhost.length > 0) {
this.dedicatedDomainId = response.listdedicatedhostsresponse.dedicatedhost[0].domainid
this.dedicatedAccountId = response.listdedicatedhostsresponse.dedicatedhost[0].accountid
}
}).catch(error => {
this.$notifyError(error)
})
},
releaseDedidcatedZone () {
api('releaseDedicatedZone', {
zoneid: this.resource.id
}).then(response => {
this.$pollJob({
jobId: response.releasededicatedzoneresponse.jobid,
successMessage: this.$t('message.dedicated.zone.released'),
successMethod: () => {
this.parentFetchData()
this.dedicatedDomainId = null
this.$store.dispatch('AddAsyncJob', {
title: this.$t('message.dedicated.zone.released'),
jobid: response.releasededicatedzoneresponse.jobid,
status: 'progress'
})
},
errorMessage: this.$t('error.release.dedicate.zone'),
errorMethod: () => {
this.parentFetchData()
},
loadingMessage: this.$t('message.releasing.dedicated.zone'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.parentFetchData()
}
})
}).catch(error => {
this.$notifyError(error)
})
},
releaseDedidcatedPod () {
api('releaseDedicatedPod', {
podid: this.resource.id
}).then(response => {
this.$pollJob({
jobId: response.releasededicatedpodresponse.jobid,
successMessage: this.$t('message.pod.dedication.released'),
successMethod: () => {
this.parentFetchData()
this.dedicatedDomainId = null
this.$store.dispatch('AddAsyncJob', {
title: this.$t('message.pod.dedication.released'),
jobid: response.releasededicatedpodresponse.jobid,
status: 'progress'
})
},
errorMessage: this.$t('error.release.dedicate.pod'),
errorMethod: () => {
this.parentFetchData()
},
loadingMessage: this.$t('message.releasing.dedicated.pod'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.parentFetchData()
}
})
}).catch(error => {
this.$notifyError(error)
})
},
releaseDedidcatedCluster () {
api('releaseDedicatedCluster', {
clusterid: this.resource.id
}).then(response => {
this.$pollJob({
jobId: response.releasededicatedclusterresponse.jobid,
successMessage: this.$t('message.cluster.dedication.released'),
successMethod: () => {
this.parentFetchData()
this.dedicatedDomainId = null
this.$store.dispatch('AddAsyncJob', {
title: this.$t('message.cluster.dedication.released'),
jobid: response.releasededicatedclusterresponse.jobid,
status: 'progress'
})
},
errorMessage: this.$t('error.release.dedicate.cluster'),
errorMethod: () => {
this.parentFetchData()
},
loadingMessage: this.$t('message.releasing.dedicated.cluster'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.parentFetchData()
}
})
}).catch(error => {
this.$notifyError(error)
})
},
releaseDedidcatedHost () {
api('releaseDedicatedHost', {
hostid: this.resource.id
}).then(response => {
this.$pollJob({
jobId: response.releasededicatedhostresponse.jobid,
successMessage: this.$t('message.host.dedication.released'),
successMethod: () => {
this.parentFetchData()
this.dedicatedDomainId = null
this.$store.dispatch('AddAsyncJob', {
title: this.$t('message.host.dedication.released'),
jobid: response.releasededicatedhostresponse.jobid,
status: 'progress'
})
},
errorMessage: this.$t('error.release.dedicate.host'),
errorMethod: () => {
this.parentFetchData()
},
loadingMessage: this.$t('message.releasing.dedicated.host'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.parentFetchData()
}
})
}).catch(error => {
this.$notifyError(error)
})
},
handleRelease () {
this.modalActive = false
if (this.$route.meta.name === 'zone') {
this.releaseDedidcatedZone()
}
if (this.$route.meta.name === 'pod') {
this.releaseDedidcatedPod()
}
if (this.$route.meta.name === 'cluster') {
this.releaseDedidcatedCluster()
}
if (this.$route.meta.name === 'host') {
this.releaseDedidcatedHost()
}
}
}
}
</script>

View File

@ -0,0 +1,139 @@
// 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="form">
<div class="form__item" :class="{'error': domainError}">
<a-spin :spinning="domainsLoading">
<p class="form__label">{{ $t('label.domain') }}<span class="required">*</span></p>
<p class="required required-label">{{ $t('label.required') }}</p>
<a-select style="width: 100%" @change="handleChangeDomain" v-model="domainId">
<a-select-option v-for="(domain, index) in domainsList" :value="domain.id" :key="index">
{{ domain.name }}
</a-select-option>
</a-select>
</a-spin>
</div>
<div class="form__item" v-if="accountsList">
<p class="form__label">{{ $t('label.account') }}</p>
<a-select style="width: 100%" @change="handleChangeAccount">
<a-select-option v-for="(account, index) in accountsList" :value="account.name" :key="index">
{{ account.name }}
</a-select-option>
</a-select>
</div>
</div>
</template>
<script>
import { api } from '@/api'
export default {
name: 'DedicateDomain',
props: {
error: {
type: Boolean,
requried: true
}
},
data () {
return {
domainsLoading: false,
domainId: null,
accountsList: null,
domainsList: null,
domainError: false
}
},
watch: {
error () {
this.domainError = this.error
}
},
mounted () {
this.fetchData()
},
methods: {
fetchData () {
this.domainsLoading = true
api('listDomains', {
listAll: true,
details: 'min'
}).then(response => {
this.domainsList = response.listdomainsresponse.domain
if (this.domainsList[0]) {
this.domainId = this.domainsList[0].id
this.handleChangeDomain(this.domainId)
}
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.domainsLoading = false
})
},
fetchAccounts () {
api('listAccounts', {
domainid: this.domainId
}).then(response => {
this.accountsList = response.listaccountsresponse.account || []
if (this.accountsList && this.accountsList.length === 0) {
this.handleChangeAccount(null)
}
}).catch(error => {
this.$notifyError(error)
})
},
handleChangeDomain (e) {
this.$emit('domainChange', e)
this.domainError = false
this.fetchAccounts()
},
handleChangeAccount (e) {
this.$emit('accountChange', e)
}
}
}
</script>
<style scoped lang="scss">
.form {
&__item {
margin-bottom: 20px;
}
&__label {
font-weight: bold;
margin-bottom: 5px;
}
}
.required {
color: #ff0000;
font-size: 12px;
&-label {
display: none;
}
}
.error {
.required-label {
display: block;
}
}
</style>

View File

@ -0,0 +1,277 @@
// 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-modal
v-model="dedicatedDomainModal"
:title="label"
:maskClosable="false"
:okText="$t('label.ok')"
:cancelText="$t('label.cancel')"
@cancel="closeModal"
@ok="handleDedicateForm">
<DedicateDomain
@domainChange="id => domainId = id"
@accountChange="id => dedicatedAccount = id"
:error="domainError" />
</a-modal>
</template>
<script>
import { api } from '@/api'
import DedicateDomain from './DedicateDomain'
export default {
components: {
DedicateDomain
},
inject: ['parentFetchData'],
props: {
active: {
type: Boolean,
required: true
},
label: {
type: String,
required: true
},
resource: {
type: Object,
required: true
},
fetchData: {
type: Function,
required: true
}
},
data () {
return {
dedicatedDomainModal: false,
domainId: null,
dedicatedAccount: null,
domainError: false
}
},
watch: {
active () {
this.dedicatedDomainModal = this.active
}
},
mounted () {
this.dedicatedDomainModal = this.active
},
methods: {
fetchParentData () {
this.fetchData()
},
closeModal () {
this.$emit('close')
},
dedicateZone () {
if (!this.domainId) {
this.domainError = true
return
}
api('dedicateZone', {
zoneId: this.resource.id,
domainId: this.domainId,
account: this.dedicatedAccount
}).then(response => {
this.$pollJob({
jobId: response.dedicatezoneresponse.jobid,
successMessage: this.$t('label.zone.dedicated'),
successMethod: () => {
this.parentFetchData()
this.fetchParentData()
this.dedicatedDomainId = this.domainId
this.dedicatedDomainModal = false
this.$store.dispatch('AddAsyncJob', {
title: this.$t('label.zone.dedicated'),
jobid: response.dedicatezoneresponse.jobid,
description: `${this.$t('label.domain.id')} : ${this.dedicatedDomainId}`,
status: 'progress'
})
},
errorMessage: this.$t('error.dedicate.zone.failed'),
errorMethod: () => {
this.parentFetchData()
this.fetchParentData()
this.dedicatedDomainModal = false
},
loadingMessage: this.$t('message.dedicating.zone'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.parentFetchData()
this.fetchParentData()
this.dedicatedDomainModal = false
}
})
}).catch(error => {
this.$notifyError(error)
this.dedicatedDomainModal = false
})
},
dedicatePod () {
if (!this.domainId) {
this.domainError = true
return
}
api('dedicatePod', {
podId: this.resource.id,
domainId: this.domainId,
account: this.dedicatedAccount
}).then(response => {
this.$pollJob({
jobId: response.dedicatepodresponse.jobid,
successMessage: this.$t('label.pod.dedicated'),
successMethod: () => {
this.parentFetchData()
this.fetchParentData()
this.dedicatedDomainId = this.domainId
this.dedicatedDomainModal = false
this.$store.dispatch('AddAsyncJob', {
title: this.$t('label.pod.dedicated'),
jobid: response.dedicatepodresponse.jobid,
description: `${this.$t('label.domainid')}: ${this.dedicatedDomainId}`,
status: 'progress'
})
},
errorMessage: this.$t('error.dedicate.pod.failed'),
errorMethod: () => {
this.parentFetchData()
this.fetchParentData()
this.dedicatedDomainModal = false
},
loadingMessage: this.$t('message.dedicating.pod'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.parentFetchData()
this.fetchParentData()
this.dedicatedDomainModal = false
}
})
}).catch(error => {
this.$notifyError(error)
this.dedicatedDomainModal = false
})
},
dedicateCluster () {
if (!this.domainId) {
this.domainError = true
return
}
api('dedicateCluster', {
clusterId: this.resource.id,
domainId: this.domainId,
account: this.dedicatedAccount
}).then(response => {
this.$pollJob({
jobId: response.dedicateclusterresponse.jobid,
successMessage: this.$t('message.cluster.dedicated'),
successMethod: () => {
this.parentFetchData()
this.fetchParentData()
this.dedicatedDomainId = this.domainId
this.dedicatedDomainModal = false
this.$store.dispatch('AddAsyncJob', {
title: this.$t('message.cluster.dedicated'),
jobid: response.dedicateclusterresponse.jobid,
description: `${this.$t('label.domainid')}: ${this.dedicatedDomainId}`,
status: 'progress'
})
},
errorMessage: this.$t('error.dedicate.cluster.failed'),
errorMethod: () => {
this.parentFetchData()
this.fetchParentData()
this.dedicatedDomainModal = false
},
loadingMessage: this.$t('message.dedicating.cluster'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.parentFetchData()
this.fetchParentData()
this.dedicatedDomainModal = false
}
})
}).catch(error => {
this.$notifyError(error)
this.dedicatedDomainModal = false
})
},
dedicateHost () {
if (!this.domainId) {
this.domainError = true
return
}
api('dedicateHost', {
hostId: this.resource.id,
domainId: this.domainId,
account: this.dedicatedAccount
}).then(response => {
this.$pollJob({
jobId: response.dedicatehostresponse.jobid,
successMessage: this.$t('message.host.dedicated'),
successMethod: () => {
this.parentFetchData()
this.fetchParentData()
this.dedicatedDomainId = this.domainId
this.dedicatedDomainModal = false
this.$store.dispatch('AddAsyncJob', {
title: this.$t('message.host.dedicated'),
jobid: response.dedicatehostresponse.jobid,
description: `${this.$t('label.domainid')}: ${this.dedicatedDomainId}`,
status: 'progress'
})
},
errorMessage: this.$t('error.dedicate.host.failed'),
errorMethod: () => {
this.parentFetchData()
this.fetchParentData()
this.dedicatedDomainModal = false
},
loadingMessage: this.$t('message.dedicating.host'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.parentFetchData()
this.fetchParentData()
this.dedicatedDomainModal = false
}
})
}).catch(error => {
this.$notifyError(error)
this.dedicatedDomainModal = false
})
},
handleDedicateForm () {
if (this.$route.meta.name === 'zone') {
this.dedicateZone()
}
if (this.$route.meta.name === 'pod') {
this.dedicatePod()
}
if (this.$route.meta.name === 'cluster') {
this.dedicateCluster()
}
if (this.$route.meta.name === 'host') {
this.dedicateHost()
}
}
}
}
</script>

View File

@ -0,0 +1,293 @@
// 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">
<a-alert
v-if="disableSettings"
banner
:message="$t('message.action.settings.warning.vm.running')" />
<div v-else>
<div v-show="!showAddDetail">
<a-button
type="dashed"
style="width: 100%"
icon="plus"
:disabled="!('updateTemplate' in $store.getters.apis && 'updateVirtualMachine' in $store.getters.apis && isAdminOrOwner())"
@click="onShowAddDetail">
{{ $t('label.add.setting') }}
</a-button>
</div>
<div v-show="showAddDetail">
<a-input-group
type="text"
compact>
<a-auto-complete
class="detail-input"
ref="keyElm"
:filterOption="filterOption"
:value="newKey"
:dataSource="Object.keys(detailOptions)"
:placeholder="$t('label.name')"
@change="e => onAddInputChange(e, 'newKey')" />
<a-input style=" width: 30px; border-left: 0; pointer-events: none; backgroundColor: #fff" placeholder="=" disabled />
<a-auto-complete
class="detail-input"
:filterOption="filterOption"
:value="newValue"
:dataSource="detailOptions[newKey]"
:placeholder="$t('label.value')"
@change="e => onAddInputChange(e, 'newValue')" />
<a-tooltip arrowPointAtCenter placement="topRight">
<template slot="title">
{{ $t('label.add.setting') }}
</template>
<a-button icon="check" @click="addDetail" class="detail-button"></a-button>
</a-tooltip>
<a-tooltip arrowPointAtCenter placement="topRight">
<template slot="title">
{{ $t('label.cancel') }}
</template>
<a-button icon="close" @click="closeDetail" class="detail-button"></a-button>
</a-tooltip>
</a-input-group>
<p v-if="error" style="color: red"> {{ $t(error) }} </p>
</div>
</div>
<a-list size="large">
<a-list-item :key="index" v-for="(item, index) in details">
<a-list-item-meta>
<span slot="title">
{{ item.name }}
</span>
<span slot="description" style="word-break: break-all">
<span v-if="item.edit" style="display: flex">
<a-auto-complete
style="width: 100%"
:value="item.value"
:dataSource="detailOptions[item.name]"
@change="val => handleInputChange(val, index)"
@pressEnter="e => updateDetail(index)" />
</span>
<span v-else>{{ item.value }}</span>
</span>
</a-list-item-meta>
<div
slot="actions"
v-if="!disableSettings && 'updateTemplate' in $store.getters.apis &&
'updateVirtualMachine' in $store.getters.apis && isAdminOrOwner() && allowEditOfDetail(item.name)">
<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"
icon="edit"
v-if="!item.edit"
@click="showEditDetail(index)" />
</div>
<div
slot="actions"
v-if="!disableSettings && 'updateTemplate' in $store.getters.apis &&
'updateVirtualMachine' in $store.getters.apis && isAdminOrOwner() && allowEditOfDetail(item.name)">
<a-popconfirm
:title="`${$t('label.delete.setting')}?`"
@confirm="deleteDetail(index)"
:okText="$t('label.yes')"
:cancelText="$t('label.no')"
placement="left"
>
<a-button shape="circle" type="danger" icon="delete" />
</a-popconfirm>
</div>
</a-list-item>
</a-list>
</a-spin>
</template>
<script>
import { api } from '@/api'
export default {
name: 'DetailSettings',
props: {
resource: {
type: Object,
required: true
}
},
data () {
return {
details: [],
detailOptions: {},
showAddDetail: false,
disableSettings: false,
newKey: '',
newValue: '',
loading: false,
resourceType: 'UserVm',
error: false
}
},
watch: {
resource: function (newItem, oldItem) {
this.updateResource(newItem)
}
},
mounted () {
this.updateResource(this.resource)
},
methods: {
filterOption (input, option) {
return (
option.componentOptions.children[0].text.toUpperCase().indexOf(input.toUpperCase()) >= 0
)
},
updateResource (resource) {
this.details = []
if (!resource) {
return
}
this.resource = resource
this.resourceType = this.$route.meta.resourceType
if (resource.details) {
this.details = Object.keys(this.resource.details).map(k => {
return { name: k, value: this.resource.details[k], edit: false }
})
}
api('listDetailOptions', { resourcetype: this.resourceType, resourceid: this.resource.id }).then(json => {
this.detailOptions = json.listdetailoptionsresponse.detailoptions.details
})
this.disableSettings = (this.$route.meta.name === 'vm' && this.resource.state !== 'Stopped')
},
allowEditOfDetail (name) {
if (this.resource.readonlyuidetails) {
if (this.resource.readonlyuidetails.split(',').map(item => item.trim()).includes(name)) {
return false
}
}
return true
},
showEditDetail (index) {
this.details[index].edit = true
this.details[index].originalValue = this.details[index].value
this.$set(this.details, index, this.details[index])
},
hideEditDetail (index) {
this.details[index].edit = false
this.details[index].value = this.details[index].originalValue
this.$set(this.details, index, this.details[index])
},
handleInputChange (val, index) {
this.details[index].value = val
this.$set(this.details, index, this.details[index])
},
onAddInputChange (val, obj) {
this.error = false
this[obj] = val
},
isAdminOrOwner () {
return ['Admin'].includes(this.$store.getters.userInfo.roletype) ||
(this.resource.domainid === this.$store.getters.userInfo.domainid && this.resource.account === this.$store.getters.userInfo.account) ||
this.resource.project && this.resource.projectid === this.$store.getters.project.id
},
runApi () {
var apiName = ''
if (this.resourceType === 'UserVm') {
apiName = 'updateVirtualMachine'
} else if (this.resourceType === 'Template') {
apiName = 'updateTemplate'
}
if (!(apiName in this.$store.getters.apis)) {
this.$notification.error({
message: this.$t('error.execute.api.failed') + ' ' + apiName,
description: this.$t('message.user.not.permitted.api')
})
return
}
const params = { id: this.resource.id }
if (this.details.length === 0) {
params.cleanupdetails = true
} else {
this.details.forEach(function (item, index) {
params['details[0].' + item.name] = item.value
})
}
this.loading = true
api(apiName, params).then(json => {
var details = {}
if (this.resourceType === 'UserVm' && json.updatevirtualmachineresponse.virtualmachine.details) {
details = json.updatevirtualmachineresponse.virtualmachine.details
} else if (this.resourceType === 'Template' && json.updatetemplateresponse.template.details) {
details = json.updatetemplateresponse.template.details
}
this.details = Object.keys(details).map(k => {
return { name: k, value: details[k], edit: false }
})
}).catch(error => {
this.$notifyError(error)
}).finally(f => {
this.loading = false
this.showAddDetail = false
this.newKey = ''
this.newValue = ''
})
},
addDetail () {
if (this.newKey === '' || this.newValue === '') {
this.error = this.$t('message.error.provide.setting')
return
}
this.error = false
this.details.push({ name: this.newKey, value: this.newValue })
this.runApi()
},
updateDetail (index) {
this.runApi()
},
deleteDetail (index) {
this.details.splice(index, 1)
this.runApi()
},
onShowAddDetail () {
this.showAddDetail = true
setTimeout(() => {
this.$refs.keyElm.focus()
})
},
closeDetail () {
this.newKey = ''
this.newValue = ''
this.error = false
this.showAddDetail = false
}
}
}
</script>
<style scoped lang="less">
.detail-input {
width: calc(calc(100% / 2) - 45px);
}
.detail-button {
width: 30px;
}
</style>

View File

@ -0,0 +1,124 @@
// 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-list
size="small"
:dataSource="fetchDetails()">
<a-list-item slot="renderItem" slot-scope="item" v-if="item in resource">
<div>
<strong>{{ item === 'service' ? $t('label.supportedservices') : $t('label.' + String(item).toLowerCase()) }}</strong>
<br/>
<div v-if="Array.isArray(resource[item]) && item === 'service'">
<div v-for="(service, idx) in resource[item]" :key="idx">
{{ service.name }} : {{ service.provider[0].name }}
</div>
</div>
<div v-else-if="$route.meta.name === 'backup' && item === 'volumes'">
<div v-for="(volume, idx) in JSON.parse(resource[item])" :key="idx">
<router-link :to="{ path: '/volume/' + volume.uuid }">{{ volume.type }} - {{ volume.path }}</router-link> ({{ parseFloat(volume.size / (1024.0 * 1024.0 * 1024.0)).toFixed(1) }} GB)
</div>
</div>
<div v-else-if="['name', 'type'].includes(item)">
<span v-if="['USER.LOGIN', 'USER.LOGOUT', 'ROUTER.HEALTH.CHECKS', 'FIREWALL.CLOSE', 'ALERT.SERVICE.DOMAINROUTER'].includes(resource[item])">{{ $t(resource[item].toLowerCase()) }}</span>
<span v-else>{{ resource[item] }}</span>
</div>
<div v-else-if="['created', 'sent', 'lastannotated'].includes(item)">
{{ $toLocaleDate(resource[item]) }}
</div>
<div v-else>
{{ resource[item] }}
</div>
</div>
</a-list-item>
<HostInfo :resource="resource" v-if="$route.meta.name === 'host' && 'listHosts' in $store.getters.apis" />
<DedicateData :resource="resource" v-if="dedicatedSectionActive" />
<VmwareData :resource="resource" v-if="$route.meta.name === 'zone' && 'listVmwareDcs' in $store.getters.apis" />
</a-list>
</template>
<script>
import DedicateData from './DedicateData'
import HostInfo from '@/views/infra/HostInfo'
import VmwareData from './VmwareData'
export default {
name: 'DetailsTab',
components: {
DedicateData,
HostInfo,
VmwareData
},
props: {
resource: {
type: Object,
required: true
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
dedicatedRoutes: ['zone', 'pod', 'cluster', 'host'],
dedicatedSectionActive: false,
projectname: ''
}
},
mounted () {
this.dedicatedSectionActive = this.dedicatedRoutes.includes(this.$route.meta.name)
},
created () {
this.dedicatedSectionActive = this.dedicatedRoutes.includes(this.$route.meta.name)
},
watch: {
resource (newItem) {
this.resource = newItem
if ('account' in this.resource && this.resource.account.startsWith('PrjAcct-')) {
this.projectname = this.resource.account.substring(this.resource.account.indexOf('-') + 1, this.resource.account.lastIndexOf('-'))
this.resource.projectname = this.projectname
}
},
$route () {
this.dedicatedSectionActive = this.dedicatedRoutes.includes(this.$route.meta.name)
this.fetchProjectAdmins()
}
},
methods: {
fetchProjectAdmins () {
if (!this.resource.owner) {
return false
}
var owners = this.resource.owner
var projectAdmins = []
for (var owner of owners) {
projectAdmins.push(Object.keys(owner).includes('user') ? owner.account + '(' + owner.user + ')' : owner.account)
}
this.resource.account = projectAdmins.join()
},
fetchDetails () {
var details = this.$route.meta.details
if (typeof details === 'function') {
details = details()
}
details = this.projectname ? [...details.filter(x => x !== 'account'), 'projectname'] : details
return details
}
}
}
</script>

View File

@ -0,0 +1,131 @@
// 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-modal
:title="$t(currentAction.label)"
:visible="showForm"
:closable="true"
:confirmLoading="currentAction.loading"
:okText="$t('label.ok')"
:cancelText="$t('label.cancel')"
style="top: 20px;"
@ok="handleSubmit"
@cancel="close"
centered
>
<a-spin :spinning="currentAction.loading">
<a-form
:form="form"
@submit="handleSubmit"
layout="vertical" >
<a-form-item
v-for="(field, fieldIndex) in currentAction.params"
:key="fieldIndex"
:label="$t(field.name)"
:v-bind="field.name"
v-if="field.name !== 'id'"
>
<span v-if="field.type==='boolean'">
<a-switch
v-decorator="[field.name, {
rules: [{ required: field.required, message: `${this.$t('message.error.required.input')}` }]
}]"
:placeholder="field.description"
/>
</span>
<span v-else-if="field.type==='uuid' || field.name==='account'">
<a-select
:loading="field.loading"
v-decorator="[field.name, {
rules: [{ required: field.required, message: `${this.$t('message.error.select')}` }]
}]"
:placeholder="field.description"
>
<a-select-option v-for="(opt, optIndex) in field.opts" :key="optIndex">
{{ opt.name || opt.description }}
</a-select-option>
</a-select>
</span>
<span v-else-if="field.type==='long'">
<a-input-number
v-decorator="[field.name, {
rules: [{ required: field.required, message: `${this.$t('message.validate.number')}` }]
}]"
:placeholder="field.description"
/>
</span>
<span v-else-if="field.name==='password'">
<a-input-password
v-decorator="[field.name, {
rules: [{ required: field.required, message: `${this.$t('message.error.required.input')}` }]
}]"
:placeholder="field.description"
/>
</span>
<span v-else>
<a-input
v-decorator="[field.name, {
rules: [{ required: field.required, message: `${this.$t('message.error.required.input')}` }]
}]"
:placeholder="field.description"
/>
</span>
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>
<script>
import ChartCard from '@/components/widgets/ChartCard'
export default {
name: 'FormView',
components: {
ChartCard
},
props: {
currentAction: {
type: Object,
required: true
},
showForm: {
type: Boolean,
default: false
},
handleSubmit: {
type: Function,
default: () => {}
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
},
methods: {
close () {
this.currentAction.loading = false
this.showForm = false
}
}
}
</script>
<style scoped>
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,218 @@
// 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-input-search
v-if="showSearch"
style="width: 25vw;float: right;margin-bottom: 10px; z-index: 8"
:placeholder="$t('label.search')"
v-model="filter"
@search="handleSearch" />
<a-table
size="small"
:columns="fetchColumns()"
:dataSource="dataSource"
:rowKey="item => item.id"
:loading="loading"
:pagination="defaultPagination"
@change="handleTableChange"
@handle-search-filter="handleTableChange" >
<template v-for="(column, index) in Object.keys(routerlinks({}))" :slot="column" slot-scope="text, item" >
<span :key="index">
<router-link :set="routerlink = routerlinks(item)" :to="{ path: routerlink[column] }" >{{ text }}</router-link>
</span>
</template>
<template slot="state" slot-scope="text">
<status :text="text ? text : ''" />{{ text }}
</template>
<template slot="status" slot-scope="text">
<status :text="text ? text : ''" />{{ text }}
</template>
</a-table>
<div v-if="!defaultPagination" style="display: block; text-align: right; margin-top: 10px;">
<a-pagination
size="small"
:current="options.page"
:pageSize="options.pageSize"
:total="total"
:showTotal="total => `${$t('label.total')} ${total} ${$t('label.items')}`"
:pageSizeOptions="device === 'desktop' ? ['20', '50', '100', '200'] : ['10', '20', '50', '100', '200']"
@change="handleTableChange"
@showSizeChange="handlePageSizeChange"
showSizeChanger>
<template slot="buildOptionText" slot-scope="props">
<span>{{ props.value }} / {{ $t('label.page') }}</span>
</template>
</a-pagination>
</div>
</div>
</template>
<script>
import { api } from '@/api'
import { mixinDevice } from '@/utils/mixin.js'
import Status from '@/components/widgets/Status'
export default {
name: 'ListResourceTable',
components: {
Status
},
mixins: [mixinDevice],
props: {
resource: {
type: Object,
default: () => {}
},
apiName: {
type: String,
default: ''
},
routerlinks: {
type: Function,
default: () => { return {} }
},
params: {
type: Object,
default: () => {}
},
columns: {
type: Array,
required: true
},
showSearch: {
type: Boolean,
default: true
},
items: {
type: Array,
default: () => []
}
},
data () {
return {
loading: false,
dataSource: [],
total: 0,
filter: '',
defaultPagination: false,
options: {
page: 1,
pageSize: 10,
keyword: null
}
}
},
watch: {
resource (newItem, oldItem) {
if (newItem !== oldItem) {
this.fetchData()
}
},
items (newItem, oldItem) {
if (newItem) {
this.dataSource = newItem
}
},
'$i18n.locale' (to, from) {
if (to !== from) {
this.fetchData()
}
}
},
mounted () {
this.fetchData()
},
methods: {
fetchData () {
if (this.items && this.items.length > 0) {
this.dataSource = this.items
this.defaultPagination = {
showSizeChanger: true,
pageSizeOptions: this.mixinDevice === 'desktop' ? ['20', '50', '100', '200'] : ['10', '20', '50', '100', '200']
}
return
}
this.loading = true
var params = { ...this.params, ...this.options }
params.listall = true
params.response = 'json'
params.details = 'min'
api(this.apiName, params).then(json => {
var responseName
var objectName
for (const key in json) {
if (key.includes('response')) {
responseName = key
break
}
}
for (const key in json[responseName]) {
if (key === 'count') {
this.total = json[responseName][key]
continue
}
objectName = key
break
}
this.dataSource = json[responseName][objectName]
if (!this.dataSource || this.dataSource.length === 0) {
this.dataSource = []
}
}).finally(() => {
this.loading = false
})
},
fetchColumns () {
var columns = []
for (const col of this.columns) {
columns.push({
dataIndex: col,
title: this.$t('label.' + col),
scopedSlots: { customRender: col }
})
}
return columns
},
handleSearch (value) {
this.filter = value
this.options.page = 1
this.options.pageSize = 10
this.options.keyword = this.filter
this.fetchData()
},
handleTableChange (page, pagesize) {
this.options.page = page
this.options.pageSize = pagesize
this.fetchData()
},
handlePageSizeChange (page, pagesize) {
this.options.page = 1
this.options.pageSize = pagesize
this.fetchData()
}
}
}
</script>

View File

@ -0,0 +1,618 @@
// 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-table
size="middle"
:loading="loading"
:columns="isOrderUpdatable() ? columns : columns.filter(x => x.dataIndex !== 'order')"
:dataSource="items"
:rowKey="(record, idx) => record.id || record.name || record.usageType || idx + '-' + Math.random()"
:pagination="false"
:rowSelection="['vm', 'alert'].includes($route.name) || $route.name === 'event' && $store.getters.userInfo.roletype === 'Admin'
? {selectedRowKeys: selectedRowKeys, onChange: onSelectChange} : null"
:rowClassName="getRowClassName"
style="overflow-y: auto"
>
<template slot="footer">
<span v-if="hasSelected">
{{ `Selected ${selectedRowKeys.length} items` }}
</span>
</template>
<!--
<div slot="expandedRowRender" slot-scope="resource">
<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"
:key="actionIndex"
placement="bottom">
<template slot="title">
{{ $t(action.label) }}
</template>
<a-button
v-if="action.api in $store.getters.apis && 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-right: 5px; margin-top: 12px"
@click="$parent.execAction(action)"
>
</a-button>
</a-tooltip>
</div>
</info-card>
</div>
-->
<span slot="name" slot-scope="text, record">
<div style="min-width: 120px" >
<QuickView
style="margin-left: 5px"
:actions="actions"
:resource="record"
:enabled="quickViewEnabled() && actions.length > 0 && columns && columns[0].dataIndex === 'name' "
@exec-action="$parent.execAction"/>
<span v-if="$route.path.startsWith('/project')" style="margin-right: 5px">
<a-button type="dashed" size="small" shape="circle" icon="login" @click="changeProject(record)" />
</span>
<os-logo v-if="record.ostypename" :osName="record.ostypename" size="1x" style="margin-right: 5px" />
<span v-if="$route.path.startsWith('/globalsetting')">{{ text }}</span>
<span v-else-if="$route.path.startsWith('/alert')">
<router-link :to="{ path: $route.path + '/' + record.id }" v-if="record.id">{{ $t(text.toLowerCase()) }}</router-link>
<router-link :to="{ path: $route.path + '/' + record.name }" v-else>{{ $t(text.toLowerCase()) }}</router-link>
</span>
<span v-else>
<router-link :to="{ path: $route.path + '/' + record.id }" v-if="record.id">{{ text }}</router-link>
<router-link :to="{ path: $route.path + '/' + record.name }" v-else>{{ text }}</router-link>
</span>
</div>
</span>
<a slot="templatetype" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: $route.path + '/' + record.templatetype }">{{ text }}</router-link>
</a>
<template slot="type" slot-scope="text">
<span v-if="['USER.LOGIN', 'USER.LOGOUT', 'ROUTER.HEALTH.CHECKS', 'FIREWALL.CLOSE', 'ALERT.SERVICE.DOMAINROUTER'].includes(text)">{{ $t(text.toLowerCase()) }}</span>
<span v-else>{{ text }}</span>
</template>
<a slot="displayname" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: $route.path + '/' + record.id }">{{ text }}</router-link>
</a>
<span slot="username" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: $route.path + '/' + record.id }" v-if="['/accountuser', '/vpnuser'].includes($route.path)">{{ text }}</router-link>
<router-link :to="{ path: '/accountuser', query: { username: record.username, domainid: record.domainid } }" v-else-if="$store.getters.userInfo.roletype !== 'User'">{{ text }}</router-link>
<span v-else>{{ text }}</span>
</span>
<span slot="ipaddress" slot-scope="text, record" href="javascript:;">
<router-link v-if="['/publicip', '/privategw'].includes($route.path)" :to="{ path: $route.path + '/' + record.id }">{{ text }}</router-link>
<span v-else>{{ text }}</span>
<span v-if="record.issourcenat">
&nbsp;
<a-tag>source-nat</a-tag>
</span>
</span>
<a slot="publicip" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: $route.path + '/' + record.id }">{{ text }}</router-link>
</a>
<span slot="traffictype" slot-scope="text" href="javascript:;">
{{ text }}
</span>
<a slot="vmname" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: '/vm/' + record.virtualmachineid }">{{ text }}</router-link>
</a>
<a slot="virtualmachinename" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: '/vm/' + record.virtualmachineid }">{{ text }}</router-link>
</a>
<span slot="hypervisor" slot-scope="text, record">
<span v-if="$route.name === 'hypervisorcapability'">
<router-link :to="{ path: $route.path + '/' + record.id }">{{ text }}</router-link>
</span>
<span v-else>{{ text }}</span>
</span>
<template slot="state" slot-scope="text">
<status :text="text ? text : ''" displayText />
</template>
<template slot="allocationstate" slot-scope="text">
<status :text="text ? text : ''" displayText />
</template>
<template slot="resourcestate" slot-scope="text">
<status :text="text ? text : ''" displayText />
</template>
<template slot="powerstate" slot-scope="text">
<status :text="text ? text : ''" displayText />
</template>
<template slot="agentstate" slot-scope="text">
<status :text="text ? text : ''" displayText />
</template>
<a slot="guestnetworkname" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: '/guestnetwork/' + record.guestnetworkid }">{{ text }}</router-link>
</a>
<a slot="associatednetworkname" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: '/guestnetwork/' + record.associatednetworkid }">{{ text }}</router-link>
</a>
<a slot="vpcname" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: '/vpc/' + record.vpcid }">{{ text }}</router-link>
</a>
<a slot="hostname" slot-scope="text, record" href="javascript:;">
<router-link v-if="record.hostid" :to="{ path: '/host/' + record.hostid }">{{ text }}</router-link>
<router-link v-else-if="record.hostname" :to="{ path: $route.path + '/' + record.id }">{{ text }}</router-link>
<span v-else>{{ text }}</span>
</a>
<a slot="storage" slot-scope="text, record" href="javascript:;">
<router-link v-if="record.storageid" :to="{ path: '/storagepool/' + record.storageid }">{{ text }}</router-link>
<span v-else>{{ text }}</span>
</a>
<template v-for="(value, name) in thresholdMapping" :slot="name" slot-scope="text, record" href="javascript:;">
<span :key="name">
<span v-if="record[value.disable]" class="alert-disable-threshold">
{{ text }}
</span>
<span v-else-if="record[value.notification]" class="alert-notification-threshold">
{{ text }}
</span>
<span style="padding: 10%;" v-else>
{{ text }}
</span>
</span>
</template>
<a slot="level" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: '/event/' + record.id }">{{ text }}</router-link>
</a>
<a slot="clustername" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: '/cluster/' + record.clusterid }">{{ text }}</router-link>
</a>
<a slot="podname" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: '/pod/' + record.podid }">{{ text }}</router-link>
</a>
<span slot="account" slot-scope="text, record">
<template v-if="record.owner">
<template v-for="(item,idx) in record.owner">
<span style="margin-right:5px" :key="idx">
<span v-if="$store.getters.userInfo.roletype !== 'User'">
<router-link v-if="'user' in item" :to="{ path: '/accountuser', query: { username: item.user, domainid: record.domainid }}">{{ item.account + '(' + item.user + ')' }}</router-link>
<router-link v-else :to="{ path: '/account', query: { name: item.account, domainid: record.domainid } }">{{ item.account }}</router-link>
</span>
<span v-else>{{ item.user ? item.account + '(' + item.user + ')' : item.account }}</span>
</span>
</template>
</template>
<template v-if="text && !text.startsWith('PrjAcct-')">
<router-link
v-if="'quota' in record && $router.resolve(`${$route.path}/${record.account}`) !== '404'"
:to="{ path: `${$route.path}/${record.account}`, query: { account: record.account, domainid: record.domainid, quota: true } }">{{ text }}</router-link>
<router-link :to="{ path: '/account/' + record.accountid }" v-else-if="record.accountid">{{ text }}</router-link>
<router-link :to="{ path: '/account', query: { name: record.account, domainid: record.domainid } }" v-else-if="$store.getters.userInfo.roletype !== 'User'">{{ text }}</router-link>
<span v-else>{{ text }}</span>
</template>
</span>
<span slot="domain" slot-scope="text, record" href="javascript:;">
<router-link v-if="record.domainid && !record.domainid.toString().includes(',') && $store.getters.userInfo.roletype !== 'User'" :to="{ path: '/domain/' + record.domainid }">{{ text }}</router-link>
<span v-else>{{ text }}</span>
</span>
<span slot="domainpath" slot-scope="text, record" href="javascript:;">
<router-link v-if="record.domainid && !record.domainid.includes(',') && $router.resolve('/domain/' + record.domainid).route.name !== '404'" :to="{ path: '/domain/' + record.domainid }">{{ text }}</router-link>
<span v-else>{{ text }}</span>
</span>
<a slot="zone" slot-scope="text, record" href="javascript:;">
<router-link v-if="record.zoneid && !record.zoneid.includes(',') && $router.resolve('/zone/' + record.zoneid).route.name !== '404'" :to="{ path: '/zone/' + record.zoneid }">{{ text }}</router-link>
<span v-else>{{ text }}</span>
</a>
<span slot="zonename" slot-scope="text, record">
<router-link v-if="$router.resolve('/zone/' + record.zoneid).route.name !== '404'" :to="{ path: '/zone/' + record.zoneid }">{{ text }}</router-link>
<span v-else>{{ text }}</span>
</span>
<a slot="readonly" slot-scope="text, record">
<status :text="record.readonly ? 'ReadOnly' : 'ReadWrite'" />
</a>
<span slot="created" slot-scope="text">
{{ $toLocaleDate(text) }}
</span>
<span slot="sent" slot-scope="text">
{{ $toLocaleDate(text) }}
</span>
<div slot="order" slot-scope="text, record" class="shift-btns">
<a-tooltip placement="top">
<template slot="title">{{ $t('label.move.to.top') }}</template>
<a-button
shape="round"
@click="moveItemTop(record)"
class="shift-btn">
<a-icon type="double-left" class="shift-btn shift-btn--rotated" />
</a-button>
</a-tooltip>
<a-tooltip placement="top">
<template slot="title">{{ $t('label.move.to.bottom') }}</template>
<a-button
shape="round"
@click="moveItemBottom(record)"
class="shift-btn">
<a-icon type="double-right" class="shift-btn shift-btn--rotated" />
</a-button>
</a-tooltip>
<a-tooltip placement="top">
<template slot="title">{{ $t('label.move.up.row') }}</template>
<a-button shape="round" @click="moveItemUp(record)" class="shift-btn">
<a-icon type="caret-up" class="shift-btn" />
</a-button>
</a-tooltip>
<a-tooltip placement="top">
<template slot="title">{{ $t('label.move.down.row') }}</template>
<a-button shape="round" @click="moveItemDown(record)" class="shift-btn">
<a-icon type="caret-down" class="shift-btn" />
</a-button>
</a-tooltip>
</div>
<template slot="value" slot-scope="text, record">
<a-input
v-if="editableValueKey === record.key"
:autoFocus="true"
:defaultValue="record.value"
:disabled="!('updateConfiguration' in $store.getters.apis)"
v-model="editableValue"
@keydown.esc="editableValueKey = null"
@pressEnter="saveValue(record)">
</a-input>
<div v-else style="width: 200px; word-break: break-all">
{{ text }}
</div>
</template>
<template slot="actions" slot-scope="text, record">
<a-button
shape="circle"
:disabled="!('updateConfiguration' in $store.getters.apis)"
v-if="editableValueKey !== record.key"
icon="edit"
@click="editValue(record)" />
<a-button
shape="circle"
:disabled="!('updateConfiguration' in $store.getters.apis)"
@click="saveValue(record)"
v-if="editableValueKey === record.key" >
<a-icon type="check-circle" theme="twoTone" twoToneColor="#52c41a" />
</a-button>
<a-button
shape="circle"
size="default"
@click="editableValueKey = null"
v-if="editableValueKey === record.key" >
<a-icon type="close-circle" theme="twoTone" twoToneColor="#f5222d" />
</a-button>
</template>
<template slot="tariffActions" slot-scope="text, record">
<a-button
shape="circle"
v-if="editableValueKey !== record.key"
:disabled="!('quotaTariffUpdate' in $store.getters.apis)"
icon="edit"
@click="editTariffValue(record)" />
<slot></slot>
</template>
</a-table>
</template>
<script>
import { api } from '@/api'
import Console from '@/components/widgets/Console'
import OsLogo from '@/components/widgets/OsLogo'
import Status from '@/components/widgets/Status'
import InfoCard from '@/components/view/InfoCard'
import QuickView from '@/components/view/QuickView'
export default {
name: 'ListView',
components: {
Console,
OsLogo,
Status,
InfoCard,
QuickView
},
props: {
columns: {
type: Array,
required: true
},
items: {
type: Array,
required: true
},
loading: {
type: Boolean,
default: false
},
actions: {
type: Array,
default: () => []
}
},
inject: ['parentFetchData', 'parentToggleLoading', 'parentEditTariffAction'],
data () {
return {
selectedRowKeys: [],
editableValueKey: null,
editableValue: '',
thresholdMapping: {
cpuused: {
notification: 'cputhreshold',
disable: 'cpudisablethreshold'
},
cpuallocated: {
notification: 'cpuallocatedthreshold',
disable: 'cpuallocateddisablethreshold'
},
memoryused: {
notification: 'memorythreshold',
disable: 'memorydisablethreshold'
},
memoryallocated: {
notification: 'memoryallocatedthreshold',
disable: 'memoryallocateddisablethreshold'
},
cpuusedghz: {
notification: 'cputhreshold',
disable: 'cpudisablethreshold'
},
cpuallocatedghz: {
notification: 'cpuallocatedthreshold',
disable: 'cpuallocateddisablethreshold'
},
memoryusedgb: {
notification: 'memorythreshold',
disable: 'memorydisablethreshold'
},
memoryallocatedgb: {
notification: 'memoryallocatedthreshold',
disable: 'memoryallocateddisablethreshold'
},
disksizeusedgb: {
notification: 'storageusagethreshold',
disable: 'storageusagedisablethreshold'
},
disksizeallocatedgb: {
notification: 'storageallocatedthreshold',
disable: 'storageallocateddisablethreshold'
}
}
}
},
computed: {
hasSelected () {
return this.selectedRowKeys.length > 0
}
},
methods: {
quickViewEnabled () {
return new RegExp(['/vm', '/kubernetes', '/ssh', '/vmgroup', '/affinitygroup',
'/volume', '/snapshot', '/backup',
'/guestnetwork', '/vpc', '/vpncustomergateway',
'/template', '/iso',
'/project', '/account',
'/zone', '/pod', '/cluster', '/host', '/storagepool', '/imagestore', '/systemvm', '/router', '/ilbvm',
'/computeoffering', '/systemoffering', '/diskoffering', '/backupoffering', '/networkoffering', '/vpcoffering'].join('|'))
.test(this.$route.path)
},
fetchColumns () {
if (this.isOrderUpdatable()) {
return this.columns
}
return this.columns.filter(x => x.dataIndex !== 'order')
},
getRowClassName (record, index) {
if (index % 2 === 0) {
return 'light-row'
}
return 'dark-row'
},
setSelection (selection) {
this.selectedRowKeys = selection
this.$emit('selection-change', this.selectedRowKeys)
},
resetSelection () {
this.setSelection([])
},
onSelectChange (selectedRowKeys, selectedRows) {
this.setSelection(selectedRowKeys)
},
changeProject (project) {
this.$store.dispatch('SetProject', project)
this.$store.dispatch('ToggleTheme', project.id === undefined ? 'light' : 'dark')
this.$message.success(this.$t('message.switch.to') + ' ' + project.name)
this.$router.push({ name: 'dashboard' })
},
saveValue (record) {
api('updateConfiguration', {
name: record.name,
value: this.editableValue
}).then(json => {
this.editableValueKey = null
this.$store.dispatch('RefreshFeatures')
this.$message.success(`${this.$t('message.setting.updated')} ${record.name}`)
if (json.updateconfigurationresponse &&
json.updateconfigurationresponse.configuration &&
!json.updateconfigurationresponse.configuration.isdynamic &&
['Admin'].includes(this.$store.getters.userInfo.roletype)) {
this.$notification.warning({
message: this.$t('label.status'),
description: this.$t('message.restart.mgmt.server')
})
}
}).catch(error => {
console.error(error)
this.$message.error(this.$t('message.error.save.setting'))
}).finally(() => {
this.$emit('refresh')
})
},
editValue (record) {
this.editableValueKey = record.key
this.editableValue = record.value
},
getUpdateApi () {
let apiString = ''
switch (this.$route.name) {
case 'template':
apiString = 'updateTemplate'
break
case 'iso':
apiString = 'updateIso'
break
case 'zone':
apiString = 'updateZone'
break
case 'computeoffering':
case 'systemoffering':
apiString = 'updateServiceOffering'
break
case 'diskoffering':
apiString = 'updateDiskOffering'
break
case 'networkoffering':
apiString = 'updateNetworkOffering'
break
case 'vpcoffering':
apiString = 'updateVPCOffering'
break
default:
apiString = 'updateTemplate'
}
return apiString
},
isOrderUpdatable () {
return this.getUpdateApi() in this.$store.getters.apis
},
handleUpdateOrder (id, index) {
this.parentToggleLoading()
const apiString = this.getUpdateApi()
return new Promise((resolve, reject) => {
api(apiString, {
id,
sortKey: index
}).then((response) => {
resolve(response)
}).catch((reason) => {
reject(reason)
})
})
},
updateOrder (data) {
const promises = []
data.forEach((item, index) => {
promises.push(this.handleUpdateOrder(item.id, index + 1))
})
Promise.all(promises).catch((reason) => {
console.log(reason)
}).finally(() => {
this.parentToggleLoading()
this.parentFetchData()
})
},
moveItemUp (record) {
const data = this.items
const index = data.findIndex(item => item.id === record.id)
if (index === 0) return
data.splice(index - 1, 0, data.splice(index, 1)[0])
this.updateOrder(data)
},
moveItemDown (record) {
const data = this.items
const index = data.findIndex(item => item.id === record.id)
if (index === data.length - 1) return
data.splice(index + 1, 0, data.splice(index, 1)[0])
this.updateOrder(data)
},
moveItemTop (record) {
const data = this.items
const index = data.findIndex(item => item.id === record.id)
if (index === 0) return
data.unshift(data.splice(index, 1)[0])
this.updateOrder(data)
},
moveItemBottom (record) {
const data = this.items
const index = data.findIndex(item => item.id === record.id)
if (index === data.length - 1) return
data.push(data.splice(index, 1)[0])
this.updateOrder(data)
},
editTariffValue (record) {
this.parentEditTariffAction(true, record)
}
}
}
</script>
<style scoped>
/deep/ .ant-table-thead {
background-color: #f9f9f9;
}
/deep/ .ant-table-small > .ant-table-content > .ant-table-body {
margin: 0;
}
/deep/ .light-row {
background-color: #fff;
}
/deep/ .dark-row {
background-color: #f9f9f9;
}
</style>
<style scoped lang="scss">
.shift-btns {
display: flex;
}
.shift-btn {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
font-size: 12px;
&:not(:last-child) {
margin-right: 5px;
}
&--rotated {
font-size: 10px;
transform: rotate(90deg);
}
}
.alert-notification-threshold {
background-color: rgba(255, 231, 175, 0.75);
color: #e87900;
padding: 10%;
}
.alert-disable-threshold {
background-color: rgba(255, 190, 190, 0.75);
color: #f50000;
padding: 10%;
}
</style>

View File

@ -0,0 +1,85 @@
// 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-popover v-if="enabled && actionsExist" triggers="hover" placement="topLeft">
<template slot="content">
<action-button
:size="size"
:actions="actions"
:dataView="true"
:resource="resource"
@exec-action="execAction" />
</template>
<a-button shape="circle" size="small" icon="more" style="float: right; background-color: transparent; border-color: transparent"/>
</a-popover>
</template>
<script>
import ActionButton from '@/components/view/ActionButton'
export default {
name: 'QuickView',
components: {
ActionButton
},
props: {
actions: {
type: Array,
default: () => []
},
enabled: {
type: Boolean,
default: true
},
size: {
type: String,
default: 'default'
},
resource: {
type: Object,
default () {
return {}
}
}
},
watch: {
actions (item) {
this.actionsExist = this.doActionsExist()
}
},
data () {
return {
actionsExist: false
}
},
mounted () {
this.actionsExist = this.doActionsExist()
},
methods: {
execAction (action) {
this.$emit('exec-action', action)
},
doActionsExist () {
return this.actions.filter(x =>
x.api in this.$store.getters.apis &&
('show' in x ? x.show(this.resource, this.$store.getters) : true) &&
x.dataView).length > 0
}
}
}
</script>

View File

@ -0,0 +1,108 @@
// 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-list
size="small"
:loading="loading"
:dataSource="usageList" >
<a-list-item slot="renderItem" slot-scope="item" class="list-item" v-if="!($route.meta.name === 'project' && item === 'project')">
<div class="list-item__container">
<strong>
{{ $t('label.' + item + 'limit') }}
</strong>
({{ resource[item + 'available'] === '-1' ? $t('label.unlimited') : resource[item + 'available'] }} {{ $t('label.available') }})
<div class="list-item__vals">
<div class="list-item__data">
{{ $t('label.used') }} / {{ $t('label.limit') }} : {{ resource[item + 'total'] }} / {{ resource[item + 'limit'] === '-1' ? $t('label.unlimited') : resource[item + 'limit'] }}
</div>
<a-progress
status="normal"
:percent="parseFloat(getPercentUsed(resource[item + 'total'], resource[item + 'limit']))"
:format="p => resource[item + 'limit'] !== '-1' && resource[item + 'limit'] !== 'Unlimited' ? p.toFixed(2) + '%' : ''" />
</div>
</div>
</a-list-item>
</a-list>
</template>
<script>
export default {
name: 'ResourceCountUsageTab',
props: {
resource: {
type: Object,
required: true
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
usageList: [
'vm', 'cpu', 'memory', 'primarystorage', 'volume', 'ip', 'network',
'vpc', 'secondarystorage', 'snapshot', 'template', 'project'
]
}
},
watch: {
resource (newData, oldData) {
if (!newData || !newData.id) {
return
}
this.resource = newData
}
},
methods: {
getPercentUsed (total, limit) {
return (limit === 'Unlimited') ? 0 : (total / limit) * 100
}
}
}
</script>
<style scoped lang="scss">
.list-item {
&__container {
max-width: 90%;
width: 100%;
@media (min-width: 760px) {
max-width: 95%;
}
}
&__title {
font-weight: bold;
}
&__data {
margin-right: 20px;
white-space: nowrap;
}
&__vals {
margin-top: 10px;
@media (min-width: 760px) {
display: flex;
}
}
}
</style>

View File

@ -0,0 +1,179 @@
// 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="formLoading">
<a-form
:form="form"
@submit="handleSubmit"
layout="vertical"
>
<a-form-item
v-for="(item, index) in dataResource"
:key="index"
v-if="item.resourcetypename !== 'project'"
:v-bind="item.resourcetypename"
:label="$t('label.max' + item.resourcetypename.replace('_', ''))">
<a-input-number
:disabled="!('updateResourceLimit' in $store.getters.apis)"
style="width: 100%;"
v-decorator="[item.resourcetype, {
initialValue: item.max
}]"
/>
</a-form-item>
<div class="card-footer">
<a-button
:disabled="!('updateResourceLimit' in $store.getters.apis)"
v-if="!($route.meta.name === 'domain' && resource.level === 0)"
:loading="formLoading"
type="primary"
@click="handleSubmit">{{ $t('label.submit') }}</a-button>
</div>
</a-form>
</a-spin>
</template>
<script>
import { api } from '@/api'
export default {
name: 'ResourceLimitTab',
props: {
resource: {
type: Object,
required: true
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
formLoading: false,
dataResource: []
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
},
mounted () {
this.fetchData()
},
watch: {
resource (newData, oldData) {
if (!newData || !newData.id) {
return
}
this.resource = newData
this.fetchData()
}
},
methods: {
getParams () {
const params = {}
if (this.$route.meta.name === 'account') {
params.account = this.resource.name
params.domainid = this.resource.domainid
} else if (this.$route.meta.name === 'domain') {
params.domainid = this.resource.id
} else { // project
params.projectid = this.resource.id
}
return params
},
async fetchData () {
const params = this.getParams()
try {
this.formLoading = true
this.dataResource = await this.listResourceLimits(params)
this.formLoading = false
} catch (e) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: e
})
this.formLoading = false
}
},
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (err) {
return
}
const arrAsync = []
const params = this.getParams()
for (const key in values) {
const input = values[key]
if (input === undefined) {
continue
}
params.resourcetype = key
params.max = input
arrAsync.push(this.updateResourceLimit(params))
}
this.formLoading = true
Promise.all(arrAsync).then(() => {
this.$message.success(this.$t('message.apply.success'))
this.fetchData()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.formLoading = false
})
})
},
listResourceLimits (params) {
return new Promise((resolve, reject) => {
let dataResource = []
api('listResourceLimits', params).then(json => {
if (json.listresourcelimitsresponse.resourcelimit) {
dataResource = json.listresourcelimitsresponse.resourcelimit
dataResource.sort((a, b) => a.resourcetype - b.resourcetype)
}
resolve(dataResource)
}).catch(error => {
reject(error.response.headers['x-description'])
})
})
},
updateResourceLimit (params) {
return new Promise((resolve, reject) => {
api('updateResourceLimit', params).then(json => {
resolve()
}).catch(error => {
reject(error)
})
})
}
}
}
</script>
<style lang="less" scoped>
.card-footer {
button + button {
margin-left: 8px;
}
}
</style>

View File

@ -0,0 +1,168 @@
// 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>
<resource-layout>
<div slot="left">
<slot name="info-card">
<info-card :resource="resource" :loading="loading" />
</slot>
</div>
<a-spin :spinning="loading" slot="right">
<a-card
class="spin-content"
:bordered="true"
style="width:100%">
<component
v-if="tabs.length === 1"
:is="tabs[0].component"
:resource="resource"
:loading="loading"
:tab="tabs[0].name" />
<a-tabs
v-else
style="width: 100%"
:animated="false"
:activeKey="activeTab || tabs[0].name"
@change="onTabChange" >
<a-tab-pane
v-for="tab in tabs"
:tab="$t('label.' + tab.name)"
:key="tab.name"
v-if="showTab(tab)">
<component :is="tab.component" :resource="resource" :loading="loading" :tab="activeTab" />
</a-tab-pane>
</a-tabs>
</a-card>
</a-spin>
</resource-layout>
</template>
<script>
import DetailsTab from '@/components/view/DetailsTab'
import InfoCard from '@/components/view/InfoCard'
import ResourceLayout from '@/layouts/ResourceLayout'
import { api } from '@/api'
import { mixinDevice } from '@/utils/mixin.js'
export default {
name: 'ResourceView',
components: {
InfoCard,
ResourceLayout
},
mixins: [mixinDevice],
props: {
resource: {
type: Object,
required: true
},
loading: {
type: Boolean,
default: false
},
tabs: {
type: Array,
default: function () {
return [{
name: 'details',
component: DetailsTab
}]
}
}
},
data () {
return {
activeTab: '',
networkService: null,
projectAccount: null
}
},
watch: {
resource: function (newItem, oldItem) {
this.resource = newItem
if (newItem.id === oldItem.id) return
if (this.resource.associatednetworkid) {
api('listNetworks', { id: this.resource.associatednetworkid, listall: true }).then(response => {
if (response && response.listnetworksresponse && response.listnetworksresponse.network) {
this.networkService = response.listnetworksresponse.network[0]
} else {
this.networkService = {}
}
})
}
},
$route: function (newItem, oldItem) {
this.setActiveTab()
}
},
mounted () {
this.setActiveTab()
},
methods: {
onTabChange (key) {
this.activeTab = key
const query = Object.assign({}, this.$route.query)
query.tab = key
history.replaceState(
{},
null,
'#' + this.$route.path + '?' + Object.keys(query).map(key => {
return (
encodeURIComponent(key) + '=' + encodeURIComponent(query[key])
)
}).join('&')
)
},
showTab (tab) {
if ('networkServiceFilter' in tab) {
if (this.resource && this.resource.virtualmachineid && !this.resource.vpcid && tab.name !== 'firewall') {
return false
}
if (this.resource && this.resource.virtualmachineid && this.resource.vpcid) {
return false
}
// dont display any option for source NAT IP of VPC
if (this.resource && this.resource.vpcid && !this.resource.issourcenat && tab.name !== 'firewall') {
return true
}
// display LB and PF options for isolated networks if static nat is disabled
if (this.resource && !this.resource.vpcid) {
if (!this.resource.isstaticnat) {
return true
} else if (tab.name === 'firewall') {
return true
}
}
return this.networkService && this.networkService.service &&
tab.networkServiceFilter(this.networkService.service)
} else if ('show' in tab) {
return tab.show(this.resource, this.$route, this.$store.getters.userInfo)
} else {
return true
}
},
setActiveTab () {
this.activeTab = this.$route.query.tab ? this.$route.query.tab : this.tabs[0].name
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,494 @@
// 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 :style="styleSearch">
<span v-if="!searchFilters || searchFilters.length === 0" style="display: flex;">
<a-input-search
style="width: 100%; display: table-cell"
:placeholder="$t('label.search')"
v-model="searchQuery"
allowClear
@search="onSearch" />
</span>
<span
v-else
class="filter-group">
<a-input-search
allowClear
class="input-search"
:placeholder="$t('label.search')"
v-model="searchQuery"
@search="onSearch">
<a-popover
placement="bottomRight"
slot="addonBefore"
trigger="click"
v-model="visibleFilter">
<template slot="content">
<a-form
style="min-width: 170px"
:form="form"
layout="vertical"
@submit="handleSubmit">
<a-form-item
v-for="(field, index) in fields"
:key="index"
:label="field.name==='keyword' ? $t('label.name') : $t('label.' + field.name)">
<a-select
allowClear
v-if="field.type==='list'"
v-decorator="[field.name]"
:loading="field.loading">
<a-select-option
v-for="(opt, idx) in field.opts"
:key="idx"
:value="opt.id">{{ $t(opt.name) }}</a-select-option>
</a-select>
<a-input
v-else-if="field.type==='input'"
v-decorator="[field.name]" />
<div v-else-if="field.type==='tag'">
<div>
<a-input-group
type="text"
size="small"
compact>
<a-input ref="input" :value="inputKey" @change="e => inputKey = e.target.value" style="width: 50px; text-align: center" :placeholder="$t('label.key')" />
<a-input style=" width: 20px; border-left: 0; pointer-events: none; backgroundColor: #fff" placeholder="=" disabled />
<a-input :value="inputValue" @change="handleValueChange" style="width: 50px; text-align: center; border-left: 0" :placeholder="$t('label.value')" />
<a-button shape="circle" size="small" @click="inputKey = inputValue = ''">
<a-icon type="close"/>
</a-button>
</a-input-group>
</div>
</div>
</a-form-item>
<div class="filter-group-button">
<a-button
class="filter-group-button-clear"
type="default"
size="small"
icon="stop"
@click="onClear">{{ $t('label.reset') }}</a-button>
<a-button
class="filter-group-button-search"
type="primary"
size="small"
icon="search"
html-type="submit"
@click="handleSubmit">{{ $t('label.search') }}</a-button>
</div>
</a-form>
</template>
<a-button
class="filter-button"
size="small"
@click="() => { searchQuery = null }">
<a-icon type="filter" :theme="Object.keys(searchParams).length > 0 ? 'twoTone' : 'outlined'" />
</a-button>
</a-popover>
</a-input-search>
</span>
</span>
</template>
<script>
import { api } from '@/api'
export default {
name: 'SearchView',
props: {
searchFilters: {
type: Array,
default: () => []
},
apiName: {
type: String,
default: () => ''
},
searchParams: {
type: Object,
default: () => {}
}
},
inject: ['parentSearch', 'parentChangeFilter'],
data () {
return {
searchQuery: null,
paramsFilter: {},
visibleFilter: false,
fields: [],
inputKey: null,
inputValue: null
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
},
watch: {
visibleFilter (newValue, oldValue) {
if (newValue) {
this.initFormFieldData()
}
},
'$route' (to, from) {
this.searchQuery = ''
if (to && to.query && 'q' in to.query) {
this.searchQuery = to.query.q
}
}
},
mounted () {
this.searchQuery = ''
if (this.$route && this.$route.query && 'q' in this.$route.query) {
this.searchQuery = this.$route.query.q
}
},
computed: {
styleSearch () {
if (!this.searchFilters || this.searchFilters.length === 0) {
return {
width: '100%',
display: 'table-cell'
}
}
return {
width: '100%',
display: 'table-cell',
lineHeight: '31px'
}
}
},
methods: {
async initFormFieldData () {
const arrayField = []
this.fields = []
this.searchFilters.forEach(item => {
let type = 'input'
if (item === 'domainid' && !('listDomains' in this.$store.getters.apis)) {
return true
}
if (item === 'account' && !('addAccountToProject' in this.$store.getters.apis || 'createAccount' in this.$store.getters.apis)) {
return true
}
if (item === 'podid' && !('listPods' in this.$store.getters.apis)) {
return true
}
if (item === 'clusterid' && !('listClusters' in this.$store.getters.apis)) {
return true
}
if (['zoneid', 'domainid', 'state', 'level', 'clusterid', 'podid'].includes(item)) {
type = 'list'
} else if (item === 'tags') {
type = 'tag'
}
this.fields.push({
type: type,
name: item,
opts: [],
loading: false
})
arrayField.push(item)
})
const promises = []
let zoneIndex = -1
let domainIndex = -1
let podIndex = -1
let clusterIndex = -1
if (arrayField.includes('state')) {
const stateIndex = this.fields.findIndex(item => item.name === 'state')
this.fields[stateIndex].loading = true
this.fields[stateIndex].opts = this.fetchState()
this.fields[stateIndex].loading = false
}
if (arrayField.includes('level')) {
const levelIndex = this.fields.findIndex(item => item.name === 'level')
this.fields[levelIndex].loading = true
this.fields[levelIndex].opts = this.fetchLevel()
this.fields[levelIndex].loading = false
}
if (arrayField.includes('zoneid')) {
zoneIndex = this.fields.findIndex(item => item.name === 'zoneid')
this.fields[zoneIndex].loading = true
promises.push(await this.fetchZones())
}
if (arrayField.includes('domainid')) {
domainIndex = this.fields.findIndex(item => item.name === 'domainid')
this.fields[domainIndex].loading = true
promises.push(await this.fetchDomains())
}
if (arrayField.includes('podid')) {
podIndex = this.fields.findIndex(item => item.name === 'podid')
this.fields[podIndex].loading = true
promises.push(await this.fetchPods())
}
if (arrayField.includes('clusterid')) {
clusterIndex = this.fields.findIndex(item => item.name === 'clusterid')
this.fields[clusterIndex].loading = true
promises.push(await this.fetchClusters())
}
Promise.all(promises).then(response => {
if (zoneIndex > -1) {
const zones = response.filter(item => item.type === 'zoneid')
if (zones && zones.length > 0) {
this.fields[zoneIndex].opts = zones[0].data
}
}
if (domainIndex > -1) {
const domain = response.filter(item => item.type === 'domainid')
if (domain && domain.length > 0) {
this.fields[domainIndex].opts = domain[0].data
}
}
if (podIndex > -1) {
const pod = response.filter(item => item.type === 'podid')
if (pod && pod.length > 0) {
this.fields[podIndex].opts = pod[0].data
}
}
if (clusterIndex > -1) {
const cluster = response.filter(item => item.type === 'clusterid')
console.log(cluster)
if (cluster && cluster.length > 0) {
this.fields[clusterIndex].opts = cluster[0].data
}
}
this.$forceUpdate()
}).finally(() => {
if (zoneIndex > -1) {
this.fields[zoneIndex].loading = false
}
if (domainIndex > -1) {
this.fields[domainIndex].loading = false
}
if (podIndex > -1) {
this.fields[podIndex].loading = false
}
if (clusterIndex > -1) {
this.fields[clusterIndex].loading = false
}
})
},
fetchZones () {
return new Promise((resolve, reject) => {
api('listZones', { listAll: true }).then(json => {
const zones = json.listzonesresponse.zone
resolve({
type: 'zoneid',
data: zones
})
}).catch(error => {
reject(error.response.headers['x-description'])
})
})
},
fetchDomains () {
return new Promise((resolve, reject) => {
api('listDomains', { listAll: true }).then(json => {
const domain = json.listdomainsresponse.domain
resolve({
type: 'domainid',
data: domain
})
}).catch(error => {
reject(error.response.headers['x-description'])
})
})
},
fetchPods () {
return new Promise((resolve, reject) => {
api('listPods', { listAll: true }).then(json => {
const pods = json.listpodsresponse.pod
resolve({
type: 'podid',
data: pods
})
}).catch(error => {
reject(error.response.headers['x-description'])
})
})
},
fetchClusters () {
return new Promise((resolve, reject) => {
api('listClusters', { listAll: true }).then(json => {
const clusters = json.listclustersresponse.cluster
resolve({
type: 'clusterid',
data: clusters
})
}).catch(error => {
reject(error.response.headers['x-description'])
})
})
},
fetchState () {
const state = []
if (this.apiName.indexOf('listVolumes') > -1) {
state.push({
id: 'Allocated',
name: 'label.allocated'
})
state.push({
id: 'Ready',
name: 'label.isready'
})
state.push({
id: 'Destroy',
name: 'label.destroy'
})
state.push({
id: 'Expunging',
name: 'label.expunging'
})
state.push({
id: 'Expunged',
name: 'label.expunged'
})
}
return state
},
fetchLevel () {
const levels = []
levels.push({
id: 'INFO',
name: 'label.info.upper'
})
levels.push({
id: 'WARN',
name: 'label.warn.upper'
})
levels.push({
id: 'ERROR',
name: 'label.error.upper'
})
return levels
},
onSearch (value) {
this.paramsFilter = {}
this.searchQuery = value
this.parentSearch({ searchQuery: this.searchQuery })
},
onClear () {
this.searchFilters.map(item => {
const field = {}
field[item] = undefined
this.form.setFieldsValue(field)
})
this.inputKey = null
this.inputValue = null
this.searchQuery = null
this.paramsFilter = {}
this.parentSearch(this.paramsFilter)
},
handleSubmit (e) {
e.preventDefault()
this.paramsFilter = {}
this.form.validateFields((err, values) => {
if (err) {
return
}
for (const key in values) {
const input = values[key]
if (input === '' || input === null || input === undefined) {
continue
}
this.paramsFilter[key] = input
}
if (this.searchFilters.includes('tags')) {
if (this.inputKey) {
this.paramsFilter['tags[0].key'] = this.inputKey
this.paramsFilter['tags[0].value'] = this.inputValue
}
}
this.parentSearch(this.paramsFilter)
})
},
handleKeyChange (e) {
this.inputKey = e.target.value
},
handleValueChange (e) {
this.inputValue = e.target.value
},
changeFilter (filter) {
this.parentChangeFilter(filter)
}
}
}
</script>
<style scoped lang="less">
.input-search {
margin-left: 10px;
}
.filter-group {
/deep/.ant-input-group-addon {
padding: 0 5px;
}
&-button {
background: inherit;
border: 0;
padding: 0;
}
&-button {
position: relative;
display: block;
min-height: 25px;
&-clear {
position: absolute;
left: 0;
}
&-search {
position: absolute;
right: 0;
}
}
/deep/.ant-input-group {
.ant-input-affix-wrapper {
width: calc(100% - 10px);
}
}
}
.filter-button {
background: inherit;
border: 0;
padding: 0;
position: relative;
display: block;
min-height: 25px;
width: 20px;
}
</style>

Some files were not shown because too many files have changed in this diff Show More