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.
@ -27,10 +27,14 @@ jdk:
|
||||
python:
|
||||
- "2.7"
|
||||
|
||||
node_js:
|
||||
- 12
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
timeout: 500
|
||||
npm: false
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
|
||||
@ -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
@ -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>
|
||||
|
||||
@ -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
@ -0,0 +1,7 @@
|
||||
{
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["require-context-hook"]
|
||||
}
|
||||
}
|
||||
}
|
||||
38
ui/.editorconfig
Normal 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
@ -0,0 +1 @@
|
||||
CS_URL=http://localhost:8080
|
||||
7
ui/.env.local.https.example
Normal 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
@ -0,0 +1 @@
|
||||
CS_URL=http://primate-qa.cloudstack.cloud:8080
|
||||
1
ui/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
public/* linguist-vendored
|
||||
39
ui/.gitignore
vendored
Normal 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
@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
125
ui/CONTRIBUTING.md
Normal 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
@ -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
@ -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
@ -0,0 +1,186 @@
|
||||
# CloudStack UI
|
||||
|
||||
A modern role-based progressive CloudStack UI based on VueJS and Ant Design.
|
||||
|
||||

|
||||
|
||||
## 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
@ -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
@ -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
@ -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
@ -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.
|
||||
661
ui/docs/full-test-plan.template.md
Normal 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
|
||||
BIN
ui/docs/screenshot-dashboard.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
181
ui/docs/smoke-test-plan.template.md
Normal 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
|
||||
@ -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
@ -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
@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
30
ui/nginx.conf
Normal 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
@ -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
165
ui/package.json
Normal 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
|
After Width: | Height: | Size: 38 KiB |
BIN
ui/public/assets/404.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
ui/public/assets/500.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
319
ui/public/assets/banner.svg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
ui/public/assets/bg-what-is-cloudstack.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
ui/public/assets/error.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
332
ui/public/assets/logo.svg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
ui/public/assets/success.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
ui/public/cloud.ico
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
7700
ui/public/color.less
vendored
Normal file
50
ui/public/config.json
vendored
Normal 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
@ -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
@ -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
2364
ui/public/locales/ca.json
Normal file
3176
ui/public/locales/de_DE.json
Normal file
3298
ui/public/locales/en.json
Normal file
2395
ui/public/locales/es.json
Normal file
2366
ui/public/locales/fr_FR.json
Normal file
528
ui/public/locales/hi.json
Normal 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
2364
ui/public/locales/it_IT.json
Normal file
2367
ui/public/locales/ja_JP.json
Normal file
2363
ui/public/locales/ko_KR.json
Normal file
2364
ui/public/locales/nb_NO.json
Normal file
2364
ui/public/locales/nl_NL.json
Normal file
2364
ui/public/locales/pl.json
Normal file
2363
ui/public/locales/pt_BR.json
Normal file
2363
ui/public/locales/ru_RU.json
Normal file
2367
ui/public/locales/zh_CN.json
Normal file
48
ui/src/App.vue
Normal 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
@ -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')
|
||||
}
|
||||
87
ui/src/assets/icons/cloudian.svg
Normal 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 |
155
ui/src/assets/icons/debian.svg
Normal 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 |
67
ui/src/assets/icons/kubernetes.svg
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
116
ui/src/components/CheckBoxSelectPair.vue
Normal 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>
|
||||
164
ui/src/components/header/HeaderNotice.vue
Normal 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>
|
||||
71
ui/src/components/header/Logo.vue
Normal 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>
|
||||
122
ui/src/components/header/ProjectMenu.vue
Normal 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>
|
||||
124
ui/src/components/header/SamlDomainSwitcher.vue
Normal 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>
|
||||
94
ui/src/components/header/TranslationMenu.vue
Normal 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>
|
||||
112
ui/src/components/header/UserMenu.vue
Normal 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>
|
||||
128
ui/src/components/menu/SideMenu.vue
Normal 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>
|
||||
19
ui/src/components/menu/index.js
Normal 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
|
||||
207
ui/src/components/menu/menu.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
173
ui/src/components/menu/menu.render.js
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
204
ui/src/components/multitab/MultiTab.vue
Normal 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">
|
||||
<!–<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>–>
|
||||
<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>
|
||||
21
ui/src/components/multitab/index.js
Normal 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
|
||||
31
ui/src/components/multitab/index.less
Normal 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;
|
||||
}
|
||||
67
ui/src/components/page/GlobalFooter.vue
Normal 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>
|
||||
147
ui/src/components/page/GlobalHeader.vue
Normal 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>
|
||||
205
ui/src/components/page/GlobalLayout.vue
Normal 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>
|
||||
250
ui/src/components/page/PageHeader.vue
Normal 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>
|
||||
141
ui/src/components/page/PageLayout.vue
Normal 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>
|
||||
76
ui/src/components/page/SHeaderNotice.vue
Normal 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>
|
||||
192
ui/src/components/view/ActionButton.vue
Normal 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>
|
||||
309
ui/src/components/view/DedicateData.vue
Normal 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>
|
||||
139
ui/src/components/view/DedicateDomain.vue
Normal 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>
|
||||
277
ui/src/components/view/DedicateModal.vue
Normal 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>
|
||||
293
ui/src/components/view/DetailSettings.vue
Normal 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>
|
||||
124
ui/src/components/view/DetailsTab.vue
Normal 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>
|
||||
131
ui/src/components/view/FormView.vue
Normal 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>
|
||||
1052
ui/src/components/view/InfoCard.vue
Normal file
218
ui/src/components/view/ListResourceTable.vue
Normal 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>
|
||||
618
ui/src/components/view/ListView.vue
Normal 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">
|
||||
|
||||
<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>
|
||||
85
ui/src/components/view/QuickView.vue
Normal 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>
|
||||
108
ui/src/components/view/ResourceCountUsage.vue
Normal 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>
|
||||
179
ui/src/components/view/ResourceLimitTab.vue
Normal 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>
|
||||
168
ui/src/components/view/ResourceView.vue
Normal 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>
|
||||
494
ui/src/components/view/SearchView.vue
Normal 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>
|
||||