From d6a60b5043c6f7c3dfc45853feb68d0aca5a4a5f Mon Sep 17 00:00:00 2001 From: pmikus Date: Mon, 19 Sep 2022 08:49:01 +0200 Subject: feat(uti): Move directory Signed-off-by: pmikus Change-Id: I7300ecfe756baaf3fbeedb020070f882cfaca445 --- csit.infra.dash/Dockerfile | 12 + .../app/.ebextensions/cron-linux.config | 14 + csit.infra.dash/app/Procfile | 1 + csit.infra.dash/app/app.ini | 19 + csit.infra.dash/app/config.py | 30 + csit.infra.dash/app/pal/__init__.py | 81 + csit.infra.dash/app/pal/data/__init__.py | 12 + csit.infra.dash/app/pal/data/data.py | 351 +++++ csit.infra.dash/app/pal/data/data.yaml | 117 ++ csit.infra.dash/app/pal/debug.py | 48 + csit.infra.dash/app/pal/news/__init__.py | 12 + csit.infra.dash/app/pal/news/layout.py | 522 +++++++ csit.infra.dash/app/pal/news/news.py | 46 + csit.infra.dash/app/pal/news/tables.py | 176 +++ csit.infra.dash/app/pal/report/__init__.py | 12 + csit.infra.dash/app/pal/report/graphs.py | 275 ++++ csit.infra.dash/app/pal/report/layout.py | 1494 ++++++++++++++++++ csit.infra.dash/app/pal/report/layout.yaml | 127 ++ csit.infra.dash/app/pal/report/report.py | 48 + csit.infra.dash/app/pal/routes.py | 32 + .../app/pal/static/dist/img/favicon.svg | 348 +++++ csit.infra.dash/app/pal/static/img/logo.svg | 348 +++++ .../app/pal/static/sass/_bootswatch.scss | 178 +++ .../app/pal/static/sass/_variables.scss | 103 ++ .../app/pal/static/sass/bootstrap/_accordion.scss | 149 ++ .../app/pal/static/sass/bootstrap/_alert.scss | 71 + .../app/pal/static/sass/bootstrap/_badge.scss | 38 + .../app/pal/static/sass/bootstrap/_breadcrumb.scss | 40 + .../pal/static/sass/bootstrap/_button-group.scss | 142 ++ .../app/pal/static/sass/bootstrap/_buttons.scss | 201 +++ .../app/pal/static/sass/bootstrap/_card.scss | 234 +++ .../app/pal/static/sass/bootstrap/_carousel.scss | 229 +++ .../app/pal/static/sass/bootstrap/_close.scss | 40 + .../app/pal/static/sass/bootstrap/_containers.scss | 41 + .../app/pal/static/sass/bootstrap/_dropdown.scss | 249 +++ .../app/pal/static/sass/bootstrap/_forms.scss | 9 + .../app/pal/static/sass/bootstrap/_functions.scss | 302 ++++ .../app/pal/static/sass/bootstrap/_grid.scss | 33 + .../app/pal/static/sass/bootstrap/_helpers.scss | 10 + .../app/pal/static/sass/bootstrap/_images.scss | 42 + .../app/pal/static/sass/bootstrap/_list-group.scss | 192 +++ .../app/pal/static/sass/bootstrap/_maps.scss | 54 + .../app/pal/static/sass/bootstrap/_mixins.scss | 43 + .../app/pal/static/sass/bootstrap/_modal.scss | 237 +++ .../app/pal/static/sass/bootstrap/_nav.scss | 172 +++ .../app/pal/static/sass/bootstrap/_navbar.scss | 278 ++++ .../app/pal/static/sass/bootstrap/_offcanvas.scss | 144 ++ .../app/pal/static/sass/bootstrap/_pagination.scss | 109 ++ .../pal/static/sass/bootstrap/_placeholders.scss | 51 + .../app/pal/static/sass/bootstrap/_popover.scss | 196 +++ .../app/pal/static/sass/bootstrap/_progress.scss | 59 + .../app/pal/static/sass/bootstrap/_reboot.scss | 610 ++++++++ .../app/pal/static/sass/bootstrap/_root.scss | 73 + .../app/pal/static/sass/bootstrap/_spinners.scss | 85 + .../app/pal/static/sass/bootstrap/_tables.scss | 164 ++ .../app/pal/static/sass/bootstrap/_toasts.scss | 71 + .../app/pal/static/sass/bootstrap/_tooltip.scss | 120 ++ .../pal/static/sass/bootstrap/_transitions.scss | 27 + .../app/pal/static/sass/bootstrap/_type.scss | 106 ++ .../app/pal/static/sass/bootstrap/_utilities.scss | 647 ++++++++ .../app/pal/static/sass/bootstrap/_variables.scss | 1634 ++++++++++++++++++++ .../pal/static/sass/bootstrap/bootstrap-grid.scss | 64 + .../static/sass/bootstrap/bootstrap-reboot.scss | 9 + .../static/sass/bootstrap/bootstrap-utilities.scss | 15 + .../app/pal/static/sass/bootstrap/bootstrap.scss | 51 + .../sass/bootstrap/forms/_floating-labels.scss | 75 + .../static/sass/bootstrap/forms/_form-check.scss | 175 +++ .../static/sass/bootstrap/forms/_form-control.scss | 194 +++ .../static/sass/bootstrap/forms/_form-range.scss | 91 ++ .../static/sass/bootstrap/forms/_form-select.scss | 71 + .../static/sass/bootstrap/forms/_form-text.scss | 11 + .../static/sass/bootstrap/forms/_input-group.scss | 132 ++ .../pal/static/sass/bootstrap/forms/_labels.scss | 36 + .../static/sass/bootstrap/forms/_validation.scss | 12 + .../static/sass/bootstrap/helpers/_clearfix.scss | 3 + .../static/sass/bootstrap/helpers/_color-bg.scss | 10 + .../sass/bootstrap/helpers/_colored-links.scss | 12 + .../static/sass/bootstrap/helpers/_position.scss | 36 + .../pal/static/sass/bootstrap/helpers/_ratio.scss | 26 + .../pal/static/sass/bootstrap/helpers/_stacks.scss | 15 + .../sass/bootstrap/helpers/_stretched-link.scss | 15 + .../sass/bootstrap/helpers/_text-truncation.scss | 7 + .../sass/bootstrap/helpers/_visually-hidden.scss | 8 + .../app/pal/static/sass/bootstrap/helpers/_vr.scss | 8 + .../pal/static/sass/bootstrap/mixins/_alert.scss | 15 + .../static/sass/bootstrap/mixins/_backdrop.scss | 14 + .../pal/static/sass/bootstrap/mixins/_banner.scss | 9 + .../sass/bootstrap/mixins/_border-radius.scss | 78 + .../static/sass/bootstrap/mixins/_box-shadow.scss | 18 + .../static/sass/bootstrap/mixins/_breakpoints.scss | 127 ++ .../pal/static/sass/bootstrap/mixins/_buttons.scss | 70 + .../pal/static/sass/bootstrap/mixins/_caret.scss | 64 + .../static/sass/bootstrap/mixins/_clearfix.scss | 9 + .../sass/bootstrap/mixins/_color-scheme.scss | 7 + .../static/sass/bootstrap/mixins/_container.scss | 11 + .../static/sass/bootstrap/mixins/_deprecate.scss | 10 + .../pal/static/sass/bootstrap/mixins/_forms.scss | 152 ++ .../static/sass/bootstrap/mixins/_gradients.scss | 47 + .../pal/static/sass/bootstrap/mixins/_grid.scss | 151 ++ .../pal/static/sass/bootstrap/mixins/_image.scss | 16 + .../static/sass/bootstrap/mixins/_list-group.scss | 24 + .../pal/static/sass/bootstrap/mixins/_lists.scss | 7 + .../static/sass/bootstrap/mixins/_pagination.scss | 10 + .../static/sass/bootstrap/mixins/_reset-text.scss | 17 + .../pal/static/sass/bootstrap/mixins/_resize.scss | 6 + .../sass/bootstrap/mixins/_table-variants.scss | 24 + .../sass/bootstrap/mixins/_text-truncate.scss | 8 + .../static/sass/bootstrap/mixins/_transition.scss | 26 + .../static/sass/bootstrap/mixins/_utilities.scss | 97 ++ .../sass/bootstrap/mixins/_visually-hidden.scss | 29 + .../pal/static/sass/bootstrap/utilities/_api.scss | 47 + .../app/pal/static/sass/bootstrap/vendor/_rfs.scss | 354 +++++ csit.infra.dash/app/pal/static/sass/lux.scss | 6 + csit.infra.dash/app/pal/stats/__init__.py | 12 + csit.infra.dash/app/pal/stats/graphs.py | 124 ++ csit.infra.dash/app/pal/stats/layout.py | 868 +++++++++++ csit.infra.dash/app/pal/stats/layout.yaml | 79 + csit.infra.dash/app/pal/stats/stats.py | 48 + .../app/pal/templates/base_layout.jinja2 | 24 + .../app/pal/templates/index_layout.jinja2 | 34 + .../app/pal/templates/news_layout.jinja2 | 17 + .../app/pal/templates/report_layout.jinja2 | 17 + .../app/pal/templates/stats_layout.jinja2 | 17 + .../app/pal/templates/trending_layout.jinja2 | 17 + csit.infra.dash/app/pal/trending/__init__.py | 12 + csit.infra.dash/app/pal/trending/graphs.py | 408 +++++ csit.infra.dash/app/pal/trending/layout.py | 1387 +++++++++++++++++ csit.infra.dash/app/pal/trending/layout.yaml | 129 ++ csit.infra.dash/app/pal/trending/trending.py | 48 + csit.infra.dash/app/pal/utils/__init__.py | 12 + csit.infra.dash/app/pal/utils/constants.py | 315 ++++ csit.infra.dash/app/pal/utils/tooltips.yaml | 42 + csit.infra.dash/app/pal/utils/url_processing.py | 99 ++ csit.infra.dash/app/pal/utils/utils.py | 344 +++++ csit.infra.dash/app/requirements.txt | 40 + csit.infra.dash/app/wsgi.py | 21 + csit.infra.dash/docker-compose.yaml | 15 + 137 files changed, 17966 insertions(+) create mode 100644 csit.infra.dash/Dockerfile create mode 100644 csit.infra.dash/app/.ebextensions/cron-linux.config create mode 100644 csit.infra.dash/app/Procfile create mode 100644 csit.infra.dash/app/app.ini create mode 100644 csit.infra.dash/app/config.py create mode 100644 csit.infra.dash/app/pal/__init__.py create mode 100644 csit.infra.dash/app/pal/data/__init__.py create mode 100644 csit.infra.dash/app/pal/data/data.py create mode 100644 csit.infra.dash/app/pal/data/data.yaml create mode 100644 csit.infra.dash/app/pal/debug.py create mode 100644 csit.infra.dash/app/pal/news/__init__.py create mode 100644 csit.infra.dash/app/pal/news/layout.py create mode 100644 csit.infra.dash/app/pal/news/news.py create mode 100644 csit.infra.dash/app/pal/news/tables.py create mode 100644 csit.infra.dash/app/pal/report/__init__.py create mode 100644 csit.infra.dash/app/pal/report/graphs.py create mode 100644 csit.infra.dash/app/pal/report/layout.py create mode 100644 csit.infra.dash/app/pal/report/layout.yaml create mode 100644 csit.infra.dash/app/pal/report/report.py create mode 100644 csit.infra.dash/app/pal/routes.py create mode 100644 csit.infra.dash/app/pal/static/dist/img/favicon.svg create mode 100644 csit.infra.dash/app/pal/static/img/logo.svg create mode 100644 csit.infra.dash/app/pal/static/sass/_bootswatch.scss create mode 100644 csit.infra.dash/app/pal/static/sass/_variables.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_accordion.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_alert.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_badge.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_breadcrumb.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_button-group.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_buttons.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_card.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_carousel.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_close.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_containers.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_dropdown.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_forms.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_functions.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_grid.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_helpers.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_images.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_list-group.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_maps.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_mixins.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_modal.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_nav.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_navbar.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_offcanvas.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_pagination.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_placeholders.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_popover.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_progress.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_reboot.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_root.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_spinners.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_tables.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_toasts.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_tooltip.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_transitions.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_type.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_utilities.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/_variables.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/bootstrap-grid.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/bootstrap-reboot.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/bootstrap-utilities.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/bootstrap.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/forms/_floating-labels.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/forms/_form-check.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/forms/_form-control.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/forms/_form-range.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/forms/_form-select.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/forms/_form-text.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/forms/_input-group.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/forms/_labels.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/forms/_validation.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/helpers/_clearfix.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/helpers/_color-bg.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/helpers/_colored-links.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/helpers/_position.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/helpers/_ratio.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/helpers/_stacks.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/helpers/_stretched-link.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/helpers/_text-truncation.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/helpers/_visually-hidden.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/helpers/_vr.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_alert.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_backdrop.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_banner.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_border-radius.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_box-shadow.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_breakpoints.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_buttons.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_caret.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_clearfix.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_color-scheme.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_container.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_deprecate.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_forms.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_gradients.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_grid.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_image.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_list-group.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_lists.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_pagination.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_reset-text.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_resize.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_table-variants.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_text-truncate.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_transition.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_utilities.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/mixins/_visually-hidden.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/utilities/_api.scss create mode 100644 csit.infra.dash/app/pal/static/sass/bootstrap/vendor/_rfs.scss create mode 100644 csit.infra.dash/app/pal/static/sass/lux.scss create mode 100644 csit.infra.dash/app/pal/stats/__init__.py create mode 100644 csit.infra.dash/app/pal/stats/graphs.py create mode 100644 csit.infra.dash/app/pal/stats/layout.py create mode 100644 csit.infra.dash/app/pal/stats/layout.yaml create mode 100644 csit.infra.dash/app/pal/stats/stats.py create mode 100644 csit.infra.dash/app/pal/templates/base_layout.jinja2 create mode 100644 csit.infra.dash/app/pal/templates/index_layout.jinja2 create mode 100644 csit.infra.dash/app/pal/templates/news_layout.jinja2 create mode 100644 csit.infra.dash/app/pal/templates/report_layout.jinja2 create mode 100644 csit.infra.dash/app/pal/templates/stats_layout.jinja2 create mode 100644 csit.infra.dash/app/pal/templates/trending_layout.jinja2 create mode 100644 csit.infra.dash/app/pal/trending/__init__.py create mode 100644 csit.infra.dash/app/pal/trending/graphs.py create mode 100644 csit.infra.dash/app/pal/trending/layout.py create mode 100644 csit.infra.dash/app/pal/trending/layout.yaml create mode 100644 csit.infra.dash/app/pal/trending/trending.py create mode 100644 csit.infra.dash/app/pal/utils/__init__.py create mode 100644 csit.infra.dash/app/pal/utils/constants.py create mode 100644 csit.infra.dash/app/pal/utils/tooltips.yaml create mode 100644 csit.infra.dash/app/pal/utils/url_processing.py create mode 100644 csit.infra.dash/app/pal/utils/utils.py create mode 100644 csit.infra.dash/app/requirements.txt create mode 100644 csit.infra.dash/app/wsgi.py create mode 100644 csit.infra.dash/docker-compose.yaml (limited to 'csit.infra.dash') diff --git a/csit.infra.dash/Dockerfile b/csit.infra.dash/Dockerfile new file mode 100644 index 0000000000..ee4ae1edd9 --- /dev/null +++ b/csit.infra.dash/Dockerfile @@ -0,0 +1,12 @@ +ARG PYTHON_VERSION=3.8 +FROM python:${PYTHON_VERSION}-buster + +WORKDIR /app + +COPY ./app/requirements.txt . + +RUN pip3 install -r requirements.txt + +EXPOSE 5000 + +CMD [ "uwsgi", "app.ini" ] \ No newline at end of file diff --git a/csit.infra.dash/app/.ebextensions/cron-linux.config b/csit.infra.dash/app/.ebextensions/cron-linux.config new file mode 100644 index 0000000000..ae8c33c814 --- /dev/null +++ b/csit.infra.dash/app/.ebextensions/cron-linux.config @@ -0,0 +1,14 @@ +files: + "/etc/cron.d/mycron": + mode: "000644" + owner: root + group: root + content: | + SHELL=/bin/bash + PATH=/sbin:/bin:/usr/sbin:/usr/bin + MAILTO=root + 0 6 * * * root /bin/echo 'c' > /tmp/masterfifo + +commands: + remove_old_cron: + command: "rm -f /etc/cron.d/mycron.bak" \ No newline at end of file diff --git a/csit.infra.dash/app/Procfile b/csit.infra.dash/app/Procfile new file mode 100644 index 0000000000..c79d502390 --- /dev/null +++ b/csit.infra.dash/app/Procfile @@ -0,0 +1 @@ +uwsgi: uwsgi app.ini diff --git a/csit.infra.dash/app/app.ini b/csit.infra.dash/app/app.ini new file mode 100644 index 0000000000..b42f63dc4b --- /dev/null +++ b/csit.infra.dash/app/app.ini @@ -0,0 +1,19 @@ +[uwsgi] +ini = :pal +py-autoreload = 0 + +[pal] +module = wsgi:app +master-fifo = /tmp/masterfifo +lazy = True +lazy-apps = True +touch-chain-reload +listen = 128 + +workers = 2 +plugin = python3 + +master = true +http-socket = :5000 +socket = /tmp/app.sock +chmod-socket = 666 diff --git a/csit.infra.dash/app/config.py b/csit.infra.dash/app/config.py new file mode 100644 index 0000000000..559864bebb --- /dev/null +++ b/csit.infra.dash/app/config.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. + +class Config: + """Flask configuration variables. + """ + # General Config + FLASK_APP = "wsgi.py" + FLASK_ENV = "production" + + # Assets + ASSETS_DEBUG = False + ASSETS_AUTO_BUILD = True + + # Static Assets + STATIC_FOLDER = "static" + TEMPLATES_FOLDER = "templates" + COMPRESSOR_DEBUG = "True" diff --git a/csit.infra.dash/app/pal/__init__.py b/csit.infra.dash/app/pal/__init__.py new file mode 100644 index 0000000000..20023ec157 --- /dev/null +++ b/csit.infra.dash/app/pal/__init__.py @@ -0,0 +1,81 @@ +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. + +"""Initialize Flask app. +""" + +import logging + +from flask import Flask +from flask_assets import Environment, Bundle + +from .utils.constants import Constants as C + + +def init_app(): + """Construct core Flask application with embedded Dash app. + """ + logging.basicConfig( + format=C.LOG_FORMAT, + datefmt=C.LOG_DATE_FORMAT, + level=C.LOG_LEVEL + ) + + logging.info("Application started.") + + app = Flask(__name__, instance_relative_config=False) + app.config.from_object("config.Config") + + with app.app_context(): + # Import parts of our core Flask app. + from . import routes + + assets = Environment() + assets.init_app(app) + + # Compile static assets. + sass_bundle = Bundle( + "sass/lux.scss", + filters="libsass", + output="dist/css/bootstrap.css", + depends="**/*.scss", + extra={ + "rel": "stylesheet" + } + ) + assets.register("sass_all", sass_bundle) + sass_bundle.build() + + # Set the time period for Trending + if C.TIME_PERIOD is None or C.TIME_PERIOD > C.MAX_TIME_PERIOD: + time_period = C.MAX_TIME_PERIOD + else: + time_period = C.TIME_PERIOD + + # Import Dash applications. + from .news.news import init_news + app = init_news(app) + + from .stats.stats import init_stats + app = init_stats(app, time_period=time_period) + + from .trending.trending import init_trending + app = init_trending(app, time_period=time_period) + + from .report.report import init_report + app = init_report(app, releases=C.RELEASES) + + return app + + +app = init_app() diff --git a/csit.infra.dash/app/pal/data/__init__.py b/csit.infra.dash/app/pal/data/__init__.py new file mode 100644 index 0000000000..5692432123 --- /dev/null +++ b/csit.infra.dash/app/pal/data/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. diff --git a/csit.infra.dash/app/pal/data/data.py b/csit.infra.dash/app/pal/data/data.py new file mode 100644 index 0000000000..77fd113a9c --- /dev/null +++ b/csit.infra.dash/app/pal/data/data.py @@ -0,0 +1,351 @@ +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. + +"""Prepare data for Plotly Dash applications. +""" + +import logging +import awswrangler as wr + +from yaml import load, FullLoader, YAMLError +from datetime import datetime, timedelta +from time import time +from pytz import UTC +from pandas import DataFrame +from awswrangler.exceptions import EmptyDataFrame, NoFilesFound + + +class Data: + """Gets the data from parquets and stores it for further use by dash + applications. + """ + + def __init__(self, data_spec_file: str, debug: bool=False) -> None: + """Initialize the Data object. + + :param data_spec_file: Path to file specifying the data to be read from + parquets. + :param debug: If True, the debuf information is printed to stdout. + :type data_spec_file: str + :type debug: bool + :raises RuntimeError: if it is not possible to open data_spec_file or it + is not a valid yaml file. + """ + + # Inputs: + self._data_spec_file = data_spec_file + self._debug = debug + + # Specification of data to be read from parquets: + self._data_spec = None + + # Data frame to keep the data: + self._data = None + + # Read from files: + try: + with open(self._data_spec_file, "r") as file_read: + self._data_spec = load(file_read, Loader=FullLoader) + except IOError as err: + raise RuntimeError( + f"Not possible to open the file {self._data_spec_file,}\n{err}" + ) + except YAMLError as err: + raise RuntimeError( + f"An error occurred while parsing the specification file " + f"{self._data_spec_file,}\n" + f"{err}" + ) + + @property + def data(self): + return self._data + + def _get_columns(self, parquet: str) -> list: + """Get the list of columns from the data specification file to be read + from parquets. + + :param parquet: The parquet's name. + :type parquet: str + :raises RuntimeError: if the parquet is not defined in the data + specification file or it does not have any columns specified. + :returns: List of columns. + :rtype: list + """ + + try: + return self._data_spec[parquet]["columns"] + except KeyError as err: + raise RuntimeError( + f"The parquet {parquet} is not defined in the specification " + f"file {self._data_spec_file} or it does not have any columns " + f"specified.\n{err}" + ) + + def _get_path(self, parquet: str) -> str: + """Get the path from the data specification file to be read from + parquets. + + :param parquet: The parquet's name. + :type parquet: str + :raises RuntimeError: if the parquet is not defined in the data + specification file or it does not have the path specified. + :returns: Path. + :rtype: str + """ + + try: + return self._data_spec[parquet]["path"] + except KeyError as err: + raise RuntimeError( + f"The parquet {parquet} is not defined in the specification " + f"file {self._data_spec_file} or it does not have the path " + f"specified.\n{err}" + ) + + def _get_list_of_files(self, + path, + last_modified_begin=None, + last_modified_end=None, + days=None) -> list: + """Get list of interested files stored in S3 compatible storage and + returns it. + + :param path: S3 prefix (accepts Unix shell-style wildcards) + (e.g. s3://bucket/prefix) or list of S3 objects paths + (e.g. [s3://bucket/key0, s3://bucket/key1]). + :param last_modified_begin: Filter the s3 files by the Last modified + date of the object. The filter is applied only after list all s3 + files. + :param last_modified_end: Filter the s3 files by the Last modified date + of the object. The filter is applied only after list all s3 files. + :param days: Number of days to filter. + :type path: Union[str, List[str]] + :type last_modified_begin: datetime, optional + :type last_modified_end: datetime, optional + :type days: integer, optional + :returns: List of file names. + :rtype: List + """ + if days: + last_modified_begin = datetime.now(tz=UTC) - timedelta(days=days) + try: + file_list = wr.s3.list_objects( + path=path, + suffix="parquet", + last_modified_begin=last_modified_begin, + last_modified_end=last_modified_end + ) + if self._debug: + logging.info("\n".join(file_list)) + except NoFilesFound as err: + logging.error(f"No parquets found.\n{err}") + except EmptyDataFrame as err: + logging.error(f"No data.\n{err}") + + return file_list + + def _create_dataframe_from_parquet(self, + path, partition_filter=None, + columns=None, + validate_schema=False, + last_modified_begin=None, + last_modified_end=None, + days=None) -> DataFrame: + """Read parquet stored in S3 compatible storage and returns Pandas + Dataframe. + + :param path: S3 prefix (accepts Unix shell-style wildcards) + (e.g. s3://bucket/prefix) or list of S3 objects paths + (e.g. [s3://bucket/key0, s3://bucket/key1]). + :param partition_filter: Callback Function filters to apply on PARTITION + columns (PUSH-DOWN filter). This function MUST receive a single + argument (Dict[str, str]) where keys are partitions names and values + are partitions values. Partitions values will be always strings + extracted from S3. This function MUST return a bool, True to read + the partition or False to ignore it. Ignored if dataset=False. + :param columns: Names of columns to read from the file(s). + :param validate_schema: Check that individual file schemas are all the + same / compatible. Schemas within a folder prefix should all be the + same. Disable if you have schemas that are different and want to + disable this check. + :param last_modified_begin: Filter the s3 files by the Last modified + date of the object. The filter is applied only after list all s3 + files. + :param last_modified_end: Filter the s3 files by the Last modified date + of the object. The filter is applied only after list all s3 files. + :param days: Number of days to filter. + :type path: Union[str, List[str]] + :type partition_filter: Callable[[Dict[str, str]], bool], optional + :type columns: List[str], optional + :type validate_schema: bool, optional + :type last_modified_begin: datetime, optional + :type last_modified_end: datetime, optional + :type days: integer, optional + :returns: Pandas DataFrame or None if DataFrame cannot be fetched. + :rtype: DataFrame + """ + df = None + start = time() + if days: + last_modified_begin = datetime.now(tz=UTC) - timedelta(days=days) + try: + df = wr.s3.read_parquet( + path=path, + path_suffix="parquet", + ignore_empty=True, + validate_schema=validate_schema, + use_threads=True, + dataset=True, + columns=columns, + partition_filter=partition_filter, + last_modified_begin=last_modified_begin, + last_modified_end=last_modified_end + ) + if self._debug: + df.info(verbose=True, memory_usage='deep') + logging.info( + u"\n" + f"Creation of dataframe {path} took: {time() - start}" + u"\n" + ) + except NoFilesFound as err: + logging.error(f"No parquets found.\n{err}") + except EmptyDataFrame as err: + logging.error(f"No data.\n{err}") + + self._data = df + return df + + def check_datasets(self, days: int=None): + """Read structure from parquet. + + :param days: Number of days back to the past for which the data will be + read. + :type days: int + """ + self._get_list_of_files(path=self._get_path("trending"), days=days) + self._get_list_of_files(path=self._get_path("statistics"), days=days) + + def read_stats(self, days: int=None) -> tuple: + """Read statistics from parquet. + + It reads from: + - Suite Result Analysis (SRA) partition, + - NDRPDR trending partition, + - MRR trending partition. + + :param days: Number of days back to the past for which the data will be + read. + :type days: int + :returns: tuple of pandas DataFrame-s with data read from specified + parquets. + :rtype: tuple of pandas DataFrame-s + """ + + l_stats = lambda part: True if part["stats_type"] == "sra" else False + l_mrr = lambda part: True if part["test_type"] == "mrr" else False + l_ndrpdr = lambda part: True if part["test_type"] == "ndrpdr" else False + + return ( + self._create_dataframe_from_parquet( + path=self._get_path("statistics"), + partition_filter=l_stats, + columns=self._get_columns("statistics"), + days=days + ), + self._create_dataframe_from_parquet( + path=self._get_path("statistics-trending-mrr"), + partition_filter=l_mrr, + columns=self._get_columns("statistics-trending-mrr"), + days=days + ), + self._create_dataframe_from_parquet( + path=self._get_path("statistics-trending-ndrpdr"), + partition_filter=l_ndrpdr, + columns=self._get_columns("statistics-trending-ndrpdr"), + days=days + ) + ) + + def read_trending_mrr(self, days: int=None) -> DataFrame: + """Read MRR data partition from parquet. + + :param days: Number of days back to the past for which the data will be + read. + :type days: int + :returns: Pandas DataFrame with read data. + :rtype: DataFrame + """ + + lambda_f = lambda part: True if part["test_type"] == "mrr" else False + + return self._create_dataframe_from_parquet( + path=self._get_path("trending-mrr"), + partition_filter=lambda_f, + columns=self._get_columns("trending-mrr"), + days=days + ) + + def read_trending_ndrpdr(self, days: int=None) -> DataFrame: + """Read NDRPDR data partition from iterative parquet. + + :param days: Number of days back to the past for which the data will be + read. + :type days: int + :returns: Pandas DataFrame with read data. + :rtype: DataFrame + """ + + lambda_f = lambda part: True if part["test_type"] == "ndrpdr" else False + + return self._create_dataframe_from_parquet( + path=self._get_path("trending-ndrpdr"), + partition_filter=lambda_f, + columns=self._get_columns("trending-ndrpdr"), + days=days + ) + + def read_iterative_mrr(self, release: str) -> DataFrame: + """Read MRR data partition from iterative parquet. + + :param release: The CSIT release from which the data will be read. + :type release: str + :returns: Pandas DataFrame with read data. + :rtype: DataFrame + """ + + lambda_f = lambda part: True if part["test_type"] == "mrr" else False + + return self._create_dataframe_from_parquet( + path=self._get_path("iterative-mrr").format(release=release), + partition_filter=lambda_f, + columns=self._get_columns("iterative-mrr") + ) + + def read_iterative_ndrpdr(self, release: str) -> DataFrame: + """Read NDRPDR data partition from parquet. + + :param release: The CSIT release from which the data will be read. + :type release: str + :returns: Pandas DataFrame with read data. + :rtype: DataFrame + """ + + lambda_f = lambda part: True if part["test_type"] == "ndrpdr" else False + + return self._create_dataframe_from_parquet( + path=self._get_path("iterative-ndrpdr").format(release=release), + partition_filter=lambda_f, + columns=self._get_columns("iterative-ndrpdr") + ) diff --git a/csit.infra.dash/app/pal/data/data.yaml b/csit.infra.dash/app/pal/data/data.yaml new file mode 100644 index 0000000000..396f1b1638 --- /dev/null +++ b/csit.infra.dash/app/pal/data/data.yaml @@ -0,0 +1,117 @@ +statistics: + path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/stats + columns: + - job + - build + - start_time + - duration +statistics-trending-ndrpdr: + path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/trending + columns: + - job + - build + - dut_type + - dut_version + - hosts + - start_time + - passed + - test_id + - result_pdr_lower_rate_value + - result_ndr_lower_rate_value +statistics-trending-mrr: + path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/trending + columns: + - job + - build + - dut_type + - dut_version + - hosts + - start_time + - passed + - test_id + - result_receive_rate_rate_avg +trending: + path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/trending + columns: + - job + - build + - start_time +trending-mrr: + path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/trending + columns: + - job + - build + - dut_type + - dut_version + - hosts + - start_time + - passed + - test_id + - version + - result_receive_rate_rate_avg + - result_receive_rate_rate_stdev + - result_receive_rate_rate_unit +trending-ndrpdr: + path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/trending + columns: + - job + - build + - dut_type + - dut_version + - hosts + - start_time + - passed + - test_id + - version + - result_pdr_lower_rate_unit + - result_pdr_lower_rate_value + - result_ndr_lower_rate_unit + - result_ndr_lower_rate_value + - result_latency_reverse_pdr_90_hdrh + - result_latency_reverse_pdr_50_hdrh + - result_latency_reverse_pdr_10_hdrh + - result_latency_reverse_pdr_0_hdrh + - result_latency_forward_pdr_90_hdrh + - result_latency_forward_pdr_50_avg + - result_latency_forward_pdr_50_hdrh + - result_latency_forward_pdr_50_unit + - result_latency_forward_pdr_10_hdrh + - result_latency_forward_pdr_0_hdrh +iterative-mrr: + path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/iterative_{release} + columns: + - job + - build + - dut_type + - dut_version + - hosts + - start_time + - passed + - test_id + - version + - result_receive_rate_rate_avg + - result_receive_rate_rate_stdev + - result_receive_rate_rate_unit + - result_receive_rate_rate_values +iterative-ndrpdr: + path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/iterative_{release} + columns: + - job + - build + - dut_type + - dut_version + - hosts + - start_time + - passed + - test_id + - version + - result_pdr_lower_rate_unit + - result_pdr_lower_rate_value + - result_ndr_lower_rate_unit + - result_ndr_lower_rate_value + - result_latency_forward_pdr_50_avg + - result_latency_forward_pdr_50_unit +# coverage-ndrpdr: +# path: str +# columns: +# - list diff --git a/csit.infra.dash/app/pal/debug.py b/csit.infra.dash/app/pal/debug.py new file mode 100644 index 0000000000..9d46d2a111 --- /dev/null +++ b/csit.infra.dash/app/pal/debug.py @@ -0,0 +1,48 @@ +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. + +"""Debug class. Only for internal debugging puproses. +""" + +import logging + +from data.data import Data +from utils.constants import Constants as C + + +logging.basicConfig( + format=u"%(asctime)s: %(levelname)s: %(message)s", + datefmt=u"%Y/%m/%d %H:%M:%S", + level=logging.INFO +) + +# Set the time period for data fetch +if C.TIME_PERIOD is None or C.TIME_PERIOD > C.MAX_TIME_PERIOD: + time_period = C.MAX_TIME_PERIOD +else: + time_period = C.TIME_PERIOD + +#data_mrr = Data( +# data_spec_file=C.DATA_SPEC_FILE, +# debug=True +#).read_trending_mrr(days=time_period) +# +#data_ndrpdr = Data( +# data_spec_file=C.DATA_SPEC_FILE, +# debug=True +#).read_trending_ndrpdr(days=time_period) + +data_list = Data( + data_spec_file=C.DATA_SPEC_FILE, + debug=True +).check_datasets(days=time_period) \ No newline at end of file diff --git a/csit.infra.dash/app/pal/news/__init__.py b/csit.infra.dash/app/pal/news/__init__.py new file mode 100644 index 0000000000..5692432123 --- /dev/null +++ b/csit.infra.dash/app/pal/news/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. diff --git a/csit.infra.dash/app/pal/news/layout.py b/csit.infra.dash/app/pal/news/layout.py new file mode 100644 index 0000000000..cd1618d719 --- /dev/null +++ b/csit.infra.dash/app/pal/news/layout.py @@ -0,0 +1,522 @@ +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. + +"""Plotly Dash HTML layout override. +""" + +import logging +import pandas as pd +import dash_bootstrap_components as dbc + +from flask import Flask +from dash import dcc +from dash import html +from dash import callback_context +from dash import Input, Output +from yaml import load, FullLoader, YAMLError + +from ..data.data import Data +from ..utils.constants import Constants as C +from ..utils.utils import classify_anomalies, show_tooltip, gen_new_url +from ..utils.url_processing import url_decode +from ..data.data import Data +from .tables import table_summary + + +class Layout: + """The layout of the dash app and the callbacks. + """ + + def __init__(self, app: Flask, html_layout_file: str, data_spec_file: str, + tooltip_file: str) -> None: + """Initialization: + - save the input parameters, + - read and pre-process the data, + - prepare data for the control panel, + - read HTML layout file, + - read tooltips from the tooltip file. + + :param app: Flask application running the dash application. + :param html_layout_file: Path and name of the file specifying the HTML + layout of the dash application. + :param data_spec_file: Path and name of the file specifying the data to + be read from parquets for this application. + :param tooltip_file: Path and name of the yaml file specifying the + tooltips. + :type app: Flask + :type html_layout_file: str + :type data_spec_file: str + :type tooltip_file: str + """ + + # Inputs + self._app = app + self._html_layout_file = html_layout_file + self._data_spec_file = data_spec_file + self._tooltip_file = tooltip_file + + # Read the data: + data_stats, data_mrr, data_ndrpdr = Data( + data_spec_file=self._data_spec_file, + debug=True + ).read_stats(days=C.NEWS_TIME_PERIOD) + + df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True) + + # Prepare information for the control panel: + self._jobs = sorted(list(df_tst_info["job"].unique())) + d_job_info = { + "job": list(), + "dut": list(), + "ttype": list(), + "cadence": list(), + "tbed": list() + } + for job in self._jobs: + lst_job = job.split("-") + d_job_info["job"].append(job) + d_job_info["dut"].append(lst_job[1]) + d_job_info["ttype"].append(lst_job[3]) + d_job_info["cadence"].append(lst_job[4]) + d_job_info["tbed"].append("-".join(lst_job[-2:])) + self.job_info = pd.DataFrame.from_dict(d_job_info) + + # Pre-process the data: + + def _create_test_name(test: str) -> str: + lst_tst = test.split(".") + suite = lst_tst[-2].replace("2n1l-", "").replace("1n1l-", "").\ + replace("2n-", "") + return f"{suite.split('-')[0]}-{lst_tst[-1]}" + + def _get_rindex(array: list, itm: any) -> int: + return len(array) - 1 - array[::-1].index(itm) + + tst_info = { + "job": list(), + "build": list(), + "start": list(), + "dut_type": list(), + "dut_version": list(), + "hosts": list(), + "failed": list(), + "regressions": list(), + "progressions": list() + } + for job in self._jobs: + # Create lists of failed tests: + df_job = df_tst_info.loc[(df_tst_info["job"] == job)] + last_build = str(max(pd.to_numeric(df_job["build"].unique()))) + df_build = df_job.loc[(df_job["build"] == last_build)] + tst_info["job"].append(job) + tst_info["build"].append(last_build) + tst_info["start"].append(data_stats.loc[ + (data_stats["job"] == job) & + (data_stats["build"] == last_build) + ]["start_time"].iloc[-1].strftime('%Y-%m-%d %H:%M')) + tst_info["dut_type"].append(df_build["dut_type"].iloc[-1]) + tst_info["dut_version"].append(df_build["dut_version"].iloc[-1]) + tst_info["hosts"].append(df_build["hosts"].iloc[-1]) + failed_tests = df_build.loc[(df_build["passed"] == False)]\ + ["test_id"].to_list() + l_failed = list() + try: + for tst in failed_tests: + l_failed.append(_create_test_name(tst)) + except KeyError: + l_failed = list() + tst_info["failed"].append(sorted(l_failed)) + + # Create lists of regressions and progressions: + l_reg = list() + l_prog = list() + + tests = df_job["test_id"].unique() + for test in tests: + tst_data = df_job.loc[df_job["test_id"] == test].sort_values( + by="start_time", ignore_index=True) + x_axis = tst_data["start_time"].tolist() + if "-ndrpdr" in test: + tst_data = tst_data.dropna( + subset=["result_pdr_lower_rate_value", ] + ) + if tst_data.empty: + continue + try: + anomalies, _, _ = classify_anomalies({ + k: v for k, v in zip( + x_axis, + tst_data["result_ndr_lower_rate_value"].tolist() + ) + }) + except ValueError: + continue + if "progression" in anomalies: + l_prog.append(( + _create_test_name(test).replace("-ndrpdr", "-ndr"), + x_axis[_get_rindex(anomalies, "progression")] + )) + if "regression" in anomalies: + l_reg.append(( + _create_test_name(test).replace("-ndrpdr", "-ndr"), + x_axis[_get_rindex(anomalies, "regression")] + )) + try: + anomalies, _, _ = classify_anomalies({ + k: v for k, v in zip( + x_axis, + tst_data["result_pdr_lower_rate_value"].tolist() + ) + }) + except ValueError: + continue + if "progression" in anomalies: + l_prog.append(( + _create_test_name(test).replace("-ndrpdr", "-pdr"), + x_axis[_get_rindex(anomalies, "progression")] + )) + if "regression" in anomalies: + l_reg.append(( + _create_test_name(test).replace("-ndrpdr", "-pdr"), + x_axis[_get_rindex(anomalies, "regression")] + )) + else: # mrr + tst_data = tst_data.dropna( + subset=["result_receive_rate_rate_avg", ] + ) + if tst_data.empty: + continue + try: + anomalies, _, _ = classify_anomalies({ + k: v for k, v in zip( + x_axis, + tst_data["result_receive_rate_rate_avg"].\ + tolist() + ) + }) + except ValueError: + continue + if "progression" in anomalies: + l_prog.append(( + _create_test_name(test), + x_axis[_get_rindex(anomalies, "progression")] + )) + if "regression" in anomalies: + l_reg.append(( + _create_test_name(test), + x_axis[_get_rindex(anomalies, "regression")] + )) + + tst_info["regressions"].append( + sorted(l_reg, key=lambda k: k[1], reverse=True)) + tst_info["progressions"].append( + sorted(l_prog, key=lambda k: k[1], reverse=True)) + + self._data = pd.DataFrame.from_dict(tst_info) + + # Read from files: + self._html_layout = str() + self._tooltips = dict() + + try: + with open(self._html_layout_file, "r") as file_read: + self._html_layout = file_read.read() + except IOError as err: + raise RuntimeError( + f"Not possible to open the file {self._html_layout_file}\n{err}" + ) + + try: + with open(self._tooltip_file, "r") as file_read: + self._tooltips = load(file_read, Loader=FullLoader) + except IOError as err: + logging.warning( + f"Not possible to open the file {self._tooltip_file}\n{err}" + ) + except YAMLError as err: + logging.warning( + f"An error occurred while parsing the specification file " + f"{self._tooltip_file}\n{err}" + ) + + self._default_period = C.NEWS_SHORT + self._default_active = (False, True, False) + self._default_table = \ + table_summary(self._data, self._jobs, self._default_period) + + # Callbacks: + if self._app is not None and hasattr(self, 'callbacks'): + self.callbacks(self._app) + + @property + def html_layout(self) -> dict: + return self._html_layout + + def add_content(self): + """Top level method which generated the web page. + + It generates: + - Store for user input data, + - Navigation bar, + - Main area with control panel and ploting area. + + If no HTML layout is provided, an error message is displayed instead. + + :returns: The HTML div with the whole page. + :rtype: html.Div + """ + + if self.html_layout: + return html.Div( + id="div-main", + className="small", + children=[ + dcc.Location(id="url", refresh=False), + dbc.Row( + id="row-navbar", + class_name="g-0", + children=[ + self._add_navbar(), + ] + ), + dbc.Row( + id="row-main", + class_name="g-0", + children=[ + self._add_ctrl_col(), + self._add_plotting_col(), + ] + ) + ] + ) + else: + return html.Div( + id="div-main-error", + children=[ + dbc.Alert( + [ + "An Error Occured", + ], + color="danger", + ), + ] + ) + + def _add_navbar(self): + """Add nav element with navigation panel. It is placed on the top. + + :returns: Navigation bar. + :rtype: dbc.NavbarSimple + """ + + return dbc.NavbarSimple( + id="navbarsimple-main", + children=[ + dbc.NavItem( + dbc.NavLink( + "Continuous Performance News", + disabled=True, + external_link=True, + href="#" + ) + ) + ], + brand="Dashboard", + brand_href="/", + brand_external_link=True, + class_name="p-2", + fluid=True, + ) + + def _add_ctrl_col(self) -> dbc.Col: + """Add column with control panel. It is placed on the left side. + + :returns: Column with the control panel. + :rtype: dbc.Col + """ + + return dbc.Col( + id="col-controls", + children=[ + self._add_ctrl_panel(), + ], + ) + + def _add_plotting_col(self) -> dbc.Col: + """Add column with tables. It is placed on the right side. + + :returns: Column with tables. + :rtype: dbc.Col + """ + + return dbc.Col( + id="col-plotting-area", + children=[ + dcc.Loading( + children=[ + dbc.Row( # Failed tests + id="row-table", + class_name="g-0 p-2", + children=self._default_table + ), + dbc.Row( + class_name="g-0 p-2", + align="center", + justify="start", + children=[ + dbc.InputGroup( + class_name="me-1", + children=[ + dbc.InputGroupText( + style=C.URL_STYLE, + children=show_tooltip( + self._tooltips, + "help-url", "URL", + "input-url" + ) + ), + dbc.Input( + id="input-url", + readonly=True, + type="url", + style=C.URL_STYLE, + value="" + ) + ] + ) + ] + ) + ] + ) + ], + width=9, + ) + + def _add_ctrl_panel(self) -> dbc.Row: + """Add control panel. + + :returns: Control panel. + :rtype: dbc.Row + """ + return dbc.Row( + id="row-ctrl-panel", + class_name="g-0", + children=[ + dbc.Row( + class_name="g-0 p-2", + children=[ + dbc.Row( + class_name="g-0", + children=[ + dbc.Label( + class_name="g-0", + children=show_tooltip(self._tooltips, + "help-summary-period", "Window") + ), + dbc.Row( + dbc.ButtonGroup( + id="bg-time-period", + class_name="g-0", + children=[ + dbc.Button( + id="period-last", + children="Last Run", + className="me-1", + outline=True, + color="info" + ), + dbc.Button( + id="period-short", + children=\ + f"Last {C.NEWS_SHORT} Runs", + className="me-1", + outline=True, + active=True, + color="info" + ), + dbc.Button( + id="period-long", + children="All Runs", + className="me-1", + outline=True, + color="info" + ) + ] + ) + ) + ] + ) + ] + ) + ] + ) + + def callbacks(self, app): + """Callbacks for the whole application. + + :param app: The application. + :type app: Flask + """ + + @app.callback( + Output("row-table", "children"), + Output("input-url", "value"), + Output("period-last", "active"), + Output("period-short", "active"), + Output("period-long", "active"), + Input("period-last", "n_clicks"), + Input("period-short", "n_clicks"), + Input("period-long", "n_clicks"), + Input("url", "href") + ) + def _update_application(btn_last: int, btn_short: int, btn_long: int, + href: str) -> tuple: + """Update the application when the event is detected. + + :returns: New values for web page elements. + :rtype: tuple + """ + + _, _, _ = btn_last, btn_short, btn_long + + periods = { + "period-last": C.NEWS_LAST, + "period-short": C.NEWS_SHORT, + "period-long": C.NEWS_LONG + } + actives = { + "period-last": (True, False, False), + "period-short": (False, True, False), + "period-long": (False, False, True) + } + + # Parse the url: + parsed_url = url_decode(href) + if parsed_url: + url_params = parsed_url["params"] + else: + url_params = None + + trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0] + if trigger_id == "url" and url_params: + trigger_id = url_params.get("period", list())[0] + + period = periods.get(trigger_id, self._default_period) + active = actives.get(trigger_id, self._default_active) + + ret_val = [ + table_summary(self._data, self._jobs, period), + gen_new_url(parsed_url, {"period": trigger_id}) + ] + ret_val.extend(active) + return ret_val diff --git a/csit.infra.dash/app/pal/news/news.py b/csit.infra.dash/app/pal/news/news.py new file mode 100644 index 0000000000..a0d05f1483 --- /dev/null +++ b/csit.infra.dash/app/pal/news/news.py @@ -0,0 +1,46 @@ +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. + +"""Instantiate the News Dash application. +""" +import dash + +from ..utils.constants import Constants as C +from .layout import Layout + + +def init_news(server): + """Create a Plotly Dash dashboard. + + :param server: Flask server. + :type server: Flask + :returns: Dash app server. + :rtype: Dash + """ + + dash_app = dash.Dash( + server=server, + routes_pathname_prefix=C.NEWS_ROUTES_PATHNAME_PREFIX, + external_stylesheets=C.EXTERNAL_STYLESHEETS + ) + + layout = Layout( + app=dash_app, + html_layout_file=C.NEWS_HTML_LAYOUT_FILE, + data_spec_file=C.DATA_SPEC_FILE, + tooltip_file=C.TOOLTIP_FILE, + ) + dash_app.index_string = layout.html_layout + dash_app.layout = layout.add_content() + + return dash_app.server diff --git a/csit.infra.dash/app/pal/news/tables.py b/csit.infra.dash/app/pal/news/tables.py new file mode 100644 index 0000000000..7c0cc66eda --- /dev/null +++ b/csit.infra.dash/app/pal/news/tables.py @@ -0,0 +1,176 @@ +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. + +"""The tables with news. +""" + +import pandas as pd +import dash_bootstrap_components as dbc + +from datetime import datetime, timedelta + + +def _table_info(job_data: pd.DataFrame) -> dbc.Table: + """Generates table with info about the job. + + :param job_data: Dataframe with information about the job. + :type job_data: pandas.DataFrame + :returns: Table with job info. + :rtype: dbc.Table + """ + return dbc.Table.from_dataframe( + pd.DataFrame.from_dict( + { + "Job": job_data["job"], + "Last Build": job_data["build"], + "Date": job_data["start"], + "DUT": job_data["dut_type"], + "DUT Version": job_data["dut_version"], + "Hosts": ", ".join(job_data["hosts"].to_list()[0]) + } + ), + bordered=True, + striped=True, + hover=True, + size="sm", + color="info" + ) + + +def _table_failed(job_data: pd.DataFrame, failed: list) -> dbc.Table: + """Generates table with failed tests from the last run of the job. + + :param job_data: Dataframe with information about the job. + :param failed: List of failed tests. + :type job_data: pandas.DataFrame + :type failed: list + :returns: Table with fialed tests. + :rtype: dbc.Table + """ + return dbc.Table.from_dataframe( + pd.DataFrame.from_dict( + { + ( + f"Last Failed Tests on " + f"{job_data['start'].values[0]} ({len(failed)})" + ): failed + } + ), + bordered=True, + striped=True, + hover=True, + size="sm", + color="danger" + ) + + +def _table_gressions(itms: dict, color: str) -> dbc.Table: + """Generates table with regressions. + + :param itms: Dictionary with items (regressions or progressions) and their + last occurence. + :param color: Color of the table. + :type regressions: dict + :type color: str + :returns: The table with regressions. + :rtype: dbc.Table + """ + return dbc.Table.from_dataframe( + pd.DataFrame.from_dict(itms), + bordered=True, + striped=True, + hover=True, + size="sm", + color=color + ) + + +def table_news(data: pd.DataFrame, job: str, period: int) -> list: + """Generates the tables with news: + 1. Falied tests from the last run + 2. Regressions and progressions calculated from the last C.NEWS_TIME_PERIOD + days. + + :param data: Trending data with calculated annomalies to be displayed in the + tables. + :param job: The job name. + :param period: The time period (nr of days from now) taken into account. + :type data: pandas.DataFrame + :type job: str + :type period: int + :returns: List of tables. + :rtype: list + """ + + last_day = datetime.utcnow() - timedelta(days=period) + r_list = list() + job_data = data.loc[(data["job"] == job)] + r_list.append(_table_info(job_data)) + + failed = job_data["failed"].to_list()[0] + if failed: + r_list.append(_table_failed(job_data, failed)) + + title = f"Regressions in the last {period} days" + regressions = {title: list(), "Last Regression": list()} + for itm in job_data["regressions"].to_list()[0]: + if itm[1] < last_day: + break + regressions[title].append(itm[0]) + regressions["Last Regression"].append( + itm[1].strftime('%Y-%m-%d %H:%M')) + if regressions["Last Regression"]: + r_list.append(_table_gressions(regressions, "warning")) + + title = f"Progressions in the last {period} days" + progressions = {title: list(), "Last Progression": list()} + for itm in job_data["progressions"].to_list()[0]: + if itm[1] < last_day: + break + progressions[title].append(itm[0]) + progressions["Last Progression"].append( + itm[1].strftime('%Y-%m-%d %H:%M')) + if progressions["Last Progression"]: + r_list.append(_table_gressions(progressions, "success")) + + return r_list + + +def table_summary(data: pd.DataFrame, jobs: list, period: int) -> list: + """Generates summary (failed tests, regressions and progressions) from the + last week. + + :param data: Trending data with calculated annomalies to be displayed in the + tables. + :param jobs: List of jobs. + :params period: The time period for the summary table. + :type data: pandas.DataFrame + :type job: str + :type period: int + :returns: List of tables. + :rtype: list + """ + + return [ + dbc.Accordion( + children=[ + dbc.AccordionItem( + title=job, + children=table_news(data, job, period) + ) for job in jobs + ], + class_name="gy-2 p-0", + start_collapsed=True, + always_open=True + ) + ] diff --git a/csit.infra.dash/app/pal/report/__init__.py b/csit.infra.dash/app/pal/report/__init__.py new file mode 100644 index 0000000000..5692432123 --- /dev/null +++ b/csit.infra.dash/app/pal/report/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. diff --git a/csit.infra.dash/app/pal/report/graphs.py b/csit.infra.dash/app/pal/report/graphs.py new file mode 100644 index 0000000000..36f28d09e8 --- /dev/null +++ b/csit.infra.dash/app/pal/report/graphs.py @@ -0,0 +1,275 @@ +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. + +""" +""" + +import re +import plotly.graph_objects as go +import pandas as pd + +from copy import deepcopy + +from ..utils.constants import Constants as C +from ..utils.utils import get_color + + +def get_short_version(version: str, dut_type: str="vpp") -> str: + """Returns the short version of DUT without build number. + + :param version: Original version string. + :param dut_type: DUT type. + :type version: str + :type dut_type: str + :returns: Short verion string. + :rtype: str + """ + + if dut_type in ("trex", "dpdk"): + return version + + s_version = str() + groups = re.search( + pattern=re.compile(r"^(\d{2}).(\d{2})-(rc0|rc1|rc2|release$)"), + string=version + ) + if groups: + try: + s_version = \ + f"{groups.group(1)}.{groups.group(2)}.{groups.group(3)}".\ + replace("release", "rls") + except IndexError: + pass + + return s_version + + +def select_iterative_data(data: pd.DataFrame, itm:dict) -> pd.DataFrame: + """Select the data for graphs and tables from the provided data frame. + + :param data: Data frame with data for graphs and tables. + :param itm: Item (in this case job name) which data will be selected from + the input data frame. + :type data: pandas.DataFrame + :type itm: str + :returns: A data frame with selected data. + :rtype: pandas.DataFrame + """ + + phy = itm["phy"].split("-") + if len(phy) == 4: + topo, arch, nic, drv = phy + if drv == "dpdk": + drv = "" + else: + drv += "-" + drv = drv.replace("_", "-") + else: + return None + + core = str() if itm["dut"] == "trex" else f"{itm['core']}" + ttype = "ndrpdr" if itm["testtype"] in ("ndr", "pdr") else itm["testtype"] + dut_v100 = "none" if itm["dut"] == "trex" else itm["dut"] + dut_v101 = itm["dut"] + + df = data.loc[( + (data["release"] == itm["rls"]) & + ( + ( + (data["version"] == "1.0.0") & + (data["dut_type"].str.lower() == dut_v100) + ) | + ( + (data["version"] == "1.0.1") & + (data["dut_type"].str.lower() == dut_v101) + ) + ) & + (data["test_type"] == ttype) & + (data["passed"] == True) + )] + regex_test = \ + f"^.*[.|-]{nic}.*{itm['framesize']}-{core}-{drv}{itm['test']}-{ttype}$" + df = df[ + (df.job.str.endswith(f"{topo}-{arch}")) & + (df.dut_version.str.contains(itm["dutver"].replace(".r", "-r").\ + replace("rls", "release"))) & + (df.test_id.str.contains(regex_test, regex=True)) + ] + + return df + + +def graph_iterative(data: pd.DataFrame, sel:dict, layout: dict, + normalize: bool) -> tuple: + """Generate the statistical box graph with iterative data (MRR, NDR and PDR, + for PDR also Latencies). + + :param data: Data frame with iterative data. + :param sel: Selected tests. + :param layout: Layout of plot.ly graph. + :param normalize: If True, the data is normalized to CPU frquency + Constants.NORM_FREQUENCY. + :param data: pandas.DataFrame + :param sel: dict + :param layout: dict + :param normalize: bool + :returns: Tuple of graphs - throughput and latency. + :rtype: tuple(plotly.graph_objects.Figure, plotly.graph_objects.Figure) + """ + + fig_tput = None + fig_lat = None + + tput_traces = list() + y_tput_max = 0 + lat_traces = list() + y_lat_max = 0 + x_lat = list() + show_latency = False + show_tput = False + for idx, itm in enumerate(sel): + itm_data = select_iterative_data(data, itm) + if itm_data.empty: + continue + phy = itm["phy"].split("-") + topo_arch = f"{phy[0]}-{phy[1]}" if len(phy) == 4 else str() + norm_factor = (C.NORM_FREQUENCY / C.FREQUENCY[topo_arch]) \ + if normalize else 1.0 + if itm["testtype"] == "mrr": + y_data_raw = itm_data[C.VALUE_ITER[itm["testtype"]]].to_list()[0] + y_data = [(y * norm_factor) for y in y_data_raw] + if len(y_data) > 0: + y_tput_max = \ + max(y_data) if max(y_data) > y_tput_max else y_tput_max + else: + y_data_raw = itm_data[C.VALUE_ITER[itm["testtype"]]].to_list() + y_data = [(y * norm_factor) for y in y_data_raw] + if y_data: + y_tput_max = \ + max(y_data) if max(y_data) > y_tput_max else y_tput_max + nr_of_samples = len(y_data) + tput_kwargs = dict( + y=y_data, + name=( + f"{idx + 1}. " + f"({nr_of_samples:02d} " + f"run{'s' if nr_of_samples > 1 else ''}) " + f"{itm['id']}" + ), + hoverinfo=u"y+name", + boxpoints="all", + jitter=0.3, + marker=dict(color=get_color(idx)) + ) + tput_traces.append(go.Box(**tput_kwargs)) + show_tput = True + + if itm["testtype"] == "pdr": + y_lat_row = itm_data[C.VALUE_ITER["pdr-lat"]].to_list() + y_lat = [(y / norm_factor) for y in y_lat_row] + if y_lat: + y_lat_max = max(y_lat) if max(y_lat) > y_lat_max else y_lat_max + nr_of_samples = len(y_lat) + lat_kwargs = dict( + y=y_lat, + name=( + f"{idx + 1}. " + f"({nr_of_samples:02d} " + f"run{u's' if nr_of_samples > 1 else u''}) " + f"{itm['id']}" + ), + hoverinfo="all", + boxpoints="all", + jitter=0.3, + marker=dict(color=get_color(idx)) + ) + x_lat.append(idx + 1) + lat_traces.append(go.Box(**lat_kwargs)) + show_latency = True + else: + lat_traces.append(go.Box()) + + if show_tput: + pl_tput = deepcopy(layout["plot-throughput"]) + pl_tput["xaxis"]["tickvals"] = [i for i in range(len(sel))] + pl_tput["xaxis"]["ticktext"] = [str(i + 1) for i in range(len(sel))] + if y_tput_max: + pl_tput["yaxis"]["range"] = [0, (int(y_tput_max / 1e6) + 1) * 1e6] + fig_tput = go.Figure(data=tput_traces, layout=pl_tput) + + if show_latency: + pl_lat = deepcopy(layout["plot-latency"]) + pl_lat["xaxis"]["tickvals"] = [i for i in range(len(x_lat))] + pl_lat["xaxis"]["ticktext"] = x_lat + if y_lat_max: + pl_lat["yaxis"]["range"] = [0, (int(y_lat_max / 10) + 1) * 10] + fig_lat = go.Figure(data=lat_traces, layout=pl_lat) + + return fig_tput, fig_lat + + +def table_comparison(data: pd.DataFrame, sel:dict, + normalize: bool) -> pd.DataFrame: + """Generate the comparison table with selected tests. + + :param data: Data frame with iterative data. + :param sel: Selected tests. + :param normalize: If True, the data is normalized to CPU frquency + Constants.NORM_FREQUENCY. + :param data: pandas.DataFrame + :param sel: dict + :param normalize: bool + :returns: Comparison table. + :rtype: pandas.DataFrame + """ + table = pd.DataFrame( + # { + # "Test Case": [ + # "64b-2t1c-avf-eth-l2xcbase-eth-2memif-1dcr", + # "64b-2t1c-avf-eth-l2xcbase-eth-2vhostvr1024-1vm-vppl2xc", + # "64b-2t1c-avf-ethip4udp-ip4base-iacl50sl-10kflows", + # "78b-2t1c-avf-ethip6-ip6scale2m-rnd "], + # "2106.0-8": [ + # "14.45 +- 0.08", + # "9.63 +- 0.05", + # "9.7 +- 0.02", + # "8.95 +- 0.06"], + # "2110.0-8": [ + # "14.45 +- 0.08", + # "9.63 +- 0.05", + # "9.7 +- 0.02", + # "8.95 +- 0.06"], + # "2110.0-9": [ + # "14.45 +- 0.08", + # "9.63 +- 0.05", + # "9.7 +- 0.02", + # "8.95 +- 0.06"], + # "2202.0-9": [ + # "14.45 +- 0.08", + # "9.63 +- 0.05", + # "9.7 +- 0.02", + # "8.95 +- 0.06"], + # "2110.0-9 vs 2110.0-8": [ + # "-0.23 +- 0.62", + # "-1.37 +- 1.3", + # "+0.08 +- 0.2", + # "-2.16 +- 0.83"], + # "2202.0-9 vs 2110.0-9": [ + # "+6.95 +- 0.72", + # "+5.35 +- 1.26", + # "+4.48 +- 1.48", + # "+4.09 +- 0.95"] + # } + ) + + return table diff --git a/csit.infra.dash/app/pal/report/layout.py b/csit.infra.dash/app/pal/report/layout.py new file mode 100644 index 0000000000..a556871084 --- /dev/null +++ b/csit.infra.dash/app/pal/report/layout.py @@ -0,0 +1,1494 @@ +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. + +"""Plotly Dash HTML layout override. +""" + +import logging +import pandas as pd +import dash_bootstrap_components as dbc + +from flask import Flask +from dash import dcc +from dash import html +from dash import callback_context, no_update, ALL +from dash import Input, Output, State +from dash.exceptions import PreventUpdate +from yaml import load, FullLoader, YAMLError +from copy import deepcopy +from ast import literal_eval + +from ..utils.constants import Constants as C +from ..utils.utils import show_tooltip, label, sync_checklists, list_tests, \ + gen_new_url, generate_options +from ..utils.url_processing import url_decode +from ..data.data import Data +from .graphs import graph_iterative, table_comparison, get_short_version, \ + select_iterative_data + + +class Layout: + """The layout of the dash app and the callbacks. + """ + + def __init__(self, app: Flask, releases: list, html_layout_file: str, + graph_layout_file: str, data_spec_file: str, tooltip_file: str) -> None: + """Initialization: + - save the input parameters, + - read and pre-process the data, + - prepare data for the control panel, + - read HTML layout file, + - read tooltips from the tooltip file. + + :param app: Flask application running the dash application. + :param releases: Lis of releases to be displayed. + :param html_layout_file: Path and name of the file specifying the HTML + layout of the dash application. + :param graph_layout_file: Path and name of the file with layout of + plot.ly graphs. + :param data_spec_file: Path and name of the file specifying the data to + be read from parquets for this application. + :param tooltip_file: Path and name of the yaml file specifying the + tooltips. + :type app: Flask + :type releases: list + :type html_layout_file: str + :type graph_layout_file: str + :type data_spec_file: str + :type tooltip_file: str + """ + + # Inputs + self._app = app + self.releases = releases + self._html_layout_file = html_layout_file + self._graph_layout_file = graph_layout_file + self._data_spec_file = data_spec_file + self._tooltip_file = tooltip_file + + # Read the data: + self._data = pd.DataFrame() + for rls in releases: + data_mrr = Data(self._data_spec_file, True).\ + read_iterative_mrr(release=rls.replace("csit", "rls")) + data_mrr["release"] = rls + data_ndrpdr = Data(self._data_spec_file, True).\ + read_iterative_ndrpdr(release=rls.replace("csit", "rls")) + data_ndrpdr["release"] = rls + self._data = pd.concat( + [self._data, data_mrr, data_ndrpdr], ignore_index=True) + + # Get structure of tests: + tbs = dict() + cols = ["job", "test_id", "test_type", "dut_version", "release"] + for _, row in self._data[cols].drop_duplicates().iterrows(): + rls = row["release"] + ttype = row["test_type"] + lst_job = row["job"].split("-") + dut = lst_job[1] + d_ver = get_short_version(row["dut_version"], dut) + tbed = "-".join(lst_job[-2:]) + lst_test_id = row["test_id"].split(".") + if dut == "dpdk": + area = "dpdk" + else: + area = "-".join(lst_test_id[3:-2]) + suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\ + replace("2n-", "") + test = lst_test_id[-1] + nic = suite.split("-")[0] + for drv in C.DRIVERS: + if drv in test: + driver = drv.replace("-", "_") + test = test.replace(f"{drv}-", "") + break + else: + driver = "dpdk" + infra = "-".join((tbed, nic, driver)) + lst_test = test.split("-") + framesize = lst_test[0] + core = lst_test[1] if lst_test[1] else "8C" + test = "-".join(lst_test[2: -1]) + + if tbs.get(rls, None) is None: + tbs[rls] = dict() + if tbs[rls].get(dut, None) is None: + tbs[rls][dut] = dict() + if tbs[rls][dut].get(d_ver, None) is None: + tbs[rls][dut][d_ver] = dict() + if tbs[rls][dut][d_ver].get(infra, None) is None: + tbs[rls][dut][d_ver][infra] = dict() + if tbs[rls][dut][d_ver][infra].get(area, None) is None: + tbs[rls][dut][d_ver][infra][area] = dict() + if tbs[rls][dut][d_ver][infra][area].get(test, None) is None: + tbs[rls][dut][d_ver][infra][area][test] = dict() + tbs[rls][dut][d_ver][infra][area][test]["core"] = list() + tbs[rls][dut][d_ver][infra][area][test]["frame-size"] = list() + tbs[rls][dut][d_ver][infra][area][test]["test-type"] = list() + if core.upper() not in \ + tbs[rls][dut][d_ver][infra][area][test]["core"]: + tbs[rls][dut][d_ver][infra][area][test]["core"].append( + core.upper()) + if framesize.upper() not in \ + tbs[rls][dut][d_ver][infra][area][test]["frame-size"]: + tbs[rls][dut][d_ver][infra][area][test]["frame-size"].append( + framesize.upper()) + if ttype == "mrr": + if "MRR" not in \ + tbs[rls][dut][d_ver][infra][area][test]["test-type"]: + tbs[rls][dut][d_ver][infra][area][test]["test-type"].append( + "MRR") + elif ttype == "ndrpdr": + if "NDR" not in \ + tbs[rls][dut][d_ver][infra][area][test]["test-type"]: + tbs[rls][dut][d_ver][infra][area][test]["test-type"].extend( + ("NDR", "PDR", )) + self._spec_tbs = tbs + + # Read from files: + self._html_layout = "" + self._graph_layout = None + self._tooltips = dict() + + try: + with open(self._html_layout_file, "r") as file_read: + self._html_layout = file_read.read() + except IOError as err: + raise RuntimeError( + f"Not possible to open the file {self._html_layout_file}\n{err}" + ) + + try: + with open(self._graph_layout_file, "r") as file_read: + self._graph_layout = load(file_read, Loader=FullLoader) + except IOError as err: + raise RuntimeError( + f"Not possible to open the file {self._graph_layout_file}\n" + f"{err}" + ) + except YAMLError as err: + raise RuntimeError( + f"An error occurred while parsing the specification file " + f"{self._graph_layout_file}\n{err}" + ) + + try: + with open(self._tooltip_file, "r") as file_read: + self._tooltips = load(file_read, Loader=FullLoader) + except IOError as err: + logging.warning( + f"Not possible to open the file {self._tooltip_file}\n{err}" + ) + except YAMLError as err: + logging.warning( + f"An error occurred while parsing the specification file " + f"{self._tooltip_file}\n{err}" + ) + + # Callbacks: + if self._app is not None and hasattr(self, 'callbacks'): + self.callbacks(self._app) + + @property + def html_layout(self): + return self._html_layout + + @property + def spec_tbs(self): + return self._spec_tbs + + @property + def data(self): + return self._data + + @property + def layout(self): + return self._graph_layout + + def add_content(self): + """Top level method which generated the web page. + + It generates: + - Store for user input data, + - Navigation bar, + - Main area with control panel and ploting area. + + If no HTML layout is provided, an error message is displayed instead. + + :returns: The HTML div with the whole page. + :rtype: html.Div + """ + + if self.html_layout and self.spec_tbs: + return html.Div( + id="div-main", + className="small", + children=[ + dbc.Row( + id="row-navbar", + class_name="g-0", + children=[ + self._add_navbar(), + ] + ), + dcc.Loading( + dbc.Offcanvas( + class_name="w-50", + id="offcanvas-metadata", + title="Throughput And Latency", + placement="end", + is_open=False, + children=[ + dbc.Row(id="metadata-tput-lat"), + dbc.Row(id="metadata-hdrh-graph"), + ] + ) + ), + dbc.Row( + id="row-main", + class_name="g-0", + children=[ + dcc.Store(id="selected-tests"), + dcc.Store(id="control-panel"), + dcc.Location(id="url", refresh=False), + self._add_ctrl_col(), + self._add_plotting_col(), + ] + ) + ] + ) + else: + return html.Div( + id="div-main-error", + children=[ + dbc.Alert( + [ + "An Error Occured", + ], + color="danger", + ), + ] + ) + + def _add_navbar(self): + """Add nav element with navigation panel. It is placed on the top. + + :returns: Navigation bar. + :rtype: dbc.NavbarSimple + """ + return dbc.NavbarSimple( + id="navbarsimple-main", + children=[ + dbc.NavItem( + dbc.NavLink( + "Iterative Test Runs", + disabled=True, + external_link=True, + href="#" + ) + ) + ], + brand="Dashboard", + brand_href="/", + brand_external_link=True, + class_name="p-2", + fluid=True, + ) + + def _add_ctrl_col(self) -> dbc.Col: + """Add column with controls. It is placed on the left side. + + :returns: Column with the control panel. + :rtype: dbc.Col + """ + return dbc.Col( + id="col-controls", + children=[ + self._add_ctrl_panel(), + ], + ) + + def _add_plotting_col(self) -> dbc.Col: + """Add column with plots and tables. It is placed on the right side. + + :returns: Column with tables. + :rtype: dbc.Col + """ + return dbc.Col( + id="col-plotting-area", + children=[ + dcc.Loading( + children=[ + dbc.Row( # Graphs + class_name="g-0 p-2", + children=[ + dbc.Col( + dbc.Row( # Throughput + id="row-graph-tput", + class_name="g-0 p-2", + children=[C.PLACEHOLDER, ] + ), + width=6 + ), + dbc.Col( + dbc.Row( # Latency + id="row-graph-lat", + class_name="g-0 p-2", + children=[C.PLACEHOLDER, ] + ), + width=6 + ) + ] + ), + dbc.Row( # Tables + id="row-table", + class_name="g-0 p-2", + children=[C.PLACEHOLDER, ] + ), + dbc.Row( # Download + id="row-btn-download", + class_name="g-0 p-2", + children=[C.PLACEHOLDER, ] + ) + ] + ) + ], + width=9 + ) + + def _add_ctrl_panel(self) -> dbc.Row: + """Add control panel. + + :returns: Control panel. + :rtype: dbc.Row + """ + return dbc.Row( + id="row-ctrl-panel", + class_name="g-0 p-2", + children=[ + dbc.Row( + class_name="g-0", + children=[ + dbc.InputGroup( + [ + dbc.InputGroupText( + children=show_tooltip(self._tooltips, + "help-release", "CSIT Release") + ), + dbc.Select( + id="dd-ctrl-rls", + placeholder=("Select a Release..."), + options=sorted( + [ + {"label": k, "value": k} \ + for k in self.spec_tbs.keys() + ], + key=lambda d: d["label"] + ) + ) + ], + class_name="mb-3", + size="sm", + ), + ] + ), + dbc.Row( + class_name="g-0", + children=[ + dbc.InputGroup( + [ + dbc.InputGroupText( + children=show_tooltip(self._tooltips, + "help-dut", "DUT") + ), + dbc.Select( + id="dd-ctrl-dut", + placeholder=( + "Select a Device under Test..." + ) + ) + ], + class_name="mb-3", + size="sm", + ), + ] + ), + dbc.Row( + class_name="g-0", + children=[ + dbc.InputGroup( + [ + dbc.InputGroupText( + children=show_tooltip(self._tooltips, + "help-dut-ver", "DUT Version") + ), + dbc.Select( + id="dd-ctrl-dutver", + placeholder=( + "Select a Version of " + "Device under Test..." + ) + ) + ], + class_name="mb-3", + size="sm", + ), + ] + ), + dbc.Row( + class_name="g-0", + children=[ + dbc.InputGroup( + [ + dbc.InputGroupText( + children=show_tooltip(self._tooltips, + "help-infra", "Infra") + ), + dbc.Select( + id="dd-ctrl-phy", + placeholder=( + "Select a Physical Test Bed " + "Topology..." + ) + ) + ], + class_name="mb-3", + size="sm", + ), + ] + ), + dbc.Row( + class_name="g-0", + children=[ + dbc.InputGroup( + [ + dbc.InputGroupText( + children=show_tooltip(self._tooltips, + "help-area", "Area") + ), + dbc.Select( + id="dd-ctrl-area", + placeholder="Select an Area...", + disabled=True, + ), + ], + class_name="mb-3", + size="sm", + ), + ] + ), + dbc.Row( + class_name="g-0", + children=[ + dbc.InputGroup( + [ + dbc.InputGroupText( + children=show_tooltip(self._tooltips, + "help-test", "Test") + ), + dbc.Select( + id="dd-ctrl-test", + placeholder="Select a Test...", + disabled=True, + ), + ], + class_name="mb-3", + size="sm", + ), + ] + ), + dbc.Row( + id="row-ctrl-framesize", + class_name="gy-1", + children=[ + dbc.Label( + children=show_tooltip(self._tooltips, + "help-framesize", "Frame Size"), + class_name="p-0" + ), + dbc.Col( + children=[ + dbc.Checklist( + id="cl-ctrl-framesize-all", + options=C.CL_ALL_DISABLED, + inline=True, + switch=False + ), + ], + width=3 + ), + dbc.Col( + children=[ + dbc.Checklist( + id="cl-ctrl-framesize", + inline=True, + switch=False + ) + ] + ) + ] + ), + dbc.Row( + id="row-ctrl-core", + class_name="gy-1", + children=[ + dbc.Label( + children=show_tooltip(self._tooltips, + "help-cores", "Number of Cores"), + class_name="p-0" + ), + dbc.Col( + children=[ + dbc.Checklist( + id="cl-ctrl-core-all", + options=C.CL_ALL_DISABLED, + inline=False, + switch=False + ) + ], + width=3 + ), + dbc.Col( + children=[ + dbc.Checklist( + id="cl-ctrl-core", + inline=True, + switch=False + ) + ] + ) + ] + ), + dbc.Row( + id="row-ctrl-testtype", + class_name="gy-1", + children=[ + dbc.Label( + children=show_tooltip(self._tooltips, + "help-ttype", "Test Type"), + class_name="p-0" + ), + dbc.Col( + children=[ + dbc.Checklist( + id="cl-ctrl-testtype-all", + options=C.CL_ALL_DISABLED, + inline=True, + switch=False + ), + ], + width=3 + ), + dbc.Col( + children=[ + dbc.Checklist( + id="cl-ctrl-testtype", + inline=True, + switch=False + ) + ] + ) + ] + ), + dbc.Row( + id="row-ctrl-normalize", + class_name="gy-1", + children=[ + dbc.Label( + children=show_tooltip(self._tooltips, + "help-normalize", "Normalize"), + class_name="p-0" + ), + dbc.Col( + children=[ + dbc.Checklist( + id="cl-ctrl-normalize", + options=[{ + "value": "normalize", + "label": ( + "Normalize results to CPU" + "frequency 2GHz" + ) + }], + value=[], + inline=True, + switch=False + ), + ] + ) + ] + ), + dbc.Row( + class_name="gy-1 p-0", + children=[ + dbc.ButtonGroup( + [ + dbc.Button( + id="btn-ctrl-add", + children="Add Selected", + class_name="me-1", + color="info" + ) + ] + ) + ] + ), + dbc.Row( + id="row-card-sel-tests", + class_name="gy-1", + style=C.STYLE_DISABLED, + children=[ + dbc.Label( + "Selected tests", + class_name="p-0" + ), + dbc.Checklist( + class_name="overflow-auto", + id="cl-selected", + options=[], + inline=False, + style={"max-height": "20em"}, + ) + ], + ), + dbc.Row( + id="row-btns-sel-tests", + style=C.STYLE_DISABLED, + children=[ + dbc.ButtonGroup( + class_name="gy-2", + children=[ + dbc.Button( + id="btn-sel-remove", + children="Remove Selected", + class_name="w-100 me-1", + color="info", + disabled=False + ), + dbc.Button( + id="btn-sel-remove-all", + children="Remove All", + class_name="w-100 me-1", + color="info", + disabled=False + ), + ] + ) + ] + ), + ] + ) + + class ControlPanel: + """A class representing the control panel. + """ + + def __init__(self, panel: dict) -> None: + """Initialisation of the control pannel by default values. If + particular values are provided (parameter "panel") they are set + afterwards. + + :param panel: Custom values to be set to the control panel. + :param default: Default values to be set to the control panel. + :type panel: dict + :type defaults: dict + """ + + # Defines also the order of keys + self._defaults = { + "dd-rls-value": str(), + "dd-dut-options": list(), + "dd-dut-disabled": True, + "dd-dut-value": str(), + "dd-dutver-options": list(), + "dd-dutver-disabled": True, + "dd-dutver-value": str(), + "dd-phy-options": list(), + "dd-phy-disabled": True, + "dd-phy-value": str(), + "dd-area-options": list(), + "dd-area-disabled": True, + "dd-area-value": str(), + "dd-test-options": list(), + "dd-test-disabled": True, + "dd-test-value": str(), + "cl-core-options": list(), + "cl-core-value": list(), + "cl-core-all-value": list(), + "cl-core-all-options": C.CL_ALL_DISABLED, + "cl-framesize-options": list(), + "cl-framesize-value": list(), + "cl-framesize-all-value": list(), + "cl-framesize-all-options": C.CL_ALL_DISABLED, + "cl-testtype-options": list(), + "cl-testtype-value": list(), + "cl-testtype-all-value": list(), + "cl-testtype-all-options": C.CL_ALL_DISABLED, + "btn-add-disabled": True, + "cl-normalize-value": list(), + "cl-selected-options": list() + } + + self._panel = deepcopy(self._defaults) + if panel: + for key in self._defaults: + self._panel[key] = panel[key] + + @property + def defaults(self) -> dict: + return self._defaults + + @property + def panel(self) -> dict: + return self._panel + + def set(self, kwargs: dict) -> None: + """Set the values of the Control panel. + + :param kwargs: key - value pairs to be set. + :type kwargs: dict + :raises KeyError: If the key in kwargs is not present in the Control + panel. + """ + for key, val in kwargs.items(): + if key in self._panel: + self._panel[key] = val + else: + raise KeyError(f"The key {key} is not defined.") + + def get(self, key: str) -> any: + """Returns the value of a key from the Control panel. + + :param key: The key which value should be returned. + :type key: str + :returns: The value of the key. + :rtype: any + :raises KeyError: If the key in kwargs is not present in the Control + panel. + """ + return self._panel[key] + + def values(self) -> tuple: + """Returns the values from the Control panel as a list. + + :returns: The values from the Control panel. + :rtype: list + """ + return tuple(self._panel.values()) + + def callbacks(self, app): + """Callbacks for the whole application. + + :param app: The application. + :type app: Flask + """ + + def _generate_plotting_area(figs: tuple, table: pd.DataFrame, + url: str) -> tuple: + """Generate the plotting area with all its content. + + :param figs: Figures to be placed in the plotting area. + :param table: A table to be placed in the plotting area bellow the + figures. + :param utl: The URL to be placed in the plotting area bellow the + tables. + :type figs: tuple of plotly.graph_objects.Figure + :type table: pandas.DataFrame + :type url: str + :returns: tuple of elements to be shown in the plotting area. + :rtype: tuple + (dcc.Graph, dcc.Graph, dbc.Table, list(dbc.Col, dbc.Col)) + """ + + (fig_tput, fig_lat) = figs + + row_fig_tput = C.PLACEHOLDER + row_fig_lat = C.PLACEHOLDER + row_table = C.PLACEHOLDER + row_btn_dwnld = C.PLACEHOLDER + + if fig_tput: + row_fig_tput = [ + dcc.Graph( + id={"type": "graph", "index": "tput"}, + figure=fig_tput + ) + ] + row_btn_dwnld = [ + dbc.Col( # Download + width=2, + children=[ + dcc.Loading(children=[ + dbc.Button( + id="btn-download-data", + children=show_tooltip(self._tooltips, + "help-download", "Download Data"), + class_name="me-1", + color="info" + ), + dcc.Download(id="download-data") + ]), + ] + ), + dbc.Col( # Show URL + width=10, + children=[ + dbc.InputGroup( + class_name="me-1", + children=[ + dbc.InputGroupText( + style=C.URL_STYLE, + children=show_tooltip(self._tooltips, + "help-url", "URL", "input-url") + ), + dbc.Input( + id="input-url", + readonly=True, + type="url", + style=C.URL_STYLE, + value=url + ) + ] + ) + ] + ) + ] + if fig_lat: + row_fig_lat = [ + dcc.Graph( + id={"type": "graph", "index": "lat"}, + figure=fig_lat + ) + ] + if not table.empty: + row_table = [ + dbc.Table.from_dataframe( + table, + id={"type": "table", "index": "compare"}, + striped=True, + bordered=True, + hover=True + ) + ] + + return row_fig_tput, row_fig_lat, row_table, row_btn_dwnld + + @app.callback( + Output("control-panel", "data"), # Store + Output("selected-tests", "data"), # Store + Output("row-graph-tput", "children"), + Output("row-graph-lat", "children"), + Output("row-table", "children"), + Output("row-btn-download", "children"), + Output("row-card-sel-tests", "style"), + Output("row-btns-sel-tests", "style"), + Output("dd-ctrl-rls", "value"), + Output("dd-ctrl-dut", "options"), + Output("dd-ctrl-dut", "disabled"), + Output("dd-ctrl-dut", "value"), + Output("dd-ctrl-dutver", "options"), + Output("dd-ctrl-dutver", "disabled"), + Output("dd-ctrl-dutver", "value"), + Output("dd-ctrl-phy", "options"), + Output("dd-ctrl-phy", "disabled"), + Output("dd-ctrl-phy", "value"), + Output("dd-ctrl-area", "options"), + Output("dd-ctrl-area", "disabled"), + Output("dd-ctrl-area", "value"), + Output("dd-ctrl-test", "options"), + Output("dd-ctrl-test", "disabled"), + Output("dd-ctrl-test", "value"), + Output("cl-ctrl-core", "options"), + Output("cl-ctrl-core", "value"), + Output("cl-ctrl-core-all", "value"), + Output("cl-ctrl-core-all", "options"), + Output("cl-ctrl-framesize", "options"), + Output("cl-ctrl-framesize", "value"), + Output("cl-ctrl-framesize-all", "value"), + Output("cl-ctrl-framesize-all", "options"), + Output("cl-ctrl-testtype", "options"), + Output("cl-ctrl-testtype", "value"), + Output("cl-ctrl-testtype-all", "value"), + Output("cl-ctrl-testtype-all", "options"), + Output("btn-ctrl-add", "disabled"), + Output("cl-ctrl-normalize", "value"), + Output("cl-selected", "options"), # User selection + State("control-panel", "data"), # Store + State("selected-tests", "data"), # Store + State("cl-selected", "value"), # User selection + Input("dd-ctrl-rls", "value"), + Input("dd-ctrl-dut", "value"), + Input("dd-ctrl-dutver", "value"), + Input("dd-ctrl-phy", "value"), + Input("dd-ctrl-area", "value"), + Input("dd-ctrl-test", "value"), + Input("cl-ctrl-core", "value"), + Input("cl-ctrl-core-all", "value"), + Input("cl-ctrl-framesize", "value"), + Input("cl-ctrl-framesize-all", "value"), + Input("cl-ctrl-testtype", "value"), + Input("cl-ctrl-testtype-all", "value"), + Input("cl-ctrl-normalize", "value"), + Input("btn-ctrl-add", "n_clicks"), + Input("btn-sel-remove", "n_clicks"), + Input("btn-sel-remove-all", "n_clicks"), + Input("url", "href") + ) + def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list, + dd_rls: str, dd_dut: str, dd_dutver: str, dd_phy: str, dd_area: str, + dd_test: str, cl_core: list, cl_core_all: list, cl_framesize: list, + cl_framesize_all: list, cl_testtype: list, cl_testtype_all: list, + cl_normalize: list, btn_add: int, btn_remove: int, + btn_remove_all: int, href: str) -> tuple: + """Update the application when the event is detected. + + :param cp_data: Current status of the control panel stored in + browser. + :param store_sel: List of tests selected by user stored in the + browser. + :param list_sel: List of tests selected by the user shown in the + checklist. + :param dd_rls: Input - Releases. + :param dd_dut: Input - DUTs. + :param dd_dutver: Input - Version of DUT. + :param dd_phy: Input - topo- arch-nic-driver. + :param dd_area: Input - Tested area. + :param dd_test: Input - Test. + :param cl_core: Input - Number of cores. + :param cl_core_all: Input - All numbers of cores. + :param cl_framesize: Input - Frame sizes. + :param cl_framesize_all: Input - All frame sizes. + :param cl_testtype: Input - Test type (NDR, PDR, MRR). + :param cl_testtype_all: Input - All test types. + :param cl_normalize: Input - Normalize the results. + :param btn_add: Input - Button "Add Selected" tests. + :param btn_remove: Input - Button "Remove selected" tests. + :param btn_remove_all: Input - Button "Remove All" tests. + :param href: Input - The URL provided by the browser. + :type cp_data: dict + :type store_sel: list + :type list_sel: list + :type dd_rls: str + :type dd_dut: str + :type dd_dutver: str + :type dd_phy: str + :type dd_area: str + :type dd_test: str + :type cl_core: list + :type cl_core_all: list + :type cl_framesize: list + :type cl_framesize_all: list + :type cl_testtype: list + :type cl_testtype_all: list + :type cl_normalize: list + :type btn_add: int + :type btn_remove: int + :type btn_remove_all: int + :type href: str + :returns: New values for web page elements. + :rtype: tuple + """ + + ctrl_panel = self.ControlPanel(cp_data) + norm = cl_normalize + + # Parse the url: + parsed_url = url_decode(href) + if parsed_url: + url_params = parsed_url["params"] + else: + url_params = None + + row_fig_tput = no_update + row_fig_lat = no_update + row_table = no_update + row_btn_dwnld = no_update + row_card_sel_tests = no_update + row_btns_sel_tests = no_update + + trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0] + + if trigger_id == "dd-ctrl-rls": + try: + options = \ + generate_options(sorted(self.spec_tbs[dd_rls].keys())) + disabled = False + except KeyError: + options = list() + disabled = True + ctrl_panel.set({ + "dd-rls-value": dd_rls, + "dd-dut-value": str(), + "dd-dut-options": options, + "dd-dut-disabled": disabled, + "dd-dutver-value": str(), + "dd-dutver-options": list(), + "dd-dutver-disabled": True, + "dd-phy-value": str(), + "dd-phy-options": list(), + "dd-phy-disabled": True, + "dd-area-value": str(), + "dd-area-options": list(), + "dd-area-disabled": True, + "dd-test-value": str(), + "dd-test-options": list(), + "dd-test-disabled": True, + "cl-core-options": list(), + "cl-core-value": list(), + "cl-core-all-value": list(), + "cl-core-all-options": C.CL_ALL_DISABLED, + "cl-framesize-options": list(), + "cl-framesize-value": list(), + "cl-framesize-all-value": list(), + "cl-framesize-all-options": C.CL_ALL_DISABLED, + "cl-testtype-options": list(), + "cl-testtype-value": list(), + "cl-testtype-all-value": list(), + "cl-testtype-all-options": C.CL_ALL_DISABLED + }) + elif trigger_id == "dd-ctrl-dut": + try: + rls = ctrl_panel.get("dd-rls-value") + dut = self.spec_tbs[rls][dd_dut] + options = generate_options(sorted(dut.keys())) + disabled = False + except KeyError: + options = list() + disabled = True + ctrl_panel.set({ + "dd-dut-value": dd_dut, + "dd-dutver-value": str(), + "dd-dutver-options": options, + "dd-dutver-disabled": disabled, + "dd-phy-value": str(), + "dd-phy-options": list(), + "dd-phy-disabled": True, + "dd-area-value": str(), + "dd-area-options": list(), + "dd-area-disabled": True, + "dd-test-value": str(), + "dd-test-options": list(), + "dd-test-disabled": True, + "cl-core-options": list(), + "cl-core-value": list(), + "cl-core-all-value": list(), + "cl-core-all-options": C.CL_ALL_DISABLED, + "cl-framesize-options": list(), + "cl-framesize-value": list(), + "cl-framesize-all-value": list(), + "cl-framesize-all-options": C.CL_ALL_DISABLED, + "cl-testtype-options": list(), + "cl-testtype-value": list(), + "cl-testtype-all-value": list(), + "cl-testtype-all-options": C.CL_ALL_DISABLED + }) + elif trigger_id == "dd-ctrl-dutver": + try: + rls = ctrl_panel.get("dd-rls-value") + dut = ctrl_panel.get("dd-dut-value") + dutver = self.spec_tbs[rls][dut][dd_dutver] + options = generate_options(sorted(dutver.keys())) + disabled = False + except KeyError: + options = list() + disabled = True + ctrl_panel.set({ + "dd-dutver-value": dd_dutver, + "dd-phy-value": str(), + "dd-phy-options": options, + "dd-phy-disabled": disabled, + "dd-area-value": str(), + "dd-area-options": list(), + "dd-area-disabled": True, + "dd-test-value": str(), + "dd-test-options": list(), + "dd-test-disabled": True, + "cl-core-options": list(), + "cl-core-value": list(), + "cl-core-all-value": list(), + "cl-core-all-options": C.CL_ALL_DISABLED, + "cl-framesize-options": list(), + "cl-framesize-value": list(), + "cl-framesize-all-value": list(), + "cl-framesize-all-options": C.CL_ALL_DISABLED, + "cl-testtype-options": list(), + "cl-testtype-value": list(), + "cl-testtype-all-value": list(), + "cl-testtype-all-options": C.CL_ALL_DISABLED + }) + elif trigger_id == "dd-ctrl-phy": + try: + rls = ctrl_panel.get("dd-rls-value") + dut = ctrl_panel.get("dd-dut-value") + dutver = ctrl_panel.get("dd-dutver-value") + phy = self.spec_tbs[rls][dut][dutver][dd_phy] + options = [{"label": label(v), "value": v} \ + for v in sorted(phy.keys())] + disabled = False + except KeyError: + options = list() + disabled = True + ctrl_panel.set({ + "dd-phy-value": dd_phy, + "dd-area-value": str(), + "dd-area-options": options, + "dd-area-disabled": disabled, + "dd-test-value": str(), + "dd-test-options": list(), + "dd-test-disabled": True, + "cl-core-options": list(), + "cl-core-value": list(), + "cl-core-all-value": list(), + "cl-core-all-options": C.CL_ALL_DISABLED, + "cl-framesize-options": list(), + "cl-framesize-value": list(), + "cl-framesize-all-value": list(), + "cl-framesize-all-options": C.CL_ALL_DISABLED, + "cl-testtype-options": list(), + "cl-testtype-value": list(), + "cl-testtype-all-value": list(), + "cl-testtype-all-options": C.CL_ALL_DISABLED + }) + elif trigger_id == "dd-ctrl-area": + try: + rls = ctrl_panel.get("dd-rls-value") + dut = ctrl_panel.get("dd-dut-value") + dutver = ctrl_panel.get("dd-dutver-value") + phy = ctrl_panel.get("dd-phy-value") + area = self.spec_tbs[rls][dut][dutver][phy][dd_area] + options = generate_options(sorted(area.keys())) + disabled = False + except KeyError: + options = list() + disabled = True + ctrl_panel.set({ + "dd-area-value": dd_area, + "dd-test-value": str(), + "dd-test-options": options, + "dd-test-disabled": disabled, + "cl-core-options": list(), + "cl-core-value": list(), + "cl-core-all-value": list(), + "cl-core-all-options": C.CL_ALL_DISABLED, + "cl-framesize-options": list(), + "cl-framesize-value": list(), + "cl-framesize-all-value": list(), + "cl-framesize-all-options": C.CL_ALL_DISABLED, + "cl-testtype-options": list(), + "cl-testtype-value": list(), + "cl-testtype-all-value": list(), + "cl-testtype-all-options": C.CL_ALL_DISABLED + }) + elif trigger_id == "dd-ctrl-test": + rls = ctrl_panel.get("dd-rls-value") + dut = ctrl_panel.get("dd-dut-value") + dutver = ctrl_panel.get("dd-dutver-value") + phy = ctrl_panel.get("dd-phy-value") + area = ctrl_panel.get("dd-area-value") + if all((rls, dut, dutver, phy, area, dd_test, )): + test = self.spec_tbs[rls][dut][dutver][phy][area][dd_test] + ctrl_panel.set({ + "dd-test-value": dd_test, + "cl-core-options": \ + generate_options(sorted(test["core"])), + "cl-core-value": list(), + "cl-core-all-value": list(), + "cl-core-all-options": C.CL_ALL_ENABLED, + "cl-framesize-options": \ + generate_options(sorted(test["frame-size"])), + "cl-framesize-value": list(), + "cl-framesize-all-value": list(), + "cl-framesize-all-options": C.CL_ALL_ENABLED, + "cl-testtype-options": \ + generate_options(sorted(test["test-type"])), + "cl-testtype-value": list(), + "cl-testtype-all-value": list(), + "cl-testtype-all-options": C.CL_ALL_ENABLED, + }) + elif trigger_id == "cl-ctrl-core": + val_sel, val_all = sync_checklists( + options=ctrl_panel.get("cl-core-options"), + sel=cl_core, + all=list(), + id="" + ) + ctrl_panel.set({ + "cl-core-value": val_sel, + "cl-core-all-value": val_all, + }) + elif trigger_id == "cl-ctrl-core-all": + val_sel, val_all = sync_checklists( + options = ctrl_panel.get("cl-core-options"), + sel=list(), + all=cl_core_all, + id="all" + ) + ctrl_panel.set({ + "cl-core-value": val_sel, + "cl-core-all-value": val_all, + }) + elif trigger_id == "cl-ctrl-framesize": + val_sel, val_all = sync_checklists( + options = ctrl_panel.get("cl-framesize-options"), + sel=cl_framesize, + all=list(), + id="" + ) + ctrl_panel.set({ + "cl-framesize-value": val_sel, + "cl-framesize-all-value": val_all, + }) + elif trigger_id == "cl-ctrl-framesize-all": + val_sel, val_all = sync_checklists( + options = ctrl_panel.get("cl-framesize-options"), + sel=list(), + all=cl_framesize_all, + id="all" + ) + ctrl_panel.set({ + "cl-framesize-value": val_sel, + "cl-framesize-all-value": val_all, + }) + elif trigger_id == "cl-ctrl-testtype": + val_sel, val_all = sync_checklists( + options = ctrl_panel.get("cl-testtype-options"), + sel=cl_testtype, + all=list(), + id="" + ) + ctrl_panel.set({ + "cl-testtype-value": val_sel, + "cl-testtype-all-value": val_all, + }) + elif trigger_id == "cl-ctrl-testtype-all": + val_sel, val_all = sync_checklists( + options = ctrl_panel.get("cl-testtype-options"), + sel=list(), + all=cl_testtype_all, + id="all" + ) + ctrl_panel.set({ + "cl-testtype-value": val_sel, + "cl-testtype-all-value": val_all, + }) + elif trigger_id == "btn-ctrl-add": + _ = btn_add + rls = ctrl_panel.get("dd-rls-value") + dut = ctrl_panel.get("dd-dut-value") + dutver = ctrl_panel.get("dd-dutver-value") + phy = ctrl_panel.get("dd-phy-value") + area = ctrl_panel.get("dd-area-value") + test = ctrl_panel.get("dd-test-value") + cores = ctrl_panel.get("cl-core-value") + framesizes = ctrl_panel.get("cl-framesize-value") + testtypes = ctrl_panel.get("cl-testtype-value") + # Add selected test to the list of tests in store: + if all((rls, dut, dutver, phy, area, test, cores, framesizes, + testtypes)): + if store_sel is None: + store_sel = list() + for core in cores: + for framesize in framesizes: + for ttype in testtypes: + if dut == "trex": + core = str() + tid = "-".join((rls, dut, dutver, + phy.replace('af_xdp', 'af-xdp'), area, + framesize.lower(), core.lower(), test, + ttype.lower())) + if tid not in [itm["id"] for itm in store_sel]: + store_sel.append({ + "id": tid, + "rls": rls, + "dut": dut, + "dutver": dutver, + "phy": phy, + "area": area, + "test": test, + "framesize": framesize.lower(), + "core": core.lower(), + "testtype": ttype.lower() + }) + store_sel = sorted(store_sel, key=lambda d: d["id"]) + row_card_sel_tests = C.STYLE_ENABLED + row_btns_sel_tests = C.STYLE_ENABLED + if C.CLEAR_ALL_INPUTS: + ctrl_panel.set(ctrl_panel.defaults) + ctrl_panel.set({ + "cl-selected-options": list_tests(store_sel) + }) + elif trigger_id == "btn-sel-remove-all": + _ = btn_remove_all + row_fig_tput = C.PLACEHOLDER + row_fig_lat = C.PLACEHOLDER + row_table = C.PLACEHOLDER + row_btn_dwnld = C.PLACEHOLDER + row_card_sel_tests = C.STYLE_DISABLED + row_btns_sel_tests = C.STYLE_DISABLED + store_sel = list() + ctrl_panel.set({"cl-selected-options": list()}) + elif trigger_id == "btn-sel-remove": + _ = btn_remove + if list_sel: + new_store_sel = list() + for item in store_sel: + if item["id"] not in list_sel: + new_store_sel.append(item) + store_sel = new_store_sel + elif trigger_id == "url": + if url_params: + try: + store_sel = literal_eval(url_params["store_sel"][0]) + norm = literal_eval(url_params["norm"][0]) + except (KeyError, IndexError): + pass + if store_sel: + row_card_sel_tests = C.STYLE_ENABLED + row_btns_sel_tests = C.STYLE_ENABLED + last_test = store_sel[-1] + test = self.spec_tbs[last_test["rls"]]\ + [last_test["dut"]][last_test["dutver"]]\ + [last_test["phy"]][last_test["area"]]\ + [last_test["test"]] + ctrl_panel.set({ + "dd-rls-value": last_test["rls"], + "dd-dut-value": last_test["dut"], + "dd-dut-options": generate_options(sorted( + self.spec_tbs[last_test["rls"]].keys())), + "dd-dut-disabled": False, + "dd-dutver-value": last_test["dutver"], + "dd-dutver-options": generate_options(sorted( + self.spec_tbs[last_test["rls"]]\ + [last_test["dut"]].keys())), + "dd-dutver-disabled": False, + "dd-phy-value": last_test["phy"], + "dd-phy-options": generate_options(sorted( + self.spec_tbs[last_test["rls"]]\ + [last_test["dut"]]\ + [last_test["dutver"]].keys())), + "dd-phy-disabled": False, + "dd-area-value": last_test["area"], + "dd-area-options": [ + {"label": label(v), "value": v} for v in \ + sorted(self.spec_tbs[last_test["rls"]]\ + [last_test["dut"]][last_test["dutver"]]\ + [last_test["phy"]].keys()) + ], + "dd-area-disabled": False, + "dd-test-value": last_test["test"], + "dd-test-options": generate_options(sorted( + self.spec_tbs[last_test["rls"]]\ + [last_test["dut"]][last_test["dutver"]]\ + [last_test["phy"]]\ + [last_test["area"]].keys())), + "dd-test-disabled": False, + "cl-core-options": generate_options(sorted( + test["core"])), + "cl-core-value": [last_test["core"].upper(), ], + "cl-core-all-value": list(), + "cl-core-all-options": C.CL_ALL_ENABLED, + "cl-framesize-options": generate_options( + sorted(test["frame-size"])), + "cl-framesize-value": \ + [last_test["framesize"].upper(), ], + "cl-framesize-all-value": list(), + "cl-framesize-all-options": C.CL_ALL_ENABLED, + "cl-testtype-options": generate_options(sorted( + test["test-type"])), + "cl-testtype-value": \ + [last_test["testtype"].upper(), ], + "cl-testtype-all-value": list(), + "cl-testtype-all-options": C.CL_ALL_ENABLED + }) + + if trigger_id in ("btn-ctrl-add", "url", "btn-sel-remove", + "cl-ctrl-normalize"): + if store_sel: + row_fig_tput, row_fig_lat, row_table, row_btn_dwnld = \ + _generate_plotting_area( + graph_iterative( + self.data, store_sel, self.layout, bool(norm) + ), + table_comparison( + self.data, store_sel, bool(norm) + ), + gen_new_url( + parsed_url, + {"store_sel": store_sel, "norm": norm} + ) + ) + ctrl_panel.set({ + "cl-selected-options": list_tests(store_sel) + }) + else: + row_fig_tput = C.PLACEHOLDER + row_fig_lat = C.PLACEHOLDER + row_table = C.PLACEHOLDER + row_btn_dwnld = C.PLACEHOLDER + row_card_sel_tests = C.STYLE_DISABLED + row_btns_sel_tests = C.STYLE_DISABLED + store_sel = list() + ctrl_panel.set({"cl-selected-options": list()}) + + if ctrl_panel.get("cl-core-value") and \ + ctrl_panel.get("cl-framesize-value") and \ + ctrl_panel.get("cl-testtype-value"): + disabled = False + else: + disabled = True + ctrl_panel.set({ + "btn-add-disabled": disabled, + "cl-normalize-value": norm + }) + + ret_val = [ + ctrl_panel.panel, store_sel, + row_fig_tput, row_fig_lat, row_table, row_btn_dwnld, + row_card_sel_tests, row_btns_sel_tests + ] + ret_val.extend(ctrl_panel.values()) + return ret_val + + @app.callback( + Output("download-data", "data"), + State("selected-tests", "data"), + Input("btn-download-data", "n_clicks"), + prevent_initial_call=True + ) + def _download_data(store_sel, n_clicks): + """Download the data + + :param store_sel: List of tests selected by user stored in the + browser. + :param n_clicks: Number of clicks on the button "Download". + :type store_sel: list + :type n_clicks: int + :returns: dict of data frame content (base64 encoded) and meta data + used by the Download component. + :rtype: dict + """ + + if not n_clicks: + raise PreventUpdate + + if not store_sel: + raise PreventUpdate + + df = pd.DataFrame() + for itm in store_sel: + sel_data = select_iterative_data(self.data, itm) + if sel_data is None: + continue + df = pd.concat([df, sel_data], ignore_index=True) + + return dcc.send_data_frame(df.to_csv, C.REPORT_DOWNLOAD_FILE_NAME) diff --git a/csit.infra.dash/app/pal/report/layout.yaml b/csit.infra.dash/app/pal/report/layout.yaml new file mode 100644 index 0000000000..c4ef13bf8b --- /dev/null +++ b/csit.infra.dash/app/pal/report/layout.yaml @@ -0,0 +1,127 @@ +plot-throughput: + xaxis: + title: "Test Cases [Index]" + autorange: True + fixedrange: False + gridcolor: "rgb(230, 230, 230)" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + tickmode: "array" + zeroline: False + yaxis: + title: "Packet Throughput [pps]" + gridcolor: "rgb(230, 230, 230)" + hoverformat: ".3s" + tickformat: ".3s" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + zeroline: False + range: [0, 50] + autosize: False + margin: + t: 50 + b: 0 + l: 80 + r: 20 + showlegend: True + legend: + orientation: "h" + font: + size: 10 + width: 700 + height: 900 + paper_bgcolor: "#fff" + plot_bgcolor: "#fff" + hoverlabel: + namelength: -1 + +plot-latency: + xaxis: + title: "Test Cases [Index]" + autorange: True + fixedrange: False + gridcolor: "rgb(230, 230, 230)" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + tickmode: "array" + zeroline: False + yaxis: + title: "Average Latency at 50% PDR [us]" + gridcolor: "rgb(230, 230, 230)" + hoverformat: ".3s" + tickformat: ".3s" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + zeroline: False + range: [0, 50] + autosize: False + margin: + t: 50 + b: 0 + l: 80 + r: 20 + showlegend: True + legend: + orientation: "h" + font: + size: 10 + width: 700 + height: 900 + paper_bgcolor: "#fff" + plot_bgcolor: "#fff" + hoverlabel: + namelength: -1 + +plot-hdrh-latency: + showlegend: True + legend: + traceorder: "normal" + orientation: "h" + xanchor: "left" + yanchor: "top" + x: 0 + y: -0.25 + bgcolor: "rgba(255, 255, 255, 0)" + bordercolor: "rgba(255, 255, 255, 0)" + xaxis: + type: "log" + title: "Percentile [%]" + autorange: False + fixedrange: True + gridcolor: "rgb(230, 230, 230)" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + tickvals: [1, 2, 1e1, 20, 1e2, 1e3, 1e4, 1e5, 1e6] + ticktext: [0, 50, 90, 95, 99, 99.9, 99.99, 99.999, 99.9999] + yaxis: + title: "One-Way Latency per Direction [us]" + gridcolor: "rgb(230, 230, 230)" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + autosize: True + paper_bgcolor: "white" + plot_bgcolor: "white" diff --git a/csit.infra.dash/app/pal/report/report.py b/csit.infra.dash/app/pal/report/report.py new file mode 100644 index 0000000000..e4565731ec --- /dev/null +++ b/csit.infra.dash/app/pal/report/report.py @@ -0,0 +1,48 @@ +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. + +"""Instantiate the Report Dash application. +""" +import dash + +from ..utils.constants import Constants as C +from .layout import Layout + + +def init_report(server, releases): + """Create a Plotly Dash dashboard. + + :param server: Flask server. + :type server: Flask + :returns: Dash app server. + :rtype: Dash + """ + + dash_app = dash.Dash( + server=server, + routes_pathname_prefix=C.REPORT_ROUTES_PATHNAME_PREFIX, + external_stylesheets=C.EXTERNAL_STYLESHEETS + ) + + layout = Layout( + app=dash_app, + releases=releases, + html_layout_file=C.REPORT_HTML_LAYOUT_FILE, + graph_layout_file=C.REPORT_GRAPH_LAYOUT_FILE, + data_spec_file=C.DATA_SPEC_FILE, + tooltip_file=C.TOOLTIP_FILE, + ) + dash_app.index_string = layout.html_layout + dash_app.layout = layout.add_content() + + return dash_app.server diff --git a/csit.infra.dash/app/pal/routes.py b/csit.infra.dash/app/pal/routes.py new file mode 100644 index 0000000000..59af748168 --- /dev/null +++ b/csit.infra.dash/app/pal/routes.py @@ -0,0 +1,32 @@ +# Copyright (c) 2022 Cisco and/or its affiliates. +# 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. + +"""Routes for parent Flask app. +""" + +from flask import current_app as app +from flask import render_template + +from .utils.constants import Constants as C + + +@app.route(C.APPLICATIN_ROOT) +def home(): + """Landing page. + """ + return render_template( + C.MAIN_HTML_LAYOUT_FILE, + title=C.TITLE, + description=C.DESCRIPTION, + template=C.TEMPLATE + ) diff --git a/csit.infra.dash/app/pal/static/dist/img/favicon.svg b/csit.infra.dash/app/pal/static/dist/img/favicon.svg new file mode 100644 index 0000000000..689757e3fd --- /dev/null +++ b/csit.infra.dash/app/pal/static/dist/img/favicon.svg @@ -0,0 +1,348 @@ + + + + + + + + + + + + + + + + + + diff --git a/csit.infra.dash/app/pal/static/img/logo.svg b/csit.infra.dash/app/pal/static/img/logo.svg new file mode 100644 index 0000000000..689757e3fd --- /dev/null +++ b/csit.infra.dash/app/pal/static/img/logo.svg @@ -0,0 +1,348 @@ + + + + + + + + + + + + + + + + + + diff --git a/csit.infra.dash/app/pal/static/sass/_bootswatch.scss b/csit.infra.dash/app/pal/static/sass/_bootswatch.scss new file mode 100644 index 0000000000..900ccfb3c1 --- /dev/null +++ b/csit.infra.dash/app/pal/static/sass/_bootswatch.scss @@ -0,0 +1,178 @@ +// Lux 5.2.1 +// Bootswatch + + +// Variables + +$web-font-path: "https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600&display=swap" !default; +@if $web-font-path { + @import url($web-font-path); +} + +:root { + color-scheme: light; +} + +// Navbar + +.navbar { + font-size: $font-size-sm; + font-weight: 600; + text-transform: uppercase; + + &-nav { + .nav-link { + padding-top: .715rem; + padding-bottom: .715rem; + } + } + + &-brand { + margin-right: 2rem; + } +} + +.bg-light { + border: 1px solid rgba(0, 0, 0, .1); + + &.navbar-fixed-top { + border-width: 0 0 1px; + } + + &.navbar-bottom-top { + border-width: 1px 0 0; + } +} + +.nav-item { + margin-right: 2rem; +} + +// Buttons + +.btn { + font-size: $font-size-sm; + text-transform: uppercase; + + &-sm { + font-size: 10px; + } + + &-warning { + &, + &:hover, + &:not([disabled]):not(.disabled):active, + &:focus { + color: $white; + } + } +} + +.btn-outline-secondary { + color: $gray-600; + border-color: $gray-600; + + &:not([disabled]):not(.disabled):hover, + &:not([disabled]):not(.disabled):focus, + &:not([disabled]):not(.disabled):active { + color: $white; + background-color: $gray-400; + border-color: $gray-400; + } + + &:not([disabled]):not(.disabled):focus { + box-shadow: 0 0 0 .2rem rgba($gray-400, .5); + } +} + +[class*="btn-outline-"] { + border-width: 2px; +} + +.border-secondary { + border: 1px solid $gray-400 !important; +} + +// Typography + +body { + font-weight: 200; + letter-spacing: 1px; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + text-transform: uppercase; + letter-spacing: 3px; +} + +.text-secondary { + color: $body-color !important; +} + +// Tables + +th { + font-size: $font-size-sm; + text-transform: uppercase; +} + +.table { + th, + td { + padding: 1.5rem; + } + + &-sm { + th, + td { + padding: .75rem; + } + } +} + +// Navs + +.dropdown-menu { + font-size: $font-size-sm; + text-transform: none; +} + +// Indicators + +.badge { + padding-top: .28rem; + + &-pill { + border-radius: 10rem; + } + + &.bg-secondary, + &.bg-light { + color: $dark; + } +} + +// Containers + +.list-group-item, +.card { + h1, + h2, + h3, + h4, + h5, + h6, + .h1, + .h2, + .h3, + .h4, + .h5, + .h6 { + color: inherit; + } +} diff --git a/csit.infra.dash/app/pal/static/sass/_variables.scss b/csit.infra.dash/app/pal/static/sass/_variables.scss new file mode 100644 index 0000000000..6bfd6408b7 --- /dev/null +++ b/csit.infra.dash/app/pal/static/sass/_variables.scss @@ -0,0 +1,103 @@ +// Lux 5.2.1 +// Bootswatch + +$theme: "lux" !default; + +// +// Color system +// + +$white: #fff !default; +$gray-100: #f8f9fa !default; +$gray-200: #f7f7f9 !default; +$gray-300: #eceeef !default; +$gray-400: #ced4da !default; +$gray-500: #adb5bd !default; +$gray-600: #919aa1 !default; +$gray-700: #55595c !default; +$gray-800: #343a40 !default; +$gray-900: #1a1a1a !default; +$black: #000 !default; + +$blue: #007bff !default; +$indigo: #6610f2 !default; +$purple: #6f42c1 !default; +$pink: #e83e8c !default; +$red: #d9534f !default; +$orange: #fd7e14 !default; +$yellow: #f0ad4e !default; +$green: #4bbf73 !default; +$teal: #20c997 !default; +$cyan: #1f9bcf !default; + +$primary: $gray-900 !default; +$secondary: $white !default; +$success: $green !default; +$info: $cyan !default; +$warning: $yellow !default; +$danger: $red !default; +$light: $white !default; +$dark: $gray-800 !default; + +$min-contrast-ratio: 2.3 !default; + +// Options + +$enable-rounded: false !default; + +// Body + +$body-color: $gray-700 !default; + +// Fonts + +// stylelint-disable-next-line value-keyword-case +$font-family-sans-serif: "Nunito Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default; +$h1-font-size: 2rem !default; +$h2-font-size: 1.75rem !default; +$h3-font-size: 1.5rem !default; +$h4-font-size: 1.25rem !default; +$h5-font-size: 1rem !default; +$h6-font-size: .75rem !default; +$headings-font-weight: 600 !default; +$headings-color: $gray-900 !default; + +// Tables + +$table-border-color: rgba(0, 0, 0, .05) !default; + +// Buttons + Forms + +$input-btn-border-width: 0 !default; + +// Buttons + +$btn-line-height: 1.5rem !default; +$input-btn-padding-y: .75rem !default; +$input-btn-padding-x: 1.5rem !default; +$input-btn-padding-y-sm: .5rem !default; +$input-btn-padding-x-sm: 1rem !default; +$input-btn-padding-y-lg: 2rem !default; +$input-btn-padding-x-lg: 2rem !default; +$btn-font-weight: 600 !default; + +// Forms + +$input-line-height: 1.5 !default; +$input-bg: $gray-200 !default; +$input-disabled-bg: $gray-300 !default; +$input-group-addon-bg: $gray-300 !default; + +// Navbar + +$navbar-padding-y: 1.5rem !default; +$navbar-dark-hover-color: $white !default; +$navbar-light-color: rgba($black, .3) !default; +$navbar-light-hover-color: $gray-900 !default; +$navbar-light-active-color: $gray-900 !default; + +// Pagination + +$pagination-border-color: transparent !default; +$pagination-hover-border-color: $pagination-border-color !default; +$pagination-disabled-border-color: $pagination-border-color !default; diff --git a/csit.infra.dash/app/pal/static/sass/bootstrap/_accordion.scss b/csit.infra.dash/app/pal/static/sass/bootstrap/_accordion.scss new file mode 100644 index 0000000000..f09601bab6 --- /dev/null +++ b/csit.infra.dash/app/pal/static/sass/bootstrap/_accordion.scss @@ -0,0 +1,149 @@ +// +// Base styles +// + +.accordion { + // scss-docs-start accordion-css-vars + --#{$prefix}accordion-color: #{$accordion-color}; + --#{$prefix}accordion-bg: #{$accordion-bg}; + --#{$prefix}accordion-transition: #{$accordion-transition}; + --#{$prefix}accordion-border-color: #{$accordion-border-color}; + --#{$prefix}accordion-border-width: #{$accordion-border-width}; + --#{$prefix}accordion-border-radius: #{$accordion-border-radius}; + --#{$prefix}accordion-inner-border-radius: #{$accordion-inner-border-radius}; + --#{$prefix}accordion-btn-padding-x: #{$accordion-button-padding-x}; + --#{$prefix}accordion-btn-padding-y: #{$accordion-button-padding-y}; + --#{$prefix}accordion-btn-color: #{$accordion-button-color}; + --#{$prefix}accordion-btn-bg: #{$accordion-button-bg}; + --#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon)}; + --#{$prefix}accordion-btn-icon-width: #{$accordion-icon-width}; + --#{$prefix}accordion-btn-icon-transform: #{$accordion-icon-transform}; + --#{$prefix}accordion-btn-icon-transition: #{$accordion-icon-transition}; + --#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon)}; + --#{$prefix}accordion-btn-focus-border-color: #{$accordion-button-focus-border-color}; + --#{$prefix}accordion-btn-focus-box-shadow: #{$accordion-button-focus-box-shadow}; + --#{$prefix}accordion-body-padding-x: #{$accordion-body-padding-x}; + --#{$prefix}accordion-body-padding-y: #{$accordion-body-padding-y}; + --#{$prefix}accordion-active-color: #{$accordion-button-active-color}; + --#{$prefix}accordion-active-bg: #{$accordion-button-active-bg}; + // scss-docs-end accordion-css-vars +} + +.accordion-button { + position: relative; + display: flex; + align-items: center; + width: 100%; + padding: var(--#{$prefix}accordion-btn-padding-y) var(--#{$prefix}accordion-btn-padding-x); + @include font-size($font-size-base); + color: var(--#{$prefix}accordion-btn-color); + text-align: left; // Reset button style + background-color: var(--#{$prefix}accordion-btn-bg); + border: 0; + @include border-radius(0); + overflow-anchor: none; + @include transition(var(--#{$prefix}accordion-transition)); + + &:not(.collapsed) { + color: var(--#{$prefix}accordion-active-color); + background-color: var(--#{$prefix}accordion-active-bg); + box-shadow: inset 0 calc(-1 * var(--#{$prefix}accordion-border-width)) 0 var(--#{$prefix}accordion-border-color); // stylelint-disable-line function-disallowed-list + + &::after { + background-image: var(--#{$prefix}accordion-btn-active-icon); + transform: var(--#{$prefix}accordion-btn-icon-transform); + } + } + + // Accordion icon + &::after { + flex-shrink: 0; + width: var(--#{$prefix}accordion-btn-icon-width); + height: var(--#{$prefix}accordion-btn-icon-width); + margin-left: auto; + content: ""; + background-image: var(--#{$prefix}accordion-btn-icon); + background-repeat: no-repeat; + background-size: var(--#{$prefix}accordion-btn-icon-width); + @include transition(var(--#{$prefix}accordion-btn-icon-transition)); + } + + &:hover { + z-index: 2; + } + + &:focus { + z-index: 3; + border-color: var(--#{$prefix}accordion-btn-focus-border-color); + outline: 0; + box-shadow: var(--#{$prefix}accordion-btn-focus-box-shadow); + } +} + +.accordion-header { + margin-bottom: 0; +} + +.accordion-item { + color: var(--#{$prefix}accordion-color); + background-color: var(--#{$prefix}accordion-bg); + border: var(--#{$prefix}accordion-border-width) solid var(--#{$prefix}accordion-border-color); + + &:first-of-type { + @include border-top-radius(var(--#{$prefix}accordion-border-radius)); + + .accordion-button { + @include border-top-radius(var(--#{$prefix}accordion-inner-border-radius)); + } + } + + &:not(:first-of-type) { + border-top: 0; + } + + // Only set a border-radius on the last item if the accordion is collapsed + &:last-of-type { + @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); + + .accordion-button { + &.collapsed { + @include border-bottom-radius(var(--#{$prefix}accordion-inner-border-radius)); + } + } + + .accordion-collapse { + @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); + } + } +} + +.accordion-body { + padding: var(--#{$prefix}accordion-body-padding-y) var(--#{$prefix}accordion-body-padding-x); +} + + +// Flush accordion items +// +// Remove borders and border-radius to keep accordion items edge-to-edge. + +.accordion-flush { + .accordion-collapse { + border-width: 0; + } + + .accordion-item { + border-right: 0; + border-left: 0; + @include border-radius(0); + + &:first-child { border-top: 0; } + &:last-child { border-bottom: 0; } + + .accordion-button { + &, + &.collapsed { + @include border-radius(0); + } + } + } +} diff --git a/csit.infra.dash/app/pal/static/sass/bootstrap/_alert.scss b/csit.infra.dash/app/pal/static/sass/bootstrap/_alert.scss new file mode 100644 index 0000000000..c8bc91b420 --- /dev/null +++ b/csit.infra.dash/app/pal/static/sass/bootstrap/_alert.scss @@ -0,0 +1,71 @@ +// +// Base styles +// + +.alert { + // scss-docs-start alert-css-vars + --#{$prefix}alert-bg: transparent; + --#{$prefix}alert-padding-x: #{$alert-padding-x}; + --#{$prefix}alert-padding-y: #{$alert-padding-y}; + --#{$prefix}alert-margin-bottom: #{$alert-margin-bottom}; + --#{$prefix}alert-color: inherit; + --#{$prefix}alert-border-color: transparent; + --#{$prefix}alert-border: #{$alert-border-width} solid var(--#{$prefix}alert-border-color); + --#{$prefix}alert-border-radius: #{$alert-border-radius}; + // scss-docs-end alert-css-vars + + position: relative; + padding: var(--#{$prefix}alert-padding-y) var(--#{$prefix}alert-padding-x); + margin-bottom: var(--#{$prefix}alert-margin-bottom); + color: var(--#{$prefix}alert-color); + background-color: var(--#{$prefix}alert-bg); + border: var(--#{$prefix}alert-border); + @include border-radius(var(--#{$prefix}alert-border-radius)); +} + +// Headings for larger alerts +.alert-heading { + // Specified to prevent conflicts of changing $headings-color + color: inherit; +} + +// Provide class for links that match alerts +.alert-link { + font-weight: $alert-link-font-weight; +} + + +// Dismissible alerts +// +// Expand the right padding and account for the close button's positioning. + +.alert-dismissible { + padding-right: $alert-dismissible-padding-r; + + // Adjust close link position + .btn-close { + position: absolute; + top: 0; + right: 0; + z-index: $stretched-link-z-index + 1; + padding: $alert-padding-y * 1.25 $alert-padding-x; + } +} + + +// scss-docs-start alert-modifiers +// Generate contextual modifier classes for colorizing the alert. + +@each $state, $value in $theme-colors { + $alert-background: shift-color($value, $alert-bg-scale); + $alert-border: shift-color($value, $alert-border-scale); + $alert-color: shift-color($value, $alert-color-scale); + + @if (contrast-ratio($alert-background, $alert-color) < $min-contrast-ratio) { + $alert-color: mix($value, color-contrast($alert-background), abs($alert-color-scale)); + } + .alert-#{$state} { + @include alert-variant($alert-background, $alert-border, $alert-color); + } +} +// scss-docs-end alert-modifiers diff --git a/csit.infra.dash/app/pal/static/sass/bootstrap/_badge.scss b/csit.infra.dash/app/pal/static/sass/bootstrap/_badge.scss new file mode 100644 index 0000000000..cc3d269556 --- /dev/null +++ b/csit.infra.dash/app/pal/static/sass/bootstrap/_badge.scss @@ -0,0 +1,38 @@ +// Base class +// +// Requires one of the contextual, color modifier classes for `color` and +// `background-color`. + +.badge { + // scss-docs-start badge-css-vars + --#{$prefix}badge-padding-x: #{$badge-padding-x}; + --#{$prefix}badge-padding-y: #{$badge-padding-y}; + @include rfs($badge-font-size, --#{$prefix}badge-font-size); + --#{$prefix}badge-font-weight: #{$badge-font-weight}; + --#{$prefix}badge-color: #{$badge-color}; + --#{$prefix}badge-border-radius: #{$badge-border-radius}; + // scss-docs-end badge-css-vars + + display: inline-block; + padding: var(--#{$prefix}badge-padding-y) var(--#{$prefix}badge-padding-x); + @include font-size(var(--#{$prefix}badge-font-size)); + font-weight: var(--#{$prefix}badge-font-weight); + line-height: 1; + color: var(--#{$prefix}badge-color); + text-align: center; + white-space: nowrap; + vertical-align: baseline; + @include border-radius(var(--#{$prefix}badge-border-radius)); + @include gradient-bg(); + + // Empty badges collapse automatically + &:empty { + display: none; + } +} + +// Quick fix for badges in buttons +.btn .badge { + position: relative; + top: -1px; +} diff --git a/csit.infra.dash/app/pal/static/sass/bootstrap/_breadcrumb.scss b/csit.infra.dash/app/pal/static/sass/bootstrap/_breadcrumb.scss new file mode 100644 index 0000000000..b8252ff215 --- /dev/null +++ b/csit.infra.dash/app/pal/static/sass/bootstrap/_breadcrumb.scss @@ -0,0 +1,40 @@ +.breadcrumb { + // scss-docs-start breadcrumb-css-vars + --#{$prefix}breadcrumb-padding-x: #{$breadcrumb-padding-x}; + --#{$prefix}breadcrumb-padding-y: #{$breadcrumb-padding-y}; + --#{$prefix}breadcrumb-margin-bottom: #{$breadcrumb-margin-bottom}; + @include rfs($breadcrumb-font-size, --#{$prefix}breadcrumb-font-size); + --#{$prefix}breadcrumb-bg: #{$breadcrumb-bg}; + --#{$prefix}breadcrumb-border-radius: #{$breadcrumb-border-radius}; + --#{$prefix}breadcrumb-divider-color: #{$breadcrumb-divider-color}; + --#{$prefix}breadcrumb-item-padding-x: #{$breadcrumb-item-padding-x}; + --#{$prefix}breadcrumb-item-active-color: #{$breadcrumb-active-color}; + // scss-docs-end breadcrumb-css-vars + + display: flex; + flex-wrap: wrap; + padding: var(--#{$prefix}breadcrumb-padding-y) var(--#{$prefix}breadcrumb-padding-x); + margin-bottom: var(--#{$prefix}breadcrumb-margin-bottom); + @include font-size(var(--#{$prefix}breadcrumb-font-size)); + list-style: none; + background-color: var(--#{$prefix}breadcrumb-bg); + @include border-radius(var(--#{$prefix}breadcrumb-border-radius)); +} + +.breadcrumb-item { + // The separator between breadcrumbs (by default, a forward-slash: "/") + + .breadcrumb-item { + padding-left: var(--#{$prefix}breadcrumb-item-padding-x); + + &::before { + float: left; // Suppress inline spacings and underlining of the separator + padding-right: var(--#{$prefix}breadcrumb-item-padding-x); + color: var(--#{$prefix}breadcrumb-divider-color); + content: var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)) #{"/* rtl:"} var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider-flipped)) #{"*/"}; + } + } + + &.active { + color: var(--#{$prefix}breadcrumb-item-active-color); + } +} diff --git a/csit.infra.dash/app/pal/static/sass/bootstrap/_button-group.scss b/csit.infra.dash/app/pal/static/sass/bootstrap/_button-group.scss new file mode 100644 index 0000000000..79b100cbfb --- /dev/null +++ b/csit.infra.dash/app/pal/static/sass/bootstrap/_button-group.scss @@ -0,0 +1,142 @@ +// Make the div behave like a button +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-flex; + vertical-align: middle; // match .btn alignment given font-size hack above + + > .btn { + position: relative; + flex: 1 1 auto; + } + + // Bring the hover, focused, and "active" buttons to the front to overlay + // the borders properly + > .btn-check:checked + .btn, + > .btn-check:focus + .btn, + > .btn:hover, + > .btn:focus, + > .btn:active, + > .btn.active { + z-index: 1; + } +} + +// Optional: Group multiple button groups together for a toolbar +.btn-toolbar { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + + .input-group { + width: auto; + } +} + +.btn-group { + @include border-radius($btn-border-radius); + + // Prevent double borders when buttons are next to each other + > :not(.btn-check:first-child) + .btn, + > .btn-group:not(:first-child) { + margin-left: -$btn-border-width; + } + + // Reset rounded corners + > .btn:not(:last-child):not(.dropdown-toggle), + > .btn.dropdown-toggle-split:first-child, + > .btn-group:not(:last-child) > .btn { + @include border-end-radius(0); + } + + // The left radius should be 0 if the button is: + // - the "third or more" child + // - the second child and the previous element isn't `.btn-check` (making it the first child visually) + // - part of a btn-group which isn't the first child + > .btn:nth-child(n + 3), + > :not(.btn-check) + .btn, + > .btn-group:not(:first-child) > .btn { + @include border-start-radius(0); + } +} + +// Sizing +// +// Remix the default button sizing classes into new ones for easier manipulation. + +.btn-group-sm > .btn { @extend .btn-sm; } +.btn-group-lg > .btn { @extend .btn-lg; } + + +// +// Split button dropdowns +// + +.dropdown-toggle-split { + padding-right: $btn-padding-x * .75; + padding-left: $btn-padding-x * .75; + + &::after, + .dropup &::after, + .dropend &::after { + margin-left: 0; + } + + .dropstart &::before { + margin-right: 0; + } +} + +.btn-sm + .dropdown-toggle-split { + padding-right: $btn-padding-x-sm * .75; + padding-left: $btn-padding-x-sm * .75; +} + +.btn-lg + .dropdown-toggle-split { + padding-right: $btn-padding-x-lg * .75; + padding-left: $btn-padding-x-lg * .75; +} + + +// The clickable button for toggling the menu +// Set the same inset shadow as the :active state +.btn-group.show .dropdown-toggle { + @include box-shadow($btn-active-box-shadow); + + // Show no shadow for `.btn-link` since it has no other button styles. + &.btn-link { + @include box-shadow(none); + } +} + + +// +// Vertical button groups +// + +.btn-group-vertical { + flex-direction: column; + align-items: flex-start; + justify-content: center; + + > .btn, + > .btn-group { + width: 100%; + } + + > .btn:not(:first-child), + > .btn-group:not(:first-child) { + margin-top: -$btn-border-width; + } + + // Reset rounded corners + > .btn:not(:last-child):not(.dropdown-toggle), + > .btn-group:not(:last-child) > .btn { + @include border-bottom-radius(0); + } + + > .btn ~ .btn, + > .btn-group:not(:first-child) > .btn { + @include border-top-radius(0); + } +} diff --git a/csit.infra.dash/app/pal/static/sass/bootstrap/_buttons.scss b/csit.infra.dash/app/pal/static/sass/bootstrap/_buttons.scss new file mode 100644 index 0000000000..c2d0773516 --- /dev/null +++ b/csit.infra.dash/app/pal/static/sass/bootstrap/_buttons.scss @@ -0,0 +1,201 @@ +// +// Base styles +// + +.btn { + // scss-docs-start btn-css-vars + --#{$prefix}btn-padding-x: #{$btn-padding-x}; + --#{$prefix}btn-padding-y: #{$btn-padding-y}; + --#{$prefix}btn-font-family: #{$btn-font-family}; + @include rfs($btn-font-size, --#{$prefix}btn-font-size); + --#{$prefix}btn-font-weight: #{$btn-font-weight}; + --#{$prefix}btn-line-height: #{$btn-line-height}; + --#{$prefix}btn-color: #{$body-color}; + --#{$prefix}btn-bg: transparent; + --#{$prefix}btn-border-width: #{$btn-border-width}; + --#{$prefix}btn-border-color: transparent; + --#{$prefix}btn-border-radius: #{$btn-border-radius}; + --#{$prefix}btn-hover-border-color: transparent; + --#{$prefix}btn-box-shadow: #{$btn-box-shadow}; + --#{$prefix}btn-disabled-opacity: #{$btn-disabled-opacity}; + --#{$prefix}btn-focus-box-shadow: 0 0 0 #{$btn-focus-width} rgba(var(--#{$prefix}btn-focus-shadow-rgb), .5); + // scss-docs-end btn-css-vars + + display: inline-block; + padding: var(--#{$prefix}btn-padding-y) var(--#{$prefix}btn-padding-x); + font-family: var(--#{$prefix}btn-font-family); + @include font-size(var(--#{$prefix}btn-font-size)); + font-weight: var(--#{$prefix}btn-font-weight); + line-height: var(--#{$prefix}btn-line-height); + color: var(--#{$prefix}btn-color); + text-align: center; + text-decoration: if($link-decoration == none, null, none); + white-space: $btn-white-space; + vertical-align: middle; + cursor: if($enable-button-pointers, pointer, null); + user-select: none; + border: var(--#{$prefix}btn-border-width) solid var(--#{$prefix}btn-border-color); + @include border-radius(var(--#{$prefix}btn-border-radius)); + @include gradient-bg(var(--#{$prefix}btn-bg)); + @include box-shadow(var(--#{$prefix}btn-box-shadow)); + @include transition($btn-transition); + + :not(.btn-check) + &:hover, + &:first-child:hover { + color: var(--#{$prefix}btn-hover-color); + text-decoration: if($link-hover-decoration == underline, none, null); + background-color: var(--#{$prefix}btn-hover-bg); + border-color: var(--#{$prefix}btn-hover-border-color); + } + + &:focus-visible { + color: var(--#{$prefix}btn-hover-color); + @include gradient-bg(var(--#{$prefix}btn-hover-bg)); + border-color: var(--#{$prefix}btn-hover-border-color); + outline: 0; + // Avoid using mixin so we can pass custom focus shadow properly + @if $enable-shadows { + box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); + } @else { + box-shadow: var(--#{$prefix}btn-focus-box-shadow); + } + } + + .btn-check:focus-visible + & { + border-color: var(--#{$prefix}btn-hover-border-color); + outline: 0; + // Avoid using mixin so we can pass custom focus shadow properly + @if $enable-shadows { + box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); + } @else { + box-shadow: var(--#{$prefix}btn-focus-box-shadow); + } + } + + .btn-check:checked + &, + :not(.btn-check) + &:active, + &:first-child:active, + &.active, + &.show { + color: var(--#{$prefix}btn-active-color); + background-color: var(--#{$prefix}btn-active-bg); + // Remove CSS gradients if they're enabled + background-image: if($enable-gradients, none, null); + border-color: var(--#{$prefix}btn-active-border-color); + @include box-shadow(var(--#{$prefix}btn-active-shadow)); + + &:focus-visible { + // Avoid using mixin so we can pass custom focus shadow properly + @if $enable-shadows { + box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow); + } @else { + box-shadow: var(--#{$prefix}btn-focus-box-shadow); + } + } + } + + &:disabled, + &.disabled, + fieldset:disabled & { + color: var(--#{$prefix}btn-disabled-color); + pointer-events: none; + background-color: var(--#{$prefix}btn-disabled-bg); + background-image: if($enable-gradients, none, null); + border-color: var(--#{$prefix}btn-disabled-border-color); + opacity: var(--#{$prefix}btn-disabled-opacity); + @include box-shadow(none); + } +} + + +// +// Alternate buttons +// + +// scss-docs-start btn-variant-loops +@each $color, $value in $theme-colors { + .btn-#{$color} { + @if $color == "light" { + @include button-variant( + $value, + $value, + $hover-background: shade-color($value, $btn-hover-bg-shade-amount), + $hover-border: shade-color($value, $btn-hover-border-shade-amount), + $active-background: shade-color($value, $btn-active-bg-shade-amount), + $active-border: shade-color($value, $btn-active-border-shade-amount) + ); + } @else if $color == "dark" { + @include button-variant( + $value, + $value, + $hover-background: tint-color($value, $btn-hover-bg-tint-amount), + $hover-border: tint-color($value, $btn-hover-border-tint-amount), + $active-background: tint-color($value, $btn-active-bg-tint-amount), + $active-border: tint-color($value, $btn-active-border-tint-amount) + ); + } @else { + @include button-variant($value, $value); + } + } +} + +@each $color, $value in $theme-colors { + .btn-outline-#{$color} { + @include button-outline-variant($value); + } +} +// scss-docs-end btn-variant-loops + + +// +// Link buttons +// + +// Make a button look and behave like a link +.btn-link { + --#{$prefix}btn-font-weight: #{$font-weight-normal}; + --#{$prefix}btn-color: #{$btn-link-color}; + --#{$prefix}btn-bg: transparent; + --#{$prefix}btn-border-color: transparent; + --#{$prefix}btn-hover-color: #{$btn-link-hover-color}; + --#{$prefix}btn-hover-border-color: transparent; + --#{$prefix}btn-active-color: #{$btn-link-hover-color}; + --#{$prefix}btn-active-border-color: transparent; + --#{$prefix}btn-disabled-color: #{$btn-link-disabled-color}; + --#{$prefix}btn-disabled-border-color: transparent; + --#{$prefix}btn-box-shadow: none; + --#{$prefix}btn-focus-shadow-rgb: #{to-rgb(mix(color-contrast($primary), $primary, 15%))}; + + text-decoration: $link-decoration; + @if $enable-gradients { + background-image: none; + } + + &:hover, + &:focus-visible { + text-decoration: $link-hover-decoration; + } + + &:focus-visible { + color: var(--#{$prefix}btn-color); + } + + &:hover { + color: var(--#{$prefix}btn-hover-color); + } + + // No need for an active state here +} + + +// +// Button Sizes +// + +.btn-lg { + @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg); +} + +.btn-sm { + @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm); +} diff --git a/csit.infra.dash/app/pal/static/sass/bootstrap/_card.scss b/csit.infra.dash/app/pal/static/sass/bootstrap/_card.scss new file mode 100644 index 0000000000..ce8c02f1f2 --- /dev/null +++ b/csit.infra.dash/app/pal/static/sass/bootstrap/_card.scss @@ -0,0 +1,234 @@ +// +// Base styles +// + +.card { + // scss-docs-start card-css-vars + --#{$prefix}card-spacer-y: #{$card-spacer-y}; + --#{$prefix}card-spacer-x: #{$card-spacer-x}; + --#{$prefix}card-title-spacer-y: #{$card-title-spacer-y}; + --#{$prefix}card-border-width: #{$card-border-width}; + --#{$prefix}card-border-color: #{$card-border-color}; + --#{$prefix}card-border-radius: #{$card-border-radius}; + --#{$prefix}card-box-shadow: #{$card-box-shadow}; + --#{$prefix}card-inner-border-radius: #{$card-inner-border-radius}; + --#{$prefix}card-cap-padding-y: #{$card-cap-padding-y}; + --#{$prefix}card-cap-padding-x: #{$card-cap-padding-x}; + --#{$prefix}card-cap-bg: #{$card-cap-bg}; + --#{$prefix}card-cap-color: #{$card-cap-color}; + --#{$prefix}card-height: #{$card-height}; + --#{$prefix}card-color: #{$card-color}; + --#{$prefix}card-bg: #{$card-bg}; + --#{$prefix}card-img-overlay-padding: #{$card-img-overlay-padding}; + --#{$prefix}card-group-margin: #{$card-group-margin}; + // scss-docs-end card-css-vars + + position: relative; + display: flex; + flex-direction: column; + min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106 + height: var(--#{$prefix}card-height); + word-wrap: break-word; + background-color: var(--#{$prefix}card-bg); + background-clip: border-box; + border: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); + @include border-radius(var(--#{$prefix}card-border-radius)); + @include box-shadow(var(--#{$prefix}card-box-shadow)); + + > hr { + margin-right: 0; + margin-left: 0; + } + + > .list-group { + border-top: inherit; + border-bottom: inherit; + + &:first-child { + border-top-width: 0; + @include border-top-radius(var(--#{$prefix}card-inner-border-radius)); + } + + &:last-child { + border-bottom-width: 0; + @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius)); + } + } + + // Due to specificity of the above selector (`.card > .list-group`), we must + // use a child selector here to prevent double borders. + > .card-header + .list-group, + > .list-group + .card-footer { + border-top: 0; + } +} + +.card-body { + // Enable `flex-grow: 1` for decks and groups so that card blocks take up + // as much space as possible, ensuring footers are aligned to the bottom. + flex: 1 1 auto; + padding: var(--#{$prefix}card-spacer-y) var(--#{$prefix}card-spacer-x); + color: var(--#{$prefix}card-color); +} + +.card-title { + margin-bottom: var(--#{$prefix}card-title-spacer-y); +} + +.card-subtitle { + margin-top: calc(-.5 * var(--#{$prefix}card-title-spacer-y)); // stylelint-disable-line function-disallowed-list + margin-bottom: 0; +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link { + &:hover { + text-decoration: if($link-hover-decoration == underline, none, null); + } + + + .card-link { + margin-left: var(--#{$prefix}card-spacer-x); + } +} + +// +// Optional textual caps +// + +.card-header { + padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x); + margin-bottom: 0; // Removes the default margin-bottom of + color: var(--#{$prefix}card-cap-color); + background-color: var(--#{$prefix}card-cap-bg); + border-bottom: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); + + &:first-child { + @include border-radius(var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius) 0 0); + } +} + +.card-footer { + padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x); + color: var(--#{$prefix}card-cap-color); + background-color: var(--#{$prefix}card-cap-bg); + border-top: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); + + &:last-child { + @include border-radius(0 0 var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius)); + } +} + + +// +// Header navs +// + +.card-header-tabs { + margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list + margin-bottom: calc(-1 * var(--#{$prefix}card-cap-padding-y)); // stylelint-disable-line function-disallowed-list + margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list + border-bottom: 0; + + .nav-link.active { + background-color: var(--#{$prefix}card-bg); + border-bottom-color: var(--#{$prefix}card-bg); + } +} + +.card-header-pills { + margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list + margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list +} + +// Card image +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: var(--#{$prefix}card-img-overlay-padding); + @include border-radius(var(--#{$prefix}card-inner-border-radius)); +} + +.card-img, +.card-img-top, +.card-img-bottom { + width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch +} + +.card-img, +.card-img-top { + @include border-top-radius(var(--#{$prefix}card-inner-border-radius)); +} + +.card-img, +.card-img-bottom { + @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius)); +} + + +// +// Card groups +// + +.card-group { + // The child selector allows nested `.card` within `.card-group` + // to display properly. + > .card { + margin-bottom: var(--#{$prefix}card-group-margin); + } + + @include media-breakpoint-up(sm) { + display: flex; + flex-flow: row wrap; + // The child selector allows nested `.card` within `.card-group` + // to display properly. + > .card { + // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4 + flex: 1 0 0%; + margin-bottom: 0; + + + .card { + margin-left: 0; + border-left: 0; + } + + // Handle rounded corners + @if $enable-rounded { + &:not(:last-child) { + @include border-end-radius(0); + + .card-img-top, + .card-header { + // stylelint-disable-next-line property-disallowed-list + border-top-right-radius: 0; + } + .card-img-bottom, + .card-footer { + // stylelint-disable-next-line property-disallowed-list + border-bottom-right-radius: 0; + } + } + + &:not(:first-child) { + @include border-start-radius(0); + + .card-img-top, + .card-header { + // stylelint-disable-next-line property-disallowed-list + border-top-left-radius: 0; + } + .card-img-bottom, + .card-footer { + // stylelint-disable-next-line property-disallowed-list + border-bottom-left-radius: 0; + } + } + } + } + } +} diff --git a/csit.infra.dash/app/pal/static/sass/bootstrap/_carousel.scss b/csit.infra.dash/app/pal/static/sass/bootstrap/_carousel.scss new file mode 100644 index 0000000000..3d8fb15a06 --- /dev/null +++ b/csit.infra.dash/app/pal/static/sass/bootstrap/_carousel.scss @@ -0,0 +1,229 @@ +// Notes on the classes: +// +// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically) +// even when their scroll action started on a carousel, but for compatibility (with Firefox) +// we're preventing all actions instead +// 2. The .carousel-item-start and .carousel-item-end is used to indicate where +// the active slide is heading. +// 3. .active.carousel-item is the current slide. +// 4. .active.carousel-item-start and .active.carousel-item-end is the current +// slide in its in-transition state. Only one of these occurs at a time. +// 5. .carousel-item-next.carousel-item-start and .carousel-item-prev.carousel-item-end +// is the upcoming slide in transition. + +.carousel { + position: relative; +} + +.carousel.pointer-event { + touch-action: pan-y; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; + @include clearfix(); +} + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + backface-visibility: hidden; + @include transition($carousel-transition); +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +/* rtl:begin:ignore */ +.carousel-item-next:not(.carousel-item-start), +.active.carousel-item-end { + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-end), +.active.carousel-item-start { + transform: translateX(-100%); +} + +/* rtl:end:ignore */ + + +// +// Alternate transitions +// + +.carousel-fade { + .carousel-item { + opacity: 0; + transition-property: opacity; + transform: none; + } + + .carousel-item.active, + .carousel-item-next.carousel-item-start, + .carousel-item-prev.carousel-item-end { + z-index: 1; + opacity: 1; + } + + .active.carousel-item-start, + .active.carousel-item-end { + z-index: 0; + opacity: 0; + @include transition(opacity 0s $carousel-transition-duration); + } +} + + +// +// Left/right controls for nav +// + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + // Use flex for alignment (1-3) + display: flex; // 1. allow flex styles + align-items: center; // 2. vertically center contents + justify-content: center; // 3. horizontally center contents + width: $carousel-control-width; + padding: 0; + color: $carousel-control-color; + text-align: center; + background: none; + border: 0; + opacity: $carousel-control-opacity; + @include transition($carousel-control-transition); + + // Hover/focus state + &:hover, + &:focus { + color: $carousel-control-color; + text-decoration: none; + outline: 0; + opacity: $carousel-control-hover-opacity; + } +} +.carousel-control-prev { + left: 0; + background-image: if($enable-gradients, linear-gradient(90deg, rgba($black, .25), rgba($black, .001)), null); +} +.carousel-control-next { + right: 0; + background-image: if($enable-gradients, linear-gradient(270deg, rgba($black, .25), rgba($black, .001)), null); +} + +// Icons for within +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: $carousel-control-icon-width; + height: $carousel-control-icon-width; + background-repeat: no-repeat; + background-position: 50%; + background-size: 100% 100%; +} + +/* rtl:options: { + "autoRename": true, + "stringMap":[ { + "name" : "prev-next", + "search" : "prev", + "replace" : "next" + } ] +} */ +.carousel-control-prev-icon { + background-image: escape-svg($carousel-control-prev-icon-bg); +} +.carousel-control-next-icon { + background-image: escape-svg($carousel-control-next-icon-bg); +} + +// Optional indicator pips/controls +// +// Add a container (such as a list) with the following class and add an item (ideally a focusable control, +// like a button) with data-bs-target for each slide your carousel holds. + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + display: flex; + justify-content: center; + padding: 0; + // Use the .carousel-control's width as margin so we don't overlay those + margin-right: $carousel-control-width; + margin-bottom: 1rem; + margin-left: $carousel-control-width; + list-style: none; + + [data-bs-target] { + box-sizing: content-box; + flex: 0 1 auto; + width: $carousel-indicator-width; + height: $carousel-indicator-height; + padding: 0; + margin-right: $carousel-indicator-spacer; + margin-left: $carousel-indicator-spacer; + text-indent: -999px; + cursor: pointer; + background-color: $carousel-indicator-active-bg; + background-clip: padding-box; + border: 0; + // Use transparent borders to increase the hit area by 10px on top and bottom. + border-top: $carousel-indicator-hit-area-height solid transparent; + border-bottom: $carousel-indicator-hit-area-height solid transparent; + opacity: $carousel-indicator-opacity; + @include transition($carousel-indicator-transition); + } + + .active { + opacity: $carousel-indicator-active-opacity; + } +} + + +// Optional captions +// +// + +.carousel-caption { + position: absolute; + right: (100% - $carousel-caption-width) * .5; + bottom: $carousel-caption-spacer; + left: (100% - $carousel-caption-width) * .5; + padding-top: $carousel-caption-padding-y; + padding-bottom: $carousel-caption-padding-y; + color: $carousel-caption-color; + text-align: center; +} + +// Dark mode carousel + +.carousel-dark { + .carousel-control-prev-icon, + .carousel-control-next-icon { + filter: $carousel-dark-control-icon-filter; + } + + .carousel-indicators [data-bs-target] { + background-color: $carousel-dark-indicator-active-bg; + } + + .carousel-caption { + color: $carousel-dark-caption-color; + } +} diff --git a/csit.infra.dash/app/pal/static/sass/bootstrap/_close.scss b/csit.infra.dash/app/pal/static/sass/bootstrap/_close.scss new file mode 100644 index 0000000000..a0813de8d3 --- /dev/null +++ b/csit.infra.dash/app/pal/static/sass/bootstrap/_close.scss @@ -0,0 +1,40 @@ +// Transparent background and border properties included for button version. +// iOS requires the button element instead of an anchor tag. +// If you want the anchor version, it requires `href="#"`. +// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile + +.btn-close { + box-sizing: content-box; + width: $btn-close-width; + height: $btn-close-height; + padding: $btn-close-padding-y $btn-close-padding-x; + color: $btn-close-color; + background: transparent escape-svg($btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements + border: 0; // for button elements + @include border-radius(); + opacity: $btn-close-opacity; + + // Override 's hover style + &:hover { + color: $btn-close-color; + text-decoration: none; + opacity: $btn-close-hover-opacity; + } + + &:focus { + outline: 0; + box-shadow: $btn-close-focus-shadow; + opacity: $btn-close-focus-opacity; + } + + &:disabled, + &.disabled { + pointer-events: none; + user-select: none; + opacity: $btn-close-disabled-opacity; + } +} + +.btn-close-white { + filter: $btn-close-white-filter; +} diff --git a/csit.infra.dash/app/pal/static/sass/bootstrap/_containers.scss b/csit.infra.dash/app/pal/static/sass/bootstrap/_containers.scss new file mode 100644 index 0000000000..83b31381bf --- /dev/null +++ b/csit.infra.dash/app/pal/static/sass/bootstrap/_containers.scss @@ -0,0 +1,41 @@ +// Container widths +// +// Set the container width, and override it for fixed navbars in media queries. + +@if $enable-container-classes { + // Single container class with breakpoint max-widths + .container, + // 100% wide container at all breakpoints + .container-fluid { + @include make-container(); + } + + // Responsive containers that are 100% wide until a breakpoint + @each $breakpoint, $container-max-width in $container-max-widths { + .container-#{$breakpoint} { + @extend .container-fluid; + } + + @include media-breakpoint-up($breakpoint, $grid-breakpoints) { + %responsive-container-#{$breakpoint} { + max-width: $container-max-width; + } + + // Extend each breakpoint which is smaller or equal to the current breakpoint + $extend-breakpoint: true; + + @each $name, $width in $grid-breakpoints { + @if ($extend-breakpoint) { + .container#{breakpoint-infix($name, $grid-breakpoints)} { + @extend %responsive-container-#{$breakpoint}; + } + + // Once the current breakpoint is reached, stop extending + @if ($breakpoint == $name) { + $extend-breakpoint: false; + } + } + } + } + } +} diff --git a/csit.infra.dash/app/pal/static/sass/bootstrap/_dropdown.scss b/csit.infra.dash/app/pal/static/sass/bootstrap/_dropdown.scss new file mode 100644 index 0000000000..8899d25a0d --- /dev/null +++ b/csit.infra.dash/app/pal/static/sass/bootstrap/_dropdown.scss @@ -0,0 +1,249 @@ +// The dropdown wrapper (`
`) +.dropup, +.dropend, +.dropdown, +.dropstart, +.dropup-center, +.dropdown-center { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; + + // Generate the caret automatically + @include caret(); +} + +// The dropdown menu +.dropdown-menu { + // scss-docs-start dropdown-css-vars + --#{$prefix}dropdown-zindex: #{$zindex-dropdown}; + --#{$prefix}dropdown-min-width: #{$dropdown-min-width}; + --#{$prefix}dropdown-padding-x: #{$dropdown-padding-x}; + --#{$prefix}dropdown-padding-y: #{$dropdown-padding-y}; + --#{$prefix}dropdown-spacer: #{$dropdown-spacer}; + @include rfs($dropdown-font-size, --#{$prefix}dropdown-font-size); + --#{$prefix}dropdown-color: #{$dropdown-color}; + --#{$prefix}dropdown-bg: #{$dropdown-bg}; + --#{$prefix}dropdown-border-color: #{$dropdown-border-color}; + --#{$prefix}dropdown-border-radius: #{$dropdown-border-radius}; + --#{$prefix}dropdown-border-width: #{$dropdown-border-width}; + --#{$prefix}dropdown-inner-border-radius: #{$dropdown-inner-border-radius}; + --#{$prefix}dropdown-divider-bg: #{$dropdown-divider-bg}; + --#{$prefix}dropdown-divider-margin-y: #{$dropdown-divider-margin-y}; + --#{$prefix}dropdown-box-shadow: #{$dropdown-box-shadow}; + --#{$prefix}dropdown-link-color: #{$dropdown-link-color}; + --#{$prefix}dropdown-link-hover-color: #{$dropdown-link-hover-color}; + --#{$prefix}dropdown-link-hover-bg: #{$dropdown-link-hover-bg}; + --#{$prefix}dropdown-link-active-color: #{$dropdown-link-active-color}; + --#{$prefix}dropdown-link-active-bg: #{$dropdown-link-active-bg}; + --#{$prefix}dropdown-link-disabled-color: #{$dropdown-link-disabled-color}; + --#{$prefix}dropdown-item-padding-x: #{$dropdown-item-padding-x}; + --#{$prefix}dropdown-item-padding-y: #{$dropdown-item-padding-y}; + --#{$prefix}dropdown-header-color: #{$dropdown-header-color}; + --#{$prefix}dropdown-header-padding-x: #{$dropdown-header-padding-x}; + --#{$prefix}dropdown-header-padding-y: #{$dropdown-header-padding-y}; + // scss-docs-end dropdown-css-vars + + position: absolute; + z-index: var(--#{$prefix}dropdown-zindex); + display: none; // none by default, but block on "open" of the menu + min-width: var(--#{$prefix}dropdown-min-width); + padding: var(--#{$prefix}dropdown-padding-y) var(--#{$prefix}dropdown-padding-x); + margin: 0; // Override default margin of ul + @include font-size(var(--#{$prefix}dropdown-font-size)); + color: var(--#{$prefix}dropdown-color); + text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) + list-style: none; + background-color: var(--#{$prefix}dropdown-bg); + background-clip: padding-box; + border: var(--#{$prefix}dropdown-border-width) solid var(--#{$prefix}dropdown-border-color); + @include border-radius(var(--#{$prefix}dropdown-border-radius)); + @include box-shadow(var(--#{$prefix}dropdown-box-shadow)); + + &[data-bs-popper] { + top: 100%; + left: 0; + margin-top: var(--#{$prefix}dropdown-spacer); + } + + @if $dropdown-padding-y == 0 { + > .dropdown-item:first-child, + > li:first-child .dropdown-item { + @include border-top-radius(var(--#{$prefix}dropdown-inner-border-radius)); + } + > .dropdown-item:last-child, + > li:last-child .dropdown-item { + @include border-bottom-radius(var(--#{$prefix}dropdown-inner-border-radius)); + } + + } +} + +// scss-docs-start responsive-breakpoints +// We deliberately hardcode the `bs-` prefix because we check +// this custom property in JS to determine Popper's positioning + +@each $breakpoint in map-keys($grid-breakpoints) { + @include media-breakpoint-up($breakpoint) { + $infix: breakpoint-infix($breakpoint, $grid-breakpoints); + + .dropdown-menu#{$infix}-start { + --bs-position: start; + + &[data-bs-popper] { + right: auto; + left: 0; + } + } + + .dropdown-menu#{$infix}-end { + --bs-position: end; + + &[data-bs-popper] { + right: 0; + left: auto; + } + } + } +} +// scss-docs-end responsive-breakpoints + +// Allow for dropdowns to go bottom up (aka, dropup-menu) +// Just add .dropup after the standard .dropdown class and you're set. +.dropup { + .dropdown-menu[data-bs-popper] { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: var(--#{$prefix}dropdown-spacer); + } + + .dropdown-toggle { + @include caret(up); + } +} + +.dropend { + .dropdown-menu[data-bs-popper] { + top: 0; + right: auto; + left: 100%; + margin-top: 0; + margin-left: var(--#{$prefix}dropdown-spacer); + } + + .dropdown-toggle { + @include caret(end); + &::after { + vertical-align: 0; + } + } +} + +.dropstart { + .dropdown-menu[data-bs-popper] { + top: 0; + right: 100%; + left: auto; + margin-top: 0; + margin-right: var(--#{$prefix}dropdown-spacer); + } + + .dropdown-toggle { + @include caret(start); + &::before { + vertical-align: 0; + } + } +} + + +// Dividers (basically an `
`) within the dropdown +.dropdown-divider { + height: 0; + margin: var(--#{$prefix}dropdown-divider-margin-y) 0; + overflow: hidden; + border-top: 1px solid var(--#{$prefix}dropdown-divider-bg); + opacity: 1; // Revisit in v6 to de-dupe styles that conflict with
element +} + +// Links, buttons, and more within the dropdown menu +// +// `