Initial Commit
This commit is contained in:
13
.vscode/extensions.json
vendored
Normal file
13
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"ms-python.flake8",
|
||||||
|
"ms-python.vscode-pylance",
|
||||||
|
"ms-python.python",
|
||||||
|
"donjayamanne.python-environment-manager",
|
||||||
|
"kevinrose.vsc-python-indent",
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"vue.volar",
|
||||||
|
"vue.vscode-typescript-vue-plugin",
|
||||||
|
"ms-azuretools.vscode-docker"
|
||||||
|
]
|
||||||
|
}
|
||||||
73
LICENSE
Normal file
73
LICENSE
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
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.
|
||||||
165
api/.dockerignore
Normal file
165
api/.dockerignore
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# ---> Python
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
dashboard_database.db
|
||||||
|
|
||||||
|
# Ignore files
|
||||||
|
.dockerignore
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
157
api/.gitignore
vendored
Normal file
157
api/.gitignore
vendored
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# ---> Python
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
dashboard_database.db
|
||||||
31
api/Dockerfile
Normal file
31
api/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# syntax = docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM python:3.11.5-slim as builder
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y libpq-dev gcc
|
||||||
|
|
||||||
|
RUN python -m venv /opt/venv
|
||||||
|
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip3 install -r requirements.txt
|
||||||
|
|
||||||
|
FROM python:3.11.5-slim
|
||||||
|
|
||||||
|
# Set working dir
|
||||||
|
COPY --from=builder /opt/venv /opt/venv
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
COPY . /usr/src/virt.dashboard
|
||||||
|
|
||||||
|
WORKDIR /usr/src/virt.dashboard/app
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD [ "waitress-serve", "--port=5000", "wsgi:app" ]
|
||||||
0
api/app/__init__.py
Normal file
0
api/app/__init__.py
Normal file
9
api/app/custom_swagger.json
Normal file
9
api/app/custom_swagger.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"title": "Virt.Dashboard API",
|
||||||
|
"description": "Backend API for Virt.Dashboard",
|
||||||
|
"termsOfService": "",
|
||||||
|
"version": "1.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
api/app/favicon.ico
Normal file
BIN
api/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
0
api/app/helpers/__init__.py
Normal file
0
api/app/helpers/__init__.py
Normal file
23
api/app/helpers/authentication.py
Normal file
23
api/app/helpers/authentication.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from flask_httpauth import HTTPBasicAuth
|
||||||
|
|
||||||
|
auth = HTTPBasicAuth()
|
||||||
|
|
||||||
|
|
||||||
|
@auth.verify_password
|
||||||
|
def verify_password(username_or_token, password):
|
||||||
|
|
||||||
|
if username_or_token == "user" and password == "passwd":
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def post_login_required(func):
|
||||||
|
def post_decorator(*args, **kwargs):
|
||||||
|
print("post_decorator ", func, *args, **kwargs)
|
||||||
|
return auth.login_required(func)(*args, **kwargs)
|
||||||
|
|
||||||
|
if func.__name__ in ("post", "patch", "delete"):
|
||||||
|
return post_decorator
|
||||||
|
|
||||||
|
return func
|
||||||
3
api/app/helpers/database.py
Normal file
3
api/app/helpers/database.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
2
api/app/helpers/math.py
Normal file
2
api/app/helpers/math.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
def safe_division(n: int, d: int, r: int = 2):
|
||||||
|
return round((n / d), r) if d else 0
|
||||||
35
api/app/main.py
Normal file
35
api/app/main.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/python
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
from helpers.database import db
|
||||||
|
from swagger import create_api
|
||||||
|
from settings import config
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.DEBUG,
|
||||||
|
format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
|
||||||
|
datefmt="%m-%d %H:%M",
|
||||||
|
handlers=[logging.FileHandler(config.DASHBOARD_LOG), logging.StreamHandler()], # noqa
|
||||||
|
)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) # noqa
|
||||||
|
|
||||||
|
app.config.update(
|
||||||
|
SQLALCHEMY_DATABASE_URI=config.SQLALCHEMY_DATABASE_URI,
|
||||||
|
DEBUG=config.DEBUG
|
||||||
|
)
|
||||||
|
db.init_app(app)
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
create_api(
|
||||||
|
app=app,
|
||||||
|
port="5000",
|
||||||
|
host="localhost",
|
||||||
|
api_prefix=config.API_PREFIX_STRING,
|
||||||
|
custom_swagger=config.CUSTOM_SWAGGER
|
||||||
|
)
|
||||||
0
api/app/models/common/__init__.py
Normal file
0
api/app/models/common/__init__.py
Normal file
9
api/app/models/common/base.py
Normal file
9
api/app/models/common/base.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from helpers.database import db
|
||||||
|
|
||||||
|
from safrs import SAFRSBase
|
||||||
|
from safrs.api_methods import search
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(SAFRSBase, db.Model):
|
||||||
|
__abstract__ = True
|
||||||
|
SAFRSBase.search = search
|
||||||
0
api/app/models/common/core/__init__.py
Normal file
0
api/app/models/common/core/__init__.py
Normal file
21
api/app/models/common/core/contour.py
Normal file
21
api/app/models/common/core/contour.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from sqlalchemy import Column, String, Integer
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class contour(BaseModel):
|
||||||
|
"""
|
||||||
|
description: Technological infrastructure contours.
|
||||||
|
"""
|
||||||
|
__tablename__ = "tcc_contours"
|
||||||
|
_s_collection_name = "contours"
|
||||||
|
_s_class_name = "contours"
|
||||||
|
exclude_rels = ["vcenter", "pcentral"]
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
name = Column(String(64), unique=True, index=True, nullable=False)
|
||||||
|
description = Column(String(255))
|
||||||
|
|
||||||
|
vcenter = relationship("vcenter", back_populates="contour")
|
||||||
|
pcentral = relationship("pcentral", back_populates="contour")
|
||||||
27
api/app/models/common/core/environment.py
Normal file
27
api/app/models/common/core/environment.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from sqlalchemy import Column, String, Integer
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class environment(BaseModel):
|
||||||
|
"""
|
||||||
|
description: Technological infrastructure environments.
|
||||||
|
"""
|
||||||
|
__tablename__ = "tcc_environments"
|
||||||
|
_s_collection_name = "environments"
|
||||||
|
_s_class_name = "environment"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
name = Column(String(32), unique=True, index=True, nullable=False)
|
||||||
|
color = Column(String(7), index=False, nullable=True)
|
||||||
|
|
||||||
|
cluster = relationship("cluster", back_populates="environment")
|
||||||
|
pelement = relationship("pelement", back_populates="environment")
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
result = BaseModel.to_dict(self)
|
||||||
|
|
||||||
|
result['name'] = result['name'].upper()
|
||||||
|
|
||||||
|
return result
|
||||||
21
api/app/models/common/core/timestamps.py
Normal file
21
api/app/models/common/core/timestamps.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import Column, String, Integer, DateTime
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class timestamp(BaseModel):
|
||||||
|
"""
|
||||||
|
description: Timestamps.
|
||||||
|
"""
|
||||||
|
__tablename__ = "tcc_timestamps"
|
||||||
|
_s_collection_name = "timestamps"
|
||||||
|
_s_class_name = "timestamps"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
name = Column(String(64), unique=True, nullable=False)
|
||||||
|
timestamp = Column(
|
||||||
|
DateTime,
|
||||||
|
default=datetime.utcnow,
|
||||||
|
onupdate=datetime.utcnow
|
||||||
|
)
|
||||||
0
api/app/models/nutanix/__init__.py
Normal file
0
api/app/models/nutanix/__init__.py
Normal file
0
api/app/models/nutanix/core/__init__.py
Normal file
0
api/app/models/nutanix/core/__init__.py
Normal file
40
api/app/models/nutanix/core/pcentral.py
Normal file
40
api/app/models/nutanix/core/pcentral.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
String,
|
||||||
|
Integer,
|
||||||
|
ForeignKey
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class pcentral(BaseModel):
|
||||||
|
"""
|
||||||
|
description: Nutanix prism central model
|
||||||
|
"""
|
||||||
|
__tablename__ = "tnc_pcentrals"
|
||||||
|
_s_collection_name = "pcentrals"
|
||||||
|
_s_class_name = "pcentral"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
name = Column(String(32), unique=True, index=True, nullable=False)
|
||||||
|
hostname = Column(String(64), unique=True, index=True, nullable=True)
|
||||||
|
ncc_version = Column(String(16), index=True, nullable=False)
|
||||||
|
aos_version = Column(String(16), index=True, nullable=False)
|
||||||
|
|
||||||
|
contour_id = Column(Integer, ForeignKey("tcc_contours.id"))
|
||||||
|
contour = relationship("contour", back_populates="pcentral")
|
||||||
|
|
||||||
|
npcreport = relationship(
|
||||||
|
"npcreport",
|
||||||
|
backref="pcentral",
|
||||||
|
cascade="save-update, delete",
|
||||||
|
lazy="dynamic"
|
||||||
|
)
|
||||||
|
pelement = relationship(
|
||||||
|
"pelement",
|
||||||
|
backref="pcentral",
|
||||||
|
cascade="save-update, delete",
|
||||||
|
lazy="dynamic"
|
||||||
|
)
|
||||||
52
api/app/models/nutanix/core/pelement.py
Normal file
52
api/app/models/nutanix/core/pelement.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
String,
|
||||||
|
Integer,
|
||||||
|
ForeignKey
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class pelement(BaseModel):
|
||||||
|
"""
|
||||||
|
description: Nutanix prism element model
|
||||||
|
"""
|
||||||
|
__tablename__ = "tnc_pelement"
|
||||||
|
_s_collection_name = "pelements"
|
||||||
|
_s_class_name = "pelement"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
ip = Column(String(15), unique=True, nullable=False)
|
||||||
|
name = Column(String(32), unique=True, nullable=False)
|
||||||
|
hostname = Column(String(64), unique=True, nullable=True)
|
||||||
|
description = Column(String(64), unique=False, nullable=False)
|
||||||
|
ncc_version = Column(String(16), index=True, nullable=False)
|
||||||
|
aos_version = Column(String(16), index=True, nullable=False)
|
||||||
|
cpu_decomiss = Column(Integer, nullable=True)
|
||||||
|
|
||||||
|
pcentral_id = Column(Integer, ForeignKey("tnc_pcentrals.id"))
|
||||||
|
environment_id = Column(Integer, ForeignKey("tcc_environments.id"))
|
||||||
|
|
||||||
|
environment = relationship("environment", back_populates="pelement")
|
||||||
|
pelement = relationship(
|
||||||
|
"npereport",
|
||||||
|
backref="pelement",
|
||||||
|
cascade="save-update, delete",
|
||||||
|
lazy="dynamic"
|
||||||
|
)
|
||||||
|
maintenance = relationship(
|
||||||
|
"nmreport",
|
||||||
|
backref="pelement",
|
||||||
|
cascade="save-update, delete",
|
||||||
|
lazy="dynamic"
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
result = BaseModel.to_dict(self)
|
||||||
|
|
||||||
|
if (result['hostname'] is None):
|
||||||
|
result['hostname'] = result['hostname']
|
||||||
|
|
||||||
|
return result
|
||||||
0
api/app/models/nutanix/reports/__init__.py
Normal file
0
api/app/models/nutanix/reports/__init__.py
Normal file
0
api/app/models/nutanix/reports/mmhosts/__init__.py
Normal file
0
api/app/models/nutanix/reports/mmhosts/__init__.py
Normal file
37
api/app/models/nutanix/reports/mmhosts/report.py
Normal file
37
api/app/models/nutanix/reports/mmhosts/report.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
String,
|
||||||
|
Integer,
|
||||||
|
DateTime,
|
||||||
|
ForeignKey
|
||||||
|
)
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class nmreport(BaseModel):
|
||||||
|
"""
|
||||||
|
description: Nutanix hosts in maintenance mode report
|
||||||
|
"""
|
||||||
|
__tablename__ = "tnrm_report"
|
||||||
|
_s_collection_name = "nmreport"
|
||||||
|
_s_class_name = "nmreport"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
ip = Column(String(15), unique=True, nullable=False)
|
||||||
|
ipmi = Column(String(15), unique=True, nullable=False)
|
||||||
|
hypervisor_name = Column(String(64), unique=True, nullable=False)
|
||||||
|
hypervisor_type = Column(String(32), index=True)
|
||||||
|
hypervisor_state = Column(String(64), index=True)
|
||||||
|
serial = Column(String(32), unique=True, nullable=False)
|
||||||
|
reason = Column(String(255), nullable=True)
|
||||||
|
date = Column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
pelement_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("tnc_pelement.id"),
|
||||||
|
nullable=False,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
34
api/app/models/nutanix/reports/prismCentral/report.py
Normal file
34
api/app/models/nutanix/reports/prismCentral/report.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
Integer,
|
||||||
|
ForeignKey
|
||||||
|
)
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class npcreport(BaseModel):
|
||||||
|
"""
|
||||||
|
description: Nutanix prism central report model
|
||||||
|
"""
|
||||||
|
__tablename__ = "tnrpc_report"
|
||||||
|
_s_collection_name = "npcreport"
|
||||||
|
_s_class_name = "prismCentral"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
vms_total = Column(Integer, nullable=False)
|
||||||
|
cert_status = Column(Integer, nullable=False)
|
||||||
|
aos_status = Column(Integer, nullable=False)
|
||||||
|
projects_total = Column(Integer, nullable=False)
|
||||||
|
|
||||||
|
alerts_crit_ack = Column(Integer, nullable=False)
|
||||||
|
alerts_crit_nack = Column(Integer, nullable=False)
|
||||||
|
alerts_warn_ack = Column(Integer, nullable=False)
|
||||||
|
alerts_warn_nack = Column(Integer, nullable=False)
|
||||||
|
|
||||||
|
pcentral_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("tnc_pcentrals.id"),
|
||||||
|
nullable=False,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
49
api/app/models/nutanix/reports/prismElement/report.py
Normal file
49
api/app/models/nutanix/reports/prismElement/report.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
String,
|
||||||
|
Integer,
|
||||||
|
ForeignKey
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class npereport(BaseModel):
|
||||||
|
"""
|
||||||
|
description: Nutanix prism element report model
|
||||||
|
"""
|
||||||
|
__tablename__ = "tnrpe_report"
|
||||||
|
_s_collection_name = "npereport"
|
||||||
|
_s_class_name = "prismElement"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
vms_total = Column(Integer, nullable=False)
|
||||||
|
cert_status = Column(Integer, nullable=False)
|
||||||
|
aos_status = Column(Integer, nullable=False)
|
||||||
|
storage_dedup = Column(Integer, nullable=False)
|
||||||
|
hosts_total = Column(Integer, nullable=False)
|
||||||
|
hosts_in_maintenance = Column(Integer, nullable=False)
|
||||||
|
storage_usage = Column(Integer, nullable=False)
|
||||||
|
cpu_usage = Column(Integer, nullable=False)
|
||||||
|
mem_usage = Column(Integer, nullable=False)
|
||||||
|
hypervisor = Column(String(32), nullable=True)
|
||||||
|
hypervisor_version = Column(String(32), nullable=True)
|
||||||
|
alerts_crit_ack = Column(Integer, nullable=False)
|
||||||
|
alerts_crit_nack = Column(Integer, nullable=False)
|
||||||
|
alerts_warn_ack = Column(Integer, nullable=False)
|
||||||
|
alerts_warn_nack = Column(Integer, nullable=False)
|
||||||
|
redundancy_factor = Column(Integer, nullable=False)
|
||||||
|
|
||||||
|
pelement_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("tnc_pelement.id"),
|
||||||
|
nullable=False,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
|
decomission = relationship(
|
||||||
|
"nureport",
|
||||||
|
backref="npereport",
|
||||||
|
cascade="save-update, delete",
|
||||||
|
lazy="dynamic"
|
||||||
|
)
|
||||||
31
api/app/models/nutanix/reports/utilization/report.py
Normal file
31
api/app/models/nutanix/reports/utilization/report.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
Integer,
|
||||||
|
Float,
|
||||||
|
ForeignKey
|
||||||
|
)
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class nureport(BaseModel):
|
||||||
|
"""
|
||||||
|
description: Nutanix utilization report
|
||||||
|
"""
|
||||||
|
__tablename__ = "tnru_report"
|
||||||
|
_s_collection_name = "nureport"
|
||||||
|
_s_class_name = "utilization"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
cpu_avg = Column(Float, nullable=False)
|
||||||
|
cpu_peak = Column(Float, nullable=False)
|
||||||
|
mem_avg = Column(Float, nullable=False)
|
||||||
|
mem_peak = Column(Float, nullable=False)
|
||||||
|
storage = Column(Float, nullable=False)
|
||||||
|
|
||||||
|
pelement_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("tnrpe_report.id"),
|
||||||
|
nullable=False,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
0
api/app/models/vmware/__init__.py
Normal file
0
api/app/models/vmware/__init__.py
Normal file
0
api/app/models/vmware/core/__init__.py
Normal file
0
api/app/models/vmware/core/__init__.py
Normal file
54
api/app/models/vmware/core/cluster.py
Normal file
54
api/app/models/vmware/core/cluster.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
String,
|
||||||
|
Integer,
|
||||||
|
ForeignKey
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class cluster(BaseModel):
|
||||||
|
"""
|
||||||
|
description: VMware cluster model
|
||||||
|
"""
|
||||||
|
__tablename__ = "tvc_clusters"
|
||||||
|
_s_collection_name = "clusters"
|
||||||
|
exclude_rels = ["capacity", "datastores", "sharedNetwork"]
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
name = Column(String(64), unique=True, index=True, nullable=False)
|
||||||
|
vcenter_id = Column(Integer, ForeignKey("tvc_vcenters.id"), nullable=False)
|
||||||
|
environment_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("tcc_environments.id"),
|
||||||
|
nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
vcenter = relationship("vcenter", back_populates="cluster")
|
||||||
|
environment = relationship("environment", back_populates="cluster")
|
||||||
|
capacity = relationship(
|
||||||
|
"capacity",
|
||||||
|
backref="cluster",
|
||||||
|
cascade="save-update, delete",
|
||||||
|
lazy="dynamic"
|
||||||
|
)
|
||||||
|
maintenance = relationship(
|
||||||
|
"maintenance",
|
||||||
|
backref="cluster",
|
||||||
|
cascade="save-update, delete",
|
||||||
|
lazy="dynamic"
|
||||||
|
)
|
||||||
|
datastores = relationship(
|
||||||
|
"datastore",
|
||||||
|
backref="cluster",
|
||||||
|
cascade="save-update, delete",
|
||||||
|
lazy="dynamic"
|
||||||
|
)
|
||||||
|
sharedNetwork = relationship(
|
||||||
|
"sharedNetwork",
|
||||||
|
backref="cluster",
|
||||||
|
cascade="save-update, delete",
|
||||||
|
lazy="dynamic"
|
||||||
|
)
|
||||||
36
api/app/models/vmware/core/vcenter.py
Normal file
36
api/app/models/vmware/core/vcenter.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
String,
|
||||||
|
Integer,
|
||||||
|
ForeignKey
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class vcenter(BaseModel):
|
||||||
|
"""
|
||||||
|
description: VMware vcenter model
|
||||||
|
"""
|
||||||
|
__tablename__ = "tvc_vcenters"
|
||||||
|
_s_collection_name = "vcenters"
|
||||||
|
|
||||||
|
exclude_rels = ["rvm_vcenter"]
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
hostname = Column(String(64), unique=True, index=True, nullable=False)
|
||||||
|
contour_id = Column(Integer, ForeignKey("tcc_contours.id"))
|
||||||
|
|
||||||
|
cluster = relationship(
|
||||||
|
"cluster",
|
||||||
|
back_populates="vcenter",
|
||||||
|
cascade="save-update, delete"
|
||||||
|
)
|
||||||
|
contour = relationship("contour", back_populates="vcenter")
|
||||||
|
|
||||||
|
vcenter = relationship(
|
||||||
|
"maintenance",
|
||||||
|
backref="vcenter",
|
||||||
|
cascade="save-update, delete"
|
||||||
|
)
|
||||||
0
api/app/models/vmware/reports/__init__.py
Normal file
0
api/app/models/vmware/reports/__init__.py
Normal file
0
api/app/models/vmware/reports/capacity/__init__.py
Normal file
0
api/app/models/vmware/reports/capacity/__init__.py
Normal file
25
api/app/models/vmware/reports/capacity/frame.py
Normal file
25
api/app/models/vmware/reports/capacity/frame.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import Column, Integer, DateTime
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class frame(BaseModel):
|
||||||
|
__tablename__ = "tvrc_frames"
|
||||||
|
_s_collection_name = "frames"
|
||||||
|
http_methods = {"get", "post", "delete"}
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
timestamp = Column(
|
||||||
|
DateTime,
|
||||||
|
default=datetime.utcnow,
|
||||||
|
onupdate=datetime.utcnow
|
||||||
|
)
|
||||||
|
|
||||||
|
capacity = relationship(
|
||||||
|
"capacity",
|
||||||
|
backref="frame",
|
||||||
|
cascade="save-update, delete"
|
||||||
|
)
|
||||||
131
api/app/models/vmware/reports/capacity/report.py
Normal file
131
api/app/models/vmware/reports/capacity/report.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
from safrs import jsonapi_rpc, SAFRSFormattedResponse
|
||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
Integer,
|
||||||
|
Float,
|
||||||
|
ForeignKey
|
||||||
|
)
|
||||||
|
|
||||||
|
from helpers.database import db
|
||||||
|
from helpers.math import safe_division
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
from models.vmware.reports.capacity.frame import frame
|
||||||
|
|
||||||
|
|
||||||
|
class capacity(BaseModel):
|
||||||
|
"""
|
||||||
|
description: Capacity report model
|
||||||
|
"""
|
||||||
|
__tablename__ = "tvrc_report"
|
||||||
|
_s_collection_name = "capacity"
|
||||||
|
|
||||||
|
http_methods = {"get", "post"}
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
hosts_on_duty = Column(Integer, nullable=False)
|
||||||
|
hosts_in_maintenance = Column(Integer, nullable=False)
|
||||||
|
hosts_reserved = Column(Integer, nullable=False)
|
||||||
|
hosts_staged_dcm = Column(Integer, nullable=False)
|
||||||
|
vms_total = Column(Integer, nullable=False)
|
||||||
|
sockets_total = Column(Integer, nullable=False)
|
||||||
|
pcpu_total = Column(Integer, nullable=False)
|
||||||
|
pcpu_host_max = Column(Integer, nullable=False)
|
||||||
|
vcpu_provisioned = Column(Integer, nullable=False)
|
||||||
|
pmemory_total = Column(Float, nullable=False)
|
||||||
|
vmemory_provisioned = Column(Float, nullable=False)
|
||||||
|
pstorage_total = Column(Float, nullable=False)
|
||||||
|
vstorage_used = Column(Float, nullable=False)
|
||||||
|
vstorage_provisioned = Column(Float, nullable=False)
|
||||||
|
rdm_total = Column(Float, nullable=False)
|
||||||
|
pcpu_ovp_target = Column(Float, nullable=False)
|
||||||
|
pmemory_ovp_target = Column(Float, nullable=False)
|
||||||
|
pstorage_ovp_target = Column(Float, nullable=False)
|
||||||
|
pcpu_retire_target = Column(Float, nullable=False)
|
||||||
|
|
||||||
|
frame_id = Column(
|
||||||
|
Integer, ForeignKey("tvrc_frames.id"),
|
||||||
|
nullable=False,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
|
cluster_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("tvc_clusters.id"),
|
||||||
|
nullable=False,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@jsonapi_rpc(http_methods=["GET"])
|
||||||
|
def get_last(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
description : Get capacity report by last frameid
|
||||||
|
summary : Get last capacity report
|
||||||
|
tags:
|
||||||
|
- capacity
|
||||||
|
produces:
|
||||||
|
- application/xml
|
||||||
|
- application/json
|
||||||
|
"""
|
||||||
|
|
||||||
|
frame_obj = db.session.query(frame).order_by(frame.id.desc()).first()
|
||||||
|
|
||||||
|
reports = db.session.query(capacity).\
|
||||||
|
filter(capacity.frame_id == frame_obj.id).all()
|
||||||
|
|
||||||
|
data = [reports]
|
||||||
|
response = SAFRSFormattedResponse(
|
||||||
|
data,
|
||||||
|
self._s_meta(),
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
len(reports)
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pcpu_ovp_current = kwargs.pop("pcpu_ovp_current", None) # noqa
|
||||||
|
pmemory_ovp_current = kwargs.pop("pmemory_ovp_current", None) # noqa
|
||||||
|
pstorage_ovp_current = kwargs.pop("pstorage_ovp_current", None) # noqa
|
||||||
|
|
||||||
|
cpu_cap_used = kwargs.pop("cpu_cap_used", None) # noqa
|
||||||
|
mem_cap_used = kwargs.pop("mem_cap_used", None) # noqa
|
||||||
|
str_cap_used = kwargs.pop("str_cap_used", None) # noqa
|
||||||
|
|
||||||
|
hosts_total = kwargs.pop("hosts_total", None) # noqa
|
||||||
|
|
||||||
|
BaseModel.__init__(self, **kwargs)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
result = BaseModel.to_dict(self)
|
||||||
|
|
||||||
|
result['pcpu_ovp_current'] = safe_division(
|
||||||
|
result['vcpu_provisioned'],
|
||||||
|
result['pcpu_total']
|
||||||
|
)
|
||||||
|
result['pmemory_ovp_current'] = safe_division(
|
||||||
|
result['vmemory_provisioned'],
|
||||||
|
result['pmemory_total']
|
||||||
|
)
|
||||||
|
result['pstorage_ovp_current'] = safe_division(
|
||||||
|
result['vstorage_provisioned'],
|
||||||
|
result['pstorage_total']
|
||||||
|
)
|
||||||
|
|
||||||
|
result['cpu_cap_used'] = round(safe_division(
|
||||||
|
result['pcpu_ovp_current'],
|
||||||
|
result['pcpu_ovp_target']
|
||||||
|
)*100)
|
||||||
|
result['mem_cap_used'] = round(safe_division(
|
||||||
|
result['pmemory_ovp_current'],
|
||||||
|
result['pmemory_ovp_target']
|
||||||
|
)*100)
|
||||||
|
result['str_cap_used'] = round(safe_division(
|
||||||
|
result['pstorage_ovp_current'],
|
||||||
|
result['pstorage_ovp_target']
|
||||||
|
)*100)
|
||||||
|
|
||||||
|
result['hosts_total'] = result['hosts_on_duty'] +\
|
||||||
|
result['hosts_reserved'] +\
|
||||||
|
result['hosts_in_maintenance']
|
||||||
|
|
||||||
|
return result
|
||||||
56
api/app/models/vmware/reports/datastores/report.py
Normal file
56
api/app/models/vmware/reports/datastores/report.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
String,
|
||||||
|
Integer,
|
||||||
|
ForeignKey
|
||||||
|
)
|
||||||
|
|
||||||
|
from helpers.math import safe_division
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class datastore(BaseModel):
|
||||||
|
"""
|
||||||
|
description: Datastores report model
|
||||||
|
"""
|
||||||
|
__tablename__ = "tvrd_report"
|
||||||
|
_s_collection_name = "datastores"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
name = Column(String(64), index=True, nullable=False)
|
||||||
|
space_used = Column(Integer, nullable=False)
|
||||||
|
space_total = Column(Integer, nullable=False)
|
||||||
|
space_provisioned = Column(Integer, nullable=False)
|
||||||
|
|
||||||
|
cluster_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("tvc_clusters.id"),
|
||||||
|
nullable=False,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
ovp_current = kwargs.pop("ovp_current", None) # noqa
|
||||||
|
space_free_percent = kwargs.pop("space_free_percent", None) # noqa
|
||||||
|
cap_used = kwargs.pop("cap_used", None) # noqa
|
||||||
|
|
||||||
|
BaseModel.__init__(self, **kwargs)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
result = BaseModel.to_dict(self)
|
||||||
|
|
||||||
|
result["ovp_current"] = safe_division(
|
||||||
|
result["space_provisioned"],
|
||||||
|
result["space_total"]
|
||||||
|
)
|
||||||
|
result["space_free_percent"] = round((1 - safe_division(
|
||||||
|
result["space_used"],
|
||||||
|
result["space_total"]
|
||||||
|
))*100, 0)
|
||||||
|
|
||||||
|
result['cap_used'] = round(safe_division(
|
||||||
|
result['ovp_current'],
|
||||||
|
2
|
||||||
|
)*100)
|
||||||
|
|
||||||
|
return result
|
||||||
0
api/app/models/vmware/reports/mmhosts/__init__.py
Normal file
0
api/app/models/vmware/reports/mmhosts/__init__.py
Normal file
38
api/app/models/vmware/reports/mmhosts/report.py
Normal file
38
api/app/models/vmware/reports/mmhosts/report.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
String,
|
||||||
|
Integer,
|
||||||
|
DateTime,
|
||||||
|
ForeignKey
|
||||||
|
)
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class maintenance(BaseModel):
|
||||||
|
__tablename__ = "tvrm_report"
|
||||||
|
_s_collection_name = "maintenance"
|
||||||
|
|
||||||
|
http_methods = {"get", "post", "delete"}
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
folder = Column(String(255), nullable=False)
|
||||||
|
hostname = Column(String(64), unique=True, nullable=False)
|
||||||
|
state = Column(String(64), index=True)
|
||||||
|
reason = Column(String(255))
|
||||||
|
placedby = Column(String(64))
|
||||||
|
placedbyFN = Column(String(64))
|
||||||
|
date = Column(DateTime)
|
||||||
|
|
||||||
|
vcenter_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("tvc_vcenters.id"),
|
||||||
|
nullable=False,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
|
cluster_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("tvc_clusters.id"),
|
||||||
|
nullable=True,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
49
api/app/models/vmware/reports/sharedNetworks/report.py
Normal file
49
api/app/models/vmware/reports/sharedNetworks/report.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
String,
|
||||||
|
Integer,
|
||||||
|
Float,
|
||||||
|
ForeignKey
|
||||||
|
)
|
||||||
|
|
||||||
|
from models.common.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class sharedNetwork(BaseModel):
|
||||||
|
"""
|
||||||
|
description: Shared Networks report model
|
||||||
|
"""
|
||||||
|
__tablename__ = "tvrsn_report"
|
||||||
|
_s_collection_name = "sharedNetworks"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
vrf = Column(String(64), nullable=False)
|
||||||
|
vrfId = Column(Integer, nullable=False, index=True)
|
||||||
|
subnet = Column(String(15), nullable=False)
|
||||||
|
subnetId = Column(Integer, nullable=False, unique=True, index=True)
|
||||||
|
subnetMask = Column(Integer, nullable=False)
|
||||||
|
requestsID = Column(String(32))
|
||||||
|
bussinessLine = Column(String(32))
|
||||||
|
subnetManager = Column(String(32), index=True)
|
||||||
|
virtSubnetName = Column(String(32))
|
||||||
|
virtSubnetUUID = Column(String(32))
|
||||||
|
freeIPPercent = Column(Float, nullable=False)
|
||||||
|
|
||||||
|
cluster_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("tvc_clusters.id"),
|
||||||
|
nullable=True,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
platform = kwargs.pop("platform", None) # noqa
|
||||||
|
|
||||||
|
BaseModel.__init__(self, **kwargs)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
result = BaseModel.to_dict(self)
|
||||||
|
|
||||||
|
result["platform"] = "vmware"
|
||||||
|
|
||||||
|
return result
|
||||||
46
api/app/settings.py
Normal file
46
api/app/settings.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import json
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
ENVIRONMENT: str = "dev"
|
||||||
|
|
||||||
|
SWAGGER_HOST: str = "localhost:5000"
|
||||||
|
|
||||||
|
API_PREFIX_STRING: str = "/api"
|
||||||
|
API_DESCRIPTION: str = "vtb_dashboard_api"
|
||||||
|
|
||||||
|
DASHBOARD_LOG: str = "vtb_dashboard_api.log"
|
||||||
|
|
||||||
|
POSTGRES_SERVER: str = "localhost"
|
||||||
|
POSTGRES_PORT: int = 5432
|
||||||
|
POSTGRES_USER: str
|
||||||
|
POSTGRES_PASSWORD: str
|
||||||
|
POSTGRES_DB: str = "virt.dashboard"
|
||||||
|
|
||||||
|
DEBUG: bool = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def SQLALCHEMY_DATABASE_URI(self):
|
||||||
|
return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}" # noqa
|
||||||
|
|
||||||
|
@property
|
||||||
|
def CUSTOM_SWAGGER(self):
|
||||||
|
with open("custom_swagger.json") as j_fp:
|
||||||
|
custom_swagger = json.load(j_fp)
|
||||||
|
custom_swagger["host"] = self.SWAGGER_HOST
|
||||||
|
return custom_swagger
|
||||||
|
|
||||||
|
model_config = SettingsConfigDict(extra='allow', env_file=".env")
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_config():
|
||||||
|
load_dotenv()
|
||||||
|
return Settings()
|
||||||
|
|
||||||
|
|
||||||
|
config = get_config()
|
||||||
59
api/app/swagger.py
Normal file
59
api/app/swagger.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from safrs import SAFRSAPI
|
||||||
|
|
||||||
|
from models.common.core.contour import contour as mcc_Contour
|
||||||
|
from models.common.core.environment import environment as mcc_Environment
|
||||||
|
from models.common.core.timestamps import timestamp as mcc_Timestamp
|
||||||
|
|
||||||
|
from models.vmware.core.vcenter import vcenter as mvc_vCenter
|
||||||
|
from models.vmware.core.cluster import cluster as mvc_Cluster
|
||||||
|
from models.vmware.reports.capacity.frame import frame as mvrc_Frame
|
||||||
|
from models.vmware.reports.capacity.report import capacity as mvrc_Report
|
||||||
|
from models.vmware.reports.mmhosts.report import maintenance as mvrm_Report
|
||||||
|
from models.vmware.reports.datastores.report import datastore as mvrd_Report # noqa
|
||||||
|
from models.vmware.reports.sharedNetworks.report import sharedNetwork as mvrsn_Report # noqa
|
||||||
|
|
||||||
|
from models.nutanix.core.pelement import pelement as mnc_Cluster
|
||||||
|
from models.nutanix.core.pcentral import pcentral as mnc_Central
|
||||||
|
from models.nutanix.reports.prismElement.report import npereport as mnrpe_Report # noqa
|
||||||
|
from models.nutanix.reports.prismCentral.report import npcreport as mnrpc_Report # noqa
|
||||||
|
from models.nutanix.reports.mmhosts.report import nmreport as mnrm_Report
|
||||||
|
from models.nutanix.reports.utilization.report import nureport as mnru_Report # noqa
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_api(
|
||||||
|
app: Flask,
|
||||||
|
host: str,
|
||||||
|
port: str,
|
||||||
|
api_prefix: str,
|
||||||
|
custom_swagger: dict = {}
|
||||||
|
) -> None:
|
||||||
|
api = SAFRSAPI(
|
||||||
|
app,
|
||||||
|
host=host,
|
||||||
|
schemes=["http", "https"],
|
||||||
|
port=port,
|
||||||
|
prefix=api_prefix,
|
||||||
|
custom_swagger=custom_swagger
|
||||||
|
)
|
||||||
|
|
||||||
|
for model in [mcc_Contour, mcc_Environment, mcc_Timestamp]:
|
||||||
|
api.expose_object(model, url_prefix="/common")
|
||||||
|
|
||||||
|
for model in [mvc_vCenter, mvc_Cluster]:
|
||||||
|
api.expose_object(model, url_prefix="/vmware")
|
||||||
|
|
||||||
|
for model in [mvrc_Report, mvrm_Report, mvrd_Report, mvrsn_Report]:
|
||||||
|
api.expose_object(model, url_prefix="/vmware/report")
|
||||||
|
|
||||||
|
api.expose_object(mvrc_Frame, url_prefix="/vmware/report/capacity")
|
||||||
|
|
||||||
|
for model in [mnc_Cluster, mnc_Central]:
|
||||||
|
api.expose_object(model, url_prefix="/nutanix")
|
||||||
|
|
||||||
|
for model in [mnrpe_Report, mnrpc_Report, mnrm_Report, mnru_Report]:
|
||||||
|
api.expose_object(model, url_prefix="/nutanix/report")
|
||||||
5
api/app/wsgi.py
Normal file
5
api/app/wsgi.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from main import app
|
||||||
|
from settings import config
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=config.DEBUG, host="0.0.0.0", port=5000)
|
||||||
31
api/requirements.txt
Normal file
31
api/requirements.txt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
aniso8601>=9.0.1
|
||||||
|
build>=0.10.0
|
||||||
|
click>=8.1.3
|
||||||
|
Flask>=2.2.5
|
||||||
|
Flask-Cors>=3.0.10
|
||||||
|
Flask-RESTful>=0.3.9
|
||||||
|
flask-restful-swagger-2>=0.35
|
||||||
|
Flask-SQLAlchemy>=3.0.3
|
||||||
|
flask-swagger-ui>=4.11.1
|
||||||
|
Flask-HTTPAuth>=4.8.0
|
||||||
|
greenlet>=2.0.2
|
||||||
|
importlib-metadata>=6.0.0
|
||||||
|
itsdangerous>=2.1.2
|
||||||
|
Jinja2>=3.1.2
|
||||||
|
MarkupSafe>=2.1.2
|
||||||
|
packaging>=23.0
|
||||||
|
pyproject-hooks>=1.0.0
|
||||||
|
pytz>=2022.7.1
|
||||||
|
PyYAML>=6.0
|
||||||
|
six>=1.16.0
|
||||||
|
SQLAlchemy>=2.0.3
|
||||||
|
tomli>=2.0.1
|
||||||
|
typing-extensions>=4.4.0
|
||||||
|
Werkzeug>=2.2.2
|
||||||
|
zipp>=3.13.0
|
||||||
|
safrs>=3.0.4
|
||||||
|
psycopg2-binary>=2.9.7
|
||||||
|
pydantic>=2.3.0
|
||||||
|
pydantic-settings>=2.0.3
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
waitress>=2.1.2
|
||||||
34
docker-compose.yml
Normal file
34
docker-compose.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
version: "3.9"
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:15.3
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: "virt_dashboard"
|
||||||
|
POSTGRES_USER: "virt_dashboard_user"
|
||||||
|
POSTGRES_PASSWORD: "EKnqZLVDZqjN"
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U virt_dashboard_user -d virt_dashboard"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
restart: unless-stopped
|
||||||
|
backend:
|
||||||
|
build: ./api/
|
||||||
|
environment:
|
||||||
|
POSTGRES_SERVER: "db"
|
||||||
|
POSTGRES_USER: "virt_dashboard_user"
|
||||||
|
POSTGRES_PASSWORD: "EKnqZLVDZqjN"
|
||||||
|
POSTGRES_DB: "virt_dashboard"
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
frontend:
|
||||||
|
build: ./frontend/
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
8
frontend/.dockerignore
Normal file
8
frontend/.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
.gitignore
|
||||||
2
frontend/.env.development
Normal file
2
frontend/.env.development
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_IPAM_URL=https://ipamuts.region.vtb.ru
|
||||||
|
VITE_BACKEND_URL=http://localhost:5000/api
|
||||||
2
frontend/.env.production
Normal file
2
frontend/.env.production
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_IPAM_URL=https://ipamuts.region.vtb.ru
|
||||||
|
VITE_BACKEND_URL=https://p0vmsv-ap5003xv.region.vtb.ru/api
|
||||||
12
frontend/.eslintrc.cjs
Normal file
12
frontend/.eslintrc.cjs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"root": true,
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/vue3-essential",
|
||||||
|
"eslint:recommended",
|
||||||
|
"@vue/eslint-config-typescript/recommended",
|
||||||
|
"@vue/eslint-config-prettier"
|
||||||
|
]
|
||||||
|
}
|
||||||
40
frontend/.gitignore
vendored
Normal file
40
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# ---> Vue
|
||||||
|
# gitignore template for Vue.js projects
|
||||||
|
#
|
||||||
|
# Recommended template: Node.gitignore
|
||||||
|
|
||||||
|
# TODO: where does this rule come from?
|
||||||
|
docs/_book
|
||||||
|
|
||||||
|
# TODO: where does this rule come from?
|
||||||
|
test/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.zip
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
18
frontend/Dockerfile
Normal file
18
frontend/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# build stage
|
||||||
|
FROM node:lts-alpine as build-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# production stage
|
||||||
|
FROM nginx:stable-alpine as production-stage
|
||||||
|
|
||||||
|
COPY ./nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
12
frontend/env.d.ts
vendored
Normal file
12
frontend/env.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_APP_TITLE: string
|
||||||
|
readonly VITE_IPAM_URL: string
|
||||||
|
readonly VITE_BACKEND_URL: string
|
||||||
|
// more env variables...
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>VTB.Dashboard</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
frontend/nginx/nginx.conf
Normal file
8
frontend/nginx/nginx.conf
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
server {
|
||||||
|
listen 3000;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
8818
frontend/package-lock.json
generated
Normal file
8818
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
58
frontend/package.json
Normal file
58
frontend/package.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "dashboard_frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check build-only",
|
||||||
|
"preview": "vite preview --port 4173",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --noEmit",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource/source-sans-pro": "^4.5.10",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||||
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
|
"@vuestic/ag-grid-theme": "^1.0.3",
|
||||||
|
"axios": "^0.27.2",
|
||||||
|
"chart.js": "^4.3.0",
|
||||||
|
"common-js": "^0.3.8",
|
||||||
|
"core-js": "^3.23.3",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"font-awesome": "^4.7.0",
|
||||||
|
"fortawesome": "^0.0.1-security",
|
||||||
|
"http-server": "^14.1.1",
|
||||||
|
"ionicons": "^4.6.3",
|
||||||
|
"lodash.pick": "^4.4.0",
|
||||||
|
"material-icons": "^1.11.4",
|
||||||
|
"vue": "^3.2.37",
|
||||||
|
"vue-apexcharts": "^1.6.2",
|
||||||
|
"vue-chartjs": "^5.2.0",
|
||||||
|
"vue-fontawesome": "^0.0.2",
|
||||||
|
"vue-i18n": "^9.1.10",
|
||||||
|
"vue-router": "^4.0.16",
|
||||||
|
"vue3-apexcharts": "^1.4.1",
|
||||||
|
"vuestic-ui": "^1.4.6",
|
||||||
|
"vuex": "^4.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rushstack/eslint-patch": "^1.1.0",
|
||||||
|
"@types/file-saver": "^2.0.5",
|
||||||
|
"@types/jest": "^28.1.4",
|
||||||
|
"@types/node": "^16.11.47",
|
||||||
|
"@vitejs/plugin-vue": "^2.3.3",
|
||||||
|
"@vue/eslint-config-prettier": "^7.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "^11.0.0",
|
||||||
|
"@vue/tsconfig": "^0.1.3",
|
||||||
|
"eslint": "^8.5.0",
|
||||||
|
"eslint-plugin-vue": "^9.0.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"sass": "^1.53.0",
|
||||||
|
"sass-loader": "^13.0.2",
|
||||||
|
"typescript": "~4.7.4",
|
||||||
|
"vite": "^2.9.14",
|
||||||
|
"vue-tsc": "^0.38.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
18
frontend/src/App.vue
Normal file
18
frontend/src/App.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<router-view/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '@/sass/main.scss';
|
||||||
|
#app {
|
||||||
|
font-family: 'Source Sans Pro', Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--va-background);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
frontend/src/api/http-common.ts
Normal file
9
frontend/src/api/http-common.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_BACKEND_URL,
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
56
frontend/src/api/report-service.ts
Normal file
56
frontend/src/api/report-service.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import http from "./http-common";
|
||||||
|
class ReportsService {
|
||||||
|
async getLastFrame() {
|
||||||
|
const response = await http.get("/vmware/report/capacity/frames/?sort=id&page[limit]=200000")
|
||||||
|
return response?.data.data[response?.data.data.length - 1]
|
||||||
|
}
|
||||||
|
async getFrames(range:{start:Date, end:Date}) {
|
||||||
|
const response = await http.get("/vmware/report/capacity/frames/?page[limit]=200000")
|
||||||
|
response.data.data.forEach((element:any) => {
|
||||||
|
element.attributes.timestamp = new Date(element.attributes.timestamp)
|
||||||
|
});
|
||||||
|
response.data.data = response.data.data.filter(
|
||||||
|
(item: any) => item.attributes.timestamp <= range.end && item.attributes.timestamp >= range.start
|
||||||
|
);
|
||||||
|
return response?.data.data
|
||||||
|
}
|
||||||
|
async getTimestamp(name:string) {
|
||||||
|
const response = await http.get(`/common/timestamps/?filter[name]=${name}`)
|
||||||
|
return response?.data.data[0]
|
||||||
|
}
|
||||||
|
getLastVmWareCapacityReport(id:number) {
|
||||||
|
return http.get(`/vmware/report/capacity/?include=cluster,cluster.environment,cluster.maintenance,cluster.vcenter,cluster.vcenter.contour&filter[frame_id]=${id}`);
|
||||||
|
}
|
||||||
|
getMultipleVmWareCapacityReport(list:number[]) {
|
||||||
|
const urls = list.map((id: any) => `/vmware/report/capacity/?include=cluster,cluster.environment,cluster.maintenance,cluster.vcenter,cluster.vcenter.contour,frame&filter[frame_id]=${id}`)
|
||||||
|
const requests = urls.map((url: string) => http.get(url));
|
||||||
|
|
||||||
|
return axios.all(requests)
|
||||||
|
}
|
||||||
|
getVmWareCapacityReport() {
|
||||||
|
return http.get("/vmware/report/capacity/?include=cluster,cluster.environment,cluster.maintenance,cluster.vcenter,cluster.vcenter.contour&filter[frame_id]=1");
|
||||||
|
}
|
||||||
|
getVmWareMaintenanceReport() {
|
||||||
|
return http.get("/vmware/report/maintenance/?include=vcenter,vcenter.contour&page[limit]=2000");
|
||||||
|
}
|
||||||
|
getVmWareDatastoresReport() {
|
||||||
|
return http.get("/vmware/report/datastores/?include=cluster,cluster.environment,cluster.vcenter,cluster.vcenter.contour&page[limit]=200000");
|
||||||
|
}
|
||||||
|
getVmWareSharedNetworksReport() {
|
||||||
|
return http.get("/vmware/report/sharedNetworks/?include=cluster,cluster.environment,cluster.vcenter,cluster.vcenter.contour&page[limit]=2000");
|
||||||
|
}
|
||||||
|
getNutanixPrismCentralReport() {
|
||||||
|
return http.get("/nutanix/report/npcreport/?include=pcentral,pcentral.pelement,pcentral.contour&page[limit]=2000");
|
||||||
|
}
|
||||||
|
getNutanixPrismElementReport() {
|
||||||
|
return http.get("/nutanix/report/npereport/?include=pelement,pelement.environment,pelement.maintenance,pelement.pcentral,pelement.pcentral.contour&page[limit]=2000");
|
||||||
|
}
|
||||||
|
getNutanixMaintenanceReport() {
|
||||||
|
return http.get("/nutanix/report/nmreport/?include=pelement,pelement.environment,pelement.pcentral,pelement.pcentral.contour&page[limit]=2000");
|
||||||
|
}
|
||||||
|
getNutanixDecomissionReport() {
|
||||||
|
return http.get("/nutanix/report/nureport/?include=npereport,npereport.pelement,npereport.pelement,npereport.pelement.environment,npereport.pelement.pcentral,npereport.pelement.pcentral.contour,npereport.pelement.maintenance&page[limit]=2000");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default new ReportsService();
|
||||||
74
frontend/src/assets/base.css
Normal file
74
frontend/src/assets/base.css
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f8f8f8;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #222222;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition: color 0.5s, background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||||
|
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
BIN
frontend/src/assets/logo.png
Normal file
BIN
frontend/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
1
frontend/src/assets/logo.svg
Normal file
1
frontend/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 174 60.56"><defs><style>.a{fill:#009de0;}.b{fill:#243571;}</style></defs><title>VTB Bank logo</title><path class="a" d="M468.36,505.87H427.74L425,513.92h40.62Zm4.39-12.08H432.32l-2.93,8.05H470Zm4.57-12.08H436.71l-2.93,8.05H474.4Z" transform="translate(-425 -481.72)"/><path class="b" d="M496.54,542.1l-15.19-48.3H492l5.85,21c1.65,6.22,2.56,9.51,3.66,14.82,1.1-4.94,1.65-7.32,3.66-14.45L511,493.79h10.25l-15.19,48.3Zm40.8,0V501.85H523.44v-8.05h38.79l-2.74,8.05H547V542.1Zm37.51-28.91h5.49a14.93,14.93,0,0,0,3.29-.18,5.47,5.47,0,0,0,3.66-5.49c0-3.29-1.46-4.76-3.66-5.31a16,16,0,0,0-3.66-.37h-5.12Zm.18,8.23V534h6.77c2,0,3.84-.18,5.12-1.46a6.76,6.76,0,0,0,2-4.94,6.41,6.41,0,0,0-1.28-4.21c-1.46-1.83-3.11-2.2-6.22-2.2H575Zm15.37-4.94h0a29.83,29.83,0,0,1,4.57,2.74c2.93,2.38,4,5.49,4,9.51,0,6.59-3.48,11.34-9.33,12.81a22.92,22.92,0,0,1-7,.73H565.15V493.79H580.7a27.33,27.33,0,0,1,7.32.73c5.67,1.46,9.33,5.31,9.33,11.53a10.32,10.32,0,0,1-3.11,7.87A19.69,19.69,0,0,1,590.4,516.48Z" transform="translate(-425 -481.72)"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
113
frontend/src/components/expandingBlock/expandingBlock.vue
Normal file
113
frontend/src/components/expandingBlock/expandingBlock.vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div class="expanding-block-background" :class= "{ fade: isExpanded }" @click="toggle"></div>
|
||||||
|
<div class="expanding-block">
|
||||||
|
<div class="expanding-block-header" @click="toggle">
|
||||||
|
<span class="expanding-block-header-icon">
|
||||||
|
<font-awesome-icon :icon="faChevronDown" class="expanding-block-icon" :class="{ rotate: isExpanded }"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="expanding-block-content" :style="{ maxHeight: contentHeight }">
|
||||||
|
<div v-if="isExpanded" ref="content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ExpandingBlock",
|
||||||
|
components: {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
contentHeight: {
|
||||||
|
type: String,
|
||||||
|
default: "0",
|
||||||
|
validator: (value) => {
|
||||||
|
// Ensure that the prop value is a valid CSS height value
|
||||||
|
const regex = /^\d+(?:px|em|rem|%|vh|vw)$/; // Matches "10px", "2em", "3rem", "50%", "80vh", "25vw", etc.
|
||||||
|
return regex.test(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isExpanded: false,
|
||||||
|
maxHeight: this.contentHeight,
|
||||||
|
faChevronDown: faChevronDown,
|
||||||
|
faChevronUp: faChevronUp,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
contentHeight(newVal) {
|
||||||
|
this.maxHeight = newVal;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggle(event) {
|
||||||
|
if (event.target.closest(".expanding-block-header") || event.target.closest(".expanding-block-background")) {
|
||||||
|
this.isExpanded = !this.isExpanded;
|
||||||
|
if (this.isExpanded) {
|
||||||
|
this.maxHeight = this.contentHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.expanding-block {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.expanding-block-header-icon {
|
||||||
|
position: fixed;
|
||||||
|
left: calc(50% - 28px);
|
||||||
|
top: 65px;
|
||||||
|
padding: 5px 20px 5px 20px;
|
||||||
|
box-shadow: 1px 5px 5px rgba(0, 0, 0, 0.12);
|
||||||
|
border-radius: 0 0 0 4px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
.expanding-block-icon {
|
||||||
|
transition: 0.3s ease-in-out;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
.expanding-block-icon.rotate {
|
||||||
|
transition: 0.3s ease-in-out;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
.expanding-block-content {
|
||||||
|
margin-top: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: fixed;
|
||||||
|
transform: translate(-50%);
|
||||||
|
left: 50%;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.12), 0px 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.expanding-block-background {
|
||||||
|
top: 65px;
|
||||||
|
left: 0px;
|
||||||
|
position: absolute;
|
||||||
|
transition: backdrop-filter 0.2s;
|
||||||
|
backdrop-filter: blur(8px) opacity(0);
|
||||||
|
height: calc(100% - 65px);
|
||||||
|
width: 100%;
|
||||||
|
z-index: 9;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.expanding-block-background.fade {
|
||||||
|
pointer-events: auto;
|
||||||
|
backdrop-filter: blur(8px) opacity(1);
|
||||||
|
z-index: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
38
frontend/src/components/icons/VaIconMenu.vue
Normal file
38
frontend/src/components/icons/VaIconMenu.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
class="va-icon-menu"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="23"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 18"
|
||||||
|
>
|
||||||
|
<g fill="none" fill-rule="nonzero" transform="translate(1 -3)">
|
||||||
|
<path d="M0 0h24v24H0z"/>
|
||||||
|
<rect width="20" height="2" x="2" y="3" :fill="color" rx="1"/>
|
||||||
|
<path :fill="color" d="M11 11h10a1 1 0 0 1 0 2H11a1 1 0 0 1 0-2zM1 11h5a1 1 0 0 1 0 2H1a1 1 0 0 1 0-2z"/>
|
||||||
|
<rect width="20" height="2" x="2" y="19" :fill="color" rx="1"/>
|
||||||
|
<path :stroke="color" stroke-width="2" d="M4 9l-3 3 3 3"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'VaIconMenu',
|
||||||
|
props: {
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: 'inherit',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.va-icon-menu {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
36
frontend/src/components/icons/VaIconMenuCollapsed.vue
Normal file
36
frontend/src/components/icons/VaIconMenuCollapsed.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
class="va-icon-menu-collapsed"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g fill="none" fill-rule="nonzero">
|
||||||
|
<path d="M0 0h24v24H0z"/>
|
||||||
|
<rect width="20" height="2" x="2" y="3" :fill="color" rx="1"/>
|
||||||
|
<path :fill="color" d="M3 11h10a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2zM20.993 11l-2.7-2.7-1.414 1.414L18.164 11H16a1 1 0 0 0 0 2h2.179l-1.3 1.3 1.414 1.414L21.007 13A1 1 0 0 0 21 11h-.007z"/>
|
||||||
|
<rect width="20" height="2" x="2" y="19" :fill="color" rx="1"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'VaIconMenuCollapsed',
|
||||||
|
props: {
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: 'inherit',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.va-icon-menu-collapsed {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
100
frontend/src/components/navbar/Navbar.vue
Normal file
100
frontend/src/components/navbar/Navbar.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-layout__navbar">
|
||||||
|
<va-navbar>
|
||||||
|
<template v-slot:left>
|
||||||
|
<div class="left">
|
||||||
|
<va-icon-menu-collapsed
|
||||||
|
@click="isSidebarMinimized = !isSidebarMinimized"
|
||||||
|
:class="{ 'x-flip': isSidebarMinimized }"
|
||||||
|
class="va-navbar__item"
|
||||||
|
:color="colors.primary"
|
||||||
|
/>
|
||||||
|
<router-link to="/">
|
||||||
|
<vuestic-logo class="vuestic-logo"/>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</va-navbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useColors } from 'vuestic-ui'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import VuesticLogo from '@/components/vuestic-logo.vue'
|
||||||
|
import VaIconMenuCollapsed from '@/components/icons/VaIconMenuCollapsed.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { VuesticLogo, VaIconMenuCollapsed },
|
||||||
|
setup() {
|
||||||
|
const { getColors } = useColors()
|
||||||
|
const colors = computed(() => getColors() )
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const isSidebarMinimized = computed({
|
||||||
|
get: () => store.state.isSidebarMinimized,
|
||||||
|
set: (value) => store.commit('updateSidebarCollapsedState', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const userName = computed(() => store.state.userName)
|
||||||
|
return {
|
||||||
|
colors,
|
||||||
|
isSidebarMinimized,
|
||||||
|
userName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.va-navbar {
|
||||||
|
box-shadow: var(--va-box-shadow);
|
||||||
|
z-index: 2;
|
||||||
|
&__center {
|
||||||
|
@media screen and (max-width: 1200px) {
|
||||||
|
.app-navbar__github-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 950px) {
|
||||||
|
.app-navbar__text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 950px) {
|
||||||
|
.left {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.app-navbar__actions {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
& > * {
|
||||||
|
margin-right: 1.5rem;
|
||||||
|
}
|
||||||
|
& > *:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-flip {
|
||||||
|
transform: scaleX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-navbar__text > * {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row md12 justify--space-between">
|
||||||
|
<va-input
|
||||||
|
class="flex mb-1"
|
||||||
|
placeholder="Search..."
|
||||||
|
v-model="filterTmp"
|
||||||
|
@keyup="clickFilterEnter"
|
||||||
|
:bordered="false"
|
||||||
|
>
|
||||||
|
<template #prependInner class="row">
|
||||||
|
<va-icon class="material-icons" color="secondary">search</va-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #appendInner class="row">
|
||||||
|
<va-chip
|
||||||
|
v-for="(item, index) in this.filters"
|
||||||
|
class="ml-2 filterTag"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
<va-button icon="clear" size="small" class="ml-1 mr-0" @click="clickFilterClear(index)" />
|
||||||
|
</va-chip>
|
||||||
|
</template>
|
||||||
|
</va-input>
|
||||||
|
|
||||||
|
<div class="flex mb-1">
|
||||||
|
<va-button
|
||||||
|
@click="updateReport"
|
||||||
|
class="update_btn mr-2"
|
||||||
|
flat
|
||||||
|
>
|
||||||
|
<va-icon
|
||||||
|
name="loop"
|
||||||
|
:spin="isLoading"
|
||||||
|
/>
|
||||||
|
</va-button>
|
||||||
|
<va-button
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
icon="save_alt"
|
||||||
|
@click="clickCSV(this.filteredItems, this.columns)"
|
||||||
|
>
|
||||||
|
CSV
|
||||||
|
</va-button>
|
||||||
|
<va-button-dropdown
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
label="Columns"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<va-checkbox class="mb-2" v-for="(item, index) in this.columnsAll" :label="item.label" v-model="this.columnsAll[index].visible" @update:model-value="calcColumns" />
|
||||||
|
</div>
|
||||||
|
</va-button-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<va-data-table
|
||||||
|
:items="items"
|
||||||
|
:columns="columns"
|
||||||
|
:filter="filter"
|
||||||
|
:hoverable="true"
|
||||||
|
@filtered="filteredItems = $event.items"
|
||||||
|
:loading="isLoading"
|
||||||
|
sticky-header
|
||||||
|
class="tableReport"
|
||||||
|
:cell-bind="getCellBind"
|
||||||
|
>
|
||||||
|
<template #cell(platform)="{ value }">
|
||||||
|
{{ value }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(controlled)="{ value,rowData }">
|
||||||
|
<a v-if="rowData.attributes.vcenter_hostname" :href="`https://${ rowData.attributes.vcenter_hostname }/ui`" class="link" target="_blank">{{ rowData.attributes.vcenter_hostname }}</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(environment)="{ value,rowData }">
|
||||||
|
<va-badge right :text="value" :color="rowData.environment.attributes?.color"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(vrf)="{ value,rowData }">
|
||||||
|
<a :href="`${ipamurl}/index.php?page=tools§ion=vrf&subnetId=${rowData.attributes.vrfId}`" class="link" target="_blank">{{ value }}</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(subnet)="{ value,rowData }">
|
||||||
|
<va-badge left color="info" v-if="checkLinkSm(rowData.attributes.requestsID) == true" >
|
||||||
|
<template #text>
|
||||||
|
<a :href="getLinkSm(rowData.attributes.requestsID)" class="linkSM" target="_blank">SM</a>
|
||||||
|
</template>
|
||||||
|
<a :href="`${ipamurl}/index.php?page=subnets§ion=20&subnetId=${rowData.attributes.subnetId}`" class="link" target="_blank">{{ value }}/{{ rowData.attributes.subnetMask }}</a>
|
||||||
|
</va-badge>
|
||||||
|
<span v-else><a :href="`${ipamurl}/index.php?page=subnets§ion=20&subnetId=${rowData.attributes.subnetId}`" class="link" target="_blank">{{ value }}/{{ rowData.attributes.subnetMask }}</a></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(freeIPPercent)="{ value }">
|
||||||
|
{{ value }} %
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</va-data-table>
|
||||||
|
<va-alert class="mt-3" color="info" outline>
|
||||||
|
<div class="row md-12 justify--space-between reportFooter">
|
||||||
|
<span>
|
||||||
|
Number of filtered items:
|
||||||
|
<va-chip>{{ filteredItems.length }}</va-chip>
|
||||||
|
</span>
|
||||||
|
<span class="reportDate mr-3">
|
||||||
|
<va-badge right class="transparentBadge dangerBadge">
|
||||||
|
<template #text v-if="checkReportDate(this.timestamp)">
|
||||||
|
<va-icon name="warning_amber_icon" size="14px" />
|
||||||
|
</template>
|
||||||
|
Data collected: {{ timestamp }}
|
||||||
|
</va-badge>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</va-alert>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import ReportsService from '@/api/report-service';
|
||||||
|
import { sortingBase } from '@/scripts/sorters';
|
||||||
|
import { formatBytes, pickClass, checkLinkSm, getLinkSm,checkReportDate } from '@/scripts/formatters';
|
||||||
|
import { clickCSV } from '@/scripts/exporters';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data () {
|
||||||
|
const columnsAll = [
|
||||||
|
{
|
||||||
|
key: 'attributes.contour_name',
|
||||||
|
label: 'Contour',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.cluster_name',
|
||||||
|
label: 'Cluster',
|
||||||
|
sortable: true,
|
||||||
|
name: 'cluster',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attribute.controlledBy',
|
||||||
|
label: 'Controlled By',
|
||||||
|
sortable: true,
|
||||||
|
name: 'controlled',
|
||||||
|
visible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.platform',
|
||||||
|
label: 'Platform',
|
||||||
|
sortable: true,
|
||||||
|
name: 'platform',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.environment_name',
|
||||||
|
label: 'Environment',
|
||||||
|
tdAlign: 'center',
|
||||||
|
thAlign: 'center',
|
||||||
|
sortable: true,
|
||||||
|
name: 'environment',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vrf',
|
||||||
|
label: 'VRF',
|
||||||
|
name: 'vrf',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.subnet',
|
||||||
|
label: 'Subnet',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'subnet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.bussinessLine',
|
||||||
|
label: 'Purpose',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.subnetManager',
|
||||||
|
label: 'Manager',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.virtSubnetName',
|
||||||
|
label: 'PG Name',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.virtSubnetUUID',
|
||||||
|
label: 'UUID',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.freeIPPercent',
|
||||||
|
label: 'IP Usage',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'freeIPPercent',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
itemsAll: [],
|
||||||
|
columns: [],
|
||||||
|
columnsAll,
|
||||||
|
filterTimeout: 0,
|
||||||
|
filterTmp: '',
|
||||||
|
filter: '',
|
||||||
|
filters: [],
|
||||||
|
isLoading: true,
|
||||||
|
filteredItems: [],
|
||||||
|
timestamp: 0,
|
||||||
|
ipamurl: import.meta.env.VITE_IPAM_URL,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if(localStorage.sharedNetworksVisibleColumns) {
|
||||||
|
let visibleColumns = JSON.parse(localStorage.sharedNetworksVisibleColumns)
|
||||||
|
|
||||||
|
this.columnsAll.forEach( item =>
|
||||||
|
visibleColumns.includes(item.key) ? item.visible = true : item.visible = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
localStorage.sharedNetworksVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
|
||||||
|
this.retreiveReport();
|
||||||
|
|
||||||
|
if (this.$route.params?.filter) {
|
||||||
|
this.filter = this.$route.params.filter
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
checkReportDate,
|
||||||
|
checkLinkSm,
|
||||||
|
getLinkSm,
|
||||||
|
sortingBase,
|
||||||
|
formatBytes,
|
||||||
|
pickClass,
|
||||||
|
clickCSV,
|
||||||
|
|
||||||
|
calcColumns () {
|
||||||
|
localStorage.sharedNetworksVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterEnter (event) {
|
||||||
|
if ((event.keyCode > 111 && event.keyCode < 136) || (event.keyCode > 15 && event.keyCode < 19)) { return }
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
if (this.filterTmp !== '') {
|
||||||
|
this.filters.unshift(this.filterTmp.toLowerCase())
|
||||||
|
this.items = this.filterItems(this.items, this.filters)
|
||||||
|
this.filterTmp = ''
|
||||||
|
this.filter = ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isLoading = true
|
||||||
|
clearTimeout(this.filterTimeout)
|
||||||
|
this.filterTimeout = setTimeout(() => {
|
||||||
|
this.filter = this.filterTmp
|
||||||
|
this.isLoading = false
|
||||||
|
},300)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterClear (index) {
|
||||||
|
this.filters.splice(index,1)
|
||||||
|
this.items = this.filterItems(this.itemsAll, this.filters)
|
||||||
|
},
|
||||||
|
|
||||||
|
filterItems(items, filters) {
|
||||||
|
filters.forEach(filter => {
|
||||||
|
items = items.filter(item => {
|
||||||
|
return Object.values(item.attributes).some( value => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.toLowerCase().includes(filter)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
|
||||||
|
getCellBind (cell, row, column) {
|
||||||
|
const classes = []
|
||||||
|
if (column.key === 'attributes.freeIPPercent') {
|
||||||
|
classes.push(this.pickClass(parseInt(cell)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return { class: classes }
|
||||||
|
},
|
||||||
|
|
||||||
|
retreiveReport() {
|
||||||
|
ReportsService.getVmWareSharedNetworksReport().then(response => {
|
||||||
|
ReportsService.getTimestamp("commonSharedNetworks").then(response => {
|
||||||
|
this.timestamp = response?.attributes?.timestamp.slice(0,response.attributes.timestamp.length-10) ?? 0
|
||||||
|
})
|
||||||
|
|
||||||
|
let clusters = response.data.included.filter(x => x.type === "cluster");
|
||||||
|
let vcenters = response.data.included.filter(x => x.type === "vcenter");
|
||||||
|
let contours = response.data.included.filter(x => x.type === "contour");
|
||||||
|
let environments = response.data.included.filter(x => x.type === "environment");
|
||||||
|
|
||||||
|
response.data.data.forEach(element => {
|
||||||
|
element["cluster"] = clusters.find(x => x.id == element.attributes.cluster_id)
|
||||||
|
element["vcenter"] = vcenters.find(x => x.id == element.cluster?.attributes.vcenter_id)
|
||||||
|
element["contour"] = contours.find(x => x.id == element.vcenter?.attributes.contour_id)
|
||||||
|
element["environment"] = environments.find(x => x.id == element.cluster?.attributes.environment_id) ?? ""
|
||||||
|
|
||||||
|
element.attributes["cluster_name"] = element.cluster?.attributes.name
|
||||||
|
element.attributes["contour_name"] = element.contour?.attributes.name
|
||||||
|
element.attributes["environment_name"] = element?.environment?.attributes?.name ?? ""
|
||||||
|
element.attributes["vcenter_hostname"] = element.vcenter?.attributes.hostname
|
||||||
|
|
||||||
|
element.attributes.freeIPPercent = parseInt(100 - element.attributes.freeIPPercent)
|
||||||
|
|
||||||
|
delete element.attributes.cluster_id
|
||||||
|
delete element.relationships
|
||||||
|
delete element.links
|
||||||
|
});
|
||||||
|
|
||||||
|
this.items = this.filterItems(response.data.data, this.filters);
|
||||||
|
this.itemsAll = response.data.data;
|
||||||
|
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReport() {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
this.retreiveReport()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
421
frontend/src/components/reports/NutanixDecomissionDatatable.vue
Normal file
421
frontend/src/components/reports/NutanixDecomissionDatatable.vue
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row md12 justify--space-between">
|
||||||
|
<va-input
|
||||||
|
class="flex mb-1"
|
||||||
|
placeholder="Search..."
|
||||||
|
v-model="filterTmp"
|
||||||
|
@keyup="clickFilterEnter"
|
||||||
|
:bordered="false"
|
||||||
|
>
|
||||||
|
<template #prependInner class="row">
|
||||||
|
<va-icon class="material-icons" color="secondary">search</va-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #appendInner class="row">
|
||||||
|
<va-chip
|
||||||
|
v-for="(item, index) in this.filters"
|
||||||
|
class="ml-2 filterTag"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
<va-button icon="clear" size="small" class="ml-1 mr-0" @click="clickFilterClear(index)" />
|
||||||
|
</va-chip>
|
||||||
|
</template>
|
||||||
|
</va-input>
|
||||||
|
|
||||||
|
<div class="flex mb-1 align-self--center">
|
||||||
|
<va-button
|
||||||
|
@click="updateReport"
|
||||||
|
class="update_btn mr-2"
|
||||||
|
flat
|
||||||
|
>
|
||||||
|
<va-icon
|
||||||
|
name="loop"
|
||||||
|
:spin="isLoading"
|
||||||
|
/>
|
||||||
|
</va-button>
|
||||||
|
<va-button
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
icon="save_alt"
|
||||||
|
@click="clickCSV(this.filteredItems, this.columns)"
|
||||||
|
>
|
||||||
|
CSV
|
||||||
|
</va-button>
|
||||||
|
<va-button-dropdown
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
label="Columns"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<va-checkbox class="mb-2" v-for="(item, index) in this.columnsAll" :label="item.label" v-model="this.columnsAll[index].visible" @update:model-value="calcColumns" />
|
||||||
|
</div>
|
||||||
|
</va-button-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<va-data-table
|
||||||
|
:items="items"
|
||||||
|
:columns="columns"
|
||||||
|
:filter="filter"
|
||||||
|
:hoverable="true"
|
||||||
|
@filtered="filteredItems = $event.items"
|
||||||
|
:loading="isLoading"
|
||||||
|
sticky-header
|
||||||
|
class="flex md12 tableReport"
|
||||||
|
:cell-bind="getCellBind"
|
||||||
|
>
|
||||||
|
|
||||||
|
<template #cell(pcentral_name)="{ value, rowData }">
|
||||||
|
<a :href="`https://${ rowData.pcentral.attributes.hostname }:9440/`" target="_blank" class="link">{{ value }}</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(pelement_name)="{ value, rowData }">
|
||||||
|
<va-badge left class="transparentBadge infoBadge">
|
||||||
|
<template v-if="rowData.pelement.attributes.description" #text>
|
||||||
|
<va-popover
|
||||||
|
:message="rowData.pelement.attributes.description"
|
||||||
|
placement="left"
|
||||||
|
>
|
||||||
|
<va-icon name="help_outline" size="14px" />
|
||||||
|
</va-popover>
|
||||||
|
</template>
|
||||||
|
<a :href="`https://${ rowData.pelement.attributes.hostname }:9440/`" target="_blank" class="link">{{ value }}</a>
|
||||||
|
</va-badge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(environment)="{ value,rowData }">
|
||||||
|
<va-badge right :text="value" :color="rowData.environment.attributes?.color"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(hosts_total)="{ value,rowData }">
|
||||||
|
|
||||||
|
<va-badge right color="warning" v-if="rowData.pelement.relationships.maintenance.meta?.count > 0">
|
||||||
|
<template #text>
|
||||||
|
<router-link :to="{ name:'nutanix-Maintenance', params: { filters: [`${rowData.attributes.pelement_name.toLowerCase()}`] } }">{{ rowData.pelement.relationships.maintenance.meta.count }}</router-link>
|
||||||
|
</template>
|
||||||
|
<span class="fixedWidth2ch">{{ value }}</span>
|
||||||
|
</va-badge>
|
||||||
|
<span v-else>{{ value }}</span>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(cpu_avg)="{ value }">
|
||||||
|
{{ value }} %
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(cpu_peak)="{ value }">
|
||||||
|
{{ value }} %
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(mem_avg)="{ value }">
|
||||||
|
{{ value }} %
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(mem_peak)="{ value }">
|
||||||
|
{{ value }} %
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(storage)="{ value }">
|
||||||
|
{{ value }} %
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</va-data-table>
|
||||||
|
<va-alert class="mt-3" color="info" outline>
|
||||||
|
<div class="row md-12 justify--space-between reportFooter">
|
||||||
|
<span>
|
||||||
|
Number of filtered items:
|
||||||
|
<va-chip>{{ filteredItems.length }}</va-chip>
|
||||||
|
</span>
|
||||||
|
<span class="reportDate mr-3">
|
||||||
|
<va-badge right class="transparentBadge dangerBadge">
|
||||||
|
<template #text v-if="checkReportDate(this.timestamp)">
|
||||||
|
<va-icon name="warning_amber_icon" size="14px" />
|
||||||
|
</template>
|
||||||
|
Data collected: {{ timestamp }}
|
||||||
|
</va-badge>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</va-alert>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import ReportsService from '@/api/report-service';
|
||||||
|
import { sortingBase } from '@/scripts/sorters';
|
||||||
|
import { pickColor,pickClass,certificateStatus,checkReportDate } from '@/scripts/formatters';
|
||||||
|
import { clickCSV } from '@/scripts/exporters';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data () {
|
||||||
|
const columnsAll = [
|
||||||
|
{
|
||||||
|
key: 'attributes.contour_name',
|
||||||
|
label: 'Contour',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pcentral_name',
|
||||||
|
label: 'Prism Central',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'pcentral_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pelement_name',
|
||||||
|
label: 'Prism Element',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'pelement_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.environment_name',
|
||||||
|
label: 'Environment',
|
||||||
|
tdAlign: 'center',
|
||||||
|
thAlign: 'center',
|
||||||
|
sortable: true,
|
||||||
|
name: 'environment',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hosts_total',
|
||||||
|
label: 'Hosts',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'hosts_total',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hosts_to_decomiss',
|
||||||
|
label: 'Hosts to decomission',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'hosts_to_decomiss',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.cpu_avg',
|
||||||
|
label: 'CPU Average',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'cpu_avg',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.cpu_peak',
|
||||||
|
label: 'CPU Peak',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'cpu_peak',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.mem_avg',
|
||||||
|
label: 'RAM Avg',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'mem_avg',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.mem_peak',
|
||||||
|
label: 'RAM Peak',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'mem_peak',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.storage',
|
||||||
|
label: 'Storage',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'storage',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
itemsAll: [],
|
||||||
|
columns: [],
|
||||||
|
columnsAll,
|
||||||
|
filterTimeout: 0,
|
||||||
|
filterTmp: '',
|
||||||
|
filter: '',
|
||||||
|
filters: [],
|
||||||
|
isLoading: true,
|
||||||
|
filteredItems: [],
|
||||||
|
timestamp: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if(localStorage.nutanixDecomissionVisibleColumns) {
|
||||||
|
let visibleColumns = JSON.parse(localStorage.nutanixDecomissionVisibleColumns)
|
||||||
|
|
||||||
|
this.columnsAll.forEach( item =>
|
||||||
|
visibleColumns.includes(item.key) ? item.visible = true : item.visible = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
localStorage.nutanixDecomissionVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
|
||||||
|
this.retreiveReport();
|
||||||
|
|
||||||
|
if (this.$route.params?.filters) {
|
||||||
|
this.filters = this.$route.params.filters
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
certificateStatus,
|
||||||
|
checkReportDate,
|
||||||
|
sortingBase,
|
||||||
|
pickColor,
|
||||||
|
pickClass,
|
||||||
|
clickCSV,
|
||||||
|
|
||||||
|
calcColumns () {
|
||||||
|
localStorage.nutanixDecomissionVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
},
|
||||||
|
|
||||||
|
getCellBind (cell, row, column) {
|
||||||
|
const classes = []
|
||||||
|
switch (column.key) {
|
||||||
|
case 'attributes.cpu_peak':
|
||||||
|
case 'attributes.cpu_avg':
|
||||||
|
classes.push(this.pickClass(parseInt(cell),-1,75,90))
|
||||||
|
break;
|
||||||
|
case 'attributes.mem_peak':
|
||||||
|
case 'attributes.mem_avg':
|
||||||
|
classes.push(this.pickClass(parseInt(cell),-1,60,80))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return { class: classes }
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterEnter (event) {
|
||||||
|
if ((event.keyCode > 111 && event.keyCode < 136) || (event.keyCode > 15 && event.keyCode < 19)) { return }
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
if (this.filterTmp !== '') {
|
||||||
|
this.filters.unshift(this.filterTmp.toLowerCase())
|
||||||
|
this.items = this.filterItems(this.items, this.filters)
|
||||||
|
this.filterTmp = ''
|
||||||
|
this.filter = ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isLoading = true
|
||||||
|
clearTimeout(this.filterTimeout)
|
||||||
|
this.filterTimeout = setTimeout(() => {
|
||||||
|
this.filter = this.filterTmp
|
||||||
|
this.isLoading = false
|
||||||
|
},300)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterClear (index) {
|
||||||
|
this.filters.splice(index,1)
|
||||||
|
this.items = this.filterItems(this.itemsAll, this.filters)
|
||||||
|
},
|
||||||
|
|
||||||
|
filterItems(items, filters) {
|
||||||
|
filters.forEach(filter => {
|
||||||
|
items = items.filter(item => {
|
||||||
|
return Object.values(item.attributes).some( value => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.toLowerCase().includes(filter)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
|
||||||
|
retreiveReport() {
|
||||||
|
ReportsService.getNutanixDecomissionReport().then(response => {
|
||||||
|
ReportsService.getTimestamp("nutanixDecomission").then(response => {
|
||||||
|
this.timestamp = response?.attributes?.timestamp.slice(0,response.attributes.timestamp.length-10) ?? 0
|
||||||
|
})
|
||||||
|
|
||||||
|
let environments = response.data.included.filter(x => x.type === "environment");
|
||||||
|
let pcentrals = response.data.included.filter(x => x.type === "pcentral");
|
||||||
|
let pelemtnts = response.data.included.filter(x => x.type === "pelement");
|
||||||
|
let contours = response.data.included.filter(x => x.type === "contour");
|
||||||
|
let reports = response.data.included.filter(x => x.type === "npereport");
|
||||||
|
|
||||||
|
response.data.data.forEach(element => {
|
||||||
|
element["pelement"] = pelemtnts.find(x => x.id == element.attributes.pelement_id)
|
||||||
|
element["pcentral"] = pcentrals.find(x => x.id == element.pelement?.attributes.pcentral_id)
|
||||||
|
element["contour"] = contours.find(x => x.id == element.pcentral?.attributes.contour_id)
|
||||||
|
element["environment"] = environments.find(x => x.id == element.pelement?.attributes.environment_id)
|
||||||
|
element["report"] = reports.find(x => x.attributes.pelement_id == element.attributes.pelement_id)
|
||||||
|
|
||||||
|
element.attributes["environment_name"] = element?.environment?.attributes?.name ?? ""
|
||||||
|
element.attributes["pelement_name"] = element.pelement?.attributes.name
|
||||||
|
element.attributes["pcentral_name"] = element.pcentral?.attributes.name
|
||||||
|
element.attributes["contour_name"] = element.contour?.attributes.name
|
||||||
|
element.attributes["hosts_total"] = element?.report.attributes.hosts_total
|
||||||
|
element.attributes["hosts_in_maintenance"] = element?.report.attributes.hosts_in_maintenance
|
||||||
|
|
||||||
|
let dcm_cpu = (((element.pelement.attributes?.cpu_decomiss ?? 80) - element?.attributes.cpu_avg)/100) * element?.attributes.hosts_total | 0
|
||||||
|
let dcm_mem = (((element.pelement.attributes?.cpu_decomiss ?? 80) - element?.attributes.mem_avg)/100) * element?.attributes.hosts_total | 0
|
||||||
|
let dcm_str = (((element.pelement.attributes?.cpu_decomiss ?? 80) - element?.attributes.storage)/100) * element?.attributes.hosts_total | 0
|
||||||
|
|
||||||
|
if (element.attributes.hosts_total - Math.min(dcm_cpu, dcm_mem, dcm_str) > (element.report.attributes.redundancy_factor*2)) {
|
||||||
|
element.attributes["hosts_to_decomiss"] = Math.min(dcm_cpu, dcm_mem, dcm_str)
|
||||||
|
} else {
|
||||||
|
element.attributes["hosts_to_decomiss"] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
delete element.attributes.pelement_id
|
||||||
|
delete element.relationships
|
||||||
|
delete element.contour
|
||||||
|
delete element.links
|
||||||
|
});
|
||||||
|
|
||||||
|
response.data.data = response.data.data.filter(item => item.attributes.environment_name != 'LAB' && item.attributes.environment_name != 'VDI')
|
||||||
|
|
||||||
|
this.items = this.filterItems(response.data.data, this.filters);
|
||||||
|
this.itemsAll = response.data.data;
|
||||||
|
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReport() {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
this.retreiveReport()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.timer {
|
||||||
|
color: var(--va-gray);
|
||||||
|
position:relative;
|
||||||
|
top:2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
369
frontend/src/components/reports/NutanixMaintenanceDatatable.vue
Normal file
369
frontend/src/components/reports/NutanixMaintenanceDatatable.vue
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row md12 justify--space-between">
|
||||||
|
<va-input
|
||||||
|
class="flex mb-1"
|
||||||
|
placeholder="Search..."
|
||||||
|
v-model="filterTmp"
|
||||||
|
@keyup="clickFilterEnter"
|
||||||
|
:bordered="false"
|
||||||
|
>
|
||||||
|
<template #prependInner class="row">
|
||||||
|
<va-icon class="material-icons" color="secondary">search</va-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #appendInner class="row">
|
||||||
|
<va-chip
|
||||||
|
v-for="(item, index) in this.filters"
|
||||||
|
class="ml-2 filterTag"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
<va-button icon="clear" size="small" class="ml-1 mr-0" @click="clickFilterClear(index)" />
|
||||||
|
</va-chip>
|
||||||
|
</template>
|
||||||
|
</va-input>
|
||||||
|
|
||||||
|
<div class="flex mb-1 align-self--center">
|
||||||
|
<va-button
|
||||||
|
@click="updateReport"
|
||||||
|
class="update_btn mr-2"
|
||||||
|
flat
|
||||||
|
>
|
||||||
|
<va-icon
|
||||||
|
name="loop"
|
||||||
|
:spin="isLoading"
|
||||||
|
/>
|
||||||
|
</va-button>
|
||||||
|
<va-button
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
icon="save_alt"
|
||||||
|
@click="clickCSV(this.filteredItems, this.columns)"
|
||||||
|
>
|
||||||
|
CSV
|
||||||
|
</va-button>
|
||||||
|
<va-button-dropdown
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
label="Columns"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<va-checkbox class="mb-2" v-for="(item, index) in this.columnsAll" :label="item.label" v-model="this.columnsAll[index].visible" @update:model-value="calcColumns" />
|
||||||
|
</div>
|
||||||
|
</va-button-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<va-data-table
|
||||||
|
:items="items"
|
||||||
|
:columns="columns"
|
||||||
|
:filter="filter"
|
||||||
|
:hoverable="true"
|
||||||
|
@filtered="filteredItems = $event.items"
|
||||||
|
:loading="isLoading"
|
||||||
|
sticky-header
|
||||||
|
class="flex md12 tableReport"
|
||||||
|
>
|
||||||
|
|
||||||
|
<template #cell(pcentral_name)="{ value, rowData }">
|
||||||
|
<a :href="`https://${ rowData.pcentral.attributes.hostname }:9440/`" target="_blank" class="link">{{ value }}</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(pelement_name)="{ value, rowData }">
|
||||||
|
<va-badge left class="transparentBadge infoBadge">
|
||||||
|
<template v-if="rowData.pelement.attributes.description" #text>
|
||||||
|
<va-popover
|
||||||
|
:message="rowData.pelement.attributes.description"
|
||||||
|
placement="left"
|
||||||
|
>
|
||||||
|
<va-icon name="help_outline" size="14px" />
|
||||||
|
</va-popover>
|
||||||
|
</template>
|
||||||
|
<a :href="`https://${ rowData.pelement.attributes.hostname }:9440/`" target="_blank" class="link">{{ value }}</a>
|
||||||
|
</va-badge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(environment)="{ value,rowData }">
|
||||||
|
<va-badge right :text="value" :color="rowData.environment.attributes?.color"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(name)="{ value,rowData }">
|
||||||
|
<va-badge left class="transparentBadge darkBadge">
|
||||||
|
<template #text>
|
||||||
|
<a :href="`ssh://${rowData.attributes.ip}`" target="_blank" class="link"><va-icon name="terminal" size="14px" color="dark" /></a>
|
||||||
|
</template>
|
||||||
|
<a :href="`https://${ rowData.attributes.ipmi }`" target="_blank" class="link">{{ value }}</a>
|
||||||
|
</va-badge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(hypervisor)="{ value }">
|
||||||
|
<va-badge right :text="value === 'kKvm' ? 'KVM': 'ESXi'" :color="value ==='kKvm' ? 'success' : 'info'"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(reasons)="{ value }">
|
||||||
|
<va-input
|
||||||
|
disabled
|
||||||
|
:v-model="value"
|
||||||
|
:max-length="255"
|
||||||
|
>
|
||||||
|
{{ value }}
|
||||||
|
<template #appendInner>
|
||||||
|
<va-icon
|
||||||
|
name="edit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</va-input>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</va-data-table>
|
||||||
|
<va-alert class="mt-3" color="info" outline>
|
||||||
|
<div class="row md-12 justify--space-between reportFooter">
|
||||||
|
<span>
|
||||||
|
Number of filtered items:
|
||||||
|
<va-chip>{{ filteredItems.length }}</va-chip>
|
||||||
|
</span>
|
||||||
|
<span class="reportDate mr-3">
|
||||||
|
<va-badge right class="transparentBadge dangerBadge">
|
||||||
|
<template #text v-if="checkReportDate(this.timestamp)">
|
||||||
|
<va-icon name="warning_amber_icon" size="14px" />
|
||||||
|
</template>
|
||||||
|
Data collected: {{ timestamp }}
|
||||||
|
</va-badge>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</va-alert>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import ReportsService from '@/api/report-service';
|
||||||
|
import { sortingBase } from '@/scripts/sorters';
|
||||||
|
import { pickColor,pickClass,certificateStatus,checkReportDate } from '@/scripts/formatters';
|
||||||
|
import { clickCSV } from '@/scripts/exporters';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data () {
|
||||||
|
const columnsAll = [
|
||||||
|
{
|
||||||
|
key: 'attributes.contour_name',
|
||||||
|
label: 'Contour',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pcentral_name',
|
||||||
|
label: 'Prism Central',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'pcentral_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pelement_name',
|
||||||
|
label: 'Prism Element',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'pelement_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.environment_name',
|
||||||
|
label: 'Environment',
|
||||||
|
tdAlign: 'center',
|
||||||
|
thAlign: 'center',
|
||||||
|
sortable: true,
|
||||||
|
name: 'environment',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hypervisor_name',
|
||||||
|
label: 'Name',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hypervisor_type',
|
||||||
|
label: 'Hypervisor',
|
||||||
|
tdAlign: 'center',
|
||||||
|
thAlign: 'center',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'hypervisor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hypervisor_state',
|
||||||
|
label: 'Status',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'state',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.serial',
|
||||||
|
label: 'Serial',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'serial',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.reason',
|
||||||
|
label: 'Reason',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'reason',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
itemsAll: [],
|
||||||
|
columns: [],
|
||||||
|
columnsAll,
|
||||||
|
filterTimeout: 0,
|
||||||
|
filterTmp: '',
|
||||||
|
filter: '',
|
||||||
|
filters: [],
|
||||||
|
isLoading: true,
|
||||||
|
filteredItems: [],
|
||||||
|
timestamp: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if(localStorage.nutanixMaintenanceVisibleColumns) {
|
||||||
|
let visibleColumns = JSON.parse(localStorage.nutanixMaintenanceVisibleColumns)
|
||||||
|
|
||||||
|
this.columnsAll.forEach( item =>
|
||||||
|
visibleColumns.includes(item.key) ? item.visible = true : item.visible = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
localStorage.nutanixMaintenanceVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
|
||||||
|
this.retreiveReport();
|
||||||
|
|
||||||
|
if (this.$route.params?.filters) {
|
||||||
|
this.filters = this.$route.params.filters
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
certificateStatus,
|
||||||
|
checkReportDate,
|
||||||
|
sortingBase,
|
||||||
|
pickColor,
|
||||||
|
pickClass,
|
||||||
|
clickCSV,
|
||||||
|
|
||||||
|
calcColumns () {
|
||||||
|
localStorage.nutanixMaintenanceVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterEnter (event) {
|
||||||
|
if ((event.keyCode > 111 && event.keyCode < 136) || (event.keyCode > 15 && event.keyCode < 19)) { return }
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
if (this.filterTmp !== '') {
|
||||||
|
this.filters.unshift(this.filterTmp.toLowerCase())
|
||||||
|
this.items = this.filterItems(this.items, this.filters)
|
||||||
|
this.filterTmp = ''
|
||||||
|
this.filter = ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isLoading = true
|
||||||
|
clearTimeout(this.filterTimeout)
|
||||||
|
this.filterTimeout = setTimeout(() => {
|
||||||
|
this.filter = this.filterTmp
|
||||||
|
this.isLoading = false
|
||||||
|
},300)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterClear (index) {
|
||||||
|
this.filters.splice(index,1)
|
||||||
|
this.items = this.filterItems(this.itemsAll, this.filters)
|
||||||
|
},
|
||||||
|
|
||||||
|
filterItems(items, filters) {
|
||||||
|
filters.forEach(filter => {
|
||||||
|
items = items.filter(item => {
|
||||||
|
return Object.values(item.attributes).some( value => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.toLowerCase().includes(filter)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
|
||||||
|
getCellBind (cell, row, column) {
|
||||||
|
const classes = []
|
||||||
|
|
||||||
|
if (column.key === "attributes.reason") {
|
||||||
|
classes.push('fieldInputCell')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { class: classes }
|
||||||
|
},
|
||||||
|
|
||||||
|
retreiveReport() {
|
||||||
|
ReportsService.getNutanixMaintenanceReport().then(response => {
|
||||||
|
ReportsService.getTimestamp("nutanixMaintenance").then(response => {
|
||||||
|
this.timestamp = response?.attributes?.timestamp.slice(0,response.attributes.timestamp.length-10) ?? 0
|
||||||
|
})
|
||||||
|
|
||||||
|
let environments = response.data.included.filter(x => x.type === "environment");
|
||||||
|
let pcentrals = response.data.included.filter(x => x.type === "pcentral");
|
||||||
|
let pelemtnts = response.data.included.filter(x => x.type === "pelement");
|
||||||
|
let contours = response.data.included.filter(x => x.type === "contour");
|
||||||
|
|
||||||
|
response.data.data.forEach(element => {
|
||||||
|
element["pelement"] = pelemtnts.find(x => x.id == element.attributes.pelement_id)
|
||||||
|
element["pcentral"] = pcentrals.find(x => x.id == element.pelement?.attributes.pcentral_id)
|
||||||
|
element["contour"] = contours.find(x => x.id == element.pcentral?.attributes.contour_id)
|
||||||
|
element["environment"] = environments.find(x => x.id == element.pelement?.attributes.environment_id)
|
||||||
|
|
||||||
|
element.attributes["environment_name"] = element?.environment?.attributes?.name ?? ""
|
||||||
|
element.attributes["pelement_name"] = element.pelement?.attributes.name
|
||||||
|
element.attributes["pcentral_name"] = element.pcentral?.attributes.name
|
||||||
|
element.attributes["contour_name"] = element.contour?.attributes.name
|
||||||
|
|
||||||
|
delete element.attributes.pelement_id
|
||||||
|
delete element.relationships
|
||||||
|
delete element.contour
|
||||||
|
delete element.links
|
||||||
|
});
|
||||||
|
|
||||||
|
this.items = this.filterItems(response.data.data, this.filters);
|
||||||
|
this.itemsAll = response.data.data;
|
||||||
|
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReport() {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
this.retreiveReport()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.timer {
|
||||||
|
color: var(--va-gray);
|
||||||
|
position:relative;
|
||||||
|
top:2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
400
frontend/src/components/reports/NutanixPrismCentralDatatable.vue
Normal file
400
frontend/src/components/reports/NutanixPrismCentralDatatable.vue
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row md12 justify--space-between">
|
||||||
|
<va-input
|
||||||
|
class="flex mb-1"
|
||||||
|
placeholder="Search..."
|
||||||
|
v-model="filter"
|
||||||
|
@keydown="clickFilterEnter"
|
||||||
|
:bordered="false"
|
||||||
|
>
|
||||||
|
<template #prependInner class="row">
|
||||||
|
<va-icon class="material-icons" color="secondary">search</va-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #appendInner class="row">
|
||||||
|
<va-chip
|
||||||
|
v-for="(item, index) in this.filters"
|
||||||
|
class="ml-2 filterTag"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
<va-button icon="clear" size="small" class="ml-1 mr-0" @click="clickFilterClear(index)" />
|
||||||
|
</va-chip>
|
||||||
|
</template>
|
||||||
|
</va-input>
|
||||||
|
|
||||||
|
<div class="flex mb-1">
|
||||||
|
<va-button
|
||||||
|
@click="updateReport"
|
||||||
|
class="update_btn mr-2"
|
||||||
|
flat
|
||||||
|
>
|
||||||
|
<va-icon
|
||||||
|
name="loop"
|
||||||
|
:spin="isLoading"
|
||||||
|
/>
|
||||||
|
</va-button>
|
||||||
|
<va-button
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
icon="save_alt"
|
||||||
|
@click="clickCSV(this.filteredItems, this.columns)"
|
||||||
|
>
|
||||||
|
CSV
|
||||||
|
</va-button>
|
||||||
|
<va-button-dropdown
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
label="Columns"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<va-checkbox class="mb-2" v-for="(item, index) in this.columnsAll" :label="item.label" v-model="this.columnsAll[index].visible" @update:model-value="calcColumns" />
|
||||||
|
</div>
|
||||||
|
</va-button-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<va-data-table
|
||||||
|
:items="items"
|
||||||
|
:columns="columns"
|
||||||
|
:filter="filter"
|
||||||
|
:hoverable="true"
|
||||||
|
@filtered="filteredItems = $event.items"
|
||||||
|
:loading="isLoading"
|
||||||
|
sticky-header
|
||||||
|
class="flex md12 tableReport"
|
||||||
|
:cell-bind="getCellBind"
|
||||||
|
>
|
||||||
|
|
||||||
|
<template #cell(pcentral_name)="{ value, rowData }">
|
||||||
|
<a :href="`https://${ rowData.pcentral.attributes.hostname }:9440/`" target="_blank" class="link">{{ value }}</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(cert_status)="{ value }">
|
||||||
|
<va-badge right :text="value" :color="pickColor(value)"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(alerts)="{ value,rowData }">
|
||||||
|
<span class="nowrap">
|
||||||
|
<b v-if="rowData.attributes.alerts_crit_nack > 0">{{ rowData.attributes.alerts_crit_nack }}/</b>
|
||||||
|
<span v-else>{{ rowData.attributes.alerts_crit_ack }}/</span>
|
||||||
|
<b v-if="rowData.attributes.alerts_warn_nack > 0">{{ rowData.attributes.alerts_warn_nack }}</b>
|
||||||
|
<span v-else>{{ rowData.attributes.alerts_warn_ack }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(aos_version)="{ value,rowData }">
|
||||||
|
|
||||||
|
<va-badge right color="info" v-if="rowData.attributes.aos_status == 1">
|
||||||
|
<template #text>
|
||||||
|
LTS
|
||||||
|
</template>
|
||||||
|
<span>{{ value }}</span>
|
||||||
|
</va-badge>
|
||||||
|
<va-badge right color="secondary" v-else-if="rowData.attributes.aos_status == 2">
|
||||||
|
<template #text>
|
||||||
|
STS
|
||||||
|
</template>
|
||||||
|
<span>{{ value }}</span>
|
||||||
|
</va-badge>
|
||||||
|
<span v-else>{{ value }}</span>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</va-data-table>
|
||||||
|
<va-alert class="mt-3" color="info" outline>
|
||||||
|
<div class="row md-12 justify--space-between reportFooter">
|
||||||
|
<span>
|
||||||
|
Number of filtered items:
|
||||||
|
<va-chip>{{ filteredItems.length }}</va-chip>
|
||||||
|
</span>
|
||||||
|
<span class="reportDate mr-3">
|
||||||
|
<va-badge right class="transparentBadge dangerBadge">
|
||||||
|
<template #text v-if="checkReportDate(this.timestamp)">
|
||||||
|
<va-icon name="warning_amber_icon" size="14px" />
|
||||||
|
</template>
|
||||||
|
Data collected: {{ timestamp }}
|
||||||
|
</va-badge>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</va-alert>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import ReportsService from '@/api/report-service';
|
||||||
|
import { sortingBase } from '@/scripts/sorters';
|
||||||
|
import { pickColor,certificateStatus,checkReportDate } from '@/scripts/formatters';
|
||||||
|
import { clickCSV } from '@/scripts/exporters';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data () {
|
||||||
|
const columnsAll = [
|
||||||
|
{
|
||||||
|
key: 'attributes.contour_name',
|
||||||
|
label: 'Contour',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pcentral_name',
|
||||||
|
label: 'Prism Central',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'pcentral_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.cert_status',
|
||||||
|
label: 'Certificate Status',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'cert_status',
|
||||||
|
tdAlign: 'center',
|
||||||
|
thAlign: 'center',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.clusters',
|
||||||
|
label: 'Clusters',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'clusters',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.alerts',
|
||||||
|
label: 'Alerts',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'alerts',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.alerts_crit_ack',
|
||||||
|
label: 'Critical (Ack)',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'alerts_crit_ack',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.alerts_crit_nack',
|
||||||
|
label: 'Critical',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'alerts_crit_nack',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.alerts_warn_ack',
|
||||||
|
label: 'Warning (Ack)',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'alerts_warn_ack',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.alerts_warn_nack',
|
||||||
|
label: 'Warning',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'alerts_warn_nack',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.aos_status',
|
||||||
|
label: 'AOS Status',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'aos_status',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vms_total',
|
||||||
|
label: 'VMs',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'vms_total',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.projects_total',
|
||||||
|
label: 'Projects',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'projects_total',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pcentral.attributes.aos_version',
|
||||||
|
label: 'AOS Version',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'aos_version',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pcentral.attributes.ncc_version',
|
||||||
|
label: 'NCC Version',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'ncc_version',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
itemsAll: [],
|
||||||
|
columns: [],
|
||||||
|
columnsAll,
|
||||||
|
filter: '',
|
||||||
|
filters: [],
|
||||||
|
isLoading: true,
|
||||||
|
filteredItems: [],
|
||||||
|
timestamp: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if(localStorage.prismCentralVisibleColumns) {
|
||||||
|
let visibleColumns = JSON.parse(localStorage.prismCentralVisibleColumns)
|
||||||
|
|
||||||
|
this.columnsAll.forEach( item =>
|
||||||
|
visibleColumns.includes(item.key) ? item.visible = true : item.visible = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
localStorage.prismCentralVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
|
||||||
|
this.retreiveReport();
|
||||||
|
|
||||||
|
if (this.$route.params?.filter) {
|
||||||
|
this.filter = this.$route.params.filter
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
certificateStatus,
|
||||||
|
checkReportDate,
|
||||||
|
sortingBase,
|
||||||
|
pickColor,
|
||||||
|
clickCSV,
|
||||||
|
|
||||||
|
calcColumns () {
|
||||||
|
localStorage.prismCentralVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
},
|
||||||
|
|
||||||
|
getCellBind (cell, row, column) {
|
||||||
|
const classes = []
|
||||||
|
switch (column.key) {
|
||||||
|
case 'attributes.alerts':
|
||||||
|
if (parseInt(row.attributes.alerts_crit_nack) > 0) {
|
||||||
|
console.log(parseInt(row.attributes.alerts_crit_nack))
|
||||||
|
classes.push('cellCritical')
|
||||||
|
} else if(parseInt(row.attributes.alerts_warn_nack) > 0) {
|
||||||
|
classes.push('cellWarning')
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return { class: classes }
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterEnter (event) {
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
if (this.filter !== '') {
|
||||||
|
this.filters.unshift(this.filter.toLowerCase())
|
||||||
|
this.items = this.filterItems(this.items, this.filters)
|
||||||
|
this.filter = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterClear (index) {
|
||||||
|
this.filters.splice(index,1)
|
||||||
|
this.items = this.filterItems(this.itemsAll, this.filters)
|
||||||
|
},
|
||||||
|
|
||||||
|
filterItems(items, filters) {
|
||||||
|
filters.forEach(filter => {
|
||||||
|
items = items.filter(item => {
|
||||||
|
return Object.values(item.attributes).some( value => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.toLowerCase().includes(filter)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
|
||||||
|
retreiveReport() {
|
||||||
|
ReportsService.getNutanixPrismCentralReport().then(response => {
|
||||||
|
ReportsService.getTimestamp("nutanixPrismCentral").then(response => {
|
||||||
|
this.timestamp = response?.attributes?.timestamp.slice(0,response.attributes.timestamp.length-10) ?? 0
|
||||||
|
})
|
||||||
|
|
||||||
|
let pcentrals = response.data.included.filter(x => x.type === "pcentral");
|
||||||
|
let contours = response.data.included.filter(x => x.type === "contour");
|
||||||
|
|
||||||
|
response.data.data.forEach(element => {
|
||||||
|
element["pcentral"] = pcentrals.find(x => x.id == element.attributes.pcentral_id)
|
||||||
|
element["contour"] = contours.find(x => x.id == element.pcentral?.attributes.contour_id)
|
||||||
|
|
||||||
|
element.attributes["pcentral_name"] = element.pcentral?.attributes.name
|
||||||
|
element.attributes["contour_name"] = element.contour?.attributes.name
|
||||||
|
|
||||||
|
element.attributes.cert_status = this.certificateStatus(element.attributes.cert_status)
|
||||||
|
element.attributes['alerts'] = parseInt(`${element.attributes.alerts_crit_nack}${element.attributes.alerts_warn_nack}${element.attributes.alerts_crit_ack}${element.attributes.alerts_warn_ack}`)
|
||||||
|
|
||||||
|
element.attributes['clusters'] = element.pcentral.relationships.pelement.meta.count
|
||||||
|
|
||||||
|
delete element.attributes.pcentral_id
|
||||||
|
delete element.relationships
|
||||||
|
delete element.contour
|
||||||
|
delete element.links
|
||||||
|
});
|
||||||
|
|
||||||
|
this.items = this.filterItems(response.data.data, this.filters);
|
||||||
|
this.itemsAll = response.data.data;
|
||||||
|
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReport() {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
this.retreiveReport()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
586
frontend/src/components/reports/NutanixPrismElementDatatable.vue
Normal file
586
frontend/src/components/reports/NutanixPrismElementDatatable.vue
Normal file
@@ -0,0 +1,586 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row md12 justify--space-between">
|
||||||
|
<va-input
|
||||||
|
class="flex mb-1"
|
||||||
|
placeholder="Search..."
|
||||||
|
v-model="filterTmp"
|
||||||
|
@keyup="clickFilterEnter"
|
||||||
|
:bordered="false"
|
||||||
|
>
|
||||||
|
<template #prependInner class="row">
|
||||||
|
<va-icon class="material-icons" color="secondary">search</va-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #appendInner class="row">
|
||||||
|
<va-chip
|
||||||
|
v-for="(item, index) in this.filters"
|
||||||
|
class="ml-2 filterTag"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
<va-button icon="clear" size="small" class="ml-1 mr-0" @click="clickFilterClear(index)" />
|
||||||
|
</va-chip>
|
||||||
|
</template>
|
||||||
|
</va-input>
|
||||||
|
|
||||||
|
<div class="flex mb-1 align-self--center">
|
||||||
|
<span class="timer mr-2">{{ timeLeft }}</span>
|
||||||
|
<va-button
|
||||||
|
@click="updateReport"
|
||||||
|
class="update_btn mr-2"
|
||||||
|
flat
|
||||||
|
>
|
||||||
|
<va-icon
|
||||||
|
name="loop"
|
||||||
|
:spin="isLoading"
|
||||||
|
/>
|
||||||
|
</va-button>
|
||||||
|
<va-button
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
icon="save_alt"
|
||||||
|
@click="clickCSV(this.filteredItems, this.columns)"
|
||||||
|
>
|
||||||
|
CSV
|
||||||
|
</va-button>
|
||||||
|
<va-button-dropdown
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
label="Columns"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<va-checkbox class="mb-2" v-for="(item, index) in this.columnsAll" :label="item.label" v-model="this.columnsAll[index].visible" @update:model-value="calcColumns" />
|
||||||
|
</div>
|
||||||
|
</va-button-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<va-data-table
|
||||||
|
:items="items"
|
||||||
|
:columns="columns"
|
||||||
|
:filter="filter"
|
||||||
|
:hoverable="true"
|
||||||
|
@filtered="filteredItems = $event.items"
|
||||||
|
:loading="isLoading"
|
||||||
|
sticky-header
|
||||||
|
class="flex md12 tableReport"
|
||||||
|
:cell-bind="getCellBind"
|
||||||
|
>
|
||||||
|
|
||||||
|
<template #cell(pcentral_name)="{ value, rowData }">
|
||||||
|
<a :href="`https://${ rowData.pcentral.attributes.hostname }:9440/`" target="_blank" class="link">{{ value }}</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(pelement_name)="{ value, rowData }">
|
||||||
|
<va-badge left class="transparentBadge infoBadge">
|
||||||
|
<template v-if="rowData.pelement.attributes.description" #text>
|
||||||
|
<va-popover
|
||||||
|
:message="rowData.pelement.attributes.description"
|
||||||
|
placement="left"
|
||||||
|
>
|
||||||
|
<va-icon name="help_outline" size="14px" />
|
||||||
|
</va-popover>
|
||||||
|
</template>
|
||||||
|
<a :href="`https://${ rowData.pelement.attributes.hostname }:9440/`" target="_blank" class="link">{{ value }}</a>
|
||||||
|
</va-badge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(environment)="{ value,rowData }">
|
||||||
|
<va-badge right :text="value" :color="rowData.environment.attributes?.color"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(cpu_usage)="{ value,rowData }" >
|
||||||
|
{{ rowData.attributes.hypervisor === 'N/A' ? 'N/A' : `${value}%` }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(mem_usage)="{ value,rowData }">
|
||||||
|
{{ rowData.attributes.hypervisor === 'N/A' ? 'N/A' : `${value}%` }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(storage_usage)="{ value,rowData }">
|
||||||
|
{{ rowData.attributes.hypervisor === 'N/A' ? 'N/A' : `${value}%` }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(storage_dedup)="{ value,rowData }">
|
||||||
|
{{ rowData.attributes.hypervisor === 'N/A' ? 'N/A' : `${value}%` }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(cert_status)="{ value,rowData }">
|
||||||
|
<va-badge right :text="value" :color="pickColor(value)" v-if="rowData.attributes.hypervisor !== 'N/A'" />
|
||||||
|
<span v-else>N/A</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(alerts)="{ value,rowData }">
|
||||||
|
<span class="nowrap">
|
||||||
|
<b v-if="rowData.attributes.alerts_crit_nack > 0">{{ rowData.attributes.alerts_crit_nack }}/</b>
|
||||||
|
<span v-else>{{ rowData.attributes.alerts_crit_ack }}/</span>
|
||||||
|
<b v-if="rowData.attributes.alerts_warn_nack > 0">{{ rowData.attributes.alerts_warn_nack }}</b>
|
||||||
|
<span v-else>{{ rowData.attributes.alerts_warn_ack }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(aos_version)="{ value,rowData }">
|
||||||
|
|
||||||
|
<va-badge right color="info" v-if="rowData.attributes.aos_status == 1">
|
||||||
|
<template #text>
|
||||||
|
LTS
|
||||||
|
</template>
|
||||||
|
<span>{{ value }}</span>
|
||||||
|
</va-badge>
|
||||||
|
<va-badge right color="divider" v-else-if="rowData.attributes.aos_status == 2">
|
||||||
|
<template #text>
|
||||||
|
STS
|
||||||
|
</template>
|
||||||
|
<span>{{ value }}</span>
|
||||||
|
</va-badge>
|
||||||
|
<span v-else>{{ value }}</span>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(hosts_total)="{ value,rowData }">
|
||||||
|
|
||||||
|
<va-badge right color="warning" v-if="rowData.pelement.relationships.maintenance.meta.count > 0">
|
||||||
|
<template #text>
|
||||||
|
<router-link :to="{ name:'nutanix-Maintenance', params: { filters: [`${rowData.attributes.pelement_name.toLowerCase()}`] } }">{{ rowData.pelement.relationships.maintenance.meta.count }}</router-link>
|
||||||
|
</template>
|
||||||
|
<span class="fixedWidth2ch">{{ value }}</span>
|
||||||
|
</va-badge>
|
||||||
|
<span v-else>{{ value }}</span>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</va-data-table>
|
||||||
|
<va-alert class="mt-3" color="info" outline>
|
||||||
|
<div class="row md-12 justify--space-between reportFooter">
|
||||||
|
<span>
|
||||||
|
Number of filtered items:
|
||||||
|
<va-chip>{{ filteredItems.length }}</va-chip>
|
||||||
|
</span>
|
||||||
|
<span class="reportDate mr-3">
|
||||||
|
<va-badge right class="transparentBadge dangerBadge">
|
||||||
|
<template #text v-if="checkReportDate(this.timestamp)">
|
||||||
|
<va-icon name="warning_amber_icon" size="14px" />
|
||||||
|
</template>
|
||||||
|
Data collected: {{ timestamp }}
|
||||||
|
</va-badge>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</va-alert>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import ReportsService from '@/api/report-service';
|
||||||
|
import { sortingBase } from '@/scripts/sorters';
|
||||||
|
import { pickColor,pickClass,certificateStatus,checkReportDate } from '@/scripts/formatters';
|
||||||
|
import { clickCSV } from '@/scripts/exporters';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data () {
|
||||||
|
const columnsAll = [
|
||||||
|
{
|
||||||
|
key: 'attributes.contour_name',
|
||||||
|
label: 'Contour',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pcentral_name',
|
||||||
|
label: 'Prism Central',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'pcentral_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pelement_name',
|
||||||
|
label: 'Prism Element',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'pelement_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.environment_name',
|
||||||
|
label: 'Environment',
|
||||||
|
tdAlign: 'center',
|
||||||
|
thAlign: 'center',
|
||||||
|
sortable: true,
|
||||||
|
name: 'environment',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.cert_status',
|
||||||
|
label: 'Certificate Status',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'cert_status',
|
||||||
|
tdAlign: 'center',
|
||||||
|
thAlign: 'center',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.alerts',
|
||||||
|
label: 'Alerts',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'alerts',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.alerts_crit_ack',
|
||||||
|
label: 'Critical (Ack)',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'alerts_crit_ack',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.alerts_crit_nack',
|
||||||
|
label: 'Critical',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'alerts_crit_nack',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.alerts_warn_ack',
|
||||||
|
label: 'Warning (Ack)',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'alerts_warn_ack',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.alerts_warn_nack',
|
||||||
|
label: 'Warning',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'alerts_warn_nack',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hypervisor',
|
||||||
|
label: 'Hypervisor',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'hypervisor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.aos_status',
|
||||||
|
label: 'AOS Status',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'aos_status',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hosts_total',
|
||||||
|
label: 'Hosts',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'hosts_total',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hosts_in_maintenance',
|
||||||
|
label: 'MM',
|
||||||
|
sortable: true,
|
||||||
|
visible: false,
|
||||||
|
name: 'hosts_in_maintenance',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vms_total',
|
||||||
|
label: 'VMs',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'vms_total',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.cpu_usage',
|
||||||
|
label: 'CPU Usage',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'cpu_usage',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.mem_usage',
|
||||||
|
label: 'Memory Usage',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'mem_usage',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.storage_usage',
|
||||||
|
label: 'Storage Usage',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'storage_usage',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.storage_dedup',
|
||||||
|
label: 'Dedup',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'storage_dedup',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.redundancy_factor',
|
||||||
|
label: 'RF',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'redundancy_factor',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hypervisor_version',
|
||||||
|
label: 'Hypervisor Version',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'hypervisor_version',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.aos_version',
|
||||||
|
label: 'AOS Version',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'aos_version',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.ncc_version',
|
||||||
|
label: 'NCC Version',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
name: 'ncc_version',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
itemsAll: [],
|
||||||
|
columns: [],
|
||||||
|
columnsAll,
|
||||||
|
filterTimeout: 0,
|
||||||
|
filterTmp: '',
|
||||||
|
filter: '',
|
||||||
|
filters: [],
|
||||||
|
isLoading: true,
|
||||||
|
filteredItems: [],
|
||||||
|
timer: 300,
|
||||||
|
timestamp: 0,
|
||||||
|
updateIntervalID: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
timeLeft() {
|
||||||
|
return (new Date(this.timer*1000).toISOString().slice(14, -5))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if(localStorage.prismElementVisibleColumns) {
|
||||||
|
let visibleColumns = JSON.parse(localStorage.prismElementVisibleColumns)
|
||||||
|
|
||||||
|
this.columnsAll.forEach( item =>
|
||||||
|
visibleColumns.includes(item.key) ? item.visible = true : item.visible = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
localStorage.prismElementVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
|
||||||
|
this.retreiveReport();
|
||||||
|
|
||||||
|
if (this.$route.params?.filters) {
|
||||||
|
this.filters = this.$route.params.filters
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateIntervalID = setInterval(this.updateReportTimer, 1000)
|
||||||
|
},
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
clearInterval(this.updateIntervalID)
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
certificateStatus,
|
||||||
|
checkReportDate,
|
||||||
|
sortingBase,
|
||||||
|
pickColor,
|
||||||
|
pickClass,
|
||||||
|
clickCSV,
|
||||||
|
|
||||||
|
calcColumns () {
|
||||||
|
localStorage.prismElementVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
},
|
||||||
|
|
||||||
|
getCellBind (cell, row, column) {
|
||||||
|
const classes = []
|
||||||
|
if ( row.attributes.hypervisor === 'N/A' ) {
|
||||||
|
if (column.key !== 'attributes.contour_name' && column.key !== 'attributes.pcentral_name' && column.key !== 'attributes.pelement_name' && column.key !== 'attributes.environment_name' && column.key !== 'attributes.aos_version' && column.key !== 'attributes.ncc_version') {
|
||||||
|
classes.push('cellCritical')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (column.key) {
|
||||||
|
case 'attributes.storage_usage':
|
||||||
|
classes.push(this.pickClass(parseInt(cell),-1,75,90))
|
||||||
|
break;
|
||||||
|
case 'attributes.cpu_usage':
|
||||||
|
case 'attributes.mem_usage':
|
||||||
|
classes.push(this.pickClass(parseInt(cell),-1,60,80))
|
||||||
|
break;
|
||||||
|
case 'attributes.alerts':
|
||||||
|
if (parseInt(row.attributes.alerts_crit_nack) > 0) {
|
||||||
|
classes.push('cellCritical')
|
||||||
|
} else if(parseInt(row.attributes.alerts_warn_nack) > 0) {
|
||||||
|
classes.push('cellWarning')
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { class: classes }
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterEnter (event) {
|
||||||
|
if ((event.keyCode > 111 && event.keyCode < 136) || (event.keyCode > 15 && event.keyCode < 19)) { return }
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
if (this.filterTmp !== '') {
|
||||||
|
this.filters.unshift(this.filterTmp.toLowerCase())
|
||||||
|
this.items = this.filterItems(this.items, this.filters)
|
||||||
|
this.filterTmp = ''
|
||||||
|
this.filter = ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isLoading = true
|
||||||
|
clearTimeout(this.filterTimeout)
|
||||||
|
this.filterTimeout = setTimeout(() => {
|
||||||
|
this.filter = this.filterTmp
|
||||||
|
this.isLoading = false
|
||||||
|
},300)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterClear (index) {
|
||||||
|
this.filters.splice(index,1)
|
||||||
|
this.items = this.filterItems(this.itemsAll, this.filters)
|
||||||
|
},
|
||||||
|
|
||||||
|
filterItems(items, filters) {
|
||||||
|
filters.forEach(filter => {
|
||||||
|
items = items.filter(item => {
|
||||||
|
return Object.values(item.attributes).some( value => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.toLowerCase().includes(filter)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
|
||||||
|
retreiveReport() {
|
||||||
|
ReportsService.getNutanixPrismElementReport().then(response => {
|
||||||
|
ReportsService.getTimestamp("nutanixPrismElement").then(response => {
|
||||||
|
this.timestamp = response?.attributes?.timestamp.slice(0,response.attributes.timestamp.length-10) ?? 0
|
||||||
|
})
|
||||||
|
|
||||||
|
let environments = response.data.included.filter(x => x.type === "environment");
|
||||||
|
let pcentrals = response.data.included.filter(x => x.type === "pcentral");
|
||||||
|
let pelemtnts = response.data.included.filter(x => x.type === "pelement");
|
||||||
|
let contours = response.data.included.filter(x => x.type === "contour");
|
||||||
|
|
||||||
|
response.data.data.forEach(element => {
|
||||||
|
element["pelement"] = pelemtnts.find(x => x.id == element.attributes.pelement_id)
|
||||||
|
element["pcentral"] = pcentrals.find(x => x.id == element.pelement?.attributes.pcentral_id)
|
||||||
|
element["contour"] = contours.find(x => x.id == element.pcentral?.attributes.contour_id)
|
||||||
|
element["environment"] = environments.find(x => x.id == element.pelement?.attributes.environment_id)
|
||||||
|
|
||||||
|
element.attributes["environment_name"] = element?.environment?.attributes?.name ?? ""
|
||||||
|
element.attributes["pelement_name"] = element.pelement?.attributes.name
|
||||||
|
element.attributes["pcentral_name"] = element.pcentral?.attributes.name
|
||||||
|
element.attributes["contour_name"] = element.contour?.attributes.name
|
||||||
|
|
||||||
|
element.attributes["aos_version"] = element.pelement?.attributes.aos_version
|
||||||
|
element.attributes["ncc_version"] = element.pelement?.attributes.ncc_version
|
||||||
|
|
||||||
|
element.attributes.cert_status = this.certificateStatus(element.attributes.cert_status)
|
||||||
|
|
||||||
|
element.attributes['alerts'] = parseInt(`${element.attributes.alerts_crit_nack}${element.attributes.alerts_warn_nack}${element.attributes.alerts_crit_ack}${element.attributes.alerts_warn_ack}`)
|
||||||
|
|
||||||
|
delete element.attributes.pelement_id
|
||||||
|
delete element.relationships
|
||||||
|
delete element.contour
|
||||||
|
delete element.links
|
||||||
|
});
|
||||||
|
|
||||||
|
this.items = this.filterItems(response.data.data, this.filters);
|
||||||
|
this.itemsAll = response.data.data;
|
||||||
|
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReport() {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
this.retreiveReport()
|
||||||
|
|
||||||
|
this.timer = 300
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReportTimer() {
|
||||||
|
if (this.timer > 0) {
|
||||||
|
this.timer--
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
this.updateReport()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.timer {
|
||||||
|
color: var(--va-gray);
|
||||||
|
position:relative;
|
||||||
|
top:2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
587
frontend/src/components/reports/VmwareCapacityDatatable.vue
Normal file
587
frontend/src/components/reports/VmwareCapacityDatatable.vue
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row md12 justify--space-between">
|
||||||
|
<va-input
|
||||||
|
class="flex mb-1"
|
||||||
|
placeholder="Search..."
|
||||||
|
v-model="filterTmp"
|
||||||
|
@keyup="clickFilterEnter"
|
||||||
|
:bordered="false"
|
||||||
|
>
|
||||||
|
<template #prependInner class="row">
|
||||||
|
<va-icon class="material-icons" color="secondary">search</va-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #appendInner class="row">
|
||||||
|
<va-chip
|
||||||
|
v-for="(item, index) in this.filters"
|
||||||
|
class="ml-2 filterTag"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
<va-button icon="clear" size="small" class="ml-1 mr-0" @click="clickFilterClear(index)" />
|
||||||
|
</va-chip>
|
||||||
|
</template>
|
||||||
|
</va-input>
|
||||||
|
|
||||||
|
<div class="flex mb-1">
|
||||||
|
<va-button
|
||||||
|
@click="updateReport"
|
||||||
|
class="update_btn mr-2"
|
||||||
|
flat
|
||||||
|
>
|
||||||
|
<va-icon
|
||||||
|
name="loop"
|
||||||
|
:spin="isLoading"
|
||||||
|
/>
|
||||||
|
</va-button>
|
||||||
|
<va-button
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
icon="save_alt"
|
||||||
|
@click="clickCSV(this.filteredItems, this.columns)"
|
||||||
|
>
|
||||||
|
CSV
|
||||||
|
</va-button>
|
||||||
|
<va-button-dropdown
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
label="Columns"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<va-checkbox class="mb-2" v-for="(item, index) in this.columnsAll" :label="item.label" v-model="this.columnsAll[index].visible" @update:model-value="calcColumns" />
|
||||||
|
</div>
|
||||||
|
</va-button-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<va-data-table
|
||||||
|
:items="items"
|
||||||
|
:columns="columns"
|
||||||
|
:filter="filter"
|
||||||
|
:hoverable="true"
|
||||||
|
@filtered="filteredItems = $event.items"
|
||||||
|
:loading="isLoading"
|
||||||
|
sticky-header
|
||||||
|
class="flex md12 tableReport"
|
||||||
|
:cell-bind="getCellBind"
|
||||||
|
>
|
||||||
|
<template #cell(vcenter_hostname)="{ value }">
|
||||||
|
<a :href="`https://${ value }/ui`" target="_blank" class="link">{{ value }}</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(environment)="{ value,rowData }">
|
||||||
|
<va-badge right :text="value" :color="rowData.environment.attributes?.color"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(hosts)="{ value,rowData }">
|
||||||
|
|
||||||
|
<va-badge right color="warning" v-if="rowData.maintenance.length > 0">
|
||||||
|
<template #text>
|
||||||
|
<router-link :to="{ name:'vmware-mmhosts', params: { filters: [`cluster/${rowData.attributes.cluster_name.toLowerCase()}`] } }">{{ rowData.maintenance.length }}</router-link>
|
||||||
|
</template>
|
||||||
|
<va-badge right class="moveRight5ch" color="success" v-if="rowData.attributes.hosts_to_decomiss > 0">
|
||||||
|
<template #text>
|
||||||
|
<router-link :to="{ name:'vmware-decomission', params: { filters: [`${rowData.attributes.cluster_name.toLowerCase()}`] } }">{{ rowData.attributes.hosts_to_decomiss }}</router-link>
|
||||||
|
</template>
|
||||||
|
</va-badge>
|
||||||
|
<span class="fixedWidth2ch">{{ value }}</span>
|
||||||
|
</va-badge>
|
||||||
|
<va-badge right class="whiteLink" color="success" v-else-if="rowData.maintenance.length == 0 && rowData.attributes.hosts_to_decomiss > 0">
|
||||||
|
<template #text>
|
||||||
|
<router-link :to="{ name:'vmware-decomission', params: { filters: [`${rowData.attributes.cluster_name.toLowerCase()}`] } }">{{ rowData.attributes.hosts_to_decomiss }}</router-link>
|
||||||
|
</template>
|
||||||
|
<span class="fixedWidth2ch">{{ value }}</span>
|
||||||
|
</va-badge>
|
||||||
|
<span v-else>{{ value }}</span>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(hosts_maintenance)="{ value,rowData }">
|
||||||
|
{{ rowData.maintenance.length }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(cpu_cap_use)="{ value }" >
|
||||||
|
{{ value }}%
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(mem_cap_use)="{ value }">
|
||||||
|
{{ value }}%
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(pmemory_total)="{ value }">
|
||||||
|
|
||||||
|
{{ formatBytes(value,2) }}
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(vmemory_provisioned)="{ value }">
|
||||||
|
|
||||||
|
{{ formatBytes(value,2) }}
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(str_cap_use)="{ value }">
|
||||||
|
{{ value }}%
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(str_used)="{ value }">
|
||||||
|
|
||||||
|
{{ formatBytes(value,2) }}
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(str_rdm_used)="{ value }">
|
||||||
|
|
||||||
|
{{ formatBytes(value,2) }}
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</va-data-table>
|
||||||
|
<va-alert class="mt-3" color="info" outline>
|
||||||
|
<div class="row md-12 justify--space-between reportFooter">
|
||||||
|
<span>
|
||||||
|
Number of filtered items:
|
||||||
|
<va-chip>{{ filteredItems.length }}</va-chip>
|
||||||
|
</span>
|
||||||
|
<span class="reportDate mr-3">
|
||||||
|
<va-badge right class="transparentBadge dangerBadge">
|
||||||
|
<template #text v-if="checkReportDate(this.timestamp)">
|
||||||
|
<va-icon name="warning_amber_icon" />
|
||||||
|
</template>
|
||||||
|
Data collected: {{ timestamp }}
|
||||||
|
</va-badge>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</va-alert>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import ReportsService from '@/api/report-service';
|
||||||
|
import { sortingBase } from '@/scripts/sorters';
|
||||||
|
import { formatBytes, pickClass, checkReportDate } from '@/scripts/formatters';
|
||||||
|
import { clickCSV } from '@/scripts/exporters';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data () {
|
||||||
|
const columnsAll = [
|
||||||
|
{
|
||||||
|
key: 'attributes.contour_name',
|
||||||
|
label: 'Contour',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vcenter_hostname',
|
||||||
|
label: 'vCenter',
|
||||||
|
sortable: true,
|
||||||
|
name: 'vcenter_hostname',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.cluster_name',
|
||||||
|
label: 'Cluster',
|
||||||
|
sortable: true,
|
||||||
|
name: 'cluster',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.environment_name',
|
||||||
|
label: 'Environment',
|
||||||
|
tdAlign: 'center',
|
||||||
|
thAlign: 'center',
|
||||||
|
sortable: true,
|
||||||
|
name: 'environment',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hosts_total',
|
||||||
|
label: 'Hosts',
|
||||||
|
sortable: true,
|
||||||
|
name: 'hosts',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'maintenance',
|
||||||
|
label: 'MM',
|
||||||
|
sortable: true,
|
||||||
|
name: 'hosts_maintenance',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hosts_reserved',
|
||||||
|
label: 'Reserved',
|
||||||
|
sortable: true,
|
||||||
|
name: 'hosts_reserved',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vms_total',
|
||||||
|
label: 'VMs',
|
||||||
|
sortable: true,
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.sockets_total',
|
||||||
|
label: 'Sockets',
|
||||||
|
sortable: true,
|
||||||
|
name: 'sockets_total',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.cpu_cap_used',
|
||||||
|
label: 'CPU provisioned',
|
||||||
|
sortable: true,
|
||||||
|
name: 'cpu_cap_use',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pcpu_ovp_target',
|
||||||
|
label: 'CPU Target',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pcpu_ovp_target',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pcpu_ovp_current',
|
||||||
|
label: 'CPU Current',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pcpu_ovp_current',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pcpu_retire_target',
|
||||||
|
label: 'CPU Decomission target',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pcpu_retire_target',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pcpu_total',
|
||||||
|
label: 'pCPU Total',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pcpu_total',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vcpu_total',
|
||||||
|
label: 'vCPU Total',
|
||||||
|
sortable: true,
|
||||||
|
name: 'vcpu_total',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vcpu_provisioned',
|
||||||
|
label: 'vCPU Provisioned',
|
||||||
|
sortable: true,
|
||||||
|
name: 'vcpu_provisioned',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vcpu_free',
|
||||||
|
label: 'vCPU Free',
|
||||||
|
sortable: true,
|
||||||
|
name: 'vcpu_free',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.mem_cap_used',
|
||||||
|
label: 'Memory provisioned',
|
||||||
|
sortable: true,
|
||||||
|
name: 'mem_cap_use',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pmemory_ovp_target',
|
||||||
|
label: 'Memory Target',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pmemory_ovp_target',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pmemory_ovp_current',
|
||||||
|
label: 'Memory Current',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pmemory_ovp_current',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pmemory_total',
|
||||||
|
label: 'pMemory Total',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pmemory_total',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vmemory_provisioned',
|
||||||
|
label: 'vMemory Provisioned',
|
||||||
|
sortable: true,
|
||||||
|
name: 'vmemory_provisioned',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.str_cap_used',
|
||||||
|
label: 'Storage provisioned',
|
||||||
|
sortable: true,
|
||||||
|
name: 'str_cap_use',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pstorage_ovp_target',
|
||||||
|
label: 'Storage Target',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pstorage_ovp_target',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pstorage_ovp_current',
|
||||||
|
label: 'Storage Current',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pstorage_ovp_current',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vstorage_used',
|
||||||
|
label: 'Storage used',
|
||||||
|
sortable: true,
|
||||||
|
name: 'str_used',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.rdm_total',
|
||||||
|
label: 'RDM size',
|
||||||
|
sortable: true,
|
||||||
|
name: 'str_rdm_used',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
itemsAll: [],
|
||||||
|
columns: [],
|
||||||
|
columnsAll,
|
||||||
|
filterTimeout: 0,
|
||||||
|
filterTmp: '',
|
||||||
|
filter: '',
|
||||||
|
filters: [],
|
||||||
|
isLoading: true,
|
||||||
|
filteredItems: [],
|
||||||
|
timestamp: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if(localStorage.capacityVisibleColumns) {
|
||||||
|
let visibleColumns = JSON.parse(localStorage.capacityVisibleColumns)
|
||||||
|
|
||||||
|
this.columnsAll.forEach( item =>
|
||||||
|
visibleColumns.includes(item.key) ? item.visible = true : item.visible = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
localStorage.capacityVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
this.retreiveReport();
|
||||||
|
|
||||||
|
if (this.$route.params?.filter) {
|
||||||
|
this.filter = this.$route.params.filter
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
checkReportDate,
|
||||||
|
sortingBase,
|
||||||
|
formatBytes,
|
||||||
|
pickClass,
|
||||||
|
clickCSV,
|
||||||
|
|
||||||
|
calcColumns () {
|
||||||
|
localStorage.capacityVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
},
|
||||||
|
|
||||||
|
getCellBind (cell, row, column) {
|
||||||
|
const keys = {
|
||||||
|
cpu: [
|
||||||
|
'attributes.cpu_cap_used',
|
||||||
|
'attributes.pcpu_ovp_target',
|
||||||
|
'attributes.pcpu_ovp_current',
|
||||||
|
'attributes.pcpu_retire_target',
|
||||||
|
],
|
||||||
|
mem: [
|
||||||
|
'attributes.mem_cap_used',
|
||||||
|
'attributes.pmemory_ovp_target',
|
||||||
|
'attributes.pmemory_ovp_current',
|
||||||
|
],
|
||||||
|
str: [
|
||||||
|
'attributes.str_cap_used',
|
||||||
|
'attributes.pstorage_ovp_target',
|
||||||
|
'attributes.pstorage_ovp_current',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const classes = []
|
||||||
|
if (keys.cpu.includes(column.key)) {
|
||||||
|
var value = column.key === 'attributes.cpu_cap_used' ? cell : row.attributes.cpu_cap_used
|
||||||
|
if (row.attributes.pcpu_ovp_target == 0 && value < 80) {
|
||||||
|
classes.push(this.pickClass(value=90))
|
||||||
|
} else {
|
||||||
|
classes.push(this.pickClass(value,80,80))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (keys.mem.includes(column.key)) {
|
||||||
|
var value = column.key === 'attributes.mem_cap_used' ? cell : row.attributes.mem_cap_used
|
||||||
|
classes.push(this.pickClass(value))
|
||||||
|
}
|
||||||
|
if (keys.str.includes(column.key)) {
|
||||||
|
var value = column.key === 'attributes.str_cap_used' ? cell : row.attributes.str_cap_used
|
||||||
|
classes.push(this.pickClass(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return { class: classes }
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterEnter (event) {
|
||||||
|
if ((event.keyCode > 111 && event.keyCode < 136) || (event.keyCode > 15 && event.keyCode < 19)) { return }
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
if (this.filterTmp !== '') {
|
||||||
|
this.filters.unshift(this.filterTmp.toLowerCase())
|
||||||
|
this.items = this.filterItems(this.items, this.filters)
|
||||||
|
this.filterTmp = ''
|
||||||
|
this.filter = ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isLoading = true
|
||||||
|
clearTimeout(this.filterTimeout)
|
||||||
|
this.filterTimeout = setTimeout(() => {
|
||||||
|
this.filter = this.filterTmp
|
||||||
|
this.isLoading = false
|
||||||
|
},300)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterClear (index) {
|
||||||
|
this.filters.splice(index,1)
|
||||||
|
this.items = this.filterItems(this.itemsAll, this.filters)
|
||||||
|
},
|
||||||
|
|
||||||
|
filterItems(items, filters) {
|
||||||
|
filters.forEach(filter => {
|
||||||
|
items = items.filter(item => {
|
||||||
|
return Object.values(item.attributes).some( value => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.toLowerCase().includes(filter)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
|
||||||
|
retreiveReport() {
|
||||||
|
ReportsService.getLastFrame().then(response => {
|
||||||
|
this.timestamp = response?.attributes?.timestamp.slice(0,response.attributes.timestamp.length-10) ?? 0
|
||||||
|
|
||||||
|
ReportsService.getLastVmWareCapacityReport(response.id).then(response => {
|
||||||
|
let clusters = response.data.included.filter(x => x.type === "cluster");
|
||||||
|
let vcenters = response.data.included.filter(x => x.type === "vcenter");
|
||||||
|
let contours = response.data.included.filter(x => x.type === "contour");
|
||||||
|
let environments = response.data.included.filter(x => x.type === "environment");
|
||||||
|
let maintenance = response.data.included.filter(x => x.type === "maintenance");
|
||||||
|
|
||||||
|
response.data.data.forEach(element => {
|
||||||
|
element["cluster"] = clusters.find(x => x.id == element.attributes.cluster_id)
|
||||||
|
element["vcenter"] = vcenters.find(x => x.id == element.cluster?.attributes.vcenter_id)
|
||||||
|
element["contour"] = contours.find(x => x.id == element.vcenter?.attributes.contour_id)
|
||||||
|
element["environment"] = environments.find(x => x.id == element.cluster?.attributes.environment_id) ?? ""
|
||||||
|
element["maintenance"] = maintenance.filter(x => x.attributes.cluster_id == element.attributes.cluster_id) ?? 0
|
||||||
|
|
||||||
|
element.attributes["cluster_name"] = element.cluster?.attributes.name
|
||||||
|
element.attributes["contour_name"] = element.contour?.attributes.name
|
||||||
|
element.attributes["environment_name"] = element?.environment?.attributes?.name ?? ""
|
||||||
|
element.attributes["vcenter_hostname"] = element.vcenter?.attributes.hostname
|
||||||
|
|
||||||
|
element.attributes["vcpu_total"] = (element.attributes.pcpu_total*element.attributes.pcpu_ovp_target | 0) ?? element.attributes.pcpu_total
|
||||||
|
|
||||||
|
var vcpu = ((element.attributes.pcpu_total*element.attributes.pcpu_ovp_target | 0) - element.attributes.vcpu_provisioned) ?? 0
|
||||||
|
if (vcpu > 0) {
|
||||||
|
element.attributes["vcpu_free"] = vcpu
|
||||||
|
} else {
|
||||||
|
element.attributes["vcpu_free"] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var pcpu = ((element.attributes.pcpu_total*element.attributes.pcpu_retire_target | 0) - ((element.attributes.vcpu_provisioned/element.attributes.pcpu_ovp_target) | 0)) ?? 0
|
||||||
|
if (pcpu > 0) {
|
||||||
|
element.attributes["pcpu_to_decomiss"] = pcpu
|
||||||
|
} else {
|
||||||
|
element.attributes["pcpu_to_decomiss"] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
element.attributes["hosts_to_decomiss"] = (element.attributes?.pcpu_to_decomiss/(element.attributes.pcpu_host_max) | 0) ?? 0
|
||||||
|
|
||||||
|
delete element.attributes.cluster_id
|
||||||
|
delete element.attributes.frame_id
|
||||||
|
delete element.relationships
|
||||||
|
delete element.links
|
||||||
|
});
|
||||||
|
|
||||||
|
this.items = this.filterItems(response.data.data, this.filters);
|
||||||
|
this.itemsAll = response.data.data;
|
||||||
|
|
||||||
|
this.isLoading = false
|
||||||
|
}).catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
}).catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReport() {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
this.retreiveReport()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
369
frontend/src/components/reports/VmwareDatastoresDatatable.vue
Normal file
369
frontend/src/components/reports/VmwareDatastoresDatatable.vue
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row md12 justify--space-between">
|
||||||
|
<va-input
|
||||||
|
class="flex mb-1"
|
||||||
|
placeholder="Search..."
|
||||||
|
v-model="filterTmp"
|
||||||
|
@keyup="clickFilterEnter"
|
||||||
|
:bordered="false"
|
||||||
|
>
|
||||||
|
<template #prependInner class="row">
|
||||||
|
<va-icon class="material-icons" color="secondary">search</va-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #appendInner class="row">
|
||||||
|
<va-chip
|
||||||
|
v-for="(item, index) in this.filters"
|
||||||
|
class="ml-2 filterTag"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
<va-button icon="clear" size="small" class="ml-1 mr-0" @click="clickFilterClear(index)" />
|
||||||
|
</va-chip>
|
||||||
|
</template>
|
||||||
|
</va-input>
|
||||||
|
|
||||||
|
<div class="flex mb-1">
|
||||||
|
<va-button
|
||||||
|
@click="updateReport"
|
||||||
|
class="update_btn mr-2"
|
||||||
|
flat
|
||||||
|
>
|
||||||
|
<va-icon
|
||||||
|
name="loop"
|
||||||
|
:spin="isLoading"
|
||||||
|
/>
|
||||||
|
</va-button>
|
||||||
|
<va-button
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
icon="save_alt"
|
||||||
|
@click="clickCSV(this.filteredItems, this.columns)"
|
||||||
|
>
|
||||||
|
CSV
|
||||||
|
</va-button>
|
||||||
|
<va-button-dropdown
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
label="Columns"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<va-checkbox class="mb-2" v-for="(item, index) in this.columnsAll" :label="item.label" v-model="this.columnsAll[index].visible" @update:model-value="calcColumns" />
|
||||||
|
</div>
|
||||||
|
</va-button-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<va-data-table
|
||||||
|
:items="items"
|
||||||
|
:columns="columns"
|
||||||
|
:filter="filter"
|
||||||
|
:hoverable="true"
|
||||||
|
@filtered="filteredItems = $event.items"
|
||||||
|
:loading="isLoading"
|
||||||
|
sticky-header
|
||||||
|
class="tableReport"
|
||||||
|
:cell-bind="getCellBind"
|
||||||
|
>
|
||||||
|
<template #cell(vcenter_hostname)="{ value }">
|
||||||
|
<a :href="`https://${ value }/ui`" target="_blank" class="link">{{ value }}</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(environment)="{ value,rowData }">
|
||||||
|
<va-badge right :text="value" :color="rowData.environment.attributes?.color"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(space_free_percent)="{ value }">
|
||||||
|
|
||||||
|
{{ value }} %
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(cap_use)="{ value }">
|
||||||
|
{{ value }}%
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(space_provisioned)="{ value }">
|
||||||
|
|
||||||
|
{{ formatBytes(value,2) }}
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(space_used)="{ value }">
|
||||||
|
|
||||||
|
{{ formatBytes(value,2) }}
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(space_total)="{ value }">
|
||||||
|
|
||||||
|
{{ formatBytes(value,2) }}
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</va-data-table>
|
||||||
|
|
||||||
|
<va-alert class="mt-3" color="info" outline>
|
||||||
|
<div class="row md-12 justify--space-between reportFooter">
|
||||||
|
<span>
|
||||||
|
Number of filtered items:
|
||||||
|
<va-chip>{{ filteredItems.length }}</va-chip>
|
||||||
|
</span>
|
||||||
|
<span class="reportDate mr-3">
|
||||||
|
<va-badge right class="transparentBadge dangerBadge">
|
||||||
|
<template #text v-if="checkReportDate(this.timestamp)">
|
||||||
|
<va-icon name="warning_amber_icon" size="14px" />
|
||||||
|
</template>
|
||||||
|
Data collected: {{ timestamp }}
|
||||||
|
</va-badge>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</va-alert>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import ReportsService from '@/api/report-service';
|
||||||
|
import { sortingBase } from '@/scripts/sorters';
|
||||||
|
import { pickClass, formatBytes, checkReportDate } from '@/scripts/formatters';
|
||||||
|
import { clickCSV } from '@/scripts/exporters';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data () {
|
||||||
|
const columnsAll = [
|
||||||
|
{
|
||||||
|
key: 'attributes.contour_name',
|
||||||
|
label: 'Contour',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vcenter_hostname',
|
||||||
|
label: 'vCenter',
|
||||||
|
sortable: true,
|
||||||
|
name: 'vcenter_hostname',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.cluster_name',
|
||||||
|
label: 'Cluster',
|
||||||
|
sortable: true,
|
||||||
|
name: 'cluster',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.environment_name',
|
||||||
|
label: 'Environment',
|
||||||
|
tdAlign: 'center',
|
||||||
|
thAlign: 'center',
|
||||||
|
sortable: true,
|
||||||
|
name: 'environment',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.name',
|
||||||
|
label: 'Datastore',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.cap_used',
|
||||||
|
label: 'Storage provisioned',
|
||||||
|
sortable: true,
|
||||||
|
name: 'cap_use',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.ovp_current',
|
||||||
|
label: 'Provision Rate',
|
||||||
|
sortable: true,
|
||||||
|
name: 'ovp_current',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.space_free_percent',
|
||||||
|
label: 'Space Usage',
|
||||||
|
sortable: true,
|
||||||
|
name: 'space_free_percent',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.space_provisioned',
|
||||||
|
label: 'Space Provisioned',
|
||||||
|
sortable: true,
|
||||||
|
name: 'space_provisioned',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.space_used',
|
||||||
|
label: 'Space Used',
|
||||||
|
sortable: true,
|
||||||
|
name: 'space_used',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.space_total',
|
||||||
|
label: 'Space Total',
|
||||||
|
sortable: true,
|
||||||
|
name: 'space_total',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
itemsAll: [],
|
||||||
|
columns: [],
|
||||||
|
columnsAll,
|
||||||
|
filterTimeout: 0,
|
||||||
|
filterTmp: '',
|
||||||
|
filter: '',
|
||||||
|
filters: [],
|
||||||
|
isLoading: true,
|
||||||
|
filteredItems: [],
|
||||||
|
timestamp: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if(localStorage.datastoresVisibleColumns) {
|
||||||
|
let visibleColumns = JSON.parse(localStorage.datastoresVisibleColumns)
|
||||||
|
|
||||||
|
this.columnsAll.forEach( item =>
|
||||||
|
visibleColumns.includes(item.key) ? item.visible = true : item.visible = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
localStorage.datastoresVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
|
||||||
|
this.retreiveReport();
|
||||||
|
|
||||||
|
if (this.$route.params?.filter) {
|
||||||
|
this.filter = this.$route.params.filter
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
checkReportDate,
|
||||||
|
sortingBase,
|
||||||
|
formatBytes,
|
||||||
|
pickClass,
|
||||||
|
clickCSV,
|
||||||
|
|
||||||
|
calcColumns () {
|
||||||
|
localStorage.datastoresVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterEnter (event) {
|
||||||
|
if ((event.keyCode > 111 && event.keyCode < 136) || (event.keyCode > 15 && event.keyCode < 19)) { return }
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
if (this.filterTmp !== '') {
|
||||||
|
this.filters.unshift(this.filterTmp.toLowerCase())
|
||||||
|
this.items = this.filterItems(this.items, this.filters)
|
||||||
|
this.filterTmp = ''
|
||||||
|
this.filter = ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isLoading = true
|
||||||
|
clearTimeout(this.filterTimeout)
|
||||||
|
this.filterTimeout = setTimeout(() => {
|
||||||
|
this.filter = this.filterTmp
|
||||||
|
this.isLoading = false
|
||||||
|
},300)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterClear (index) {
|
||||||
|
this.filters.splice(index,1)
|
||||||
|
this.items = this.filterItems(this.itemsAll, this.filters)
|
||||||
|
},
|
||||||
|
|
||||||
|
filterItems(items, filters) {
|
||||||
|
filters.forEach(filter => {
|
||||||
|
items = items.filter(item => {
|
||||||
|
return Object.values(item.attributes).some( value => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.toLowerCase().includes(filter)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
|
||||||
|
getCellBind (cell, row, column) {
|
||||||
|
const classes = []
|
||||||
|
if (column.key === 'attributes.space_free_percent') {
|
||||||
|
classes.push(this.pickClass(cell,80,90))
|
||||||
|
}
|
||||||
|
|
||||||
|
return { class: classes }
|
||||||
|
},
|
||||||
|
|
||||||
|
retreiveReport() {
|
||||||
|
ReportsService.getVmWareDatastoresReport().then(response => {
|
||||||
|
ReportsService.getTimestamp("vmwareDatastores").then(response => {
|
||||||
|
this.timestamp = response?.attributes?.timestamp.slice(0,response.attributes.timestamp.length-10) ?? 0
|
||||||
|
})
|
||||||
|
|
||||||
|
let clusters = response.data.included.filter(x => x.type === "cluster");
|
||||||
|
let vcenters = response.data.included.filter(x => x.type === "vcenter");
|
||||||
|
let contours = response.data.included.filter(x => x.type === "contour");
|
||||||
|
let environments = response.data.included.filter(x => x.type === "environment");
|
||||||
|
|
||||||
|
response.data.data.forEach(element => {
|
||||||
|
element["cluster"] = clusters.find(x => x.id == element.attributes.cluster_id)
|
||||||
|
element["vcenter"] = vcenters.find(x => x.id == element.cluster?.attributes.vcenter_id)
|
||||||
|
element["contour"] = contours.find(x => x.id == element.vcenter?.attributes.contour_id)
|
||||||
|
element["environment"] = environments.find(x => x.id == element.cluster?.attributes.environment_id) ?? ""
|
||||||
|
|
||||||
|
element.attributes["cluster_name"] = element.cluster?.attributes.name
|
||||||
|
element.attributes["contour_name"] = element.contour?.attributes.name
|
||||||
|
element.attributes["environment_name"] = element?.environment?.attributes?.name ?? ""
|
||||||
|
element.attributes["vcenter_hostname"] = element.vcenter?.attributes.hostname
|
||||||
|
|
||||||
|
element.attributes.space_free_percent = parseInt(100 - element.attributes.space_free_percent)
|
||||||
|
|
||||||
|
delete element.attributes.cluster_id
|
||||||
|
delete element.relationships
|
||||||
|
delete element.links
|
||||||
|
});
|
||||||
|
|
||||||
|
this.items = this.filterItems(response.data.data, this.filters);
|
||||||
|
this.itemsAll = response.data.data;
|
||||||
|
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReport() {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
this.retreiveReport()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
442
frontend/src/components/reports/VmwareDecomissionDatatable.vue
Normal file
442
frontend/src/components/reports/VmwareDecomissionDatatable.vue
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row md12 justify--space-between">
|
||||||
|
<va-input
|
||||||
|
class="flex mb-1"
|
||||||
|
placeholder="Search..."
|
||||||
|
v-model="filterTmp"
|
||||||
|
@keyup="clickFilterEnter"
|
||||||
|
:bordered="false"
|
||||||
|
>
|
||||||
|
<template #prependInner class="row">
|
||||||
|
<va-icon class="material-icons" color="secondary">search</va-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #appendInner class="row">
|
||||||
|
<va-chip
|
||||||
|
v-for="(item, index) in this.filters"
|
||||||
|
class="ml-2 filterTag"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
<va-button icon="clear" size="small" class="ml-1 mr-0" @click="clickFilterClear(index)" />
|
||||||
|
</va-chip>
|
||||||
|
</template>
|
||||||
|
</va-input>
|
||||||
|
|
||||||
|
<div class="flex mb-1">
|
||||||
|
<va-button
|
||||||
|
@click="updateReport"
|
||||||
|
class="update_btn mr-2"
|
||||||
|
flat
|
||||||
|
>
|
||||||
|
<va-icon
|
||||||
|
name="loop"
|
||||||
|
:spin="isLoading"
|
||||||
|
/>
|
||||||
|
</va-button>
|
||||||
|
<va-button
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
icon="save_alt"
|
||||||
|
@click="clickCSV(this.filteredItems, this.columns)"
|
||||||
|
>
|
||||||
|
CSV
|
||||||
|
</va-button>
|
||||||
|
<va-button-dropdown
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
label="Columns"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<va-checkbox class="mb-2" v-for="(item, index) in this.columnsAll" :label="item.label" v-model="this.columnsAll[index].visible" @update:model-value="calcColumns" />
|
||||||
|
</div>
|
||||||
|
</va-button-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<va-data-table
|
||||||
|
:items="items"
|
||||||
|
:columns="columns"
|
||||||
|
:filter="filter"
|
||||||
|
:hoverable="true"
|
||||||
|
@filtered="filteredItems = $event.items"
|
||||||
|
:loading="isLoading"
|
||||||
|
sticky-header
|
||||||
|
class="flex md12 tableReport"
|
||||||
|
:row-bind="getRowBind"
|
||||||
|
>
|
||||||
|
<template #cell(vcenter_hostname)="{ value }">
|
||||||
|
<a :href="`https://${ value }/ui`" target="_blank" class="link">{{ value }}</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(environment)="{ value,rowData }">
|
||||||
|
<va-badge right :text="value" :color="rowData.environment.attributes?.color"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(hosts)="{ value,rowData }">
|
||||||
|
|
||||||
|
<va-badge right color="warning" v-if="rowData.maintenance.length > 0">
|
||||||
|
<template #text>
|
||||||
|
<router-link :to="{ name:'vmware-mmhosts', params: { filters: [`cluster/${rowData.attributes.cluster_name.toLowerCase()}`] } }">{{ rowData.maintenance.length }}</router-link>
|
||||||
|
</template>
|
||||||
|
<span class="fixedWidth2ch">{{ value }}</span>
|
||||||
|
</va-badge>
|
||||||
|
<span v-else>{{ value }}</span>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(hosts_maintenance)="{ value,rowData }">
|
||||||
|
{{ rowData.maintenance.length }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(cpu_cap_use)="{ value }" >
|
||||||
|
{{ value }}%
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(pcpu_retire_target)="{ value }" >
|
||||||
|
{{ value*100 }}%
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</va-data-table>
|
||||||
|
<va-alert class="mt-3" color="info" outline>
|
||||||
|
<div class="row md-12 justify--space-between reportFooter">
|
||||||
|
<span>
|
||||||
|
Number of filtered items:
|
||||||
|
<va-chip>{{ filteredItems.length }}</va-chip>
|
||||||
|
</span>
|
||||||
|
<span class="reportDate mr-3">
|
||||||
|
<va-badge right class="transparentBadge dangerBadge">
|
||||||
|
<template #text v-if="checkReportDate(this.timestamp)">
|
||||||
|
<va-icon name="warning_amber_icon" />
|
||||||
|
</template>
|
||||||
|
Data collected: {{ timestamp }}
|
||||||
|
</va-badge>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</va-alert>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import ReportsService from '@/api/report-service';
|
||||||
|
import { sortingBase } from '@/scripts/sorters';
|
||||||
|
import { formatBytes, pickClass, checkReportDate } from '@/scripts/formatters';
|
||||||
|
import { clickCSV } from '@/scripts/exporters';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data () {
|
||||||
|
const columnsAll = [
|
||||||
|
{
|
||||||
|
key: 'attributes.contour_name',
|
||||||
|
label: 'Contour',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vcenter_hostname',
|
||||||
|
label: 'vCenter',
|
||||||
|
sortable: true,
|
||||||
|
name: 'vcenter_hostname',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.cluster_name',
|
||||||
|
label: 'Cluster',
|
||||||
|
sortable: true,
|
||||||
|
name: 'cluster',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.environment_name',
|
||||||
|
label: 'Environment',
|
||||||
|
tdAlign: 'center',
|
||||||
|
thAlign: 'center',
|
||||||
|
sortable: true,
|
||||||
|
name: 'environment',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hosts_total',
|
||||||
|
label: 'Hosts',
|
||||||
|
sortable: true,
|
||||||
|
name: 'hosts',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hosts_to_decomiss',
|
||||||
|
label: 'Hosts to decomission',
|
||||||
|
sortable: true,
|
||||||
|
name: 'hosts_to_decomiss',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pcpu_to_decomiss',
|
||||||
|
label: 'pCPU to decomiss',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pcpu_to_decomiss',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hosts_staged_dcm',
|
||||||
|
label: 'Staged to decomission',
|
||||||
|
sortable: true,
|
||||||
|
name: 'hosts_staged_dcm',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'maintenance',
|
||||||
|
label: 'MM',
|
||||||
|
sortable: true,
|
||||||
|
name: 'hosts_maintenance',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hosts_reserved',
|
||||||
|
label: 'Reserved',
|
||||||
|
sortable: true,
|
||||||
|
name: 'hosts_reserved',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vms_total',
|
||||||
|
label: 'VMs',
|
||||||
|
sortable: true,
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.sockets_total',
|
||||||
|
label: 'Sockets',
|
||||||
|
sortable: true,
|
||||||
|
name: 'sockets_total',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.cpu_cap_used',
|
||||||
|
label: 'CPU provisioned',
|
||||||
|
sortable: true,
|
||||||
|
name: 'cpu_cap_use',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
visible: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pcpu_ovp_target',
|
||||||
|
label: 'CPU Target',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pcpu_ovp_target',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pcpu_ovp_current',
|
||||||
|
label: 'CPU Current',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pcpu_ovp_current',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pcpu_retire_target',
|
||||||
|
label: 'CPU Decomission target',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pcpu_retire_target',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.pcpu_total',
|
||||||
|
label: 'pCPU Total',
|
||||||
|
sortable: true,
|
||||||
|
name: 'pcpu_total',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vcpu_provisioned',
|
||||||
|
label: 'vCPU Provisioned',
|
||||||
|
sortable: true,
|
||||||
|
name: 'vcpu_provisioned',
|
||||||
|
sortingFn: (a,b) => this.sortingBase(a,b),
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
itemsAll: [],
|
||||||
|
columns: [],
|
||||||
|
columnsAll,
|
||||||
|
filterTimeout: 0,
|
||||||
|
filterTmp: '',
|
||||||
|
filter: '',
|
||||||
|
filters: [],
|
||||||
|
isLoading: true,
|
||||||
|
filteredItems: [],
|
||||||
|
timestamp: 0,
|
||||||
|
showBlock: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if(localStorage.decomissionVisibleColumns) {
|
||||||
|
let visibleColumns = JSON.parse(localStorage.decomissionVisibleColumns)
|
||||||
|
|
||||||
|
this.columnsAll.forEach( item =>
|
||||||
|
visibleColumns.includes(item.key) ? item.visible = true : item.visible = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
localStorage.decomissionVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
this.retreiveReport();
|
||||||
|
|
||||||
|
|
||||||
|
if (this.$route.params?.filters) {
|
||||||
|
this.filters = this.$route.params.filters
|
||||||
|
|
||||||
|
this.columnsAll[2].visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
checkReportDate,
|
||||||
|
sortingBase,
|
||||||
|
formatBytes,
|
||||||
|
pickClass,
|
||||||
|
clickCSV,
|
||||||
|
|
||||||
|
calcColumns () {
|
||||||
|
localStorage.decomissionVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
},
|
||||||
|
|
||||||
|
getRowBind (row) {
|
||||||
|
const classes = []
|
||||||
|
if (row.attributes.hosts_staged_dcm >= 16) {
|
||||||
|
classes.push('rowCritical')
|
||||||
|
}
|
||||||
|
return { class: classes }
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterEnter (event) {
|
||||||
|
if ((event.keyCode > 111 && event.keyCode < 136) || (event.keyCode > 15 && event.keyCode < 19)) { return }
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
if (this.filterTmp !== '') {
|
||||||
|
this.filters.unshift(this.filterTmp.toLowerCase())
|
||||||
|
this.items = this.filterItems(this.items, this.filters)
|
||||||
|
this.filterTmp = ''
|
||||||
|
this.filter = ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isLoading = true
|
||||||
|
clearTimeout(this.filterTimeout)
|
||||||
|
this.filterTimeout = setTimeout(() => {
|
||||||
|
this.filter = this.filterTmp
|
||||||
|
this.isLoading = false
|
||||||
|
},300)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterClear (index) {
|
||||||
|
this.filters.splice(index,1)
|
||||||
|
this.items = this.filterItems(this.itemsAll, this.filters)
|
||||||
|
},
|
||||||
|
|
||||||
|
filterItems(items, filters) {
|
||||||
|
filters.forEach(filter => {
|
||||||
|
items = items.filter(item => {
|
||||||
|
return Object.values(item.attributes).some( value => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.toLowerCase().includes(filter)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
|
||||||
|
retreiveReport() {
|
||||||
|
ReportsService.getLastFrame().then(response => {
|
||||||
|
this.timestamp = response?.attributes?.timestamp.slice(0,response.attributes.timestamp.length-10) ?? 0
|
||||||
|
|
||||||
|
ReportsService.getLastVmWareCapacityReport(response.id).then(response => {
|
||||||
|
let clusters = response.data.included.filter(x => x.type === "cluster");
|
||||||
|
let vcenters = response.data.included.filter(x => x.type === "vcenter");
|
||||||
|
let contours = response.data.included.filter(x => x.type === "contour");
|
||||||
|
let environments = response.data.included.filter(x => x.type === "environment");
|
||||||
|
let maintenance = response.data.included.filter(x => x.type === "maintenance");
|
||||||
|
|
||||||
|
response.data.data.forEach(element => {
|
||||||
|
element["cluster"] = clusters.find(x => x.id == element.attributes.cluster_id)
|
||||||
|
element["vcenter"] = vcenters.find(x => x.id == element.cluster?.attributes.vcenter_id)
|
||||||
|
element["contour"] = contours.find(x => x.id == element.vcenter?.attributes.contour_id)
|
||||||
|
element["environment"] = environments.find(x => x.id == element.cluster?.attributes.environment_id) ?? ""
|
||||||
|
element["maintenance"] = maintenance.filter(x => x.attributes.cluster_id == element.attributes.cluster_id) ?? 0
|
||||||
|
|
||||||
|
element.attributes["cluster_name"] = element.cluster?.attributes.name
|
||||||
|
element.attributes["contour_name"] = element.contour?.attributes.name
|
||||||
|
element.attributes["environment_name"] = element?.environment?.attributes?.name ?? ""
|
||||||
|
element.attributes["vcenter_hostname"] = element.vcenter?.attributes.hostname
|
||||||
|
|
||||||
|
var pcpu = ((element.attributes.pcpu_total*element.attributes.pcpu_retire_target | 0) - ((element.attributes.vcpu_provisioned/element.attributes.pcpu_ovp_target) | 0)) ?? 0
|
||||||
|
if (pcpu > 0) {
|
||||||
|
element.attributes["pcpu_to_decomiss"] = pcpu
|
||||||
|
} else {
|
||||||
|
element.attributes["pcpu_to_decomiss"] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
element.attributes["hosts_to_decomiss"] = (element.attributes?.pcpu_to_decomiss/(element.attributes.pcpu_host_max) | 0) ?? 0
|
||||||
|
|
||||||
|
|
||||||
|
delete element.attributes.cluster_id
|
||||||
|
delete element.attributes.frame_id
|
||||||
|
delete element.relationships
|
||||||
|
delete element.links
|
||||||
|
});
|
||||||
|
|
||||||
|
response.data.data = response.data.data.filter(item => item.attributes.contour_name != 'VDI')
|
||||||
|
|
||||||
|
this.items = this.filterItems(response.data.data, this.filters);
|
||||||
|
this.itemsAll = response.data.data;
|
||||||
|
|
||||||
|
this.isLoading = false
|
||||||
|
}).catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
}).catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReport() {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
this.retreiveReport()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
329
frontend/src/components/reports/VmwareMaintenanceDatatable.vue
Normal file
329
frontend/src/components/reports/VmwareMaintenanceDatatable.vue
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row md12 justify--space-between">
|
||||||
|
<va-input
|
||||||
|
class="flex mb-1"
|
||||||
|
placeholder="Search..."
|
||||||
|
v-model="filterTmp"
|
||||||
|
@keyup="clickFilterEnter"
|
||||||
|
:bordered="false"
|
||||||
|
>
|
||||||
|
<template #prependInner class="row">
|
||||||
|
<va-icon class="material-icons" color="secondary">search</va-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #appendInner class="row">
|
||||||
|
<va-chip
|
||||||
|
v-for="(item, index) in this.filters"
|
||||||
|
class="ml-2 filterTag"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
<va-button icon="clear" size="small" class="ml-1 mr-0" @click="clickFilterClear(index)" />
|
||||||
|
</va-chip>
|
||||||
|
</template>
|
||||||
|
</va-input>
|
||||||
|
|
||||||
|
<div class="flex mb-1">
|
||||||
|
<va-button
|
||||||
|
@click="updateReport"
|
||||||
|
class="update_btn mr-2"
|
||||||
|
flat
|
||||||
|
>
|
||||||
|
<va-icon
|
||||||
|
name="loop"
|
||||||
|
:spin="isLoading"
|
||||||
|
/>
|
||||||
|
</va-button>
|
||||||
|
<va-button
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
icon="save_alt"
|
||||||
|
@click="clickCSV(this.filteredItems, this.columns)"
|
||||||
|
>
|
||||||
|
CSV
|
||||||
|
</va-button>
|
||||||
|
<va-button-dropdown
|
||||||
|
class="justify--end mr-2"
|
||||||
|
outline
|
||||||
|
label="Columns"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<va-checkbox class="mb-2" v-for="(item, index) in this.columnsAll" :label="item.label" v-model="this.columnsAll[index].visible" @update:model-value="calcColumns" />
|
||||||
|
</div>
|
||||||
|
</va-button-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<va-data-table
|
||||||
|
:items="items"
|
||||||
|
:columns="columns"
|
||||||
|
:filter="filter"
|
||||||
|
:hoverable="true"
|
||||||
|
@filtered="filteredItems = $event.items"
|
||||||
|
:loading="isLoading"
|
||||||
|
sticky-header
|
||||||
|
class="tableReport"
|
||||||
|
>
|
||||||
|
<template #cell(vcenter_hostname)="{ value }">
|
||||||
|
<a :href="`https://${ value }/ui`" target="_blank" class="link">{{ value }}</a>
|
||||||
|
</template>
|
||||||
|
<template #cell(reason)="{ value }" class="cellReason">
|
||||||
|
<p v-if="value.length > 36">
|
||||||
|
<va-popover :message="value" :close-on-content-click="false">
|
||||||
|
{{ value.slice(0, 33) }}...
|
||||||
|
</va-popover>
|
||||||
|
</p>
|
||||||
|
<p v-else>{{ value }}</p>
|
||||||
|
</template>
|
||||||
|
<template #cell(hostname)="{ value, rowData }">
|
||||||
|
<va-badge left color="info" v-if="checkLinkSm(rowData.attributes.reason) == true" >
|
||||||
|
<template #text>
|
||||||
|
<a :href="getLinkSm(rowData.attributes.reason)" class="linkSM" target="_blank">SM</a>
|
||||||
|
</template>
|
||||||
|
<span>{{ value }}</span>
|
||||||
|
</va-badge>
|
||||||
|
<span v-else>{{ value }}</span>
|
||||||
|
</template>
|
||||||
|
<template #cell(status)="{ value }">
|
||||||
|
<va-badge right :text="value" :color="pickColor(value)"/>
|
||||||
|
</template>
|
||||||
|
<template #cell(date)="{ value }">
|
||||||
|
{{ value.slice(0,16) }}
|
||||||
|
</template>
|
||||||
|
</va-data-table>
|
||||||
|
|
||||||
|
<va-alert class="mt-3" color="info" outline>
|
||||||
|
<div class="row md-12 justify--space-between reportFooter">
|
||||||
|
<span>
|
||||||
|
Number of filtered items:
|
||||||
|
<va-chip>{{ filteredItems.length }}</va-chip>
|
||||||
|
</span>
|
||||||
|
<span class="reportDate mr-3">
|
||||||
|
<va-badge right class="transparentBadge dangerBadge">
|
||||||
|
<template #text v-if="checkReportDate(this.timestamp)">
|
||||||
|
<va-icon name="warning_amber_icon" size="14px" />
|
||||||
|
</template>
|
||||||
|
Data collected: {{ timestamp }}
|
||||||
|
</va-badge>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</va-alert>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import ReportsService from '@/api/report-service';
|
||||||
|
import { getLinkSm, checkLinkSm, pickColor, checkReportDate } from '@/scripts/formatters';
|
||||||
|
import { sortingBase } from '@/scripts/sorters';
|
||||||
|
import { clickCSV } from '@/scripts/exporters';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data () {
|
||||||
|
const columnsAll = [
|
||||||
|
{
|
||||||
|
key: 'attributes.contour_name',
|
||||||
|
label: 'Contour',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.vcenter_hostname',
|
||||||
|
label: 'vCenter',
|
||||||
|
sortable: true,
|
||||||
|
name: 'vcenter_hostname',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.folder',
|
||||||
|
label: 'Folder',
|
||||||
|
sortable: true,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.hostname',
|
||||||
|
label: 'Hostname',
|
||||||
|
sortable: true,
|
||||||
|
name: 'hostname',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.state',
|
||||||
|
label: 'Status',
|
||||||
|
sortable: true,
|
||||||
|
name: 'status',
|
||||||
|
visible: true,
|
||||||
|
tdAlign: 'center',
|
||||||
|
thAlign: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.reason',
|
||||||
|
label: 'Reason',
|
||||||
|
sortable: true,
|
||||||
|
name: 'reason',
|
||||||
|
visible: true,
|
||||||
|
tdClass: "cellReason",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.placedbyFN',
|
||||||
|
label: 'PlacedBy',
|
||||||
|
sortable: true,
|
||||||
|
name: 'placedby',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.placedby',
|
||||||
|
label: 'PlacedBy Login',
|
||||||
|
sortable: true,
|
||||||
|
name: 'placedbySAM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributes.date',
|
||||||
|
label: 'Date',
|
||||||
|
sortable: true,
|
||||||
|
name: 'date',
|
||||||
|
visible: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
itemsAll: [],
|
||||||
|
columns: [],
|
||||||
|
columnsAll,
|
||||||
|
filterTimeout: 0,
|
||||||
|
filterTmp: '',
|
||||||
|
filter: '',
|
||||||
|
filters: [],
|
||||||
|
isLoading: true,
|
||||||
|
filteredItems: [],
|
||||||
|
timestamp: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if(localStorage.maintenanceVisibleColumns) {
|
||||||
|
let visibleColumns = JSON.parse(localStorage.maintenanceVisibleColumns)
|
||||||
|
|
||||||
|
this.columnsAll.forEach( item =>
|
||||||
|
visibleColumns.includes(item.key) ? item.visible = true : item.visible = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
localStorage.maintenanceVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$route.params?.filters) {
|
||||||
|
this.filters = this.$route.params.filters
|
||||||
|
|
||||||
|
this.columnsAll[2].visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.retreiveReport();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
checkReportDate,
|
||||||
|
getLinkSm,
|
||||||
|
checkLinkSm,
|
||||||
|
pickColor,
|
||||||
|
sortingBase,
|
||||||
|
clickCSV,
|
||||||
|
|
||||||
|
calcColumns () {
|
||||||
|
localStorage.maintenanceVisibleColumns = JSON.stringify(
|
||||||
|
this.columnsAll.filter(item => item.visible).map(a => a.key)
|
||||||
|
)
|
||||||
|
this.columns = this.columnsAll.filter(item => item.visible);
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterEnter (event) {
|
||||||
|
if ((event.keyCode > 111 && event.keyCode < 136) || (event.keyCode > 15 && event.keyCode < 19)) { return }
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
if (this.filterTmp !== '') {
|
||||||
|
this.filters.unshift(this.filterTmp.toLowerCase())
|
||||||
|
this.items = this.filterItems(this.items, this.filters)
|
||||||
|
this.filterTmp = ''
|
||||||
|
this.filter = ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isLoading = true
|
||||||
|
clearTimeout(this.filterTimeout)
|
||||||
|
this.filterTimeout = setTimeout(() => {
|
||||||
|
this.filter = this.filterTmp
|
||||||
|
this.isLoading = false
|
||||||
|
},300)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFilterClear (index) {
|
||||||
|
this.filters.splice(index,1)
|
||||||
|
this.items = this.filterItems(this.itemsAll, this.filters)
|
||||||
|
},
|
||||||
|
|
||||||
|
filterItems(items, filters) {
|
||||||
|
filters.forEach(filter => {
|
||||||
|
items = items.filter(item => {
|
||||||
|
return Object.values(item.attributes).some( value => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.toLowerCase().includes(filter)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
|
||||||
|
retreiveReport() {
|
||||||
|
ReportsService.getVmWareMaintenanceReport().then(response => {
|
||||||
|
ReportsService.getTimestamp("vmwareMaintenance").then(response => {
|
||||||
|
this.timestamp = response?.attributes?.timestamp.slice(0,response.attributes.timestamp.length-10) ?? 0
|
||||||
|
})
|
||||||
|
|
||||||
|
let vcenters = response.data.included.filter(x => x.type === "vcenter");
|
||||||
|
let contours = response.data.included.filter(x => x.type === "contour");
|
||||||
|
|
||||||
|
response.data.data.forEach(element => {
|
||||||
|
element["vcenter"] = vcenters.find(x => x.id == element.attributes.vcenter_id)
|
||||||
|
element["contour"] = contours.find(x => x.id == element.vcenter?.attributes.contour_id)
|
||||||
|
|
||||||
|
element.attributes["contour_name"] = element.contour?.attributes.name
|
||||||
|
element.attributes["vcenter_hostname"] = element.vcenter?.attributes.hostname
|
||||||
|
|
||||||
|
delete element.attributes.vcenter_id
|
||||||
|
delete element.attributes.cluster_id
|
||||||
|
delete element.relationships
|
||||||
|
delete element.links
|
||||||
|
});
|
||||||
|
|
||||||
|
this.items = this.filterItems(response.data.data, this.filters);
|
||||||
|
this.itemsAll = response.data.data;
|
||||||
|
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReport() {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
this.retreiveReport()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
75
frontend/src/components/sidebar/NavigationRoutes.js
Normal file
75
frontend/src/components/sidebar/NavigationRoutes.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
export default {
|
||||||
|
root: {
|
||||||
|
name: '/',
|
||||||
|
displayName: 'navigationRoutes.home',
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'dashboard',
|
||||||
|
displayName: 'Dashboard',
|
||||||
|
meta: {
|
||||||
|
icon: 'vuestic-iconset-dashboard',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reports',
|
||||||
|
displayName: 'Reports',
|
||||||
|
meta: {
|
||||||
|
icon: 'vuestic-iconset-tables',
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'vmware-capacity',
|
||||||
|
displayName: 'Capacity',
|
||||||
|
label: 'vmware',
|
||||||
|
color: 'primary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vmware-mmhosts',
|
||||||
|
displayName: 'Maintenance'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vmware-datastores',
|
||||||
|
displayName: 'Datastores'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vmware-decomission',
|
||||||
|
displayName: 'Decomission'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nutanix-prismCentral',
|
||||||
|
displayName: 'Prism Central',
|
||||||
|
label: 'Nutanix',
|
||||||
|
color: 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nutanix-prismElement',
|
||||||
|
displayName: 'Prism Element',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nutanix-Maintenance',
|
||||||
|
displayName: 'Maintenance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nutanix-Decomission',
|
||||||
|
displayName: 'Decomission',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'common-sharedNetworks',
|
||||||
|
displayName: 'Shared Networks',
|
||||||
|
label: 'common',
|
||||||
|
color: 'info',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'settings',
|
||||||
|
displayName: 'Settings',
|
||||||
|
meta: {
|
||||||
|
icon: 'vuestic-iconset-settings',
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
90
frontend/src/components/sidebar/Sidebar.vue
Normal file
90
frontend/src/components/sidebar/Sidebar.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<va-sidebar
|
||||||
|
:width="width"
|
||||||
|
:minimized="minimized"
|
||||||
|
:minimizedWidth="minimizedWidth"
|
||||||
|
>
|
||||||
|
<menu-minimized v-if="minimized" :items="items" />
|
||||||
|
<menu-accordion v-else :items="items" />
|
||||||
|
</va-sidebar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useGlobalConfig } from 'vuestic-ui';
|
||||||
|
import MenuAccordion from './menu/MenuAccordion.vue';
|
||||||
|
import MenuMinimized from './menu/MenuMinimized.vue';
|
||||||
|
import NavigationRoutes from './NavigationRoutes';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "app-sidebar",
|
||||||
|
components: {
|
||||||
|
MenuAccordion,
|
||||||
|
MenuMinimized,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
width: { type: String, default: '16rem' },
|
||||||
|
color: { type: String, default: "secondary" },
|
||||||
|
minimized: { type: Boolean, required: true },
|
||||||
|
minimizedWidth: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
items: NavigationRoutes.routes,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
computedClass() {
|
||||||
|
return {
|
||||||
|
"app-sidebar--minimized": this.minimized
|
||||||
|
};
|
||||||
|
},
|
||||||
|
colors() {
|
||||||
|
return useGlobalConfig().getGlobalConfig().colors
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.va-sidebar {
|
||||||
|
.va-collapse__body {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu {
|
||||||
|
padding: 2rem 0;
|
||||||
|
&__inner {
|
||||||
|
padding-bottom: 8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
&-content {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.va-sidebar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .va-sidebar--minimized {
|
||||||
|
// width: auto !important;
|
||||||
|
// }
|
||||||
|
</style>
|
||||||
79
frontend/src/components/sidebar/menu/MenuAccordion.vue
Normal file
79
frontend/src/components/sidebar/menu/MenuAccordion.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<va-accordion class="sidebar-accordion va-sidebar__menu__inner" v-model="accordionValue" multiply>
|
||||||
|
<va-collapse v-for="(route, idx) in items" :key="idx">
|
||||||
|
<template #header>
|
||||||
|
<va-sidebar-item :active="isRouteActive(route)" :to="route.children ? undefined : { name: route.name }">
|
||||||
|
<va-sidebar-item-content>
|
||||||
|
<va-icon :name="route.meta.icon" class="va-sidebar-item__icon"/>
|
||||||
|
|
||||||
|
<va-sidebar-item-title>
|
||||||
|
{{ route.displayName }}
|
||||||
|
</va-sidebar-item-title>
|
||||||
|
|
||||||
|
<va-icon v-if="route.children" :name="accordionValue[idx] ? 'expand_less' : 'expand_more'" />
|
||||||
|
</va-sidebar-item-content>
|
||||||
|
</va-sidebar-item>
|
||||||
|
</template>
|
||||||
|
<template v-for="(child, index) in route.children" :key="index">
|
||||||
|
<div>
|
||||||
|
<va-list-label :color="child.color" v-if='child.label'>
|
||||||
|
<va-divider>
|
||||||
|
<span style="font-size: 0.650rem; font-weight: 700;">
|
||||||
|
{{ child.label }}
|
||||||
|
</span>
|
||||||
|
</va-divider>
|
||||||
|
</va-list-label>
|
||||||
|
<va-sidebar-item :active="isRouteActive(child)" :to="{ name: child.name }">
|
||||||
|
<va-sidebar-item-content>
|
||||||
|
<div class="va-sidebar-item__icon"/>
|
||||||
|
<va-sidebar-item-title>
|
||||||
|
{{ child.displayName }}
|
||||||
|
</va-sidebar-item-title>
|
||||||
|
</va-sidebar-item-content>
|
||||||
|
</va-sidebar-item>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</va-collapse>
|
||||||
|
</va-accordion>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "AppMenuAccordion",
|
||||||
|
props: {
|
||||||
|
items: { type: Array, default: () => [] }
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
accordionValue: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.accordionValue = this.items.map(i => this.isItemExpanded(i));
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isGroup(item) {
|
||||||
|
return !!item.children;
|
||||||
|
},
|
||||||
|
isRouteActive(item) {
|
||||||
|
return item.name === this.$route.name;
|
||||||
|
},
|
||||||
|
isItemExpanded(item) {
|
||||||
|
if (!item.children) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCurrentItemActive = this.isRouteActive(item);
|
||||||
|
const isChildActive = !!item.children.find(child =>
|
||||||
|
child.children ? this.isItemExpanded(child) : this.isRouteActive(child)
|
||||||
|
);
|
||||||
|
|
||||||
|
return isCurrentItemActive || isChildActive;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
113
frontend/src/components/sidebar/menu/MenuMinimized.vue
Normal file
113
frontend/src/components/sidebar/menu/MenuMinimized.vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<va-dropdown
|
||||||
|
v-for="(route, idx) in items"
|
||||||
|
:key="idx"
|
||||||
|
position="right"
|
||||||
|
fixed
|
||||||
|
:offset="[0, 8]"
|
||||||
|
:preventOverflow="false"
|
||||||
|
v-model="dropdownsValue[idx]"
|
||||||
|
>
|
||||||
|
<template #anchor>
|
||||||
|
<va-sidebar-item :active="isItemChildsActive(route)" :to="route.children ? undefined : { name: route.name }">
|
||||||
|
<va-sidebar-item-content>
|
||||||
|
<va-icon :name="route.meta.icon" class="va-sidebar-item__icon"/>
|
||||||
|
<va-icon v-if="route.children" class="more_icon" :name="dropdownsValue[idx] ? 'chevron_left' : 'chevron_right'"/>
|
||||||
|
</va-sidebar-item-content>
|
||||||
|
</va-sidebar-item>
|
||||||
|
</template>
|
||||||
|
<div class="sidebar-item__children">
|
||||||
|
<template v-for="(child, index) in route.children" :key="index">
|
||||||
|
<div>
|
||||||
|
<va-list-label :color="child.color" v-if='child.label'>
|
||||||
|
{{ (child.label) }}
|
||||||
|
</va-list-label>
|
||||||
|
<va-sidebar-item :active="isRouteActive(child)" :to="{ name: child.name }">
|
||||||
|
<va-sidebar-item-content>
|
||||||
|
<va-sidebar-item-title>
|
||||||
|
{{ child.displayName }}
|
||||||
|
</va-sidebar-item-title>
|
||||||
|
</va-sidebar-item-content>
|
||||||
|
</va-sidebar-item>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</va-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useGlobalConfig } from 'vuestic-ui';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "AppMenuMinimized",
|
||||||
|
props: {
|
||||||
|
items: { type: Array, default: () => [] }
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
dropdownsValue: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
theme() {
|
||||||
|
return useGlobalConfig().getGlobalConfig().colors
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isGroup(item) {
|
||||||
|
return !!item.children;
|
||||||
|
},
|
||||||
|
isRouteActive(item) {
|
||||||
|
return item.name === this.$route.name;
|
||||||
|
},
|
||||||
|
isItemChildsActive(item) {
|
||||||
|
if (!item.children) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCurrentItemActive = this.isRouteActive(item);
|
||||||
|
const isChildActive = !!item.children.find(child =>
|
||||||
|
child.children ? this.isItemChildsActive(child) : this.isRouteActive(child)
|
||||||
|
);
|
||||||
|
|
||||||
|
return isCurrentItemActive || isChildActive;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sidebar-item {
|
||||||
|
position: relative;
|
||||||
|
&__children {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: visible;
|
||||||
|
min-width: 8rem;
|
||||||
|
color: var(--va-gray);
|
||||||
|
background: var(--va-white);
|
||||||
|
box-shadow: var(--va-box-shadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.va-sidebar-item {
|
||||||
|
&__icon {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.more_icon {
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
24
frontend/src/components/vuestic-logo.vue
Normal file
24
frontend/src/components/vuestic-logo.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<img src="../assets/logo.png" :height="height*1.5">
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { useColors } from "vuestic-ui";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "VaIconVuestic",
|
||||||
|
|
||||||
|
props: {
|
||||||
|
height: { type: [Number, String], default: 16 },
|
||||||
|
color: { type: [String], default: "primary" },
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
colorsComputed() {
|
||||||
|
const { getColor, shiftHSLAColor } = useColors();
|
||||||
|
const color = getColor(this.color, "primary");
|
||||||
|
|
||||||
|
return { start: color, end: shiftHSLAColor(color, { l: -20 }) };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
209
frontend/src/components/widgets/VmwareDecomissionWidget.vue
Normal file
209
frontend/src/components/widgets/VmwareDecomissionWidget.vue
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
<template>
|
||||||
|
<va-inner-loading :loading="isLoading" class="container">
|
||||||
|
<div class="chart">
|
||||||
|
<apexchart
|
||||||
|
type="line"
|
||||||
|
height="240"
|
||||||
|
width="640"
|
||||||
|
:options="chartOptions"
|
||||||
|
:series="series"
|
||||||
|
></apexchart>
|
||||||
|
</div>
|
||||||
|
<div class="stats">
|
||||||
|
<p>Hosts total: <span class="va-text-bold mr-2"><b>{{ hosts_total }}</b></span></p>
|
||||||
|
<p>Decomissioned: <span class="va-text-bold mr-2"><b>{{ hosts_decomissed }}</b></span></p>
|
||||||
|
<p>Staged to decomission: <span class="va-text-bold mr-2"><b>{{ hosts_staged_dcm }}</b></span></p>
|
||||||
|
</div>
|
||||||
|
<div class="dateRange">
|
||||||
|
<va-date-input
|
||||||
|
mode="range"
|
||||||
|
v-model="dateRange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="button">
|
||||||
|
<va-button
|
||||||
|
@click=changeRange
|
||||||
|
outline
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</va-button>
|
||||||
|
</div>
|
||||||
|
</va-inner-loading>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import ReportsService from '@/api/report-service';
|
||||||
|
import VueApexCharts from "vue3-apexcharts";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
apexchart: VueApexCharts,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
hosts_total: 0,
|
||||||
|
hosts_staged_dcm: 0,
|
||||||
|
hosts_decomissed: 0,
|
||||||
|
dateRange: {
|
||||||
|
start: new Date((new Date()).valueOf() - 7 * 24 * 60 * 60 * 1000),
|
||||||
|
end: new Date()
|
||||||
|
},
|
||||||
|
chartOptions: {
|
||||||
|
chart: {
|
||||||
|
type: 'line',
|
||||||
|
height: 240,
|
||||||
|
stacked: false,
|
||||||
|
toolbar: {
|
||||||
|
show: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataLabels: {
|
||||||
|
enabled: true,
|
||||||
|
enabledOnSeries: [2],
|
||||||
|
},
|
||||||
|
responsive: [{
|
||||||
|
breakpoint: 480,
|
||||||
|
}],
|
||||||
|
stroke: {
|
||||||
|
curve: "smooth",
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
type: "datetime",
|
||||||
|
categories: [],
|
||||||
|
},
|
||||||
|
yaxis: [{
|
||||||
|
title: {
|
||||||
|
text: "Hosts total",
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
opposite: true,
|
||||||
|
title: {
|
||||||
|
text: "Hosts to decomission"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "Hosts total",
|
||||||
|
type: "column",
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hosts to decomission",
|
||||||
|
type: "column",
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hosts decomissioned",
|
||||||
|
type: "line",
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.updateChart()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateChart() {
|
||||||
|
this.series.forEach(item => {item.data.length = 0})
|
||||||
|
this.chartOptions.xaxis.categories.length = 0
|
||||||
|
this.retreiveData()
|
||||||
|
},
|
||||||
|
changeRange() {
|
||||||
|
//this.dateRange.end = new Date(this.dateRange.end.valueOf() + 1 * 24 * 60 * 60 * 1000)
|
||||||
|
this.updateChart()
|
||||||
|
},
|
||||||
|
retreiveData() {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
ReportsService.getFrames(this.dateRange).then(response => {
|
||||||
|
ReportsService.getMultipleVmWareCapacityReport(response.map(item => item.id)).then(response => {
|
||||||
|
response.forEach(item => {
|
||||||
|
item.data.timestamp = new Date(item.data.included.filter(x => x.type === "frame")[0].attributes.timestamp)
|
||||||
|
})
|
||||||
|
response.sort((a, b) => a.data.timestamp.getTime() - b.data.timestamp.getTime())
|
||||||
|
let previous_hosts = 0
|
||||||
|
|
||||||
|
response.forEach(item => {
|
||||||
|
let clusters = item.data.included.filter(x => x.type === "cluster");
|
||||||
|
let vcenters = item.data.included.filter(x => x.type === "vcenter");
|
||||||
|
let contours = item.data.included.filter(x => x.type === "contour");
|
||||||
|
|
||||||
|
let hosts_total = 0
|
||||||
|
let hosts_staged_dcm = 0
|
||||||
|
let hosts_decomissed = 0
|
||||||
|
|
||||||
|
item.data.data.forEach(report => {
|
||||||
|
report["cluster"] = clusters.find(x => x.id == report.attributes.cluster_id)
|
||||||
|
report["vcenter"] = vcenters.find(x => x.id == report.cluster?.attributes.vcenter_id)
|
||||||
|
report["contour"] = contours.find(x => x.id == report.vcenter?.attributes.contour_id)
|
||||||
|
|
||||||
|
report.attributes["contour_name"] = report.contour?.attributes.name
|
||||||
|
|
||||||
|
if (report.contour?.attributes.name === 'VDI') { return }
|
||||||
|
|
||||||
|
hosts_total += report.attributes.hosts_total
|
||||||
|
hosts_staged_dcm += report.attributes.hosts_staged_dcm
|
||||||
|
})
|
||||||
|
|
||||||
|
if (previous_hosts > 0) {
|
||||||
|
hosts_decomissed = (previous_hosts - hosts_total) ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
previous_hosts = hosts_total
|
||||||
|
|
||||||
|
this.chartOptions.xaxis.categories.push(item.data.timestamp.toString())
|
||||||
|
|
||||||
|
this.series[0].data.push(hosts_total)
|
||||||
|
this.series[1].data.push(hosts_staged_dcm)
|
||||||
|
this.series[2].data.push(hosts_decomissed > 0 ? hosts_decomissed : 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
this.hosts_total = this.series[0].data[this.series[0].data.length-1] ?? 0
|
||||||
|
this.hosts_staged_dcm = this.series[1].data[this.series[1].data.length - 1] ?? 0
|
||||||
|
let sum = 0;
|
||||||
|
this.series[2].data.forEach(a => sum += a)
|
||||||
|
this.hosts_decomissed = sum
|
||||||
|
//this.hosts_decomissed = this.series[0].data[0] - this.series[0].data[this.series[0].data.length-1]
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
}).catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
margin: 10px;
|
||||||
|
grid-template-columns: auto auto auto;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
gap: 6px 6px;
|
||||||
|
grid-template-areas:
|
||||||
|
"chart dateRange"
|
||||||
|
"chart stats"
|
||||||
|
"chart button";
|
||||||
|
}
|
||||||
|
.chart {
|
||||||
|
justify-self: start;
|
||||||
|
align-self: center;
|
||||||
|
grid-area: chart;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
height: 100%;
|
||||||
|
justify-self: center;
|
||||||
|
align-self: center;
|
||||||
|
grid-area: stats;
|
||||||
|
}
|
||||||
|
.dateRange { grid-area: dateRange; justify-self: center;}
|
||||||
|
.button { grid-area: button; justify-self: center; }
|
||||||
|
</style>
|
||||||
588
frontend/src/i18n/en.json
Normal file
588
frontend/src/i18n/en.json
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"agree": "I agree to",
|
||||||
|
"createAccount": "Create account",
|
||||||
|
"createNewAccount": "Create New Account",
|
||||||
|
"email": "Email",
|
||||||
|
"login": "Login",
|
||||||
|
"password": "Password",
|
||||||
|
"recover_password": "Recover password",
|
||||||
|
"sign_up": "Sign Up",
|
||||||
|
"keep_logged_in": "Keep me logged in",
|
||||||
|
"termsOfUse": "Terms of Use.",
|
||||||
|
"reset_password": "Reset password"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"title": "This page’s gone fishing.",
|
||||||
|
"text": "If you feel that it’s not right, please send us a message at ",
|
||||||
|
"back_button": "Back to dashboard"
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"singleIcon": "Single Icon",
|
||||||
|
"twoIcons": "Two Icons",
|
||||||
|
"large": "Large",
|
||||||
|
"numbers": "Numbers",
|
||||||
|
"halves": "Halves",
|
||||||
|
"small": "Small"
|
||||||
|
},
|
||||||
|
"typography": {
|
||||||
|
"primary": "Primary text styles",
|
||||||
|
"secondary": "Secondary text styles"
|
||||||
|
},
|
||||||
|
"colorPickers": {
|
||||||
|
"simple": "Simple",
|
||||||
|
"slider": "Slider",
|
||||||
|
"advanced": "Advanced"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"advanced": "Buttons With Icons",
|
||||||
|
"size": "Button Sizes",
|
||||||
|
"tags": "Button Tags",
|
||||||
|
"button": "Button",
|
||||||
|
"buttonGroups": "Button Groups",
|
||||||
|
"buttonsDropdown": "Buttons with dropdown",
|
||||||
|
"split": "Split",
|
||||||
|
"splitTo": "Split to",
|
||||||
|
"customIcon": "Custom icon",
|
||||||
|
"content": "Content",
|
||||||
|
"buttonToggles": "Button Toggles",
|
||||||
|
"pagination": "Pagination",
|
||||||
|
"a-link": "Open EpicSpinners",
|
||||||
|
"router-link": "Move to Charts",
|
||||||
|
"colors": "Button Colors",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"dropdown": "DROPDOWN",
|
||||||
|
"hover": "HOVER",
|
||||||
|
"types": "Button Types",
|
||||||
|
"pressed": "PRESSED",
|
||||||
|
"default": "Default",
|
||||||
|
"outline": "Outline",
|
||||||
|
"flat": "Flat",
|
||||||
|
"large": "Large",
|
||||||
|
"small": "Small",
|
||||||
|
"normal": "Normal",
|
||||||
|
"success": "Success",
|
||||||
|
"info": "Info",
|
||||||
|
"danger": "Danger",
|
||||||
|
"warning": "Warning",
|
||||||
|
"gray": "Gray",
|
||||||
|
"dark": "Dark"
|
||||||
|
},
|
||||||
|
"charts": {
|
||||||
|
"horizontalBarChart": "Horizontal Bar Chart",
|
||||||
|
"verticalBarChart": "Vertical Bar Chart",
|
||||||
|
"lineChart": "Line Chart",
|
||||||
|
"pieChart": "Pie Chart",
|
||||||
|
"donutChart": "Donut Chart",
|
||||||
|
"bubbleChart": "Bubble Chart"
|
||||||
|
},
|
||||||
|
"collapse": {
|
||||||
|
"basic": "Basic Collapse",
|
||||||
|
"collapseWithBackground": "Collapse with background",
|
||||||
|
"collapseWithCustomHeader": "Collapse with custom header",
|
||||||
|
"firstHeader": "Expand This Block",
|
||||||
|
"secondHeader": "Another Block",
|
||||||
|
"content": {
|
||||||
|
"title": "Great Things",
|
||||||
|
"text": "There is something about parenthood that gives us a sense of history and a deeply rooted desire to send on into the next generation the great things we have discovered about life."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sliders": {
|
||||||
|
"slider": "Sliders",
|
||||||
|
"range": "Ranges",
|
||||||
|
"simple": "simple",
|
||||||
|
"value": "Value",
|
||||||
|
"label": "Label",
|
||||||
|
"labelPlusIcon": "label + icon",
|
||||||
|
"pins": "Pins",
|
||||||
|
"pinsAndValue": "pins & value"
|
||||||
|
},
|
||||||
|
"popovers": {
|
||||||
|
"popover": "Popover",
|
||||||
|
"popoverStyle": "Popover Style",
|
||||||
|
"popoverPlacement": "Popover Placement",
|
||||||
|
"minimalTooltip": "Minimal Tooltip",
|
||||||
|
"anotherOneTooltip": "Another One Tooltip"
|
||||||
|
},
|
||||||
|
"datepickers": {
|
||||||
|
"dateOfBirth": "Date of birth",
|
||||||
|
"daysOfTheWeek": "Days of the week",
|
||||||
|
"setupMeeting": "Setup meeting",
|
||||||
|
"upcomingVacation": "Upcoming vacation",
|
||||||
|
"multipleAndDisabledDates": "Multiple & disabled dates",
|
||||||
|
"inline": "Inline"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"versions": "Versions",
|
||||||
|
"setupRemoteConnections": "Setup Remote Connections",
|
||||||
|
"currentVisitors": "Current Visitors",
|
||||||
|
"charts": {
|
||||||
|
"trendyTrends": "Trendy Trends",
|
||||||
|
"showInMoreDetail": "Show in more detail",
|
||||||
|
"loadingSpeed": "Loading speed",
|
||||||
|
"topContributors": "Top contributors",
|
||||||
|
"showNextFive": "Show next five",
|
||||||
|
"commits": "Commits"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"componentRichTheme": "component-rich theme",
|
||||||
|
"completedPullRequests": "completed pull requests",
|
||||||
|
"users": "users",
|
||||||
|
"points": "points",
|
||||||
|
"units": "units",
|
||||||
|
"exploreGallery": "Explore gallery",
|
||||||
|
"viewLibrary": "View Library",
|
||||||
|
"commits": "commits",
|
||||||
|
"components": "components",
|
||||||
|
"teamMembers": "team members"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"title": "Awesome table",
|
||||||
|
"brief": "Brief",
|
||||||
|
"detailed": "Detailed",
|
||||||
|
"resolve": "Resolve",
|
||||||
|
"resolved": "Resolved"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"overview": {
|
||||||
|
"title": "Overview",
|
||||||
|
"built": "Built with Vue.js framework",
|
||||||
|
"free": "Absolutely free for everyone",
|
||||||
|
"fresh": "Fresh and crisp design",
|
||||||
|
"mobile": "Responsive and optimized for mobile",
|
||||||
|
"components": "Tons of useful components",
|
||||||
|
"nojQuery": "Completely jQuery free"
|
||||||
|
},
|
||||||
|
"billingAddress": {
|
||||||
|
"title": "Billing Address",
|
||||||
|
"personalInfo": "Personal Info",
|
||||||
|
"firstName": "First name & Last Name",
|
||||||
|
"email": "Email",
|
||||||
|
"address": "Address",
|
||||||
|
"companyInfo": "Company Info",
|
||||||
|
"city": "City",
|
||||||
|
"country": "Country",
|
||||||
|
"infiniteConnections": "Infinite connections",
|
||||||
|
"addConnection": "Add Connection"
|
||||||
|
},
|
||||||
|
"bankDetails": {
|
||||||
|
"title": "Bank Details",
|
||||||
|
"detailsFields": "Details Fields",
|
||||||
|
"bankName": "Bank Name",
|
||||||
|
"accountName": "Account Name",
|
||||||
|
"sortCode": "Sort Code",
|
||||||
|
"accountNumber": "Account Number",
|
||||||
|
"notes": "Notes",
|
||||||
|
"sendDetails": "Send Details"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navigationLayout": "navigation layout",
|
||||||
|
"topBarButton": "Top Bar",
|
||||||
|
"sideBarButton": "Side Bar"
|
||||||
|
},
|
||||||
|
"notificationsPage": {
|
||||||
|
"notifications": {
|
||||||
|
"title": "Notifications",
|
||||||
|
"gray": "Processing",
|
||||||
|
"dark": "New Label",
|
||||||
|
"success": "Paid",
|
||||||
|
"successMessage": "You successfully read this important alert message.",
|
||||||
|
"info": "Info",
|
||||||
|
"infoMessage": "This alert needs your attention, but it's not super important.",
|
||||||
|
"warning": "On Hold",
|
||||||
|
"warningMessage": "Better check yourself, you're not looking too good.",
|
||||||
|
"danger": "Danger",
|
||||||
|
"dangerMessage": "Change a few things up and try submitting again."
|
||||||
|
},
|
||||||
|
"popovers": {
|
||||||
|
"title": "Tooltips & Popovers",
|
||||||
|
"popoverTitleLabel": "Popover Title",
|
||||||
|
"popoverTextLabel": "Popover Text",
|
||||||
|
"popoverIconLabel": "Popover Icon (fontawesome)",
|
||||||
|
"showPopover": "Show Popover",
|
||||||
|
"topTooltip": "top",
|
||||||
|
"rightTooltip": "rightside",
|
||||||
|
"leftTooltip": "left",
|
||||||
|
"bottomTooltip": "below"
|
||||||
|
},
|
||||||
|
"toasts": {
|
||||||
|
"title": "Toasts",
|
||||||
|
"textLabel": "Text",
|
||||||
|
"durationLabel": "Duration (milliseconds)",
|
||||||
|
"iconLabel": "Icon (fontawesome)",
|
||||||
|
"fullWidthLabel": "Fullwidth",
|
||||||
|
"launchToast": "Launch toast"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forms": {
|
||||||
|
"controls": {
|
||||||
|
"title": "Checkboxes, Radios, Switches, Toggles",
|
||||||
|
"radioDisabled": "Disabled Radio",
|
||||||
|
"radio": "Radio",
|
||||||
|
"subscribe": "Subscribe to newsletter",
|
||||||
|
"unselected": "Unselected checkbox",
|
||||||
|
"selected": "Selected checkbox",
|
||||||
|
"readonly": "Readonly checkbox",
|
||||||
|
"disabled": "Disabled checkbox",
|
||||||
|
"error": "Checkbox with error",
|
||||||
|
"errorMessage": "Checkbox with error messages"
|
||||||
|
},
|
||||||
|
"dateTimePicker": {
|
||||||
|
"title": "Date time pickers",
|
||||||
|
"basic": "Basic",
|
||||||
|
"time": "Time",
|
||||||
|
"range": "Range",
|
||||||
|
"multiple": "Multiple",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"customFirstDay": "Custom first day",
|
||||||
|
"customDateFormat": "Custom date format"
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"emailValidatedSuccess": "Email (validated with success)",
|
||||||
|
"emailValidated": "Email (validated)",
|
||||||
|
"inputWithIcon": "Input With Icon",
|
||||||
|
"inputWithButton": "Input With Button",
|
||||||
|
"inputWithClearButton": "Input With Clear Button",
|
||||||
|
"inputWithRoundButton": "Input With Round Button",
|
||||||
|
"textInput": "Text Input",
|
||||||
|
"textInputWithDescription": "Text Input (with description)",
|
||||||
|
"textArea": "Text Area",
|
||||||
|
"title": "Inputs",
|
||||||
|
"upload": "UPLOAD"
|
||||||
|
},
|
||||||
|
"mediumEditor": {
|
||||||
|
"title": "Medium Editor"
|
||||||
|
},
|
||||||
|
"selects": {
|
||||||
|
"country": "Country Select",
|
||||||
|
"countryMulti": "Country Multi Select",
|
||||||
|
"multi": "Multi Select",
|
||||||
|
"simple": "Simple Select",
|
||||||
|
"searchable": "Select with search",
|
||||||
|
"searchableMulti": "Multi Select with search",
|
||||||
|
"title": "Selects"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"desktop": "Desktop Grid",
|
||||||
|
"fixed": "Fixed Grid",
|
||||||
|
"offsets": "Offsets",
|
||||||
|
"responsive": "Responsive Grid"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"back": "Back to all icons",
|
||||||
|
"none": "No icons found",
|
||||||
|
"search": "Icon search",
|
||||||
|
"title": "Icons"
|
||||||
|
},
|
||||||
|
"spinners": {
|
||||||
|
"title": "Spinners",
|
||||||
|
"poweredBy": "Powered by"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"brazilian_portuguese": "Português",
|
||||||
|
"english": "English",
|
||||||
|
"spanish": "Spanish",
|
||||||
|
"simplified_chinese": "Simplified Chinese",
|
||||||
|
"persian": "Persian"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"auth": "Auth",
|
||||||
|
"rating": "Rating",
|
||||||
|
"buttons": "Buttons",
|
||||||
|
"charts": "Charts",
|
||||||
|
"colorPickers": "Color Pickers",
|
||||||
|
"collapses": "Collapses",
|
||||||
|
"timelines": "Timelines",
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"formElements": "Form Elements",
|
||||||
|
"forms": "Forms",
|
||||||
|
"mediumEditor": "Medium Editor",
|
||||||
|
"grid": "Grid",
|
||||||
|
"icons": "Icons",
|
||||||
|
"cards": "Cards",
|
||||||
|
"spinners": "Spinners",
|
||||||
|
"login": "Login",
|
||||||
|
"maps": "Maps",
|
||||||
|
"pages": "Pages",
|
||||||
|
"modals": "Modals",
|
||||||
|
"notifications": "Notifications",
|
||||||
|
"progressBars": "Progress Bars",
|
||||||
|
"signUp": "Sign up",
|
||||||
|
"statistics": "Statistics",
|
||||||
|
"lists": "Lists",
|
||||||
|
"tables": "Tables",
|
||||||
|
"markupTables": "Markup Tables",
|
||||||
|
"dataTables": "Data Tables",
|
||||||
|
"chips": "Chips",
|
||||||
|
"tabs": "Tabs",
|
||||||
|
"typography": "Typography",
|
||||||
|
"uiElements": "UI Elements",
|
||||||
|
"treeView": "Tree view",
|
||||||
|
"dateTimePickers": "Date time pickers",
|
||||||
|
"fileUpload": "File Upload",
|
||||||
|
"colors": "Colors",
|
||||||
|
"spacing": "Spacing",
|
||||||
|
"sliders": "Sliders",
|
||||||
|
"popovers": "Popovers",
|
||||||
|
"chat": "Chat",
|
||||||
|
"google-maps": "Google Maps",
|
||||||
|
"yandex-maps": "Yandex Maps",
|
||||||
|
"leaflet-maps": "Leaflet Maps",
|
||||||
|
"bubble-maps": "Bubble Maps",
|
||||||
|
"line-maps": "Line Maps",
|
||||||
|
"login-singup": "Login/Signup",
|
||||||
|
"404-pages": "404 Pages",
|
||||||
|
"faq": "Faq",
|
||||||
|
"reports": "Reports",
|
||||||
|
"vmware-capacity": "Capacity",
|
||||||
|
"vmware-mmhosts": "Hosts in maintenance",
|
||||||
|
"vmware-sharedNetworks": "Shared networks",
|
||||||
|
"nutanix-prismCentral": "Prism Central"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"vmware": "vmware",
|
||||||
|
"nutanix": "Nutanix"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"all": "See all messages",
|
||||||
|
"new": "New messages from {name}",
|
||||||
|
"mark_as_read": "Mark As Read"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"close": "Close",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"large": "Large",
|
||||||
|
"largeTitle": "Large Modal",
|
||||||
|
"medium": "Medium",
|
||||||
|
"mediumTitle": "Medium Modal",
|
||||||
|
"small": "Small",
|
||||||
|
"smallTitle": "Small Modal",
|
||||||
|
"static": "Static",
|
||||||
|
"staticMessage": "This is a static modal, backdrop or escape click will not close it.",
|
||||||
|
"staticTitle": "Static Modal",
|
||||||
|
"title": "Modals",
|
||||||
|
"titlePosition": "Modal Position",
|
||||||
|
"top": "Top",
|
||||||
|
"right": "Right",
|
||||||
|
"bottom": "Bottom",
|
||||||
|
"left": "Left",
|
||||||
|
"message": "There are three species of zebras: the plains zebra, the mountain zebra and the Grévy's zebra. The plains zebra and the mountain zebra belong to the subgenus Hippotigris, but Grévy's\n zebra is the sole species of subgenus\n Dolichohippus. The latter resembles an ass, to which it is closely\n related, while the former two are more\n horse-like. All three belong to the genus Equus, along with other living\n equids."
|
||||||
|
},
|
||||||
|
"dropdown": {
|
||||||
|
"default": "Default",
|
||||||
|
"withArrow": "With arrow",
|
||||||
|
"note": "Note",
|
||||||
|
"noteText": "Dropdown will open in the specified direction if there is enough space on the screen, otherwise the direction will automatically change",
|
||||||
|
"top": "TOP",
|
||||||
|
"right": "RIGHT",
|
||||||
|
"bottom": "BOTTOM",
|
||||||
|
"left": "LEFT"
|
||||||
|
},
|
||||||
|
"fileUpload": {
|
||||||
|
"advancedMediaGallery": "Advanced, Media Gallery",
|
||||||
|
"advancedUploadList": "Advanced, Upload List",
|
||||||
|
"mediaGallery": "Media Gallery",
|
||||||
|
"uploadList": "Upload List",
|
||||||
|
"single": "Single",
|
||||||
|
"dragNdropFiles": "Drag’n’drop files or",
|
||||||
|
"uploadedOn": "Uploaded on",
|
||||||
|
"fileDeleted": "File was successfully deleted",
|
||||||
|
"undo": "Undo",
|
||||||
|
"preview": "Preview",
|
||||||
|
"delete": "Delete",
|
||||||
|
"deleteFile": "Delete file",
|
||||||
|
"uploadFile": "Upload file",
|
||||||
|
"uploadMedia": "Upload media",
|
||||||
|
"addAttachment": "Add attachment",
|
||||||
|
"modalTitle": "File validation",
|
||||||
|
"modalText": "File type is incorrect!"
|
||||||
|
},
|
||||||
|
"chips": {
|
||||||
|
"chips": {
|
||||||
|
"title": "Chips",
|
||||||
|
"primary": "Primary chip",
|
||||||
|
"secondary": "Secondary chip",
|
||||||
|
"success": "Success chip",
|
||||||
|
"info": "Info chip",
|
||||||
|
"danger": "Danger chip",
|
||||||
|
"warning": "Warning chip",
|
||||||
|
"gray": "Gray chip",
|
||||||
|
"dark": "Dark chip"
|
||||||
|
},
|
||||||
|
"badges": {
|
||||||
|
"title": "Badges",
|
||||||
|
"primary": "Primary badge",
|
||||||
|
"secondary": "Secondary badge",
|
||||||
|
"success": "Success badge",
|
||||||
|
"info": "Info badge",
|
||||||
|
"danger": "Danger badge",
|
||||||
|
"warning": "Warning badge",
|
||||||
|
"gray": "Gray badge",
|
||||||
|
"dark": "Dark badge"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"messageUs": "Web development inquiries:",
|
||||||
|
"repository": "GitHub Repo"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"all": "See all notifications",
|
||||||
|
"mark_as_read": "Mark as read",
|
||||||
|
"sentMessage": "sent you a message",
|
||||||
|
"uploadedZip": "uploaded a new Zip file with {type}",
|
||||||
|
"startedTopic": "started a new topic"
|
||||||
|
},
|
||||||
|
"timelines": {
|
||||||
|
"horizontalSimple": "Horizontal Simple",
|
||||||
|
"horizontalCards": "Horizontal Cards",
|
||||||
|
"verticalSimple": "Vertical Simple",
|
||||||
|
"verticalLabel": "Vertical With Label",
|
||||||
|
"verticalCentered": "Vertical Centered",
|
||||||
|
"horizontalActionFirst": "Complete drafts",
|
||||||
|
"horizontalActionSecond": "Push site live",
|
||||||
|
"horizontalActionThird": "Start ICO",
|
||||||
|
"titleFirst": "Make design",
|
||||||
|
"titleSecond": "Develop an app",
|
||||||
|
"titleThird": "Submit an app",
|
||||||
|
"titleDateFirst": "",
|
||||||
|
"titleDateSecond": "May 22 10:00",
|
||||||
|
"titleDateThird": "July 19 17:45",
|
||||||
|
"firstDate": "February 2018",
|
||||||
|
"secondDate": "March 2018",
|
||||||
|
"thirdDate": "April 2018",
|
||||||
|
"contentFirst": "The unique stripes of zebras make them one of the animals most familiar to people. They occur in a variety of habitats, such as grasslands, savannas, woodlands, thorny scrublands.",
|
||||||
|
"contentSecond": "They occur in a variety of habitats, such as grasslands, savannas, woodlands, thorny scrublands.",
|
||||||
|
"contentThird": "However, various anthropogenic factors have had a severe impact on zebra populations"
|
||||||
|
},
|
||||||
|
"progressBars": {
|
||||||
|
"circle": "Circle",
|
||||||
|
"horizontal": "Horizontal",
|
||||||
|
"colors": "Colors"
|
||||||
|
},
|
||||||
|
"lists": {
|
||||||
|
"customers": "Customers",
|
||||||
|
"recentMessages": "Recent Messages",
|
||||||
|
"archieved": "Archieved",
|
||||||
|
"starterKit": "Starter Kit",
|
||||||
|
"notifications": "Notifications",
|
||||||
|
"routerSupport": "Router Support"
|
||||||
|
},
|
||||||
|
"tables": {
|
||||||
|
"basic": "Basic Table",
|
||||||
|
"stripedHoverable": "Striped, Hoverable",
|
||||||
|
"labelsActions": "Labels, Actions as Buttons",
|
||||||
|
"sortingPaginationActionsAsIcons": "Sorting, Pagination, Actions as Icons",
|
||||||
|
"star": "Star",
|
||||||
|
"unstar": "Unstar",
|
||||||
|
"edit": "Edit",
|
||||||
|
"delete": "Delete",
|
||||||
|
"searchByName": "Search by name",
|
||||||
|
"searchTrendsBadges": "Search, Trends, Badges",
|
||||||
|
"perPage": "Per Page",
|
||||||
|
"report": "Report",
|
||||||
|
"infiniteScroll": "Infinite Scroll",
|
||||||
|
"selectable": "Selectable",
|
||||||
|
"selected": "Selected",
|
||||||
|
"serverSidePagination": "Server-Side Pagination",
|
||||||
|
"emptyTable": "Empty Table",
|
||||||
|
"noDataAvailable": "No Data Available.",
|
||||||
|
"noReport": "There is no data to display. Report will be available on November 3, 2018.",
|
||||||
|
"loading": "Loading",
|
||||||
|
"headings": {
|
||||||
|
"email": "Email",
|
||||||
|
"name": "Name",
|
||||||
|
"firstName": "First Name",
|
||||||
|
"lastName": "Last Name",
|
||||||
|
"status": "Status",
|
||||||
|
"country": "Country",
|
||||||
|
"location": "Location"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"language": "Change Language",
|
||||||
|
"logout": "Logout",
|
||||||
|
"profile": "My Profile"
|
||||||
|
},
|
||||||
|
"treeView": {
|
||||||
|
"basic": "Basic",
|
||||||
|
"icons": "Icons",
|
||||||
|
"selectable": "Selectable",
|
||||||
|
"editable": "Editable",
|
||||||
|
"advanced": "Advanced"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "Chat",
|
||||||
|
"sendButton": "Send"
|
||||||
|
},
|
||||||
|
"spacingPlayground": {
|
||||||
|
"value": "Value",
|
||||||
|
"margin": "Margin",
|
||||||
|
"padding": "Padding"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"title": "Spacing"
|
||||||
|
},
|
||||||
|
"cards": {
|
||||||
|
"cards": "Cards",
|
||||||
|
"fixed": "Fixed",
|
||||||
|
"floating": "Floating",
|
||||||
|
"contentText": "The unique stripes of zebras make them one of the animals most familiar to people.",
|
||||||
|
"contentTextLong": "The unique stripes of zebras make them one of the animals most familiar to people. They occur in a variety of habitats, such as grasslands, savannas, woodlands, thorny scrublands, mountains, and coastal hills. Various anthropogenic factors have had a severe impact on zebra populations, in particular hunting for skins and habitat destruction. Grévy's zebra and the mountain zebra are endangered. While plains zebras are much more plentiful, one subspecies, the quagga.",
|
||||||
|
"rowHeight": "Row height",
|
||||||
|
"title": {
|
||||||
|
"default": "Default",
|
||||||
|
"withControls": "With controls",
|
||||||
|
"customHeader": "Custom header",
|
||||||
|
"withoutHeader": "Without header",
|
||||||
|
"withImage": "With Image",
|
||||||
|
"withTitleOnImage": "With title on image",
|
||||||
|
"withCustomTitleOnImage": "With custom title on image",
|
||||||
|
"withStripe": "With stripe",
|
||||||
|
"withBackground": "With background"
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"main": "Main",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"showMore": "Show More",
|
||||||
|
"readMore": "Show More"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"edit": "Edit",
|
||||||
|
"setAsDefault": "Set as default",
|
||||||
|
"delete": "Delete",
|
||||||
|
"traveling": "Traveling",
|
||||||
|
"france": "France",
|
||||||
|
"review": "Review",
|
||||||
|
"feedback": "Leave feedback",
|
||||||
|
"readFull": "Read full article",
|
||||||
|
"secondaryAction": "Secondary action",
|
||||||
|
"action1": "Action 1",
|
||||||
|
"action2": "Action 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"themeColors": "Theme Colors",
|
||||||
|
"extraColors": "Extra Colors",
|
||||||
|
"gradients": {
|
||||||
|
"basic": {
|
||||||
|
"title": "Button Gradients"
|
||||||
|
},
|
||||||
|
"hovered": {
|
||||||
|
"title": "Hovered Button Gradients",
|
||||||
|
"text": "Lighten 15% applied to an original style (gradient or flat color) for hover state."
|
||||||
|
},
|
||||||
|
"pressed": {
|
||||||
|
"title": "Hovered Button Gradients",
|
||||||
|
"text": "Darken 15% applied to an original style (gradient or flat color) for pressed state."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"alignment": "Tabs Alignment",
|
||||||
|
"overflow": "Tabs Overflow",
|
||||||
|
"hidden": "Tabs with Hidden slider",
|
||||||
|
"grow": "Tabs Grow"
|
||||||
|
}
|
||||||
|
}
|
||||||
150
frontend/src/layout/app-layout.vue
Normal file
150
frontend/src/layout/app-layout.vue
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-layout">
|
||||||
|
<navbar />
|
||||||
|
<div class="app-layout__content">
|
||||||
|
<div class="app-layout__sidebar-wrapper" :class="{ minimized: isSidebarMinimized }">
|
||||||
|
<div v-if="isFullScreenSidebar" class="d-flex justify--end">
|
||||||
|
<va-button
|
||||||
|
class="px-4 py-4"
|
||||||
|
icon="close"
|
||||||
|
flat
|
||||||
|
color="dark"
|
||||||
|
@click="onCloseSidebarButtonClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<sidebar
|
||||||
|
:width="sidebarWidth"
|
||||||
|
:minimized="isSidebarMinimized"
|
||||||
|
:minimizedWidth="sidebarMinimizedWidth"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="app-layout__page">
|
||||||
|
<div class="layout fluid gutter--xl">
|
||||||
|
<router-view/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
import { onBeforeRouteUpdate } from 'vue-router';
|
||||||
|
import Sidebar from '@/components/sidebar/Sidebar.vue';
|
||||||
|
import Navbar from '@/components/navbar/Navbar.vue';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'app-layout',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Navbar, Sidebar
|
||||||
|
},
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const store = useStore()
|
||||||
|
const mobileBreakPointPX = 640
|
||||||
|
const tabletBreakPointPX = 768
|
||||||
|
|
||||||
|
const sidebarWidth = ref('16rem')
|
||||||
|
const sidebarMinimizedWidth = ref(undefined)
|
||||||
|
|
||||||
|
const isMobile = ref(false)
|
||||||
|
const isTablet = ref(false)
|
||||||
|
const isSidebarMinimized = computed(() => store.state.isSidebarMinimized)
|
||||||
|
const checkIsTablet = () => window.innerWidth <= tabletBreakPointPX
|
||||||
|
const checkIsMobile = () => window.innerWidth <= mobileBreakPointPX
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
store.commit('updateSidebarCollapsedState', checkIsTablet())
|
||||||
|
|
||||||
|
isMobile.value = checkIsMobile()
|
||||||
|
isTablet.value = checkIsTablet()
|
||||||
|
sidebarMinimizedWidth.value = isMobile.value ? 0 : '4rem'
|
||||||
|
sidebarWidth.value = isTablet.value ? '100%' : '16rem'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', onResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', onResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeRouteUpdate(() => {
|
||||||
|
if (checkIsTablet()) {
|
||||||
|
// Collapse sidebar after route change for Mobile
|
||||||
|
store.commit('updateSidebarCollapsedState', true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onResize()
|
||||||
|
|
||||||
|
const isFullScreenSidebar = computed(() => isTablet.value && !isSidebarMinimized.value)
|
||||||
|
|
||||||
|
const onCloseSidebarButtonClick = () => {
|
||||||
|
store.commit('updateSidebarCollapsedState', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSidebarMinimized,
|
||||||
|
sidebarWidth, sidebarMinimizedWidth,
|
||||||
|
isFullScreenSidebar, onCloseSidebarButtonClick
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
$mobileBreakPointPX: 640px;
|
||||||
|
$tabletBreakPointPX: 768px;
|
||||||
|
|
||||||
|
.app-layout {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
&__navbar {
|
||||||
|
min-height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 4rem);
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
@media screen and (max-width: $tabletBreakPointPX) {
|
||||||
|
height: calc(100vh - 6.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-layout__sidebar-wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--va-white);
|
||||||
|
|
||||||
|
@media screen and (max-width: $tabletBreakPointPX) {
|
||||||
|
&:not(.minimized) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.va-sidebar:not(.va-sidebar--minimized) {
|
||||||
|
// Z-index fix for preventing overflow for close button
|
||||||
|
z-index: -1;
|
||||||
|
.va-sidebar__menu {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__page {
|
||||||
|
flex-grow: 2;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
20
frontend/src/main.ts
Normal file
20
frontend/src/main.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createVuestic } from 'vuestic-ui'
|
||||||
|
import { VuesticPlugin } from 'vuestic-ui';
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import store from './store'
|
||||||
|
import router from './router'
|
||||||
|
import vuesticGlobalConfig from './services/vuestic-ui/global-config'
|
||||||
|
|
||||||
|
import 'vuestic-ui/dist/vuestic-ui.css'
|
||||||
|
import './sass/overrides.scss'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(router)
|
||||||
|
app.use(store)
|
||||||
|
app.use(createVuestic({ config: vuesticGlobalConfig }))
|
||||||
|
|
||||||
|
//app.use(VuesticPlugin, vuesticGlobalConfig)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
104
frontend/src/pages/404/VaPageNotFound.vue
Normal file
104
frontend/src/pages/404/VaPageNotFound.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div class="va-page-not-found justify--center pb-5" :style="pageNotFoundStyle" v-bind="$attrs">
|
||||||
|
<div class="va-page-not-found__inner align--center">
|
||||||
|
<slot name="image"/>
|
||||||
|
<div class="va-page-not-found__title text--center mb-4">This page’s gone fishing.</div>
|
||||||
|
<div class="va-page-not-found__text px-4 text--center">
|
||||||
|
<span>
|
||||||
|
If you feel that it’s not right, please send us a message at
|
||||||
|
</span>
|
||||||
|
<a href="mailto:vmwareadmin@vtb.ru" :style="{color: theme.primary}" class="link">vmwareadmin@vtb.ru</a>
|
||||||
|
</div>
|
||||||
|
<slot/>
|
||||||
|
<!-- <va-button v-if="!withoutButton" :to="{ name: 'dashboard' }">{{$t('404.back_button')}}</va-button> -->
|
||||||
|
</div>
|
||||||
|
<div class="va-page-not-found__wallpaper">
|
||||||
|
<wallpaper :color="wallpaperColor"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Wallpaper from './Wallpaper.vue'
|
||||||
|
import { useGlobalConfig } from 'vuestic-ui'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'va-page-not-found',
|
||||||
|
// mixins: [ColorThemeMixin],
|
||||||
|
// inject: ['contextConfig'],
|
||||||
|
components: {
|
||||||
|
Wallpaper,
|
||||||
|
},
|
||||||
|
props: { withoutButton: Boolean },
|
||||||
|
computed: {
|
||||||
|
theme() {
|
||||||
|
return useGlobalConfig().getGlobalConfig().colors
|
||||||
|
},
|
||||||
|
pageNotFoundStyle () {
|
||||||
|
return {
|
||||||
|
// color: this.contextConfig.invertedColor ? this.$themes.dark : 'white',
|
||||||
|
color: 'var(--va-gray)',
|
||||||
|
// backgroundColor: this.contextConfig.invertedColor ? 'white' : this.$themes.danger,
|
||||||
|
backgroundColor: this.theme.danger,
|
||||||
|
// backgroundImage: this.contextConfig.gradient && 'linear-gradient(to right, #ff2175, #d30505)',
|
||||||
|
backgroundImage: 'linear-gradient(to right, var(--va-white), var(--va-white))',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wallpaperColor () {
|
||||||
|
// return this.contextConfig.invertedColor ? this.$themes.dark : '#e4ff32'
|
||||||
|
return 'var(--va-primary)'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.va-page-not-found {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
// @include media-breakpoint-down(sm) {
|
||||||
|
// padding-top: 8%;
|
||||||
|
// }
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
.va-icon-vuestic {
|
||||||
|
width: 19rem;
|
||||||
|
height: 2rem;
|
||||||
|
margin-bottom: 13%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1.25em;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
// @include media-breakpoint-down(xs) {
|
||||||
|
// font-size: 1.5rem;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__wallpaper {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 1rem;
|
||||||
|
width: 30%;
|
||||||
|
height: 40%;
|
||||||
|
|
||||||
|
// @include media-breakpoint-down(xs) {
|
||||||
|
// display: none;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
33
frontend/src/pages/404/VaPageNotFoundLargeText.vue
Normal file
33
frontend/src/pages/404/VaPageNotFoundLargeText.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<va-page-not-found class="va-page-not-found-large-text">
|
||||||
|
<template #image>
|
||||||
|
<div class="va-page-not-found-large-text__number">404</div>
|
||||||
|
</template>
|
||||||
|
</va-page-not-found>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import VaPageNotFound from './VaPageNotFound.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'va-page-not-found-large-text',
|
||||||
|
components: {
|
||||||
|
VaPageNotFound,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.va-page-not-found-large-text {
|
||||||
|
&__number {
|
||||||
|
font-size: 21rem;
|
||||||
|
// color: $white;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
font-size: 6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
21
frontend/src/pages/404/Wallpaper.vue
Normal file
21
frontend/src/pages/404/Wallpaper.vue
Normal file
File diff suppressed because one or more lines are too long
93
frontend/src/router/index.ts
Normal file
93
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
||||||
|
import AppLayout from '@/layout/app-layout.vue'
|
||||||
|
|
||||||
|
import RouteViewComponent from './route-view.vue'
|
||||||
|
|
||||||
|
const routes: Array<RouteRecordRaw> = [
|
||||||
|
{
|
||||||
|
name: 'root',
|
||||||
|
path: '/',
|
||||||
|
redirect: { name: 'dashboard' },
|
||||||
|
component: AppLayout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'dashboard',
|
||||||
|
path: 'dashboard',
|
||||||
|
component: () => import('@/views/dashboardView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reports',
|
||||||
|
path: 'reports',
|
||||||
|
component: RouteViewComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'vmware-capacity',
|
||||||
|
path: 'vmware-capacity',
|
||||||
|
component: () => import('@/views/vmwareCapacityView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vmware-mmhosts',
|
||||||
|
path: 'vmware-mmhosts',
|
||||||
|
component: () => import('@/views/vmwareMaintenanceView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vmware-datastores',
|
||||||
|
path: 'vmware-datastores',
|
||||||
|
component: () => import('@/views/vmwareDatastoresView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vmware-decomission',
|
||||||
|
path: 'vmware-decomission',
|
||||||
|
component: () => import('@/views/vmwareDecomissionView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'common-sharedNetworks',
|
||||||
|
path: 'common-sharedNetworks',
|
||||||
|
component: () => import('@/views/commonSharedNetworksView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nutanix-prismCentral',
|
||||||
|
path: 'nutanix-prismCentral',
|
||||||
|
component: () => import('@/views/nutanixPrismCentralView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nutanix-prismElement',
|
||||||
|
path: 'nutanix-prismElement',
|
||||||
|
component: () => import('@/views/nutanixPrismElementView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nutanix-Maintenance',
|
||||||
|
path: 'nutanix-Maintenance',
|
||||||
|
component: () => import('@/views/nutanixMaintenanceView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nutanix-Decomission',
|
||||||
|
path: 'nutanix-Decomission',
|
||||||
|
component: () => import('@/views/nutanixDecomissionView.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'settings',
|
||||||
|
path: 'settings',
|
||||||
|
component: RouteViewComponent,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'not-found',
|
||||||
|
path: '/pages/not-found-404',
|
||||||
|
component: () => import('@/pages/404/VaPageNotFoundLargeText.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
redirect: { name: 'not-found' },
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
3
frontend/src/router/route-view.vue
Normal file
3
frontend/src/router/route-view.vue
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<router-view/>
|
||||||
|
</template>
|
||||||
144
frontend/src/sass/custom.scss
Normal file
144
frontend/src/sass/custom.scss
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
.tableReport {
|
||||||
|
max-height: calc(100vh - 280px);
|
||||||
|
.va-data-table__table-tr {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
&:nth-child(2n) {
|
||||||
|
&:not(.selected) {
|
||||||
|
background-color: #DDDFE0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&--Decomission {
|
||||||
|
max-height: calc(100vh - 280px - 280px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reportView {
|
||||||
|
.va-card {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
&__title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.reportFooter {
|
||||||
|
margin: 0 !important;
|
||||||
|
.reportDate {
|
||||||
|
position: relative;
|
||||||
|
top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vcsa-link:link {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.vcsa-link:visited {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.vcsa-link:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.linkSM {
|
||||||
|
color:white;
|
||||||
|
}
|
||||||
|
.badge-upperCase {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.fixedWidth2ch {
|
||||||
|
width: 2ch;
|
||||||
|
}
|
||||||
|
.moveRight5ch {
|
||||||
|
position: absolute;
|
||||||
|
left: 5ch;
|
||||||
|
a, a:link, a:visited, a:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.whiteLink {
|
||||||
|
a, a:link, a:visited, a:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rowCritical {
|
||||||
|
background-color: var(--va-danger) !important;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.cellWarning {
|
||||||
|
background-color: var(--va-focus);
|
||||||
|
}
|
||||||
|
.cellCritical {
|
||||||
|
background-color: var(--va-danger);
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.cellGreen {
|
||||||
|
background-color: #40e583;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.cellReason {
|
||||||
|
max-width: 36ch;
|
||||||
|
}
|
||||||
|
.update_btn.update_btn {
|
||||||
|
.va-button {
|
||||||
|
&__content {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filterTag.filterTag {
|
||||||
|
padding-right: 0;
|
||||||
|
border-right: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
.va-chip {
|
||||||
|
&__content {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.transparentBadge.transparentBadge {
|
||||||
|
span {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.infoBadge {
|
||||||
|
span {
|
||||||
|
color: var(--va-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dangerBadge {
|
||||||
|
span {
|
||||||
|
color: var(--va-danger);
|
||||||
|
span {
|
||||||
|
i {
|
||||||
|
font-size: 14px !important;
|
||||||
|
width: 14px;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.darkBadge {
|
||||||
|
span {
|
||||||
|
color: var(--va-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes pulse {
|
||||||
|
0% { -webkit-transform: scale(1); }
|
||||||
|
50% { -webkit-transform: scale(1.1); }
|
||||||
|
100% { -webkit-transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.1); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldInputCell.fieldInputCell {
|
||||||
|
padding-top: 0px !important;
|
||||||
|
padding-bottom: 0px !important;
|
||||||
|
}
|
||||||
BIN
frontend/src/sass/icon-fonts/brandico/brandico.eot
Normal file
BIN
frontend/src/sass/icon-fonts/brandico/brandico.eot
Normal file
Binary file not shown.
144
frontend/src/sass/icon-fonts/brandico/brandico.scss
Normal file
144
frontend/src/sass/icon-fonts/brandico/brandico.scss
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'brandico';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
src: url('./sass/icon-fonts/brandico/brandico.eot');
|
||||||
|
src:
|
||||||
|
url('./sass/icon-fonts/brandico/brandico.eot#iefix') format('eot'),
|
||||||
|
url('./sass/icon-fonts/brandico/brandico.woff') format('woff'),
|
||||||
|
url('./sass/icon-fonts/brandico/brandico.ttf') format('truetype'),
|
||||||
|
url('./sass/icon-fonts/brandico/brandico.svg#brandico') format('svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
[class*="brandico-"]::before {
|
||||||
|
font-family: 'brandico', sans-serif;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-facebook::before {
|
||||||
|
content: "\f300";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-facebook-rect::before {
|
||||||
|
content: "\f301";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-twitter::before {
|
||||||
|
content: "\f302";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-twitter-bird::before {
|
||||||
|
content: "\f303";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-vimeo::before {
|
||||||
|
content: "\f30f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-vimeo-rect::before {
|
||||||
|
content: "\f30e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-tumblr::before {
|
||||||
|
content: "\f311";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-tumblr-rect::before {
|
||||||
|
content: "\f310";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-googleplus-rect::before {
|
||||||
|
content: "\f309";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-github-text::before {
|
||||||
|
content: "\f307";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-github::before {
|
||||||
|
content: "\f308";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-skype::before {
|
||||||
|
content: "\f30b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-icq::before {
|
||||||
|
content: "\f304";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-yandex::before {
|
||||||
|
content: "\f305";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-yandex-rect::before {
|
||||||
|
content: "\f306";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-vkontakte-rect::before {
|
||||||
|
content: "\f30a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-odnoklassniki::before {
|
||||||
|
content: "\f30c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-odnoklassniki-rect::before {
|
||||||
|
content: "\f30d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-friendfeed::before {
|
||||||
|
content: "\f312";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-friendfeed-rect::before {
|
||||||
|
content: "\f313";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-blogger::before {
|
||||||
|
content: "\f314";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-blogger-rect::before {
|
||||||
|
content: "\f315";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-deviantart::before {
|
||||||
|
content: "\f316";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-jabber::before {
|
||||||
|
content: "\f317";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-lastfm::before {
|
||||||
|
content: "\f318";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-lastfm-rect::before {
|
||||||
|
content: "\f319";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-linkedin::before {
|
||||||
|
content: "\f31a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-linkedin-rect::before {
|
||||||
|
content: "\f31b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-picasa::before {
|
||||||
|
content: "\f31c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-wordpress::before {
|
||||||
|
content: "\f31d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-instagram::before {
|
||||||
|
content: "\f31e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandico-instagram-filled::before {
|
||||||
|
content: "\f31f";
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user