Initial Commit

This commit is contained in:
2023-09-08 19:05:37 +03:00
commit 7e60195cb7
185 changed files with 27107 additions and 0 deletions

13
.vscode/extensions.json vendored Normal file
View 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
View 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.

2
README.md Normal file
View File

@@ -0,0 +1,2 @@
# vtb_dashboard_api

165
api/.dockerignore Normal file
View 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
View 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
View 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
View File

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

View 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

View File

@@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

2
api/app/helpers/math.py Normal file
View 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
View 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
)

View File

View 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

View File

View 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")

View 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

View 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
)

View File

View File

View 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"
)

View 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

View 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
)

View 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
)

View 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"
)

View 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
)

View File

View File

View 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"
)

View 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"
)

View 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"
)

View 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

View 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

View 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
)

View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,8 @@
node_modules/
dist/
.vscode/
Dockerfile
.dockerignore
.gitignore

View 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
View 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
View 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
View 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
View 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
View 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
View 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>

View 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

File diff suppressed because it is too large Load Diff

58
frontend/package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

18
frontend/src/App.vue Normal file
View 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>

View 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',
}
});

View 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();

View 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View 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

View 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>

View 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>

View 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>

View 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>

View File

@@ -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&section=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&section=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&section=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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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,
},
],
}

View 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>

View 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>

View 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>

View 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>

View 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
View 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 pages gone fishing.",
"text": "If you feel that its 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": "Dragndrop 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"
}
}

View 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
View 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')

View 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 pages gone fishing.</div>
<div class="va-page-not-found__text px-4 text--center">
<span>
If you feel that its 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>

View 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>

File diff suppressed because one or more lines are too long

View 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

View File

@@ -0,0 +1,3 @@
<template>
<router-view/>
</template>

View 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;
}

Binary file not shown.

View 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